pax_global_header00006660000000000000000000000064151672077540014530gustar00rootroot0000000000000052 comment=04e6d055a1886a2a8e8590b51aed7a95a7172298 jeremyevans-roda-4f30bb3/000077500000000000000000000000001516720775400154105ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/.ci.gemfile000066400000000000000000000021661516720775400174200ustar00rootroot00000000000000source 'https://rubygems.org' if RUBY_VERSION < '2' gem 'rake', '<10' elsif RUBY_VERSION < '2.3' gem 'rake', '<13' else gem 'rake' end if RUBY_VERSION < '2.0.0' gem 'mime-types', '< 3' gem "tilt", '<2.0.11' else gem "tilt" end if RUBY_VERSION >= '3.4' gem 'rdoc' end if RUBY_VERSION < '3.1.0' && RUBY_VERSION >= '3.0.0' gem 'json', '2.5.1' elsif RUBY_VERSION < '2.0.0' gem 'json', '<1.8.5' elsif RUBY_VERSION < '2.3.0' gem 'json', '<2.6' else gem 'json' end case RUBY_VERSION[0, 3] when '1.9', '2.0' gem 'rack', '<1.6' when '2.1', '2.2' gem 'rack', '<2' when '2.3' gem 'rack', '<2.1' when '2.5' gem 'rack', '<2.2' when '2.6' gem 'rack', '<3' when '2.7' gem 'rack', '<3.1' when '2.4', '3.3' # Test main branch of Rack for lowest and highest supported # Ruby version gem 'rack', :git => 'https://github.com/rack/rack' else gem 'rack' end if RUBY_VERSION < '2.4.0' # Until mintest 5.12.0 is fixed gem 'minitest', '5.11.3' else gem 'minitest' end if RUBY_VERSION >= '3.1.0' gem 'net-smtp' end gem "minitest-global_expectations" gem "minitest-hooks" gem "erubi" gem "rack_csrf" gem "mail" jeremyevans-roda-4f30bb3/.github/000077500000000000000000000000001516720775400167505ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/.github/workflows/000077500000000000000000000000001516720775400210055ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/.github/workflows/ci.yml000066400000000000000000000013121516720775400221200ustar00rootroot00000000000000name: CI on: push: branches: [ master ] pull_request: branches: [ master ] permissions: contents: read jobs: tests: strategy: fail-fast: false matrix: os: [ubuntu-latest] 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, "4.0", jruby-9.3, jruby-9.4, jruby-10.0, truffleruby-head ] include: - { os: ubuntu-22.04, ruby: "1.9.3" } runs-on: ${{ matrix.os }} name: ${{ matrix.ruby }} env: BUNDLE_GEMFILE: .ci.gemfile steps: - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - run: bundle exec rake spec_ci jeremyevans-roda-4f30bb3/.gitignore000066400000000000000000000001661516720775400174030ustar00rootroot00000000000000/roda-*.gem /rdoc/ /coverage/ /www/public/*.html /www/public/rdoc/ /spec/iv-*.erb /spec/pid-* /spec/render_coverage-* jeremyevans-roda-4f30bb3/CHANGELOG000066400000000000000000001015051516720775400166240ustar00rootroot00000000000000=== 3.103.0 (2026-04-13) * Add hash_public plugin for serving static files with paths that change based on content (yykamei, jeremyevans) (#417, #418) * Add ip_from_header plugin, getting request IP address from specified header (jeremyevans) === 3.102.0 (2026-03-13) * Extract send_file plugin from sinatra_helpers plugin (jeremyevans) (#412) * Extract response_attachment plugin from sinatra_helpers plugin (jeremyevans) === 3.101.0 (2026-02-13) * Add bearer_token plugin for retrieving bearer token from HTTP Authorization header (jeremyevans) === 3.100.0 (2026-01-12) * Add sec_fetch_site_csrf plugin, for CSRF protection using Sec-Fetch-Site header (jeremyevans) === 3.99.0 (2025-12-12) * Add support for set matchers by default, as Set will be a core class in Ruby 4 (jeremyevans) === 3.98.0 (2025-11-13) * Add support for the :env_key option to the sessions plugin (jeremyevans) (#403) === 3.97.0 (2025-10-13) * Add map_matcher plugin, for matching next segment in request path to a hash key, yielding hash value (jeremyevans) === 3.96.0 (2025-09-12) * Add redirect_path plugin, for automatically calling path with the redirect argument if not given a string (jeremyevans) === 3.95.0 (2025-08-12) * Support typecast_query_params and typecast_body_params in typecast_params plugin (jeremyevans) * Raise Roda::RodaPlugins::Sessions::CookieTooLarge for cookies where total cookie size is over 4K (not just cookie value) (jeremyevans) * Add response_content_type plugin for more easily setting content-type for responses (jeremyevans) === 3.94.0 (2025-07-14) * Add view_subdir_leading_slash plugin for using view subdirectory unless template name starts with slash (jeremyevans) (#395) * Optimize render_each and each_part default local selection on Ruby 3+/!Windows (jeremyevans) * Optimize render_each and each_part template selection when freezing app with :assume_fixed_locals render plugin option (jeremyevans) === 3.93.0 (2025-06-12) * Numerous minor performance improvements, mostly from rubocop-performance (jeremyevans) * Support invalid_value_message for custom invalid value messages per-type in typecast_params plugin (jeremyevans) === 3.92.0 (2025-05-13) * Add each_part plugin, with a simpler method for using render_each with locals (jeremyevans) * Support :assume_fixed_locals render option in render_each plugin (jeremyevans) === 3.91.0 (2025-04-11) * Support returns: :buffer method and plugin option in capture_erb plugin (jeremyevans) * Allow render_each to accept a block and yield renderings, instead of returning a concatenation of the renderings (jeremyevans) === 3.90.0 (2025-03-11) * Set temporary name on Ruby 3.3+ for remaining anonymous modules and classes (jeremyevans) * Make send_file in sinatra_helpers use a response body that implements to_path (jeremyevans) (#379) * Escape newlines in common logger lines (jeremyevans) === 3.89.0 (2025-02-12) * Support passing keyword arguments to mailer plugin mail/sendmail class methods (jeremyevans) * Improve performance when using :assume_fixed_locals render plugin option, when using compiled methods and freezing the app (jeremyevans) * Add part plugin, with a simpler (and better performing when using :assume_fixed_locals) method for rendering with locals (jeremyevans) * Support :assume_fixed_locals render plugin option for better caching when all templates use fixed locals (jeremyevans) === 3.88.0 (2025-01-14) * Support fixed locals in templates when using Tilt 2.6+ (jeremyevans) * Make default_headers plugin correctly handle mixed/upper case content-type header on Rack 3 (jeremyevans) (#373) * Make json_parser plugin handle case where Rack::Request#POST is called previously for env on Rack 3 (jeremyevans) (#372) * Fix strict_unused_block warnings when running specs on Ruby 3.4 (jeremyevans) = 3.87.0 (2024-12-17) * Add host_routing plugin for routing based on request host header (jeremyevans) * Do not write non-String to response body when using custom_block_results plugin (jeremyevans) = 3.86.0 (2024-11-12) * Add conditional_sessions plugin, for using the sessions plugin for only a subset of requests (jeremyevans) * In permissions_policy plugin, add response.skip_permissions_policy! to avoid setting header (jeremyevans) * Make Roda.freeze work if already frozen when using the autoload_{hash_branches,named_routes} plugins (jeremyevans) * In content_security_policy plugin, add response.skip_content_security_policy! to avoid setting header (jeremyevans) = 3.85.0 (2024-10-11) * Avoid deprecation warning in public plugin when using Ruby 3.4.0-preview2 (jeremyevans) * Evaluate class_matcher and symbol_matcher blocks in route-block context (jeremyevans) * Allow class_matcher and symbol_matcher blocks to return non-arrays (jeremyevans) * Make class_matcher and symbol_matcher plugin be able to build on top of existing registered matchers (jeremyevans) * Make capture_erb plugin not break if String#capture is defined (jeremyevans) = 3.84.0 (2024-09-12) * Add hsts plugin for setting Strict-Transport-Security header (jeremyevans) * Remove documentation from the gem to reduce gem size by 25% (jeremyevans) = 3.83.0 (2024-08-12) * Add assume_ssl plugin for making request ssl? method always return true (jeremyevans) = 3.82.0 (2024-07-12) * Add :encodings option to public plugin to support configurable encoding order (jeremyevans) * Add :zstd option to public plugin to supplement it to serve zstd-compressed files with .zst extension (jeremyevans) * Make capture_erb plugin call integrate better with erubi/capture_block (jeremyevans) = 3.81.0 (2024-06-12) * Make assets plugin :early_hints option follow Rack 3 SPEC if using Rack 3 (jeremyevans) * Correctly parse Ruby 3.4 backtraces in exception_page plugin (jeremyevans) * Support :until and :seconds option in hmac_paths plugin, for paths valid only until a specific time (jeremyevans) = 3.80.0 (2024-05-10) * Support :namespace option in hmac_paths plugin, allowing for easy per-user/per-group HMAC paths (jeremyevans) = 3.79.0 (2024-04-12) * Do not update template mtime when there is an error reloading templates in the render plugin (jeremyevans) * Add hmac_paths plugin for preventing path enumeration and supporting access control (jeremyevans) = 3.78.0 (2024-03-13) * Add permissions_policy plugin for setting Permissions-Policy header (jeremyevans) = 3.77.0 (2024-02-12) * Support formaction/formmethod attributes in forms in route_csrf plugin (jeremyevans) = 3.76.0 (2024-01-12) * Support :filter plugin option in error_mail and error_email for filtering parameters, environment variables, and session values (jeremyevans) (#346) * Set temporary name on Ruby 3.3 in middleware plugin for middleware class created (janko) (#344) * Add break plugin, for using break inside a routing block to return from the block and keep routing (jeremyevans) = 3.75.0 (2023-12-14) * Add cookie_flags plugin, for overriding, warning, or raising for incorrect cookie flags (jeremyevans) = 3.74.0 (2023-11-13) * Add redirect_http_to_https plugin, helping to ensure future requests from the browser are submitted via HTTPS (jeremyevans) = 3.73.0 (2023-10-13) * Support :next_if_not_found option for middleware plugin (jeremyevans) (#334) * Remove dependency on base64 library from sessions and route_csrf plugin, as it will not be part of the standard library in Ruby 3.4+ (jeremyevans) = 3.72.0 (2023-09-12) * Add invalid_request_body plugin for custom handling of invalid request bodies (jeremyevans) * Warn when defining method that expects 1 argument when block requires multiple arguments when :check_arity option is set to :warn (jeremyevans) * Implement the match_hooks plugin using the match_hook_args plugin (jeremyevans) = 3.71.0 (2023-08-14) * Add match_hook_args plugin, similar to match_hooks but support matchers and block args as hook arguments (jeremyevans) = 3.70.0 (2023-07-12) * Add plain_hash_response_headers plugin, using a plain hash for response headers on Rack 3 for much better performance (jeremyevans) * Use lower case response header keys by default on Rack 3, instead of relying on Rack::Headers conversion (jeremyevans) = 3.69.0 (2023-06-13) * Allow symbol_matcher in symbol_matchers plugin to take a block to allow type conversion (jeremyevans) = 3.68.0 (2023-05-11) * Make Roda.run in multi_run plugin accept blocks to allow autoloading the apps to dispatch to (jeremyevans) = 3.67.0 (2023-04-12) * Add custom_block_results plugin for registering custom block result handlers (jeremyevans) = 3.66.0 (2023-03-13) * Support overriding exception page assets via exception_page_{css,js} instance methods (jeremyevans) (#306) * Avoid keeping reference to Roda instance that caches an inline template (jeremyevans) * Add render_coverage plugin, using tilt 2.1 features to allow for compiled templates in Ruby <3.2 (jeremyevans) = 3.65.0 (2023-02-13) * Make indifferent_params plugin work with changes in rack main branch (jeremyevans) * Add autoload_named_routes plugin for autoloading file for a named route when there is a request for that route (jeremyevans) * Make path method in path plugin accept class name string/symbol with :class_name option to register classes without forcing autoloads (jeremyevans) = 3.64.0 (2023-01-12) * Automatically expand paths for autoload_hash_branches files, so that relative paths work (jeremyevans) * Make autoload_hash_branches plugin eagerly load the branches when freezing the application (jeremyevans) * Add erb_h plugin for faster (if slightly less safe) html escaping using erb/escape (jeremyevans) = 3.63.0 (2022-12-16) * Make mailer plugin set configured content type for body part for emails with attachments when using mail 2.8+ (jeremyevans) * Add autoload_hash_branches plugin for autoloading file for a hash branch when there is a request for that branch (jeremyevans) * Add mailer plugin :terminal option to make r.mail use a terminal match when provided arguments (jeremyevans) = 3.62.0 (2022-11-14) * Add typecast_params_sized_integers plugin for converting parameters to sized integers (jeremyevans) * Add Integer_matcher_max plugin for setting maximum integer value matched by the Integer matcher (jeremyevans) * Allow class matchers in the class_matchers plugin to skip matching based on regexp match values (jeremyevans) * Fix RodaRequest#matched_path when using unescape_path plugin (jeremyevans) (#286) = 3.61.0 (2022-10-12) * Make Integer matcher limit integer segments to 100 characters by default (jeremyevans) * Limit input bytesize by default for integer, float, and date/time typecasts in typecast_params (jeremyevans) = 3.60.0 (2022-09-13) * Add link_to plugin with link_to method for creating HTML links (jeremyevans) = 3.59.0 (2022-08-12) * Add additional_render_engines plugin, for considering multiple render engines for templates (jeremyevans) * Fix typo in private method name in delete_empty_headers plugin (mculpt) (#279) = 3.58.0 (2022-07-13) * Add filter_common_logger plugin for skipping the logging of certain requests when using the common_logger plugin (jeremyevans) * Make exception_page plugin use Exception#detailed_message on Ruby 3.2+ (jeremyevans) * Make heartbeat plugin compatible with recent changes in the rack master branch (jeremyevans) = 3.57.0 (2022-06-14) * Make static_routing plugin depend on the hash_paths instead of the hash_routes plugin (jeremyevans) * Split hash_branches and hash_paths plugins from hash_routes plugin (jeremyevans) * Hex escape unprintable characters in common_logger plugin output (jeremyevans) * Add hash_branch_view_subdir plugin for automatically appending a view subdirectory on a successful hash branch (jeremyevans) = 3.56.0 (2022-05-13) * Make status_303 plugin use 303 responses for HTTP/2 and higher versions (jeremyevans) * Add RodaRequest#http_version for determining the HTTP version in use (jeremyevans) * Do not set a body for 405 responses when using the verb methods in the not_allowed plugin (jeremyevans) (#267) * Support status_handler method :keep_headers option in status_handler plugin (jeremyevans) (#267) * Make not_allowed plugin have r.root return 405 responses for non-GET requests (jeremyevans) (#266) * In Rack 3, only require the parts of rack used by Roda, instead of requiring rack itself and relying on autoload (jeremyevans) * Add run_require_slash plugin, for skipping application dispatch for remaining paths that would violate Rack SPEC (jeremyevans) = 3.55.0 (2022-04-12) * Allow passing blocks to the view method in the render plugin (jeremyevans) (#262) * Add :forward_response_headers middleware plugin option to use app headers as default for response (janko) (#259) = 3.54.0 (2022-03-14) * Make chunked plugin not use Transfer-Encoding: chunked by default (jeremyevans) * Make run_handler plugin close bodies for upstream 404 responses when using not_found: :pass (jeremyevans) * Drop all 1xx bodies in the drop body plugin (jeremyevans) * Do not set a Content-Length header for 205 responses on Rack <2.0.2 (jeremyevans) * Use Rack::Files instead of Rack::File if available, to avoid deprecation warnings (jeremyevans) * Work with Rack 3 SPEC, using Rack::Headers to handle lowercasing header keys on Rack 3 (jeremyevans) * Allow overriding script tag type attribute returned by assets method in assets plugin (pusewicz) (#250) * Make reloading render plugin after additional_view_directories plugin retain :allowed_paths (jeremyevans) = 3.53.0 (2022-02-14) * Make indifferent_params plugin support rack main branch (jeremyevans) * Add additional_view_directories plugin, for checking multiple view directories for templates (jeremyevans) (#229) = 3.52.0 (2022-01-14) * Fix return value of Roda.freeze when multi_route plugin is used (jeremyevans) (#240) * Use faster OpenSSL::Digest instead of Digest for assets plugin SRI support (jeremyevans) * Drop development dependency on haml (jeremyevans) * Make the path method in the path plugin handle blocks that accept keyword arguments in Ruby 3+ (adam12) (#227) * Support typecast_params :date_parse_input_handler plugin option for handling input to date parsing methods (jeremyevans) = 3.51.0 (2021-12-15) * Avoid method redefinition warning in error_handler plugin in verbose warning mode (jeremyevans) * Allow run in multi_run plugin to be called without an app to remove existing handler (jeremyevans) * Allow route in named_routes plugin to be called without a block to remove existing handler (jeremyevans) = 3.50.0 (2021-11-12) * Add capture_erb plugin for capturing ERB template blocks, instead of injecting them into the template output (jeremyevans) * Add inject_erb plugin for injecting content directly into ERB template output (jeremyevans) * Allow hash_branch and hash_path in hash_routes plugin to be called without a block to remove existing handler (jeremyevans) = 3.49.0 (2021-10-13) * Switch block_given? to defined?(yield) (jeremyevans) * Automatically optimize remaining r.is/r.get/r.post calls with a single argument (jeremyevans) = 3.48.0 (2021-09-13) * Extract named_routes plugin from multi_route plugin (jeremyevans) = 3.47.0 (2021-08-13) * Automatically optimize remaining r.on calls with a single argument (jeremyevans) = 3.46.0 (2021-07-12) * Automatically optimize r.on/r.is/r.get/r.post methods with a single string, String, Integer, or regexp argument (jeremyevans) = 3.45.0 (2021-06-14) * Make typecast_params plugin check for null bytes in strings by default, with :allow_null_bytes option for previous behavior (jeremyevans) = 3.44.0 (2021-05-12) * Add optimized_segment_matchers plugin for optimized matchers for a single String class argument (jeremyevans) * Use RFC 5987 UTF-8 and ISO-8859-1 encoded filenames when using send_file and attachment in the sinatra_helpers plugin (jeremyevans) = 3.43.1 (2021-04-13) * [SECURITY] Fix issue where loading content_security_policy plugin after default_headers plugin had no effect (jeremyevans) = 3.43.0 (2021-04-12) * Add host_authorization plugin, for checking that requests are submitted using an approved host (jeremyevans) = 3.42.0 (2021-03-12) * Make Roda.plugin support plugins using keyword arguments in Ruby 3 (jeremyevans) * Make Roda.use support middleware using keyword arguments in Ruby 3 (pat) (#207) * Support common_logger plugin :method option for specifying the method to call on the logger (fnordfish, jeremyevans) (#206) * Add recheck_precompiled_assets plugin for checking for updates to the precompiled asset metadata file (jeremyevans) * Make compile_assets class method in assets plugin use an atomic approach to writing precompiled metadata file (jeremyevans) = 3.41.0 (2021-02-17) * Improve view performance with :content option up to 3x by calling compiled template methods directly (jeremyevans) = 3.40.0 (2021-01-14) * Add freeze_template_caches! to the precompile_templates plugin, which ensures all templates are precompiled, and speeds up template access (jeremyevans) * Add precompile_views to the precompile_templates plugin, which precompiles the optimized render methods (jeremyevans) * Have RodaCache#freeze return the frozen internal hash (which no longer needs a mutex for thread-safety) (jeremyevans) * Speed up the view method in the render plugin even more when freezing the application (jeremyevans) * Speed up the view method in the render plugin when called with a single argument (jeremyevans) = 3.39.0 (2020-12-15) * Speed up relative_path plugin if relative_path or relative_prefix is called more than once (jeremyevans) * Avoid method redefinition warnings in verbose warning mode (jeremyevans) * Make typecast_params.convert! handle explicit nil values the same as missing values (jeremyevans) = 3.38.0 (2020-11-16) * Make error_email and error_mail plugins rescue invalid parameter errors when preparing the email body (jeremyevans) = 3.37.0 (2020-10-16) * Add custom_matchers plugin, for supporting arbitrary objects as matchers (jeremyevans) = 3.36.0 (2020-09-14) * Add multi_public plugin, for serving files from multiple public directories (jeremyevans) * Support report-to directive in the content_security_policy plugin (jeremyevans) * Add Vary response header when using type_routing plugin with Accept request header to prevent caching issues (jeremyevans) = 3.35.0 (2020-08-14) * Add r plugin for r method for accessing request, useful when r local variable is not in scope (jeremyevans) * Warn when loading a plugin with arguments or a block if the plugin does not accept arguments or block (jeremyevans) = 3.34.0 (2020-07-14) * Remove unnecessary conditionals (jeremyevans) * Allow loading the match_affix plugin with a single argument (jeremyevans) * Do not include pre/post context sections if empty in the exception_page plugin (jeremyevans) = 3.33.0 (2020-06-16) * Add :brotli option to public plugin to supplement it to serve brotli-compressed files like :gzip does for gzipped files (hmdne) (#194) * Add url method to path plugin, similar to path but returning the entire URL (jeremyevans) = 3.32.0 (2020-05-15) * Make :dependencies option in assets plugin work correctly with render plugin template caching (jeremyevans) (#191) * Support render method :dependencies option for specifying which files to check for modification (jgarth, jeremyevans) (#192) * Add each_partial to the partials plugin for rendering a partial for each element in an enumerable (jeremyevans) * Make render_each in render_each plugin handle template names with directories and extensions (jeremyevans) = 3.31.0 (2020-04-15) * Add :relative option to path method in path plugin, for generating a method returning relative paths (jeremyevans) * Add relative_path plugin, for turning absolute paths to paths relative to the current request (jeremyevans) = 3.30.0 (2020-03-13) * Support :relative_paths assets plugin option to use relative paths for the assets (jeremyevans) * Make run_append_slash and run_handler plugins work when used together (janko) (#185) * Make :header matcher in header_matchers plugin work for Content-Type and Content-Length (jeremyevans) (#184) = 3.29.0 (2020-02-14) * Remove specs and old release notes from the gem to reduce gem size by over 35% (jeremyevans) * Raise RodaError if trying to load a plugin that is not a module (jeremyevans) * Include SCRIPT_NAME when logging in common logger plugin (jeremyevans) * Handle invalid POST data when using the exception_page plugin (jeremyevans) = 3.28.0 (2020-01-15) * Add session_created_at and session_updated_at methods to the sessions plugin (jeremyevans) * Make upgrading from rack session cookie in sessions plugin work with rack 2.0.8 (jeremyevans) * Make json_parser parse request body as json even if request body has already been read (jeremyevans) = 3.27.0 (2019-12-13) * Allow json_parser return correct result for invalid JSON if the params_capturing plugin is used (jeremyevans) (#180) * Add multibyte_string_matcher plugin for matching multibyte characters (jeremyevans) * Split roda.rb into separate files (janko) (#177) = 3.26.0 (2019-11-18) * Combine multiple asset files with a newline when compiling them, avoiding corner cases with comments (ameuret) (#176) * Add asychronous streaming support to the streaming plugin (janko) (#175) = 3.25.0 (2019-10-15) * Support change in tilt 2.0.10 private API to continue to support compiled templates, with up to 33% performance improvement (jeremyevans) * Improve render performance with :locals option up to 75% by calling compiled template methods directly (jeremyevans) = 3.24.0 (2019-09-13) * Fix Proc.new warning in module_include plugin on Ruby 2.7+ (jeremyevans) * Improve render_each performance by calling compiled template methods directly (jeremyevans) = 3.23.0 (2019-08-13) * Make roda/session_middleware work if type_routing plugin is loaded into Roda itself (jeremyevans) (#169) * Handle requests with nothing before extension in the path in the type_routing plugin (jeremyevans) (#168) * Always show line number in exception_page output in exception_page plugin (jeremyevans) * Improve render/view performance up to 2x in development mode in the default case by calling compiled template methods directly (jeremyevans) = 3.22.0 (2019-07-12) * Improve render performance up to 4x in the default case by calling compiled template methods directly (jeremyevans) = 3.21.0 (2019-06-14) * Cache compiled templates in development mode, until the template files are modified (jeremyevans) = 3.20.0 (2019-05-16) * Set Content-Length header to 0 for empty 205 responses (jeremyevans) = 3.19.0 (2019-04-12) * Allow assets plugin :timestamp_paths option to be a string to specify a custom separator (jeremyevans) * Fix handling for blocks with arity > 1 where expected arity is 1 (jeremyevans) * Improve performance for handling blocks with arity 0 where expected arity is 1 by avoiding instance_exec (jeremyevans) * Improve terminal maching by around 4x (jeremyevans) * Improve symbol matching by 10-20% (jeremyevans) * Improve string matching by 10-20% (jeremyevans) * Automatically load the direct_call plugin when freezing if no middleware is used for better performance (jeremyevans) * Delay building rack app until Roda.app is called (jeremyevans) * Add hash_routes plugin for O(1) route dispatching at any level in the routing tree (jeremyevans) * Add support for per-cookie cipher secrets in the sessions plugin, and enable them by default (jeremyevans) * Add match_hook plugin for calling hooks when there is a successful match block (adam12) (#164) = 3.18.0 (2019-03-15) * Add direct_call plugin for making Roda.call skip middleware, allowing more optimization when dispatching routes (jeremyevans) * Improve performance of default_headers plugin by directly defining set_default_headers (jeremyevans) * Improve performance when freezing app if certain methods have not been overridden (jeremyevans) * Support :check_arity and :check_dynamic_arity app options for whether/how to check arity for blocks used to define methods (jeremyevans) * Improve performance of the status_handler plugin by using methods instead of instance_exec (jeremyevans) * Remove r.static_route method from the static_routing plugin (jeremyevans) * Improve performance of the static_routing plugin by using methods instead of instance_exec (jeremyevans) * Add support for the route_block_args plugin to the route_csrf plugin (jeremyevans) * Improve performance of the route_csrf plugin by using a method instead of instance_exec (jeremyevans) * Improve performance of the route_block_args plugin by using a method instead of instance_exec (jeremyevans) * Improve performance of the path plugin by using methods instead of instance_exec (jeremyevans) * Improve performance of the named_templates plugin by using methods instead of instance_exec (jeremyevans) * Improve performance of the multi_route plugin by using methods instead of instance_exec (jeremyevans) * Improve performance of the hooks plugin by using methods instead of instance_exec (jeremyevans) * Improve performance of the mail_processor plugin by using methods instead of instance_exec (jeremyevans) * Improve performance of the default_status plugin by directly defining the default_status method (jeremyevans) * Improve performance of class_level_routing plugin using methods instead of instance_exec (jeremyevans) * Do not have route_block_args plugin affect class_level_routes plugin (jeremyevans) * Integrate internal after hook with error_handler plugin (jeremyevans) * Improve performance of internal before and after hooks (jeremyevans) * Improve performance by using method instead of instance_exec for main route block (jeremyevans) * Add Roda.define_roda_method for defining instance methods instead of using instance_exec (jeremyevans) * Include cookie_options when clearing the cookie (#162, #163) (eiko, jeremyevans) = 3.17.0 (2019-02-15) * Improve performance in the common case for RodaResponse#finish (jeremyevans) * Support before hooks in the hooks plugin in the mailer and mail_processor plugins (jeremyevans) * Allow set_layout_opts in view_options plugin to override layout if render plugin :layout option is given (jeremyevans) * Add route_block_args plugin to control which arguments are yielded to the route block (jeremyevans, chrisfrank) (#159) = 3.16.0 (2019-01-18) * Add mail_processor plugin for processing mail using a routing tree (jeremyevans) = 3.15.0 (2018-12-14) * Support render plugin :escape option to be a string or array of strings and only add :escape option for those template engines (jeremyevans) (#158) * Add :skip_missing option to convert!/convert_each! in the typecast_params plugin to support not storing keys not present in params (jeremyevans) = 3.14.1 (2018-11-29) * SECURITY: content_for plugin no longer post-processes block result with template engine (jeremyevans) = 3.14.0 (2018-11-16) * Add :raise option to convert!/convert_each! in the typecast_params plugin to support not raising for missing keys (celsworth) (#153) * Do not persist convert!/convert_each! :symbolize setting in the typecast_params plugin (jeremyevans) = 3.13.0 (2018-10-12) * Make Stream#write in streaming plugin return number of bytes written instead of self, so it works with IO.copy_stream (jeremyevans) * Add exception_page plugin for showing a page with debugging information for a given exception (jeremyevans) * Make common_logger plugin handle raised errors (jeremyevans) = 3.12.0 (2018-09-14) * Add common_logger plugin for common log support (jeremyevans) = 3.11.0 (2018-08-15) * Disable default compression of sessions over 128 bytes in the sessions plugin (jeremyevans) * Log but otherwise ignore exceptions raised by after processing of error handler response (jeremyevans) * Modify internal before/after processing to avoid plugin load order issues (jeremyevans) = 3.10.0 (2018-07-18) * Remove flash key from session if new flash is empty when rotating flash (jeremyevans) * Speed up RodaRequest initialization by avoiding 1-2 method calls (jeremyevans) * Add roda/session_middleware (RodaSessionMiddleware), usable as a middleware by any Rack app to use Roda's session support (jeremyevans) * Add sessions plugin for more secure (encrypted+signed) sessions (jeremyevans) * Support :json_parser and :json_serializer application options as default implementations for parsing/serializing JSON (jeremyevans) * Add :handle_result option to middleware plugin for modifying rack result before returning it (jeremyevans) * Make the flash plugin work correctly when sessions are serialized with JSON (jeremyevans) * Make Integer in typecast_params handle Numeric input, and require that Numeric input not have fractional parts (jeremyevans) (#146) = 3.9.0 (2018-06-11) * Add route_csrf plugin for CSRF protection, offering more control, better security, and request-specific tokens compared to rack_csrf (jeremyevans) = 3.8.0 (2018-05-17) * Accept convert_each! :keys option that is Proc or Method in typecast_params plugin (jeremyevans) * Make convert_each! in typecast_params plugin handle hashes with '0'..'N' keys without :keys option (jeremyevans) = 3.7.0 (2018-04-20) * Make response_request plugin work with error_handler and class_level_routing plugins (jeremyevans) * Add content_security_policy plugin for setting an appropriate Content-Security-Policy header (jeremyevans) = 3.6.0 (2018-03-26) * Add :wrap option to json_parser plugin, for whether/how to wrap the uploaded JSON object (jeremyevans) (#142) * Add :early_hints option to the assets plugin, for supporting sending early hints for calls to assets (jeremyevans) * Add early_hints plugin for sending 103 Early Hint responses, currently only working on puma (jeremyevans) = 3.5.0 (2018-02-14) * Add request_aref plugin for configuring behavior of request [] and []= methods (jeremyevans) * Make public plugin not add Content-Type header when serving 304 response for gzipped file (jeremyevans) * Make content_for call with block convert block result to string before passing to tilt (jeremyevans) (#135) = 3.4.0 (2018-01-12) * Add middleware_stack plugin for removing middleware and inserting middleware before the end of the stack (jeremyevans) * Make head plugin handle closing existing response bodies if the body responds to close (Eric Wong) = 3.3.0 (2017-12-14) * Add typecast_params plugin for converting param values to explicit types (jeremyevans) = 3.2.0 (2017-11-16) * Use microseconds in assets plugin :timestamp_paths timestamps (jeremyevans) * Add timestamp_public plugin for serving static files with paths that change based on modify timestamp (jeremyevans) = 3.1.0 (2017-10-13) * Make set_layout_locals and set_view_locals in branch_locals plugin work when the other is not called (jeremyevans) * Add :timestamp_paths option to assets plugin to include timestamps in paths in non-compiled mode (jeremyevans) * Handle ExecJS::RuntimeUnavailable when testing for javascript compression support using uglifier (jeremyevans) * Remove deprecated Roda.thread_safe_cache and RodaRequest#placeholder_string_matcher? methods (jeremyevans) = 3.0.0 (2017-09-15) * Make defined symbol_matcher and hash_matcher match methods private (jeremyevans) * Use public_send instead of send unless calling private methods is expected (jeremyevans) * Compute multi_run regexp when freezing app to avoid thread safety issues at runtime (jeremyevans) * Remove deprecated support for using undefined multi_route namespaces when routing (jeremyevans) * Make it possible to reset :include_request options to false for json and json_parser plugins (jeremyevans) * Deprecate RodaRequest#placeholder_string_matcher? private method (jeremyevans) * Deprecate Roda.thread_safe_cache, use RodaCache directly (jeremyevans) * Make using an app as middleware always create a subclass of the app (jeremyevans) * Enable SHA256 subresource integrity by default in assets plugin (jeremyevans) * Make subclassing a roda app always inherit the render cache (jeremyevans) * Make :cache=>nil render plugin option still allow caching via :cache render method option (jeremyevans) * Make content_for plugin append to existing content by default (jeremyevans) * Make :host matcher in the header_matchers plugin always yield captures if given a regexp (jeremyevans) * Make :header matcher in the header_matchers plugin now always prefix header with HTTP_ (jeremyevans) * Remove deprecated support for locals handling at the plugin level in the render plugin (jeremyevans) * Remove deprecated support for handling locals in the view_options plugin (jeremyevans) * Remove deprecated support for :ext option in render plugin (jeremyevans) * Remove deprecated view_subdirs alias for view_options plugin (jeremyevans) * Remove deprecated support for EventMachine and Stream#callback method in the streaming plugin (jeremyevans) * Drop support for ruby 1.8.7 (jeremyevans) * Make using an unsupported matcher raise error by default (jeremyevans) * Make having a match/route block return an unsupported value raise error by default (jeremyevans) * Remove deprecated :format, :opt, and :optd symbol matchers in symbol_matchers plugin (jeremyevans) * Remove deprecated support for placeholders in string matchers (jeremyevans) * Remove deprecated constants and plugins (jeremyevans) === Older See doc/CHANGELOG.old jeremyevans-roda-4f30bb3/CONTRIBUTING000066400000000000000000000035351516720775400172500ustar00rootroot00000000000000Issue Guidelines ---------------- 1) Issues should only be created for things that are definitely bugs. If you are not sure that the behavior is a bug, ask about it on GitHub Discussions or the ruby-roda Google Group. GitHub Issues should not be used as a help forum. 2) If you are sure it is a bug, then post a complete description of the issue, the simplest possible self-contained example showing the problem, and the full backtrace of any exception. Pull Request Guidelines ----------------------- 1) Try to include tests for all new features and substantial bug fixes. 2) Try to include documentation for all new features. In most cases this should include RDoc method documentation, but updates to the README is also appropriate in some cases. 3) Follow the style conventions of the surrounding code. In most cases, this is standard ruby style. 4) Do not submit whitespace changes with code changes. Roda is not pedantic about trailing whitespace, so if you have an editor that automatically strips trailing whitespace, you may want to turn that feature off. 5) All code in pull requests is assummed to be MIT licensed. Do not submit a pull request if that isn't the case. Code of Conduct --------------- This code of conduct applies to all of the project's "collaborative space", which is defined as community communications channels, including the Google Group, GitHub project, and source code repository. 1) Participants must ensure that their language and actions are free of personal attacks and remarks disparaging to people or groups. 2) Behaviour which can be reasonably considered harassment will not be tolerated. 3) Discussion should be limited to the project and related technologies. You can report a violation of this code of conduct to the project maintainer, who will take appropriate action. jeremyevans-roda-4f30bb3/MIT-LICENSE000066400000000000000000000022531516720775400170460ustar00rootroot00000000000000Copyright (c) 2014-2026 Jeremy Evans and contributors Copyright (c) 2010-2014 Michel Martens, Damian Janowski and Cyril David Copyright (c) 2008-2009 Christian Neukirchen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. jeremyevans-roda-4f30bb3/README.rdoc000066400000000000000000001162301516720775400172210ustar00rootroot00000000000000rdoc-image:https://roda.jeremyevans.net/images/roda-logo.svg A routing tree web toolkit, designed for building fast and maintainable web applications in Ruby. == Table of contents - {Installation}[#label-Installation] - {Resources}[#label-Resources] - {Goals}[#label-Goals] - {Usage}[#label-Usage] - {Running the application}[#label-Running+the+Application] - {The routing tree}[#label-The+Routing+Tree] - {Matchers}[#label-Matchers] - {Optional segments}[#label-Optional+segments] - {Match/Route Block Return Values}[#label-Match-2FRoute+Block+Return+Values] - {Status codes}[#label-Status+Codes] - {Verb methods}[#label-Verb+Methods] - {Root method}[#label-Root+Method] - {Request and Response}[#label-Request+and+Response] - {Pollution}[#label-Pollution] - {Composition}[#label-Composition] - {Testing}[#label-Testing] - {Settings}[#label-Settings] - {Rendering}[#label-Rendering] - {Security}[#label-Security] - {Code Reloading}[#label-Code+Reloading] - {Plugins}[#label-Plugins] - {No introspection}[#label-No+Introspection] - {Inspiration}[#label-Inspiration] - {Ruby Support Policy}[#label-Ruby+Support+Policy] == Installation $ gem install roda == Resources Website :: http://roda.jeremyevans.net Source :: http://github.com/jeremyevans/roda Bugs :: http://github.com/jeremyevans/roda/issues Discussion Forum (GitHub Discussions) :: https://github.com/jeremyevans/roda/discussions Alternate Discussion Forum (Google Group) :: http://groups.google.com/group/ruby-roda == Goals Roda is designed with the following SUPER goals in mind: * Simplicity * Understandability * Performance * Extensibility * Reliability === Simplicity Roda is designed to be simple, both internally and externally. It uses a routing tree to enable you to write simpler and DRYer code. === Understandability Roda is designed to avoid hidden control flow, so it is easy to follow the control flow you write in your routing tree to see the application's entire handling process for a given request. === Performance Roda has low per-request overhead, and the use of a routing tree and intelligent caching of internal datastructures makes it significantly faster than other popular ruby web frameworks. === Extensibility Roda is built completely out of plugins, which makes it very extensible. You can override any part of Roda and call super to get the default behavior. === Reliability Roda supports and encourages immutability. Roda apps are designed to be frozen in production, which eliminates possible thread safety issues. Additionally, Roda limits the instance variables, constants, and methods that it uses, so that they do not conflict with the ones you use for your application. == Usage Here's a simple application, showing how the routing tree works: # cat config.ru require "roda" class App < Roda route do |r| # GET / request r.root do r.redirect "/hello" end # /hello branch r.on "hello" do # Set variable for all routes in /hello branch @greeting = 'Hello' # GET /hello/world request r.get "world" do "#{@greeting} world!" end # /hello request r.is do # GET /hello request r.get do "#{@greeting}!" end # POST /hello request r.post do puts "Someone said #{@greeting}!" r.redirect end end end end end run App.freeze.app Here's a breakdown of what is going on in the block above: The +route+ block is called whenever a new request comes in. It is yielded an instance of a subclass of Rack::Request with some additional methods for matching routes. By convention, this argument should be named +r+. The primary way routes are matched in Roda is by calling +r.on+, +r.is+, +r.root+, +r.get+, or +r.post+. Each of these "routing methods" takes a "match block". Each routing method takes each of the arguments (called matchers) that are given and tries to match it to the current request. If the method is able to match all of the arguments, it yields to the match block; otherwise, the block is skipped and execution continues. - +r.on+ matches if all of the arguments match. - +r.is+ matches if all of the arguments match and there are no further entries in the path after matching. - +r.get+ matches any +GET+ request when called without arguments. - +r.get+ (when called with any arguments) matches only if the current request is a +GET+ request and there are no further entries in the path after matching. - +r.root+ only matches a +GET+ request where the current path is +/+. If a routing method matches and control is yielded to the match block, whenever the match block returns, Roda will return the Rack response array (containing status, headers, and body) to the caller. If the match block returns a string and the response body hasn't already been written to, the block return value will be interpreted as the body for the response. If none of the routing methods match and the route block returns a string, it will be interpreted as the body for the response. +r.redirect+ immediately returns the response, allowing for code such as r.redirect(path) if some_condition. If +r.redirect+ is called without arguments and the current request method is not +GET+, it redirects to the current path. The +.freeze.app+ at the end is optional. Freezing the app makes modifying app-level settings raise an error, alerting you to possible thread-safety issues in your application. It is recommended to freeze the app in production and during testing. The +.app+ is an optimization, which saves a few method calls for every request. == Running the Application Running a Roda application is similar to running any other rack-based application that uses a +config.ru+ file. You can start a basic server using +rackup+, +puma+, +unicorn+, +passenger+, or any other webserver that can handle +config.ru+ files: $ rackup == The Routing Tree Roda is called a routing tree web toolkit because the way most sites are structured, routing takes the form of a tree (based on the URL structure of the site). In general: - +r.on+ is used to split the tree into different branches. - +r.is+ finalizes the routing path. - +r.get+ and +r.post+ handle specific request methods. So, a simple routing tree might look something like this: r.on "a" do # /a branch r.on "b" do # /a/b branch r.is "c" do # /a/b/c request r.get do end # GET /a/b/c request r.post do end # POST /a/b/c request end r.get "d" do end # GET /a/b/d request r.post "e" do end # POST /a/b/e request end end It's also possible to handle the same requests, but structure the routing tree by first branching on the request method: r.get do # GET r.on "a" do # GET /a branch r.on "b" do # GET /a/b branch r.is "c" do end # GET /a/b/c request r.is "d" do end # GET /a/b/d request end end end r.post do # POST r.on "a" do # POST /a branch r.on "b" do # POST /a/b branch r.is "c" do end # POST /a/b/c request r.is "e" do end # POST /a/b/e request end end end This allows you to easily separate your +GET+ request handling from your +POST+ request handling. If you only have a small number of +POST+ request URLs and a large number of +GET+ request URLs, this may make things easier. However, routing first by the path and last by the request method is likely to lead to simpler and DRYer code. This is because you can act on the request at any point during the routing. For example, if all requests in the +/a+ branch need access permission +A+ and all requests in the +/a/b+ branch need access permission +B+, you can easily handle this in the routing tree: r.on "a" do # /a branch check_perm(:A) r.on "b" do # /a/b branch check_perm(:B) r.is "c" do # /a/b/c request r.get do end # GET /a/b/c request r.post do end # POST /a/b/c request end r.get "d" do end # GET /a/b/d request r.post "e" do end # POST /a/b/e request end end Being able to operate on the request at any point during the routing is one of the major advantages of Roda. == Matchers Other than +r.root+, the routing methods all take arguments called matchers. If all of the matchers match, the routing method yields to the match block. Here's an example showcasing how different matchers work: class App < Roda route do |r| # GET / r.root do "Home" end # GET /about r.get "about" do "About" end # GET /post/2011/02/16/hello r.get "post", Integer, Integer, Integer, String do |year, month, day, slug| "#{year}-#{month}-#{day} #{slug}" #=> "2011-02-16 hello" end # GET /username/foobar branch r.on "username", String, method: :get do |username| user = User.find_by_username(username) # GET /username/foobar/posts r.is "posts" do # You can access user here, because the blocks are closures. "Total Posts: #{user.posts.size}" #=> "Total Posts: 6" end # GET /username/foobar/following r.is "following" do user.following.size.to_s #=> "1301" end end # /search?q=barbaz r.get "search" do "Searched for #{r.params['q']}" #=> "Searched for barbaz" end r.is "login" do # GET /login r.get do "Login" end # POST /login?user=foo&password=baz r.post do "#{r.params['user']}:#{r.params['password']}" #=> "foo:baz" end end end end Here's a description of the matchers. Note that "segment", as used here, means one part of the path preceded by a +/+. So, a path such as +/foo/bar//baz+ has four segments: +/foo+, +/bar+, +/+, and +/baz+. The +/+ here is considered the empty segment. === String If a string does not contain a slash, it matches a single segment containing the text of the string, preceded by a slash. "" # matches "/" "foo" # matches "/foo" "foo" # does not match "/food" If a string contains any slashes, it matches one additional segment for each slash: "foo/bar" # matches "/foo/bar" "foo/bar" # does not match "/foo/bard" === Regexp Regexps match one or more segments by looking for the pattern, preceded by a slash, and followed by a slash or the end of the path: /foo\w+/ # matches "/foobar" /foo\w+/ # does not match "/foo/bar" /foo/i # matches "/foo", "/Foo/" /foo/i # does not match "/food" If any patterns are captured by the Regexp, they are yielded: /foo\w+/ # matches "/foobar", yields nothing /foo(\w+)/ # matches "/foobar", yields "bar" === Class There are two classes that are supported as matchers, String and Integer. String :: matches any non-empty segment, yielding the segment except for the preceding slash Integer :: matches any segment of 0-9, returns matched values as integers Using String and Integer is the recommended way to handle arbitrary segments String # matches "/foo", yields "foo" String # matches "/1", yields "1" String # does not match "/" Integer # does not match "/foo" Integer # matches "/1", yields 1 Integer # does not match "/" === Symbol Symbols match any nonempty segment, yielding the segment except for the preceding slash: :id # matches "/foo" yields "foo" :id # does not match "/" Symbol matchers operate the same as the class String matcher, and is the historical way to do arbitrary segment matching. It is recommended to use the class String matcher in new code as it is a bit more intuitive. === Proc Procs match unless they return false or nil: proc{true} # matches anything proc{false} # does not match anything Procs don't capture anything by default, but they can do so if you add the captured text to +r.captures+. === Arrays Arrays match when any of their elements match. If multiple matchers are given to +r.on+, they all must match (an AND condition). If an array of matchers is given, only one needs to match (an OR condition). Evaluation stops at the first matcher that matches. Additionally, if the matched object is a String, the string is yielded. This makes it easy to handle multiple strings without a Regexp: ['page1', 'page2'] # matches "/page1", "/page2" [] # does not match anything === Sets Sets match if the next segment in the request path matches one of the elements in the set (non-String elements in the set are ignored). The matched element is yielded. Set['page1', 'page2'] # matches "/page1", "/page2" Set[] # does not match anything In general, a Set matcher has the same behavior as an Array matcher containing only the set's string elements, but will perform better for when there are a large number of elements in the set. === Hash Hashes allow easily calling specialized match methods on the request. The default registered matchers included with Roda are documented below. Some plugins add additional hash matchers, and the hash_matcher plugin allows for easily defining your own: class App < Roda plugin :hash_matcher hash_matcher(:foo) do |v| # ... end route do |r| r.on foo: 'bar' do # ... end end end ==== :all The +:all+ matcher matches if all of the entries in the given array match, so r.on all: [String, String] do # ... end is the same as: r.on String, String do # ... end The reason it also exists as a separate hash matcher is so you can use it inside an array matcher, so: r.on ['foo', {all: ['foos', Integer]}] do end would match +/foo+ and +/foos/10+, but not +/foos+. ==== :method The +:method+ matcher matches the method of the request. You can provide an array to specify multiple request methods and match on any of them: {method: :post} # matches POST {method: ['post', 'patch']} # matches POST and PATCH === true If +true+ is given directly as a matcher, it always matches. === false, nil If +false+ or +nil+ is given directly as a matcher, it doesn't match anything. === Everything else Everything else raises an error, unless support is specifically added for it (some plugins add support for additional matcher types). == Optional segments There are multiple ways you can handle optional segments in Roda. For example, let's say you want to accept both +/items/123+ and +/items/123/456+, with 123 being the item's id, and 456 being some optional data. The simplest way to handle this is by treating this as two separate routes with a shared branch: r.on "items", Integer do |item_id| # Shared code for branch here # /items/123/456 r.is Integer do |optional_data| end # /items/123 r.is do end end This works well for many cases, but there are also cases where you really want to treat it as one route with an optional segment. One simple way to do that is to use a parameter instead of an optional segment (e.g. /items/123?opt=456). r.is "items", Integer do |item_id| optional_data = r.params['opt'].to_s end However, if you really do want to use a optional segment, there are a couple different ways to use matchers to do so. One is using an array matcher where the last element is true: r.is "items", Integer, [String, true] do |item_id, optional_data| end Note that this technically yields only one argument instead of two arguments if the optional segment isn't provided. An alternative way to implement this is via a regexp: r.is "items", /(\d+)(?:\/(\d+))?/ do |item_id, optional_data| end == Match/Route Block Return Values If the response body has already been written to by calling +response.write+ directly, then any return value of a match block or route block is ignored. If the response body has not already been written to, then the match block or route block return value is inspected: String :: used as the response body nil, false :: ignored everything else :: raises an error Plugins can add support for additional match block and route block return values. One example of this is the json plugin, which allows returning arrays and hashes in match and route blocks and converts those directly to JSON and uses the JSON as the response body. == Status Codes When it comes time to finalize a response, if a status code has not been set manually and anything has been written to the response, the response will use a 200 status code. Otherwise, it will use a 404 status code. This enables the principle of least surprise to work: if you don't handle an action, a 404 response is assumed. You can always set the status code manually, via the +status+ attribute for the response. route do |r| r.get "hello" do response.status = 200 end end When redirecting, the response will use a 302 status code by default. You can change this by passing a second argument to +r.redirect+: route do |r| r.get "hello" do r.redirect "/other", 301 # use 301 Moved Permanently end end == Verb Methods As displayed above, Roda has +r.get+ and +r.post+ methods for matching based on the HTTP request method. If you want to match on other HTTP request methods, use the all_verbs plugin. When called without any arguments, these match as long as the request has the appropriate method, so: r.get do end matches any +GET+ request, and r.post do end matches any +POST+ request If any arguments are given to the method, these match only if the request method matches, all arguments match, and the path has been fully matched by the arguments, so: r.post "" do end matches only +POST+ requests where the current path is +/+. r.get "a/b" do end matches only +GET+ requests where the current path is +/a/b+. The reason for this difference in behavior is that if you are not providing any arguments, you probably don't want to also test for an exact match with the current path. If that is something you do want, you can provide +true+ as an argument: r.on "foo" do r.get true do # Matches GET /foo, not GET /foo/.* end end If you want to match the request method and do only a partial match on the request path, you need to use +r.on+ with the :method hash matcher: r.on "foo", method: :get do # Matches GET /foo(/.*)? end == Root Method As displayed above, you can also use +r.root+ as a match method. This method matches +GET+ requests where the current path is +/+. +r.root+ is similar to r.get "", except that it does not consume the +/+ from the path. Unlike the other matching methods, +r.root+ takes no arguments. Note that +r.root+ does not match if the path is empty; you should use r.get true for that. If you want to match either the empty path or +/+, you can use r.get ["", true], or use the slash_path_empty plugin. Note that +r.root+ only matches +GET+ requests. So, to handle POST / requests, use r.post ''. == Request and Response While the request object is yielded to the +route+ block, it is also available via the +request+ method. Likewise, the response object is available via the +response+ method. The request object is an instance of a subclass of Rack::Request, with some additional methods. If you want to extend the request and response objects with additional modules, you can use the module_include plugin. == Pollution Roda tries very hard to avoid polluting the scope of the +route+ block. This should make it unlikely that Roda will cause namespace issues with your application code. Some of the things Roda does: - The only instance variables defined by default in the scope of the +route+ block are @_request and @_response. All instance variables in the scope of the +route+ block used by plugins that ship with Roda are prefixed with an underscore. - The main methods defined, beyond the default methods for +Object+, are +env+, +opts+, +request+, +response+, and +session+. +call+ and +_call+ are also defined, but are deprecated. All other methods defined are prefixed with +_roda_+ - Constants inside the Roda namespace are all prefixed with +Roda+ (e.g., Roda::RodaRequest). == Composition You can mount any Rack app (including another Roda app), with its own middlewares, inside a Roda app, using +r.run+: class API < Roda route do |r| r.is do # ... end end end class App < Roda route do |r| r.on "api" do r.run API end end end run App.app This will take any path starting with +/api+ and send it to +API+. In this example, +API+ is a Roda app, but it could easily be a Sinatra, Rails, or other Rack app. When you use +r.run+, Roda calls the given Rack app (+API+ in this case); whatever the Rack app returns will be returned as the response for the current application. If you have a lot of rack applications that you want to dispatch to, and which one to dispatch to is based on the request path prefix, look into the +multi_run+ plugin. === hash_branches plugin If you are just looking to split up the main route block up by branches, you should use the +hash_branches+ plugin, which keeps the current scope of the +route+ block: class App < Roda plugin :hash_branches hash_branch "api" do |r| r.is do # ... end end route do |r| r.hash_branches end end run App.app This allows you to set instance variables in the main +route+ block and still have access to them inside the +api+ +route+ block. == Testing It is very easy to test Roda with {Rack::Test}[https://github.com/rack-test/rack-test] or {Capybara}[https://github.com/teamcapybara/capybara]. Roda's own tests use {minitest/spec}[https://github.com/seattlerb/minitest]. The default Rake task will run the specs for Roda. == Settings Each Roda app can store settings in the +opts+ hash. The settings are inherited by subclasses. Roda.opts[:layout] = "guest" class Users < Roda; end class Admin < Roda opts[:layout] = "admin" end Users.opts[:layout] # => 'guest' Admin.opts[:layout] # => 'admin' Feel free to store whatever you find convenient. Note that when subclassing, Roda only does a shallow clone of the settings. If you store nested structures and plan to mutate them in subclasses, it is your responsibility to dup the nested structures inside +Roda.inherited+ (making sure to call +super+). This should be done so that modifications to the parent class made after subclassing do _not_ affect the subclass, and vice-versa. The plugins that ship with Roda freeze their settings and only allow modification to their settings by reloading the plugin, and external plugins are encouraged to follow this approach. The following options are respected by the default library or multiple plugins: :add_script_name :: Prepend the SCRIPT_NAME for the request to paths. This is useful if you mount the app as a path under another app. :check_arity :: Whether arity for blocks passed to Roda should be checked to determine if they can be used directly to define methods or need to be wrapped. By default, for backwards compatibility, this is true, so Roda will check blocks and handle cases where the arity of the block does not match the expected arity. This can be set to +:warn+ to issue warnings whenever Roda detects an arity mismatch. If set to +false+, Roda does not check the arity of blocks, which can result in failures at runtime if the arity of the block does not match what Roda expects. Note that Roda does not check the arity for lambda blocks, as those are strict by default. :check_dynamic_arity :: Similar to :check_arity, but used for checking blocks where the number of arguments Roda will call the blocks with is not possible to determine when defining the method. By default, Roda checks arity for such methods, but doing so actually slows the method down even if the number of arguments matches the expected number of arguments. :freeze_middleware :: Whether to freeze all middleware when building the rack app. :json_parser :: A callable for parsing JSON (+JSON.parse+ in general used by default). :json_serializer :: A callable for serializing JSON (+to_json+ in general used by default). :root :: Set the root path for the app. This defaults to the current working directory of the process. :sessions_convert_symbols :: This should be set to +true+ if the sessions in use do not support roundtripping of symbols (for example, when sessions are serialized via JSON). There may be other options supported by individual plugins, if so it will be mentioned in the documentation for the plugin. == Rendering Roda ships with a +render+ plugin that provides helpers for rendering templates. It uses {Tilt}[https://github.com/rtomayko/tilt], a gem that interfaces with many template engines. The +erb+ engine is used by default. Note that in order to use this plugin you need to have Tilt installed, along with the templating engines you want to use. This plugin adds the +render+ and +view+ methods, for rendering templates. By default, +view+ will render the template inside the default layout template; +render+ will just render the template. class App < Roda plugin :render route do |r| @var = '1' r.get "render" do # Renders the views/home.erb template, which will have access to # the instance variable @var, as well as local variable content. render("home", locals: {content: "hello, world"}) end r.get "view" do @var2 = '1' # Renders the views/home.erb template, which will have access to the # instance variables @var and @var2, and takes the output of that and # renders it inside views/layout.erb (which should yield where the # content should be inserted). view("home") end end end You can override the default rendering options by passing a hash to the plugin: class App < Roda plugin :render, escape: true, # Automatically escape output in erb templates using Erubi's escaping support views: 'admin_views', # Default views directory layout_opts: {template: 'admin_layout', engine: 'html.erb'}, # Default layout options template_opts: {default_encoding: 'UTF-8'} # Default template options end == Security Web application security is a very large topic, but here are some things you can do with Roda to prevent some common web application vulnerabilities. === Session Security By default, Roda doesn't turn on sessions, and if you don't need sessions, you can skip this section. If you do need sessions, Roda offers two recommended ways to implement cookie-based sessions. If you do not need any session support in middleware, and only need session support in the Roda application, then use the sessions plugin: require 'roda' class App < Roda plugin :sessions, secret: ENV['SESSION_SECRET'] end The +:secret+ option should be a randomly generated string of at least 64 bytes. If you have middleware that need access to sessions, then use the +RodaSessionMiddleware+ that ships with Roda: require 'roda' require 'roda/session_middleware' class App < Roda use RodaSessionMiddleware, secret: ENV['SESSION_SECRET'] end If you need non-cookie based sessions (such as sessions stored in a database), you should use an appropriate external middleware. It is possible to use other session cookie middleware such as Rack::Session::Cookie, but other middleware may not have the same security features that Roda's session support does. For example, the session cookies used by the Rack::Session::Cookie middleware provided by Rack before Rack 3 are not encrypted, just signed to prevent tampering. For any cookie-based sessions, make sure that the necessary secrets (+:secret+ option) are not disclosed to an attacker. Knowledge of the secret(s) can allow an attacker to inject arbitrary session values. In the case of Rack::Session::Cookie, that can also lead remote code execution. === Cross Site Request Forgery (CSRF) CSRF can be prevented by using the +route_csrf+ plugin that ships with Roda. The +route_csrf+ plugin uses modern security practices to create CSRF tokens, requires request-specific tokens by default, and offers control to the user over where in the routing tree that CSRF tokens are checked. For example, if you are using the +public+ plugin to serve static files and the +assets+ plugin to serve assets, you wouldn't need to check for CSRF tokens for either of those, so you could put the CSRF check after those in the routing tree, but before handling other requests: route do |r| r.public r.assets check_csrf! # Must call this to check for valid CSRF tokens # ... end === Cross Site Scripting (XSS) The easiest way to prevent XSS with Roda is to use a template library that automatically escapes output by default. The +:escape+ option to the +render+ plugin sets the ERB template processor to escape by default, so that in your templates: <%= '<>' %> # outputs <> <%== '<>' %> # outputs <> When using the +:escape+ option, you will need to ensure that your layouts are not escaping the output of the content template: <%== yield %> # not <%= yield %> This support requires {Erubi}[https://github.com/jeremyevans/erubi]. === Unexpected Parameter Types Rack converts submitted parameters into a hash of strings, arrays, and nested hashes. Since the user controls the submission of parameters, you should treat any submission of parameters with caution, and should be explicitly checking and/or converting types before using any submitted parameters. One way to do this is explicitly after accessing them: # Convert foo_id parameter to an integer request.params['foo_id'].to_i However, it is easy to forget to convert the type, and if the user submits +foo_id+ as a hash or array, a NoMethodError will be raised. Worse is if you do: some_method(request.params['bar']) Where +some_method+ supports both a string argument and a hash argument, and you expect the parameter will be submitted as a string, and +some_method+'s handling of a hash argument performs an unauthorized action. Roda ships with a +typecast_params+ plugin that can easily handle the typecasting of submitted parameters, and it is recommended that all Roda applications that deal with parameters use it or another tool to explicitly convert submitted parameters to the expected types. === Content Security Policy The Content-Security-Policy HTTP header can be used to instruct the browser on what types of content to allow and where content can be loaded from. Roda ships with a +content_security_policy+ plugin that allows for the easy configuration of the content security policy. Here's an example of a fairly restrictive content security policy configuration: class App < Roda plugin :content_security_policy do |csp| csp.default_src :none # deny everything by default csp.style_src :self csp.script_src :self csp.connect_src :self csp.img_src :self csp.font_src :self csp.form_action :self csp.base_uri :none csp.frame_ancestors :none csp.block_all_mixed_content csp.report_uri 'CSP_REPORT_URI' end end === Other Security Related HTTP Headers You may want to look into setting the following HTTP headers, which can be done at the web server level, but can also be done at the application level using using the +default_headers+ plugin: Strict-Transport-Security :: Enforces SSL/TLS Connections to the application. X-Content-Type-Options :: Forces some browsers to respect a declared Content-Type header. X-Frame-Options :: Provides click-jacking protection by not allowing usage inside a frame. Only include this if you want to support and protect old browsers that do not support Content-Security-Policy. Example: class App < Roda plugin :default_headers, 'Content-Type'=>'text/html', 'Strict-Transport-Security'=>'max-age=63072000; includeSubDomains', 'X-Content-Type-Options'=>'nosniff', 'X-Frame-Options'=>'deny' end === Rendering Templates Derived From User Input Roda's rendering plugin by default checks that rendered templates are inside the views directory. This is because rendering templates outside the views directory is not commonly needed, and it prevents a common attack (which is especially severe if there is any location on the file system that users can write files to). You can specify which directories are allowed using the +:allowed_paths+ render plugin option. If you really want to turn path checking off, you can do so via the check_paths: false render plugin option. == Code Reloading Roda does not ship with integrated support for code reloading, but there are rack-based reloaders that will work with Roda apps. {Zeitwerk}[https://github.com/fxn/zeitwerk] (which Rails now uses for reloading) can be used with Roda. It requires minimal setup and handles most cases. It overrides +require+ when activated. If it can meet the needs of your application, it's probably the best approach. {rack-unreloader}[https://github.com/jeremyevans/rack-unreloader] uses a fast approach to reloading while still being fairly safe, as it only reloads files that have been modified, and unloads constants defined in the files before reloading them. It can handle advanced cases that Zeitwerk does not support, such as classes defined in multiple files (common when using separate route files for different routing branches in the same application). However, rack-unreloader does not modify core classes and using it requires modifying your application code to use rack-unreloader specific APIs, which may not be simple. {AutoReloader}[https://github.com/rosenfeld/auto_reloader] provides transparent reloading for all files reached from one of the +reloadable_paths+ option entries, by detecting new top-level constants and removing them when any of the reloadable loaded files changes. It overrides +require+ and +require_relative+ when activated (usually in the development environment). No configurations other than +reloadable_paths+ are required. {rerun}[https://github.com/alexch/rerun] uses a fork/exec approach for loading new versions of your app. It work without any changes to application code, but may be slower as they have to reload the entire application on every change. However, for small apps that load quickly, it may be a good approach. There is no one reloading solution that is the best for all applications and development approaches. Consider your needs and the tradeoffs of each of the reloading approaches, and pick the one you think will work best. If you are unsure where to start, it may be best to start with Zeitwerk, and only consider other options if it does not work well for you. == Plugins By design, Roda has a very small core, providing only the essentials. All nonessential features are added via plugins. Roda's plugins can override any Roda method and call +super+ to get the default behavior, which makes Roda very extensible. {Roda ships with a large number of plugins}[http://roda.jeremyevans.net/documentation.html#included-plugins], and {some other libraries ship with support for Roda}[http://roda.jeremyevans.net/documentation.html#external]. === How to create plugins Authoring your own plugins is pretty straightforward. Plugins are just modules, which may contain any of the following modules: InstanceMethods :: module included in the Roda class ClassMethods :: module that extends the Roda class RequestMethods :: module included in the class of the request RequestClassMethods :: module extending the class of the request ResponseMethods :: module included in the class of the response ResponseClassMethods :: module extending the class of the response If the plugin responds to +load_dependencies+, it will be called first, and should be used if the plugin depends on another plugin. If the plugin responds to +configure+, it will be called last, and should be used to configure the plugin. Both +load_dependencies+ and +configure+ are called with the additional arguments and block that was given to the plugin call. So, a simple plugin to add an instance method would be: module MarkdownHelper module InstanceMethods def markdown(str) BlueCloth.new(str).to_html end end end Roda.plugin MarkdownHelper === Registering plugins If you want to ship a Roda plugin in a gem, but still have Roda load it automatically via Roda.plugin :plugin_name, you should place it where it can be required via +roda/plugins/plugin_name+ and then have the file register it as a plugin via Roda::RodaPlugins.register_plugin. It's recommended, but not required, that you store your plugin module in the Roda::RodaPlugins namespace: class Roda module RodaPlugins module Markdown module InstanceMethods def markdown(str) BlueCloth.new(str).to_html end end end register_plugin :markdown, Markdown end end To avoid namespace pollution, you should avoid creating your module directly in the +Roda+ namespace. Additionally, any instance variables created inside +InstanceMethods+ should be prefixed with an underscore (e.g., @_variable) to avoid polluting the scope. Finally, do not add any constants inside the InstanceMethods module, add constants to the plugin module itself (+Markdown+ in the above example). If you are planning on shipping your plugin in an external gem, it is recommended that you follow {standard gem naming conventions for extensions}[http://guides.rubygems.org/name-your-gem/]. So if your plugin module is named +FooBar+, your gem name should be roda-foo_bar. == No Introspection Because a routing tree does not store the routes in a data structure, but directly executes the routing tree block, you cannot introspect the routes when using a routing tree. If you would like to introspect your routes when using Roda, there is an external plugin named {roda-route_list}[https://github.com/jeremyevans/roda-route_list], which allows you to add appropriate comments to your routing files, and has a parser that will parse those comments into routing metadata that you can then introspect. == Inspiration Roda was inspired by {Sinatra}[http://www.sinatrarb.com] and {Cuba}[http://cuba.is]. It started out as a fork of Cuba, from which it borrows the idea of using a routing tree (which Cuba in turn took from {Rum}[https://github.com/chneukirchen/rum]). From Sinatra, it takes the ideas that route blocks should return the request bodies and that routes should be canonical. Roda's plugin system is based on the plugin system used by {Sequel}[http://sequel.jeremyevans.net]. == Ruby Support Policy Roda fully supports the currently supported versions of Ruby (MRI) and JRuby. It may support unsupported versions of Ruby or JRuby, but such support may be dropped in any minor version if keeping it becomes a support issue. The minimum Ruby version required to run the current version of Roda is 1.9.2, and the minimum JRuby version is 9.0.0.0. == License MIT == Maintainer Jeremy Evans jeremyevans-roda-4f30bb3/Rakefile000066400000000000000000000061641516720775400170640ustar00rootroot00000000000000require "rake" require "rake/clean" NAME = 'roda' VERS = lambda do require_relative 'lib/roda/version' Roda::RodaVersion end CLEAN.include ["#{NAME}-*.gem", "rdoc", "coverage", "www/public/*.html", "www/public/rdoc", "spec/assets/app.*.css", "spec/assets/app.*.js", "spec/assets/app.*.css.gz", "spec/assets/app.*.js.gz", "spec/iv.erb"] # Gem Packaging and Release desc "Packages #{NAME}" task :package=>[:clean] do |p| sh %{gem build #{NAME}.gemspec} end ### RDoc desc "Generate rdoc" task :website_rdoc do rdoc_dir = "www/public/rdoc" rdoc_opts = ["--line-numbers", "--inline-source", '--title', 'Roda: Routing tree web toolkit'] begin gem 'hanna' rdoc_opts.concat(['-f', 'hanna']) rescue Gem::LoadError end rdoc_opts.concat(['--main', 'README.rdoc', "-o", rdoc_dir]) rdoc_opts.concat(%w"README.rdoc CHANGELOG doc/CHANGELOG.old MIT-LICENSE" + Dir["lib/**/*.rb"] + Dir["doc/**/*.rdoc"] + Dir['doc/release_notes/*.txt']) FileUtils.rm_rf(rdoc_dir) require "rdoc" RDoc::RDoc.new.document(rdoc_opts) end ### Website desc "Make local version of website" task :website_base do sh %{#{FileUtils::RUBY} -I lib www/make_www.rb} end desc "Make local version of website, with rdoc" task :website => [:website_base, :website_rdoc] desc "Serve local version of website via rackup" task :serve => :website do sh %{#{FileUtils::RUBY} -C www -S rackup} end ### Specs spec = proc do |env| env.each{|k,v| ENV[k] = v} sh "#{FileUtils::RUBY} #{"-w" if RUBY_VERSION >= '3'} #{'-W:strict_unused_block' if RUBY_VERSION >= '3.4'} spec/all.rb" env.each{|k,v| ENV.delete(k)} if File.directory?('.sass-cache') require 'fileutils' FileUtils.rm_r('.sass-cache') end end desc "Run specs" task "spec" do spec.call({}) end task :default=>:spec desc "Run specs with method visibility checking" task "spec_vis" do spec.call('CHECK_METHOD_VISIBILITY'=>'1') end desc "Run specs with coverage" task "spec_cov" do spec.call('COVERAGE'=>'< 4') spec.call('COVERAGE'=>'< 3.1') spec.call('COVERAGE'=>'< 3') spec.call('COVERAGE'=>'< 1.6', 'RODA_RENDER_COMPILED_METHOD_SUPPORT'=>'no') end desc "Run specs with Rack::Lint" task "spec_lint" do spec.call('LINT'=>'1') end desc "Run specs in CI mode" task "spec_ci" do # Use LINT on about half of the tested Ruby versions, # rotating each day. spec.call(RUBY_VERSION[2].to_i.odd? ^ Time.now.wday.odd? ? {} : {'LINT'=>'1'}) end ### Other desc "Print #{NAME} version" task :version do puts VERS.call end desc "Start an IRB shell using the extension" task :irb do require 'rbconfig' ruby = ENV['RUBY'] || File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['ruby_install_name']) irb = ENV['IRB'] || File.join(RbConfig::CONFIG['bindir'], File.basename(ruby).sub('ruby', 'irb')) sh %{#{irb} -I lib -r #{NAME}} end # Other desc "Check documentation for plugin files" task :check_plugin_doc do text = File.binread('www/pages/documentation.erb') skip = %w'delay_build' Dir['lib/roda/plugins/*.rb'].map{|f| File.basename(f).sub('.rb', '') if File.size(f)}.sort.each do |f| puts f if !f.start_with?('_') && !skip.include?(f) && !text.include?(">#{f}<") end end jeremyevans-roda-4f30bb3/doc/000077500000000000000000000000001516720775400161555ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/doc/CHANGELOG.old000066400000000000000000000626211516720775400201530ustar00rootroot00000000000000= 2.29.0 (2017-08-16) * Deprecate accessing multi_route namespace when there are no routes (jeremyevans) * Deprecate additional internal constants (jeremyevans) * Respect :root app option when using :layout_opts=>:views render plugin option (jeremyevans) * Deprecate rendering templates outside of render plugin :allowed_paths option by default (jeremyevans) * Deprecate :cache=>nil/false render plugin option overriding :cache render/view method option (jeremyevans) * Deprecate using :header matcher in header_matchers plugin without :header_matcher_prefix app option (jeremyevans) * Deprecate using content_for multiple times with the same key in the content_for plugin unless :append plugin option is used (jeremyevans) * Deprecate use of :host matcher with regexp value in header_matchers plugin without :host_matcher_captures app option (jeremyevans) * Deprecate view_options plugin locals handling, move to the new branch_locals plugin (jeremyevans) * Deprecate render plugin locals handling, move to the new render_locals plugin (jeremyevans) * Deprecate the :ext render method and plugin option (jeremyevans) * Deprecate the view_subdirs plugin alias for the view_options plugin (jeremyevans) * Deprecate Stream#callback in the streaming plugin (jeremyevans) * Deprecate the automatic support for EventMachine in the streaming plugin (jeremyevans) * Deprecate static_path_info plugin, which has been a no-op in Roda 2 (jeremyevans) * Deprecate render plugin :escape option loading Erubis escaping support (jeremyevans) * Deprecate the per_thread_caching plugin (jeremyevans) * Deprecate the websockets plugin (jeremyevans) * Deprecate treating unsupported matchers as always matching (jeremyevans) * Deprecate ignoring unsupported match block return values (jeremyevans) * Deprecate the :format, :opt, and :optd default symbol matchers in the symbol_matchers plugin (jeremyevans) * Deprecate use of placeholders in string matchers by default, add placeholder_string_matchers plugin for it (jeremyevans) = 2.28.0 (2017-07-14) * Deprecate unneeded internal constants (jeremyevans) * Optimize for ruby 2.3+ using frozen string literals instead of constants (jeremyevans) * Move 303 default redirect status from sinatra_helpers to status_303 plugin, so it can be loaded separately (plujon) (#122) = 2.27.0 (2017-06-14) * Add class_matchers plugin for matching other classes (in addition to String/Integer), with user specified regexps and type conversion (jeremyevans) * Support String class matcher for non-empty segments, same behavior as symbol matchers but more intuitive and DRY (jeremyevans) * Support Integer class matcher for \d+ segments, yielding matched values as integers (jeremyevans) = 2.26.0 (2017-05-16) * Support :skip_middleware option to csrf plugin to add only the methods and not add the middleware (luciusgone) (#118) * Handle multiple types with matching suffixes in the type_routing plugin (e.g. tar.gz and gz) (tomdalling) (#117) = 2.25.0 (2017-04-18) * Add error_mail plugin, similar to error_email but using mail instead of net/smtp directly (jeremyevans) = 2.24.0 (2017-03-15) * Have h plugin use cgi/escape if available for faster escaping (jeremyevans) * Add disallow_file_uploads plugin for raising an exception if a multipart file upload is attempted (jeremyevans) * Add strip_path_prefix plugin for stripping prefixes off of internal absolute paths, making them relative paths (jeremyevans) * Add Roda.expand_path method to DRY up path expansion (jeremyevans) * Support :freeze_middleware option, which freezes all middleware instances when building the rack app (jeremyevans) * Allow middleware plugin to accept a block that will be used to configure the application when used as middleware (jeremyevans) * Support an options hash when loading the cookies plugin, that will be used as the defaults for setting and deleting cookies (mwpastore, jeremyevans) (#112) * Make the static_routing plugin work with the hooks plugin if the hooks plugin is loaded first (jeremyevans) (#110) * Do not modify the render plugin's cache if loading the plugin multiple times (jeremyevans) = 2.23.0 (2017-02-24) * Add :inherit_cache render plugin option, to create a copy of the cache for subclasses, instead of using an empty cache (jeremyevans) * In development mode, default to :explicit_cache=>true, :cache=>true instead of :cache=>false (jeremyevans) * Add :explicit_cache render plugin option, to only cache templates if the :cache option is given to render/view (jeremyevans) * Add error_email_content method to error_email plugin (jeremyevans) * Make error_email method in error_email plugin support non-exception arguments (jeremyevans) * Make Roda.freeze in the static_routing plugin return self (jeremyevans) = 2.22.0 (2017-01-20) * Add support for :verbatim_string_matcher option, for making all string matchers match verbatim (jeremyevans) * Add support for :unsupported_matcher => :raise option, for raising on unsupported matcher values (jeremyevans) * Add support for :unsupported_block_result => :raise option, for raising on unsupported route/match block return values (jeremyevans) = 2.21.0 (2016-12-16) * Add handle_stream_error method to streaming plugin, for handling errors when using stream(:loop=>true) (jeremyevans) = 2.20.0 (2016-11-13) * Support :escape=>:erubi option in the render plugin to use the erubi template engine (jeremyevans) = 2.19.0 (2016-10-14) * Don't add Content-Type/Content-Length headers for 1xx, 204, 205, 304 statuses (celsworth, jeremyevans) (#101, #102) * Optimize indifferent_params plugin when using Rack 2 (jeremyevans) * Fix assets_paths method in assets plugin when subresource integrity is used (jeremyevans, celsworth) * Make assets plugin depend on h plugin, instead of using Rack::Utils.escape_html (jeremyevans) * Make h plugin not escape / (celsworth, jeremyevans) (#100) = 2.18.0 (2016-09-13) * Add assets_preloading plugin, for creating link tags or Link header for preloading assets (celsworth, jeremyevans) (#98) * Add assets_paths method to assets plugin, for just the paths to the assets, instead of the full tags (celsworth) (#96) * Make type_routing plugin work correctly with public plugin (celsworth, jeremyevans) (#95) * Add static_routing plugin for 3-4x increase in performance for large numbers of static routes (jeremyevans) * Make head plugin work with not_allowed plugin if loaded after (jeremyevans) (#92) = 2.17.0 (2016-08-13) * Add :postprocessor option to assets plugin, for postprocessing assets (e.g. autoprefixing CSS) (celsworth) (#86) * Fix path passed to rack apps when using r.run and the type_routing plugin (jeremyevans) (#82) * Support :classes option to error_handler plugin for overriding which exception classes to rescue (jeremyevans) * Support :layout_opts=>:merge_locals option in render plugin for merging view template locals into layout template locals (jeremyevans) (#80) * Support :sri option to assets plugin to enable subresource integrity (jeremyevans) * Add run_append_slash plugin, so r.run uses "/" instead of "" for app's PATH_INFO (kenaniah) (#77) = 2.16.0 (2016-07-13) * Add type_routing plugin, for routing based on path extensions and Accept headers (Papierkorb, jeremyevans) (#75) * Add unescape_path plugin, for decoding URL-encoded PATH_INFO before routing (jeremyevans) (#74) * Add request_headers plugin, for simpler access to request headers (celsworth) (#72) = 2.15.0 (2016-06-13) * Add public plugin for r.public method for serving all files in the public directory (jeremyevans) * Make send_file in sinatra_helpers plugin work with Rack 2 (jeremyevans) * Make :header matcher prefixes the env key with HTTP_ if application :header_matcher_prefix option is set (timothypage, jeremyevans) (#69) * Add content_for plugin :append option to support appending to the existing content (evanleck, jeremyevans) (#66) = 2.14.0 (2016-05-13) * Add symbol_status plugin for using symbols as status codes (Papierkorb) (#65) * Make middleware plugin also run the application's middleware (jeremyevans) = 2.13.0 (2016-04-14) * Add :check_paths and :allowed_paths to render plugin options to avoid security issues with template rendering (jeremyevans) = 2.12.0 (2016-03-15) * Allow error handler access to the request's remaining_path (jeremyevans) * Add optimized_string_matchers plugin, containing optimized matchers for single string arguments (jeremyevans) * Optimize string matching code for strings without placeholders for up to a 60% performance increase (jeremyevans) * Optimize symbol matching code for up to a 60% performance increase (jeremyevans) = 2.11.0 (2016-02-16) * Support :scope option in render plugin, for specifying object in which to evaluate the template (jeremyevans) * Make minjs compressor support in assets plugin support latest version of Minjs (jeremyevans) * Add params_capturing plugin, for storing matcher captures in the request params (jeremyevans) = 2.10.0 (2016-01-15) * Do not override existing Content-Type header in json plugin (jeremyevans) * Add :content_type option to json plugin to override Content-Type header used (Kyrremann) (#58) * Add support for running with --enable-frozen-string-literal on ruby 2.3 (jeremyevans) * Add Streaming::Stream#write method so that IO.copy_stream will work (janko-m) (#56) = 2.9.0 (2015-12-15) * Support passing the content as a string argument instead of a block in the content_for plugin (badosu) (#52) = 2.8.0 (2015-11-16) * Add multi_view plugin for easily setting up routing for rendering multiple views (jeremyevans) * Make content_for plugin work with haml and potentially other non-erb template engines (plukevdh) (#50) = 2.7.0 (2015-10-13) * Add run_handler plugin for modifying rack response arrays when using r.run, and continuing routing for 404 responses (jeremyevans) * Add response_request plugin allowing response object access to request object (jeremyevans) * Add default_status plugin for overriding the default response status (celsworth) (#47) * Make RodaCache synchronize access on MRI (jeremyevans) * Support opts[:host_matcher_captures] = true to make :host=>/regexp/ matcher yield captures in the header_matchers plugin (jeremyevans) * Allow Roda.rewrite_path to take a block in the path_rewriter plugin (Freaky) (#45) = 2.6.0 (2015-09-14) * Add :params and :params! matchers to param_matchers plugin (jeremyevans) * Merge options when loading csrf plugin multiple times (jeremyevans) * Allow request.halt to work in before hooks in the hooks plugin (celsworth) (#38) = 2.5.1 (2015-08-13) * Allow multi_route and middleware plugins to work together (janko-m) (#36) = 2.5.0 (2015-07-14) * Make :by_name option to path plugin default to true in development (jeremyevans) * Add :cache_class option to render plugin, for customized template cache behavior (celsworth) (#34) * Add :compiled_asset_host option to assets plugin, to use a host for compiled assets (jeremyevans) * Allow r.multi_run to take a block that is called with the prefix before dispatching to the rack app (mikz) (#32) = 2.4.0 (2015-06-15) * Add websockets plugin, for integration with faye-websocket (jeremyevans) * Add status_handler plugin, similar to not_found but for any status code (celsworth) (#29) * Support Closure Compiler, Uglifier, and MinJS for compressing javascript in the assets plugin (jeremyevans) * Make Roda.plugin always return nil (jeremyevans) * Add :gzip option to assets plugin (jeremyevans) = 2.3.0 (2015-05-13) * Make assets plugin work better with json plugin when r.assets is the last method called in a route block (jeremyevans) (#27) * Support no_mail! method in the mailer plugin, for skipping an email (jeremyevans) * Add precompile_templates plugin, for saving memory when using a forking webserver (jeremyevans) * Document how to allow per-branch HTML escaping of <%= %> in the view_options plugin (jeremyevans) * Add :include_request option to json and json_parser plugins to include request in :serializer/:parser call (janko-m) (#26) * Optimize template cache lookup in render plugin when :cache_key is given (jeremyevans) * Add :engine_opts option to render plugin, for specifying per-template engine options (jeremyevans) * The render plugin and render/view :ext option is now replaced by the :engine option (jeremyevans) * Add path_rewriter plugin, for rewriting paths before routing (jeremyevans) * Add :cache_key option to render/view to explicitly set the template cache key (jeremyevans) * Don't cache templates if :template_block is given to render/view, unless :cache=>true is used (jeremyevans) * Add :cache option to render/view to force caching or not caching the template (jeremyevans) * Avoid rehashing hashes at runtime in plugins (jeremyevans) * Add heartbeat plugin for heartbeat support (jeremyevans) * Support :serializer option in json plugin (janko-m) (#21) * Add json_parser plugin, for parsing request bodies in JSON format (jeremyevans) = 2.2.0 (2015-04-13) * Add :escaper render plugin option to support custom escaping of <%= %> tags when :escape is used (jeremyevans) * Add :escape_safe_classes render plugin option, to not escape certain string subclasses when :escape is used (jeremyevans) * Split partials method from padrino_render plugin into partials plugin (kematzy) (#19) * Add shared_vars plugin, for sharing variables between multiple Roda apps (jeremyevans) * Add delay method to chunked plugin, for delaying a block execution until right before content template rendering (jeremyevans) * Have default Content-Type header when using the default_headers plugin (jeremyevans) * Add :by_name option to the path plugin, for registering classes by name, useful when reloading code (jeremyevans) * Add Roda.path_block to get the block related to the given class used for Road#path (jeremyevans) * Make Roda#path work correctly in subclasses (jeremyevans) = 2.1.0 (2015-03-13) * Have add_file in the mailer plugin support blocks, which are called after the file has been added (jeremyevans) * Add append_view_subdir to view_options, for appending to an existing view subdirectory (jeremyevans) * Rename view_subdirs plugin to view_options, add support for branch/route specific view/layout options/locals (jeremyevans) * Merge :locals set in the render plugin options into :locals provided in call to view/render (jeremyevans) * Add support for registering classes in the path plugin for use with Roda#path (jeremyevans) * Use :add_script_name app option as default for path method :add_script_name option in path plugin (jeremyevans) * Support :add_script_name app option in assets plugin, to prefix URLs with SCRIPT_NAME (jeremyevans) * Make r.multi_route in multi_route plugin work without any named routes defined (jeremyevans) * Add :static plugin, for more easily serving static files (jeremyevans) * Recognize Roda :root option in render and assets plugins (jeremyevans) * Make :layout=>false option in render plugin override previous layout template (jeremyevans) * Make add_file in the mailer plugin add the files after the email body instead of before (jeremyevans) = 2.0.0 (2015-02-13) * Allow Roda app to be used as a regular rack app even when using the middleware plugin (jeremyevans) * Make render plugin :layout option always be true or false (jeremyevans) * Make :layout=>true view option use the default layout (jeremyevans) * Make error_handler plugin rescue ScriptError in addition to StandardError (jeremyevans) * Make halt plugin integrate with symbol_views, json, and similar plugins (jeremyevans) * Add padrino_render plugin, adding render/partial methods that work similar to Padrino (jeremyevans) * Add Roda#render_template private method for template rendering, for use by plugins (jeremyevans) * Make Roda#initialize take env hash, #call take route_block, remove private #_route (jeremyevans) * Remove keep_remaining_path/update_remaining_path private request methods (jeremyevans) * Don't modify SCRIPT_NAME/PATH_INFO during routing, merging static_path_info plugin into core (jeremyevans) * Remove code deprecated in Roda 1.3.0 (jeremyevans) = 1.3.0 (2015-01-13) * Make static_path_info plugin restore original SCRIPT_NAME/PATH_INFO before returning from r.run (jeremyevans) * Add RodaMajorVersion, RodaMinorVersion, and RodaPatchVersion (jeremyevans) * Add delete_empty_headers plugin for deleting response headers that are empty before return response (jeremyevans) * Make freeze class method freeze internal data structures to avoid thread safety issues (jeremyevans) * Deprecate mutating plugin option hashes for chunked, default_headers, error_email, json, and render plugins (jeremyevans) * Fix subclassing app and using r.multi_run in subclass in multi_run plugin (jeremyevans) * Support :classes option in json plugin to set the classes to use (jeremyevans) * Improve performance in default_headers plugin by not duping the headers (jeremyevans) * Use :template_opts instead of :opts for providing options to the template in the render plugin (jeremyevans) * Support :match_header_yield Roda option in the header_matchers plugin, causing the :header match to yield the value (jeremyevans) * Move :param and :param! hash matchers to the param_matchers plugin (jeremyevans) * Add path_matchers plugin, for :extension, :prefix, and :suffix hash matchers (jeremyevans) * Move Roda.hash_matcher to hash_matcher plugin (jeremyevans) * Move Roda.request_module and .response_module to module_include plugin (jeremyevans) * Move RodaResponse#set_cookie and #delete_cookie to cookies plugin (jeremyevans) * Deprecate RodaRequest#full_path_info, use #path instead (jeremyevans) * Add class_delegate to the delegate plugin (jeremyevans) * Make not_found plugin clear headers for response if it is not found (jeremyevans) * Make error_handler plugin use a new response instead of reusing existing response (jeremyevans) * Make RodaResponse a subclass of Object instead of Rack::Response (jeremyevans) = 1.2.0 (2014-12-17) * Don't override explicit nil :default_encoding template option in the render plugin (jeremyevans) * Add remaining_path and matched_path request methods (jeremyevans) * Add slash_path_emty plugin, for considering a path of "/" as empty when doing a terminal match (jeremyevans) * Remove def_verb_method request class method (jeremyevans) * Support :add_script_name, :name, :url, and :url_only options when creating named paths in the path plugin (jeremyevans) * Add match_affix plugin, for overriding default prefix/suffix used in match patterns (jeremyevans) * Add empty_root plugin, for making root matcher also match empty string (jeremyevans) * Add roda_class instance methods to RodaRequest and RodaResponse, to DRY up plugin code (jeremyevans) * Add sinatra_helpers plugin, porting Sinatra::Helpers methods not covered by other plugins (jeremyevans) * Don't set the default headers until the response is finished (jeremyevans) * Add RodaRequest#default_redirect_status, so plugins can override the default status used for redirects (jeremyevans) * Add drop_body plugin, for automatically dropping body and Content-{Length,Type} headers based on response status (jeremyevans) * Add clear_middleware! class method, for clearing the current middleware (jeremyevans) * Add inherit_middleware class accessor, allowing users to turn off middleware inheritance (jeremyevans) * Add multi_run plugin, for dispatching to multiple rack applications based on the request path prefix (jeremyevans) * Add environments plugin, for handling development/test/production environments (jeremyevans) * Do not cache templates by default if RACK_ENV is development (jeremyevans) * Add delay_build plugin, to delay building the rack app until Roda.app is called (jeremyevans) * Add :user_agent hash matcher to the header_matchers plugin (jeremyevans) * Fix caching of templates in the render plugin when :opts or :template_class is used (jeremyevans) * Require loading the render plugin again if you want to change the default layout (jeremyevans) * Pass :css_opts and :js_opts as template options (via :opts) instead of render options when rendering (jeremyevans) * Only pass :opts hash to template class during rendering, instead of all render/view options (jeremyevans) * Support :template_class option in the render plugin for overriding template class to use (jeremyevans) * Automatically dup unfrozen Array/Hash opts values when subclassing (jeremyevans) * Add named_templates plugin, for creating inline templates by name, instead of storing them in the file system (jeremyevans) * Support :template option in for render/view to specify template to use, instead of requiring separate argument (jeremyevans) * Add class_level_routing plugin, for a DSL similar to Sinatra (jeremyevans) * Make RodaRequest.consume_pattern not capture pattern by default (jeremyevans) * Add static_path_info plugin, making Roda not modify PATH_INFO or SCRIPT_NAME during routing (jeremyevans) * Use local/instance variable lookups instead of method calls to improve performance (jeremyevans) * Add RodaRequest#session, and have #session delegate to that (jeremyevans) * Add delegate plugin, for easily creating methods that delegate to request or response (jeremyevans) * Add mailer plugin, allowing use of a routing tree for email instead of web responses (jeremyevans) = 1.1.0 (2014-11-11) * Add assets plugin, for rendering assets on the fly, or compiling them to a single compressed file (cj, jeremyevans) (#5) * Make InstanceMethods in plugins not include constants, as they would pollute the constant namespace (jeremyevans) * Make response.finish add the Content-Length header, not response.write (jeremyevans) * Add response.finish_with_body to override response body used (jeremyevans) * Use allocate instead of new in rack app (jeremyevans) * Add chunked plugin, for easy streaming of template responses using Transfer-Encoding: chunked (jeremyevans) * Add namespace support to the multi_route plugin, to support more complex applications (jeremyevans) * Make r.multi_route use named route return value if not passed a block (jeremyevans) * Make r.multi_route prefer longer route if multiple routes have the same prefix (jeremyevans) * Add caching plugin, for handling http caching (jeremyevans) * Support adding middleware after the route block has been added (jeremyevans) * Allow Roda subclasses to use route block from superclass (jeremyevans) * Have r.multi_route ignore non-String named routes (jeremyevans) * Pick up newly added named routes while running in the multi_route plugin, useful for development (jeremyevans) * Add path plugin, for named path support (jeremyevans) (#4) * Add error_email plugin, for easily emailing an error notification for an exception (jeremyevans) = 1.0.0 (2014-08-19) * Don't have :extension hash matcher force a terminal match (jeremyevans) * Add :content option to view method in render plugin to use given content instead of rendering a template (jeremyevans) * Add :escape option to render plugin for using erb templates where <%= %> escapes and <%== %> does not (jeremyevans) * Make multi_route plugin route("route_name") method a request method instead of an instance method (jeremyevans) * Add r.multi_route method to multi_route plugin, for dispatching to named route based on first segment in path (jeremyevans) * Allow non-GET requests to use r.redirect with no argument, redirecting to current path (jeremyevans) * Add head plugin, for handling HEAD requests like GET requests with an empty body (jeremyevans) * Optimize consuming patterns by using a positive lookahead assertion (jeremyevans) * Add not_allowed plugin, for automatically returning 405 Method Not Allowed responses (jeremyevans) * Optimize match blocks with no arguments (jeremyevans) * Add content_for plugin, for storing content in one template and retrieving it in another (jeremyevans) * Add render_each plugin, for rendering a template for each value in an enumerable (jeremyevans) * Add backtracking_array plugin, allowing array matchers to backtrack if later matchers do not match (jeremyevans) * Add :all hash matcher, allowing array matchers to include conditions where you want to match multiple conditions (jeremyevans) * Add json plugin, allowing match blocks to return arrays/hashes, returning JSON (jeremyevans) * Add view_subdirs plugin, for setting a subdirectory for views on a per-request basis (jeremyevans) * Allow default halt method to take no arguments, and use the current response (jeremyevans) * Add symbol_views plugin, allowing match blocks to return a template name symbol (jeremyevans) * Add per_thread_caching plugin, for using separate caches per thread instead of shared thread-safe caches (jeremyevans) * Add hash_matcher class method, for easily creating hash match methods (jeremyevans) * Add symbol_matchers plugin, for using symbol-specific matching regexps (jeremyevans) * Add csrf plugin for csrf protection using rack_csrf (jeremyevans) * Optimize r.is, r.get, r.post and similar methods by reducing the number of Array objects created (jeremyevans) * Support RequestClassMethods and ResponseClassMethods in plugins (jeremyevans) * Add Roda::RodaCache for a thread safe cache, currently used for match patterns, templates, and plugins (jeremyevans) * Optimize matching by caching consume regexp for strings, regexp, symbol, and :extension matchers (jeremyevans) * Add r.root for GET / requests, for easier to read version of r.get "" (jeremyevans) * Optimize r.is terminal matcher, remove :term hash matcher (jeremyevans) * Make flash plugin no longer depend on sinatra-flash (jeremyevans) * Move version file to roda/version so it can be required separately without loading dependencies (jeremyevans) = 0.9.0 (2014-07-30) * Initial public release jeremyevans-roda-4f30bb3/doc/conventions.rdoc000066400000000000000000000141671516720775400214040ustar00rootroot00000000000000= Conventions This guide goes over conventions for directory layout and file layout for Roda applications. You are free to ignore these conventions, they mostly exist to help users who are unsure how to structure their Roda applications. == Directory Layout Which directory layout to use should reflect the size of your application. === Small Applications For a small application, the following directory layout is recommended: Rakefile app_name.rb assets/ config.ru db.rb migrate/ models.rb models/ public/ spec/ views/ +app_name.rb+ should contain the Roda application, and should reflect the name of your application. So, if your application is named +FooBar+, you should use +foo_bar.rb+. +config.ru+ should contain the code the webserver uses to determine which application to run. +views/+ should contain your template files. This assumes you are using the +render+ plugin and server-side rendering. If you are creating a single page application and just serving JSON, then you won't need a +views+ directory. For small applications, all view files should be in the +views+ directory. +public/+ should contain any static files that should be served directly by the webserver. Again, for pure JSON applications, you won't need a +public+ directory. +assets/+ should contain the source files for your CSS and javascript assets. If you are not using the +assets+ plugin, you won't need an +assets+ directory. +db.rb+ should contain the minimum code to setup a database connection, without loading any of the applications models. This can be required in cases where you don't want the models loaded, such as when running migrations. This file should be required by +models.rb+. +models.rb+ should contain all code related to your ORM. This file should be required by +app_name.rb+. This keeps your model code separate from your web code, making it easier to use outside of your web code. It allows you to get an IRB shell for accessing your models via irb -r ./models, without loading the Roda application. +models/+ should contain your ORM models, with a separate file per model class. +migrate/+ should create your database migration files, if you are using an ORM that uses migrations. +spec/+ (or +test/+ should contain your specifications/tests. For a small application, it's recommended to have a single file for your model tests, and a single file for your web/integration tests. +Rakefile+ should contain the rake tasks for the application. The convention is that the default rake task will run all specs/tests related to the application. If you are using the +assets+ plugin, you should have an assets:precompile task for precompiling assets. === Large Applications Large applications generally need more structure: Rakefile app_name.rb assets/ helpers/ migrate/ models.rb models/ public/ routes/ prefix1.rb prefix2.rb spec/ models/ web/ views/ prefix1/ prefix2/ For larger apps, the +Rakefile+, +assets/+, +migrate+, +models.rb+, +models/+, +public/+, remain the same. +app_name.rb+ should use the +hash_branch_view_subdir+ plugin (which builds on the +hash_branches+ and +view_options+ plugin), or the +multi_run+ plugin. The routes used by the +hash_branches+ or +multi_run+ should be stored in routing files in the +routes/+ directory, with one file per prefix. For specs/tests, you should have +spec/models/+ and +spec/web/+, with one file per model in +spec/models/+ and one file per prefix in +spec/web/+. Substitute +spec+ with +test+ if that is what you are using as the name of the directory. You should have a separate view subdirectory per prefix. With the +hash_branch_view_subdir+, the application will automatically set a separate view subdirectory per routing tree branch. +helpers/+ should be used to store helper methods for your application, that you call in your routing files and views. In a small application, these methods should just be specified in +app_name.rb+ === Really Large Applications For very large applications, it's expected that there will be deviations from these conventions. However, it is recommended to use the +hash_branch_view_subdir+ or +multi_run+ plugins to organize your application, and have subdirectories in the +routes/+ directory, and nested subdirectories in the +views/+ directory. == Roda Application File Layout === Small Applications For a small application, the convention in Roda is to layout your Roda application file (+app_name.rb+) like this: require 'roda' require_relative 'models' class AppName < Roda SOME_CONSTANT = 1 use SomeMiddleware plugin :render, escape: true plugin :assets route do |r| # ... end def view_method 'foo' end end You should first require +roda+ and +./models+, followed by any other libraries needed by the application. You should subclass Roda and make the application's name the name of the Roda subclass. Inside the subclass, you first define the constants used by the application. Then you add any middleware used by the application, followed by loading any plugins used by the application. Then you add the route block for the application. After the route block, define the instance methods used in your route block or views. === Large Applications For larger applications, there are some slight changes to the Roda application file layout: require 'roda' require_relative 'models' class AppName < Roda SOME_CONSTANT = 1 use SomeMiddleware plugin :render, escape: true, layout: './layout' plugin :assets plugin :hash_branch_view_subdir Dir['routes/*.rb'].each{|f| require_relative f} route do |r| r.hash_branches('') r.root do # ... end end Dir['helpers/*.rb'].each{|f| require_relative f} end After loading the +hash_branch_view_subdir+ plugin, you require all of your routing files. Inside your route block, instead of defining your routes, you just call +r.hash_branches+, which will dispatch to all of your routing files. After your route block, you require all of your helper files containing the instance methods for your route block or views, instead of defining the methods directly. jeremyevans-roda-4f30bb3/doc/release_notes/000077500000000000000000000000001516720775400210055ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/doc/release_notes/1.0.0.txt000066400000000000000000000225111516720775400222030ustar00rootroot00000000000000= New Plugins * A csrf plugin has been added for CSRF prevention, using Rack::Csrf. It also adds helper methods for views such as csrf_tag. * A symbol_matchers plugin has been added, for customizing the regexps used per symbol. This also affects the use of embedded colons in strings. This supports the following symbol regexps by default: :d :: (\d+), a decimal segment :format :: (?:\.(\w+))?, an optional format/extension :opt :: (?:\/([^\/]+))?, an optional segment :optd :: (?:\/(\d+))?, an optional decimal segment :rest :: (.*), all remaining characters, if any :w :: (\w+), a alphanumeric segment This allows you to write code such as: plugin :symbol_matchers route do |r| r.is "track/:d" do end end And have it only match routes such as /track/123, not /track/abc. Note that :opt, :optd, and :format are only going to make sense when used as embedded colons in strings, due to how segment matching works. You can add your own symbol matchers using the symbol_matcher class method: plugin :symbol_matchers symbol_matcher :slug, /([\w-]+)/ route do |r| r.on :slug do end end * A symbol_views plugin has been added, which allows match blocks to return symbols, which are interpreted as template names: plugin :symbol_views route do |r| :template_name # same as view :template_name end * A json plugin has been added, which allows match blocks to return arrays or hashes, and uses a JSON version of them as the response body: plugin :json route do |r| {'a'=>[1,2,3]} # response: {"a":[1,2,3]} end This also sets the Content-Type of the response to application/json. To convert additional object types to JSON, you can modify json_response_classes: plugin :json json_response_classes << Sequel::Model * A view_subdirs plugin has been added for setting a default subdirectory to use for views: Roda.route do |r| r.on "admin" do set_view_subdir "admin" r.is do view "index" # uses admin/index view end end end * A render_each plugin has been added, for rendering the same template for multiple objects, and returning the concatenation of all of the output: <%= render_each([1,2,3], 'number') %> This renders the number template 3 times. Each time the template is rendered, a local variable named number will be present with the current entry in the enumerable. You can control the name of the local variable using the :local option: <%= render_each([1,2,3], 'number', :local=>:n) %> * A content_for plugin has been added, for storing content in one template and retrieving that content in a different template (such as the layout). To set content, you call content_for with a block: <% content_for :foo do %> content for foo <% end %> To retrieve content, you call content_for without a block: <%= content_for :foo %> This plugin probably only works when using erb templates. * A not_allowed plugin has been added, for automatically returning 405 Method Not Allowed responses when a route is handled for a different request method than the one used. For this routing tree: plugin :not_allowed route do |r| r.get "foo" do end end If you submit a POST /foo request, it will return a 405 error instead of a 404 error. This also handles cases when multiple methods are supported for a single path, so for this routing tree: route do |r| r.is "foo" do r.get do end r.post do end end end If you submit a DELETE /foo request, it will return a 405 error instead of a 404 error. * A head plugin has been added, automatically handling HEAD requests the same as GET requests, except returning an empty body. So for this routing tree: plugin :head route do |r| r.get "foo" do end end A request for HEAD /foo will return a 200 result instead of a 404 error. * A backtracking_array plugin has been added, which makes matching backtrack to the next entry in an array if a later matcher fails. For example, the following code does not match /foo/bar by default in Roda: r.is ['foo', 'foo/bar'] do end This is because the 'foo' entry in the array matches, so the array matches. However, after the array is matched, the terminal matcher added by r.is fails to match. That causes the routing method not to match the request, so the match block is not called. With the backtracking_array plugin, failures of later matchers after an array matcher backtrack so the next entry in the array is tried. * A per_thread_caching plugin has been added, allowing you to change from a thread-safe shared cache to a per-thread cache, which may be faster on alternative ruby implementations, at the cost of additional memory usage. = New Features * The hash_matcher class method has been added to make it easier to define custom hash matchers: hash_matcher(:foo) do |v| self['foo'] == v end route do |r| r.on :foo=>'bar' do # matches when param foo has value bar end end * An r.root routing method has been added for handling GET requests where the current path is /. This is basically a faster and simpler version of r.get "", except it does not consume the / from the path. * The r.halt method now works without an argument, in which case it uses the current response. * The r.redirect method now works without an argument for non-GET requests, redirecting to the current path. * An :all hash matcher has been added, which takes an array and matches only if all of the elements match. This is mainly designed for usage inside an array matcher, so: r.on ["foo", {:all=>["bar", :id]}] do end will match either /foo or /bar/123, but not /bar. * The render plugin's view method now accepts a :content option, in which case it uses the content directly without running it through the template engine. This is useful if you have arbitrary content you want rendered inside the layout. * The render plugin now accepts an :escape option, in which case it will automatically set the default :engine_class for erb templates to an Erubis::EscapedEruby subclass. This changes the behavior of erb templates such that: <%= '' %> # <escaped> <%== '' %> # This makes it easier to protect against XSS attacks in your templates, as long as you only use <%== %> for content that has already been escaped. Note that similar behavior is available in Erubis by default, using the :opts=>{:escape_html=>true} render option, but that doesn't handle postfix conditionals in <%= %> tags. * The multi_route plugin now has an r.multi_route method, which will attempt to dispatch to one of the named routes based on first segment in the path. So this routing tree: plugin :multi_route route "a" do |r| r.is "c" do "e" end end route "b" do |r| r.is "d" do "f" end end route do |r| r.multi_route end will return "e" for /a/c and "f" for /b/d. * Plugins can now override request and response class methods using RequestClassMethods and ResponseClassMethods modules. = Optimizations * String, hash, and symbol matchers are now much faster by caching the underlying regexp. * String, hash, and symbol matchers are now faster by using a regexp positive lookahead assertion instead of an additional capture. * Terminal matching in the r.is, r.get, and r.post routing methods is now faster, as it does not use a hash matcher internally. * The routing methods are now faster by reducing the number of Array objects created. * Calling routing methods without arguments is now faster. * The r.get method is now faster by reducing the number of string allocations. * Many request methods are faster by reducing the number of method calls used. * Template caching no longer uses a mutex on MRI, since one is not needed for thread safety there. = Other Improvements * The flash plugin now implements its own flash hash instead of using sinatra-flash. It is now slightly faster and handles nil keys in #keep and #discard. * Roda's version is now stored in roda/version.rb so that it can be required without requiring Roda itself. = Backwards Compatibility * The multi_route plugin's route instance method has been changed to a request method. So the new usage is: plugin :multi_route route "a" do |r| end route do |r| r.route "a" # instead of: route "a" end * The session key used for the flash hash in the flash plugin is now :_flash, not :flash. * The :extension matcher now longer forces a terminal match, use one of the routing methods that forces a terminal match if you want that behavior. * The :term hash matcher has been removed. * The r.consume private method now takes the exact regexp to use to search the current path, it no longer enforces a preceeding slash and that the match end on a segment boundary. * Dynamically constructing match patterns is now a potential memory leak due to them being cached. So you shouldn't do things like: r.on r['param'] do end * Many private routing methods were changed or removed, if you were using them, you'll probably need to update your code. jeremyevans-roda-4f30bb3/doc/release_notes/1.1.0.txt000066400000000000000000000167071516720775400222160ustar00rootroot00000000000000= New Plugins * An assets plugin has been added, for rendering your CSS and javascript asset files on the fly in development, and compiling them to a single, compressed file in production. When loading the plugin, you just specify the assets to use via :css and/or :js options: plugin :assets, :css=>'some_file.scss', :js=>'some_file.coffee' Inside your Roda.route block, you call r.assets to serve the assets: route do |r| r.assets end In your views, you can call the assets method, which returns strings containing link/script tags for your assets: <%= assets(:css) %> <%= assets(:js) %> In production mode, you call compile_assets after loading the plugin, and it will compile all of the asset files into a single file per type, optionally compress it (using yuicompressor), and write the file to the public folder where it can be served by the webserver. In compiled mode, calling assets in your views will reference the compiled file. It's possible to precompile your assets before application boot, so they don't need to be compiled every time your application boots. The assets plugin also supports asset groups, useful when different sections of your application use different sets of assets. * A chunked plugin has been added, for streaming template rendering using Transfer-Encoding: chunked. By default, this flushes the rendering of the top part of the layout template before rendering the content template, allowing the client to load the assets necessary to fully render the page while the content template is still being rendered on the server. This can significantly decrease client rendering times. To use chunked encoding for a response, just call the chunked method instead of view: r.root do chunked(:index) end If you want to execute code after flushing the top part of the layout template, but before rendering the content template, pass a block to chunked: r.root do chunked(:index) do # expensive calculation here end end If you want to use chunked encoding for all responses, pass the :chunk_by_default option when loading the plugin: plugin :chunked, :chunk_by_default => true Inside your layout or content templates, you can call the flush method to flush the current result of the template to the user, useful for streaming large datasets. <% (1..100).each do |i| %> <%= i %> <% sleep 0.1 %> <% flush %> <% end %> * A caching plugin has been added, for simple HTTP caching support. The implementation is based on Sinatra's, and offers r.last_modifed and r.etag methods for conditional responses: r.get '/albums/:d' do |album_id| @album = Album[album_id] r.last_modified @album.updated_at r.etag @album.sha1 view('album') end This also adds response.cache_control and response.expires methods for setting the Cache-Control/Expires headers for the response. * A path plugin has been added for simple support for named paths: plugin :path path :foo, '/foo' # foo_path => '/foo' path :bar do |bar| # bar_path(bar) => '/bar/1' "/bar/#{bar.id}" end * An error_email plugin has been added, for easily emailing error notifications for an exception. This is designed for use with the error_handler plugin, and should only be used in low-traffic environments: plugin :error_email, :to=>'to@example.com', :from=>'from@example.com' plugin :error_handler do |e| error_email(e) 'Internal Server Error' end = multi_route Plugin Improvements * The multi_route plugin now supports namespaces, allowing it to support routing trees of arbitrary complexity. Previously, only a single namespace was supported. For example, if you want to store your named routes in a directory tree: /routes/foo.rb /routes/foo/baz.rb /routes/foo/quux.rb /routes/bar.rb /routes/bar/baz.rb /routes/bar/quux.rb You can load all of the routing files in the routes subdirectory tree, and structure your routing tree as follows: # app.rb route do |r| r.multi_route end # routes/foo.rb route('foo') do |r| check_foo_access! r.multi_route('foo') end # routes/bar.rb route('bar') do |r| check_bar_access! r.multi_route('bar') end # routes/foo/baz.rb route('baz', 'foo') do # ... end * Newly added named routes are now picked up while running, useful in development when using code reloading. * r.multi_route now ignores non-String named routes, allowing you to only dispatch to the String named routes. Previously, calling r.multi_route when any non-String names routes were present resulted in an error. * r.multi_route now prefers longer routes to shorter routes if routes have the same prefix. This can fix behavior if you have named routes such as "foo" and "foo/bar". * If you don't pass a block to r.multi_route, it will use the return value of the named route as the block value to return, instead of always returning nil. = Optimizations * Dispatch speed is slightly improved by using allocate instead of new to create new instances. * Hash allocations in the render plugin have been reduced. = Other Improvements * The Roda.route block is now inherited when subclassing, making it possible to subclass a Roda application and have the subclass work without adding a route block. * Middleware added to a Roda app after the Roda.route method is called are now used instead of being ignored. * A response.finish_with_body method has been added, for overriding the response body to use. This is useful if you want to support arbitrary response bodies. * The render plugin now defaults the locals argument to an empty frozen hash instead of nil when rendering templates via tilt. This is faster as it avoids a hash allocation inside tilt, and also works with broken external tilt templates that require that the locals argument be a hash. * Plugins that ship with Roda no longer set constants inside InstanceMethods. Instead, the constants are set at the plugin module level. This is done to avoid polluting the namespace of the application with the constants. Roda's policy is that all internal constants inside the Roda namespace are prefixed with Roda, so they don't pollute the user's namespace, and setting these constants inside InstanceMethods in plugins violated that policy. = Backwards Compatibility * response.write no longer sets a Content-Length header. Instead, response.finish sets it. This is faster if you call response.write multiple times, and more correct if you call response.finish without calling response.write. * In the render plugin, modifying render_opts directly is now deprecated and will raise an error in the next major release (the hash will be frozen). Instead, users should call plugin :render again with a new hash, which will be merged into the existing render_opts hash. * Moving plugin's constants from InstanceMethods to the plugin level can break applications where the constant was referenced directly. For example, if you were doing: Roda::SESSION_KEY to get the constant for the session key, you would now need to use: Roda::RodaPlugins::Base::SESSION_KEY In general, it is recommended to not reference such constants at all. If you think there should be a general reason to access them, request that a method is added that returns them. jeremyevans-roda-4f30bb3/doc/release_notes/1.2.0.txt000066400000000000000000000313461516720775400222130ustar00rootroot00000000000000= New Plugins * A static_path_info plugin has been added, which doesn't modify SCRIPT_NAME/PATH_INFO during routing, only before dispatching the request to another rack application via r.run. This is faster and avoids problems caused by changing SCRIPT_NAME/PATH_INFO during routing, such as methods that return paths that depend on SCRIPT_NAME. This behavior will become Roda's default starting in Roda 2, and it is recommended that all Roda apps use it. * A mailer plugin has been added, which allows you to use Roda's render plugin to create email bodies, and allows you to use Roda's routing tree features to DRY up your mailing code similar to how it DRYs up your web code. Here is an example routing tree using the mailer plugin: class Mailer < Roda plugin :render plugin :mailer route do |r| r.on "user/:d" do |user_id| # DRY up code by setting shared behavior in higher level # branches, instead of duplicating it inside each subtree. @user = User[user_id] from 'notifications@example.com' to @user.email r.mail "open_account" do subject 'Welcome to example.com' render(:open_account) end r.mail "close_account" do subject 'Thank you for using example.com' render(:close_account) end end end end With your routing tree setup, you can use the sendmail method to send email: Mailer.sendmail("/user/1/open_account") If you want a Mail::Message object returned for further modification before sending, you can use mail instead of of sendmail: Mailer.mail("/user/2/close_account").deliver * A delegate plugin has been added, allowing you to easily create methods in the route block scope that delegate to the request or response. While Roda does not pollute your namespaces by default, this allows you to choose to do so yourself if you find it offers a nicer API. Example: class App < Roda plugin :delegate request_delegate :root, :on, :is, :get, :post, :redirect route do |r| root do redirect "/hello" end on "hello" do get "world" do "Hello world!" end is do get do "Hello!" end post do puts "Someone said hello!" redirect end end end end end * A class_level_routing plugin has been added, allowing you to define your routes at the class level if desired. The routes defined at the class level can still use a routing tree for further routing. Example: class App < Roda plugin :class_level_routing root do request.redirect "/hello" end get "hello/world" do "Hello world!" end is "hello" do request.get do "Hello!" end request.post do puts "Someone said hello!" request.redirect end end end * A named_templates plugin has been added, for creating inline templates associated with a given name, that are used by the render plugin's render/view method in preference to templates stored in the filesystem. This makes it simpler to ship single-file Roda applications that use templates. Example: class App < Roda plugin :named_templates template :layout do "<%= yield %>" end template :index do "

Hello <%= @user %>!

" end route do |r| @user = 'You' render(:index) end # => "

Hello You!

" end * A multi_run plugin has been added, for dispatching to multiple rack applications based on the request path prefix. This provides a similar API as the multi_route plugin, but allows you to separate your applications per routing subtree, as opposed to multi_route which uses the same application for all routing subtrees. With the multi_run plugin, you call the class level run method with the routing prefix and the rack application to use, and you call r.multi_run to dispatch to all of the applications based on the prefix. class App < Roda plugin :multi_run run "foo", Foo run "bar", Bar run "baz", Baz route do |r| r.multi_run end end In this case, Foo, Bar, and Baz, can be subclasses of App, which allows them to share methods that should be shared, but still define methods themselves that are not shared by the other applications. * A sinatra_helpers plugin has been added, that ports over most of the Sinatra::Helpers methods that haven't already been added by other plugins. All of the methods are added either to the request or response class as appropriate. By default, delegate methods are also added to the route block scope, but you can turn this off by passing a :delegate=>false option when loading the plugin, which avoids polluting the route block namespace. The sinatra_helpers plugin adds the following request methods: * back * error * not_found * uri * send_file And the following response methods: * body * body= * status * headers * mime_type * content_type * attachment * informational? * success? * redirect? * client_error? * not_found? * server_error? * A slash_path_empty plugin has been added, which changes Roda so that "/" is considered an empty path when doing a terminal match via r.is or r.get/r.post with a path. class App < Roda plugin :slash_path_empty route do |r| r.get "albums" do # matches both GET /albums and GET /albums/ end end end * An empty_root plugin has been added, which makes r.root match the empty string, in addition to /. This can be useful in cases where a partial match on the patch has been completed. class App < Roda plugin :empty_root route do |r| r.on "albums" do r.root do # matches both GET /albums and GET /albums/ end end end end * A match_affix plugin has been added, for overriding the default prefix/suffix used in match patterns. For example, if you want to require that a leading / be specified in your routes. and you want to consume any trailing slash: class App < Roda plugin :match_affix, "", /(\/|\z)/ route do |r| r.on "/albums" do |s| # GET /albums # s => "" # GET /albums/ # s => "/" end end end * An environments plugin has been added, giving some simple helpers for executing code in different environments. Example: class App < Roda plugin :environments environment # => :development development? # => true test? # => false production? # => false # Set the environment for the application self.environment = :test test? # => true configure do # called, as no environments given end configure :development, :production do # not called, as no environments match end configure :test do # called, as environment given matches current environment end end * A drop_body plugin has been added, which automatically drops the body, Content-Type header, and Content-Length header when the response status indicates no body (100-102, 204, 205, 304). * A delay_build plugin has been added, which delays building the rack application until Roda.app is called, and only rebuilds the rack application if build! is called. This removes O(n^2) performance in the pathological case of adding a route block and then calling Roda.use many times to add middlewares, though you have to add a few hundred middlewares for the difference to be noticeable. = New Features * r.remaining_path and r.matched_path have been added for returning the remaining path that will be used for matching, and for returning the path already matched. Currently, these just provide the PATH_INFO and SCRIPT_NAME, but starting in Roda 2 PATH_INFO and SCRIPT_NAME will not be modified during routing, and you'll need to use these methods if you want to find out the remaining or already matched paths. * The render plugin now supports a :template option to render/view to specify the template to use, instead of requiring a separate argument. * The render plugin now supports a :template_class option, allowing you to override the default template class that Roda would use. * The render plugin now supports a :template_block option, specifying the block to pass when creating a template. * The path class method added by the path plugin now accepts :name, :url, :url_only, and :add_script_name options: :name :: Specifies name for method :url :: Creates a url method in addition to a path method :url_only :: Only creates a url method, not a path method :add_script_name :: prefixes the path with SCRIPT_NAME Note that if you plan to use :add_script_name, you should use the static_path_info plugin so that the method created does not return different results depending on where you are in the routing tree. * A :user_agent hash matcher has been added to the header_matchers plugin. * An inherit_middleware class accessor has been added. This can be set to false if you do not want subclasses to inherit middleware from the superclass. This is useful if the superclass dispatches to the subclass via r.run, as otherwise it would have to run the same middleware stack twice. * A clear_middleware! class accessor has been added, allowing you to clear the current middleware stack. * RodaRequest#default_redirect_status has been added, allowing plugins to override the default status used for redirect if a status is not given. * Roda{Request,Response}#roda_class has been added, which returns the Roda class related to the given request/response. = Other Improvements * The render plugin no longer caches templates by default if RACK_ENV is development. * When subclassing a Roda app, unfrozen Array/Hash entries in the opts hash are now duped into the subclass, so the subclass no longer needs to dup them manually. Note that plugins that use nested arrays/hashes in the opts hash still need to dup manually inside ClassMethods#inherited. For the plugins where it is possible, it is recommended to store plugin options in a frozen object in the opts hash, and require loading the plugin again to modify the plugin options. * Caching of templates is now fixed when the render/view :opts is used to specify template options per-call. * An explicit :default_encoding of nil in the render plugin's :opts hash is no longer overwritten with Encoding.default_external. * Roda#session now returns the same object as RodaRequest#session. * The view_subdirs, content_for, and render_each plugins now all depend on the render plugin. * The not_allowed plugin now depends on the all_verbs plugin. * Local/instance variables are now used in more places instead of method calls, improving performance. = Backwards Compatibility * The render plugin's render/view methods no longer pass the given hash directly to the underlying template. To pass options to the template engine, use a separate hash under the :opts key: render :file, :opts=>{:foo=>'bar'} This is more consistent with the class-level render plugin options, which also uses :opts to pass options to the template engine. The :js_opts and :css_opts options to the assets plugin are now passed as the :opts hash, so they continue to affect the template engine, so they no longer specify general render method options. * Modifying render_opts :layout after loading the render plugin now has no effect. You need to use plugin :render, :layout=>'...' to set the layout to use now. * Default headers are not set on a response until the response is finished. This allows you to check for header presence during routing to detect whether the header was specifically set for the current request. * RodaRequest.consume_pattern no longer captures anything by default. Previously, it did so in order to update SCRIPT_NAME, but that is now handled differently. This should only affect external plugins that attempt to override RodaRequest#consume. * RodaRequest.def_verb_method has been removed. * The hooks, default_headers, json, and multi_route plugins all store their class-level metadata in the opts hash instead of separate class instance variables. This should have no affect unless you were accessing the class instance variables directly. * The render plugin internals changed significantly, it now passes internal data using a hash. This should only affect users that were overriding render plugin methods. jeremyevans-roda-4f30bb3/doc/release_notes/1.3.0.txt000066400000000000000000000102151516720775400222040ustar00rootroot00000000000000= Preparation for Roda 2 * In Roda 2 (the next version), the PATH_INFO and SCRIPT_NAME env variables will not be modified during routing. Instead, Roda will use the static_path_info plugin behavior by default. Users are strongly encouraged to use the static_path_info plugin to make sure their apps will work with Roda 2. * In Roda 2, Roda#initialize will take the env hash, and #call will take the route block. The private #_route method will be eliminated. This should not affect applications, but it will affect plugins that override these methods. * The issues mentioned below should all have deprecation warnings, except where noted. == New Plugins to Replace Deprecated Features * RodaResponse#set_cookie and #delete_cookie have been moved to the cookies plugin. * Roda.request_module and .response_module have been moved to the module_include plugin. * Roda.hash_matcher has been moved to the hash_matcher plugin. * The :extension hash matcher has been moved to the path_machers plugin. This plugin also contains new :prefix and :suffix hash matchers. * The :param and :param! hash matchers have been moved to the param_matchers plugin. == Other Deprecation Issues * The :opts render plugin option and :opts option to render and view are now deprecated. Use the :template_opts option instead. * RodaRequest#full_path_info has been deprecated, switch to using #path. * Mutating plugin option hashes for the chunked, default_headers, error_email, json, and render plugins is now deprecated, these option hashes will be frozen in Roda 2. * The :header hash matcher in the header_matchers plugin now gives a deprecation warning, because in Roda 2, the matcher will yield the value of the header to the block. To get the new behavior and silence the deprecation warning, you need to set an option: Roda.opts[:match_header_yield] = true * Mutating json_result_classes directly in the json plugin is now deprecated. You should now pass a :classes option to the plugin specifying the classes to convert, if you want to handle classes other than Array and Hash. There will not be a deprecation warning if you attempt to mutate json_result_classes. = New Features * The Roda.freeze method now freezes internal datastructures to avoid thread safety issues. The plugins that ship with Roda will freeze their datastructures when Roda.freeze is called. It is recommended that production applications call freeze on the application after fully loading it, especially if they are using a threaded webserver and a non-MRI ruby. * A delete_empty_headers plugin has been added that automatically deletes headers set to the empty string. This makes it simpler to delete default headers if they shouldn't be set for a specific request. * A class_delegate method has been added to the delegate plugin. This makes it easier to create instance methods that call class methods. * Roda::RodaMajorVersion, RodaMinorVersion, and RodaPatchVersion constants have been added. = Other Improvements * The error_handler plugin now uses a new response instead of reusing the existing response. This fixes cases where changing the Content-Type and then raising an exception would result in a error page that used the previously set Content-Type. * The not_found plugin now clears previous set headers before calling not found. This fixes cases where Content-Type was set previously, and also fixes an incorrect Content-Length being used for the response. * The static_path_info plugin now restores the original SCRIPT_NAME and PATH_INFO before returning from r.run, fixing usage with some middleware. * The multi_run plugin now works when subclassing the app. * The default_headers plugin is now faster by skipping an unnecessary hash duplication. * A Gemfile has been added to make development slightly easier. = Backwards Compatibility * RodaResponse is no longer a subclass of Rack::Response, it is now a subclass of Object. This shouldn't have an effect unless you were calling a method on an instance that was defined by Rack::Response and not RodaResponse, but most of those methods would have raised exceptions. jeremyevans-roda-4f30bb3/doc/release_notes/2.0.0.txt000066400000000000000000000046731516720775400222150ustar00rootroot00000000000000= Backwards Compatibility * RodaResponse#set_cookie and #delete_cookie have been removed. * Roda.request_module and .response_module have been removed. * Roda.hash_matcher has been removed. * The :extension hash matcher has been removed. * The :param and :param! hash matchers have been removed. * RodaRequest#full_path_info has been removed. * The :opts render plugin option is no longer respected, Use the :template_opts option instead. * Plugin option hashes for the chunked, default_headers, error_email, and render plugins are now frozen. * The :header hash matcher in the header_matchers plugin now yields the header value to the block. * Roda.json_result_classes in the json plugin is now frozen. * The PATH_INFO and SCRIPT_NAME env variables are no longer modified during routing. * Roda#initialize now takes an env hash, and #call now takes the route block. The private #_route method has been removed. * RodaRequest#keep_remaining_path/#updating_remaining_path private methods have been removed. * The render plugin's :layout option is now always set to true or false, specifying whether a layout should be used by default. The template used for a layout is now located as the :template option inside :layout_opts. = New Plugins * A padrino_render plugin has been added, which adds render/partial methods that work similarly to Padrino's. = Other New Features * A Roda#render_template private method has been added to the render plugin. All internal users of render should switch to calling render_template. * The halt plugin now integrates with the symbol_views and json plugins, allowing things like: r.halt(:template) r.halt('key'=>'value') = Other Improvements * The error_handler plugin now rescues ScriptError in addition to StandardError. This handles SyntaxError (raised by ERB), LoadError (raised by require), and NotImplementedError (raised by TSort). * Using a :layout=>true option to the render plugin's view method now uses the default layout template, instead of a template named 'true'. It can be used to force a layout even if the render plugin has been configured to not use a layout by default. * Roda apps that use the middleware plugin can now be used as regular rack apps. Previously, using the middleware plugin made it impossible to use the app as a regular rack app. * Roda#request and #response are now faster. * Roda avoids creating unnecessary hashes in more places now. jeremyevans-roda-4f30bb3/doc/release_notes/2.1.0.txt000066400000000000000000000106311516720775400222050ustar00rootroot00000000000000= New Plugins * A view_options plugin has been added, for branch/route specific setting of view and layout options and locals. This allows for DRYer code when you want to change the view or layout settings for an entire routing branch. Options and locals set at the branch or route level have higher priority than those set at the plugin level, but lower priority than those provided as arguments to the render/view methods. Example: class App < Roda plugin :view_options route do |r| r.on 'albums' do layout_options :template=>'layouts/3_columns' layout_locals :heading=>'Albums' view_options :ext=>'haml' view_locals :name=>'Foo' # ... end end end The view_options plugin is also a superset of the previous view_subdirs plugin, and attempts to load view_subdirs will now load view_options. In addition to set_view_subdir, the view_options plugin now supports append_view_subdir, which will append a subdirectory to an existing subdirectory, which makes it simpler to deal with nested view file hierarchies. * A static plugin has been added for easily serving static files using Rack::Static. Example: class App < Roda plugin :static, ['/js', '/css'] # or: plugin :static, ['/js', '/css'], :root=>'pub' end = Other New Features * Roda now supports a :root option for the application that sets the root directory. This is useful if the application's files are not stored in the process's working directory, which is common for processes containing of multiple Roda applications. By setting the :root option, plugins that use the file system will default to making relative paths relative to the :root option instead of the process's working directory. The assets, render, and static plugins currently support the :root option. Example: class App < Roda opts[:root] = File.dirname(__FILE__) end * Roda now supports an :add_script_name option for the application, which makes plugins automatically prepend the SCRIPT_NAME for the request's environment to any paths created. This allows Roda applications to work transparently whenever they are mounted inside of another rack application. The assets and path plugins currently recognize the :add_script_name option. Example: class App < Roda opts[:add_script_name] = true end * The path plugin now adds a Roda#path method, which creates paths based on the type of argument used. You can register classes with the path plugin by providing Roda.path with a class, which will cause Roda#path to recognize them and handle them accordingly. Example: class App < Roda plugin :path path(Track){|track| "/albums/#{track.album_id}/tracks/#{track.number}"} route do r.get 'tracks/:id' do |track_id| r.redirect(path(Track[track_id])) end end end = Other Improvements * add_file in the mailer plugin now adds the files after the email body instead of before. This fixes some issues where the email body would end up empty, due to issues with the mail gem's API. add_file now accepts a block, and the block is called after the file has been attached. Among other things, this allows you to change the content_type for an attached file: add_file 'path/to/file' do response.mail.attachments.last.content_type = 'text/foo' end * r.multi_route in the multi_route plugin now works if there are no named routes defined. * A render plugin :locals option is now respected, setting defaults to use for locals in views. Additionally, a :locals option in the :layout_opts option is now respected for setting locals in layouts. If both the render plugin option is set and :locals is passed to render/view, the two will be merged together. Previously, providing a :locals option to render/view would cause the plugin level option to be ignored. = Backwards Compatibility * Using the render plugin :layout=>nil option now removes any layout template set previously using :layout. Previously, the layout template would still be kept, but it would not be used by default. * Accessing attachments after adding a file using add_file in the mailer plugin no longer works, as the adding is now delayed until after the body is set. You should now pass a block to add_file if you want to access the attachment after it has been added. jeremyevans-roda-4f30bb3/doc/release_notes/2.10.0.txt000066400000000000000000000015411516720775400222650ustar00rootroot00000000000000= New Features * The json plugin now accepts a :content_type option, which will override the default Content-Type response header used for responses. * Stream#write has been added to the streaming plugin, allowing the following type of code to work: stream do |out| IO.copy_stream(StringIO.new(content), out) end = Other Improvements * Roda now works with ruby 2.3's --enable-frozen-string-literal, and all of the library files are set to use frozen string literals by default. Most of roda's plugin-specific dependencies were found to have issues with frozen string literals, and while pull requests have been sent to fix the issues, it's unlikely that you would currently be able to use --enable-frozen-string-literal in production. * The json plugin will no longer override a Content-Type header if one is already set. jeremyevans-roda-4f30bb3/doc/release_notes/2.11.0.txt000066400000000000000000000041451516720775400222710ustar00rootroot00000000000000= New Features * A params_capturing plugin has been added, which makes string and symbol matchers update the request params with the value of the captured segments, using the matcher as the key: plugin :params_capturing route do |r| # GET /foo/123/abc/67 r.on("foo/:bar/:baz", :quux) do r[:bar] #=> '123' r[:baz] #=> 'abc' r[:quux] #=> '67' end end Note that this updating of the request params using the matcher as the key is only done if all arguments to the matcher are symbols or strings. All matchers will update the request params by adding all captured segments to the captures key, including symbol and string matchers: r.on(:x, /(\d+)\/(\w+)/, ':y') do r[:x] #=> nil r[:y] #=> nil r[:captures] #=> ["foo", "123", "abc", "67"] end Note that the request params captures entry will be appended to with each nested match: r.on(:w) do r.on(:x) do r.on(:y) do r.on(:z) do r[:captures] # => ["foo", "123", "abc", "67"] end end end end Note that any existing params captures entry will be overwritten by this plugin. You can use r.GET or r.POST to get the underlying entry, depending on how it was submitted. Also note that the param keys are actually stored in r.params as strings and not symbols (r[] converts the argument to a string before looking it up in r.params). Also note that this plugin will not work correctly if you are using the symbol_matchers plugin with custom symbol matching and are using symbols that capture multiple values or no values. * A :scope option is now supported by render/view in the render plugin, which allows you to specify the object in which context to evaluate the template. = Other Improvements * The assets plugin's support for the Minjs javascript minifier now supports the latest version (0.4.1). = Backwards Compatibility * Support for Minjs <0.4.0 has been dropped from the assets plugin. Please upgrade Minjs at the same time you upgrade Roda if you are using both of them. jeremyevans-roda-4f30bb3/doc/release_notes/2.12.0.txt000066400000000000000000000026121516720775400222670ustar00rootroot00000000000000= New Features * An optimized_string_matchers plugin has been added, which contains optimized matchers for single strings. r.on_branch is an optimized version of r.on and r.is_exactly is optimized version of r.is: plugin :optimized_string_matchers route do |r| r.on_branch "x" do # matches /x and paths starting with /x/ r.is_exactly "y" do # matches /x/y end end end Both of these methods will work even if the strings have placeholders, but no captures will be yielded to the blocks. * The error_handler plugin now has access to the request's remaining_path when handling an error. You can then compare the remaining_path to path_info to see how much of request was already routed, which can be useful when reporting errors. = Other Improvements * String matching for strings without placeholders is now 60% faster as it uses optimized string operations instead of a regexp match. * Symbol matching is now 60% faster as it uses optimized string operations instead of a regexp match. = Backwards Compatibility * The match methods no longer automatically reset the remaining_path via ensure. This means that using non-local jumps out of the code such as begin/rescue and throw/catch will not reset remaining_path automatically. Users that want to reset remaining path in such cases should use their own ensure blocks. jeremyevans-roda-4f30bb3/doc/release_notes/2.13.0.txt000066400000000000000000000010031516720775400222610ustar00rootroot00000000000000= New Features * The render plugin now supports :check_paths and :allowed_paths options. Setting :check_paths to true will turn on path checking of template files. By default, template files are required to be in the :views directory, otherwise an exception will be raised. Using the :check_paths option can prevent security issues when template names are derived from user input. The :allowed_paths option overrides which path prefixes are allowed. In Roda 3, :check_paths will default to true. jeremyevans-roda-4f30bb3/doc/release_notes/2.14.0.txt000066400000000000000000000025111516720775400222670ustar00rootroot00000000000000= New Features * A symbol_status plugin has been added for using symbolic status names in response.status=: class App < Roda plugin :symbol_status route do |r| r.is "needs_authorization" response.status = :unauthorized end end = Other Improvements * The middleware plugin will now also run the application's middleware when the application is used as middleware. For example, if you have the following code in your config.ru file: class App < Roda plugin :csrf plugin :middleware route{} end use App previously, the csrf protection would not be enforced, as it uses a middleware instead of being part of the application. Now, csrf protection will be enforced. This change makes it so the Roda application operates the same way regardless of whether it is run as the rack application or used as rack middleware. Because of this change, if you are nesting roda applications using the middleware plugin, you may need to use the middleware plugin's :env_var option to specify the environment variable used to indicate to the Roda application that it is being run as middleware. = Backwards Compatibility * See above changes to the middleware plugin if you are using middleware inside a Roda application that uses the middleware plugin. jeremyevans-roda-4f30bb3/doc/release_notes/2.15.0.txt000066400000000000000000000032771516720775400223020ustar00rootroot00000000000000= New Features * A public plugin has been added. This adds an r.public method for serving all files in the public directory. The public plugin can replace usage of the static plugin, and is more flexible. You can place r.public at any point in the routing tree, and it will use the remaining path to lookup the file in the public directory. You can also pass the :gzip option when loading the plugin, and it will serve already gzipped files to the client if the client supports gzipped transfer encoding and the file exists with a .gz extension. Example: plugin :public route do |r| # Serve public files before routing r.public # ... end * The :header matcher added by the header_matchers plugin now automatically prefixes the key with HTTP_ when looking it up in the environment, if the :header_matcher_prefix application option is set. This behavior will probably be the default in Roda 3. # Before r.on :header => 'http_accept' # Now, with :header_matcher_prefix=>true application option r.on :header => 'accept' * The content_for plugin now accepts an :append=>true option to support appending to the existing content instead of overwriting the existing content if called multiple times. This behavior will probably be the default in Roda 3. # Before content_for(:form, 'a') content_for(:form, 'b') content_for(:form) # => 'b' # Now, with :append=>true option content_for(:form, 'a') content_for(:form, 'b') content_for(:form) # => 'ab' = Other Improvements * The r.send_file method in the sinatra_helpers plugin now works correctly when using rack 2. * The specs now run correctly on Windows. jeremyevans-roda-4f30bb3/doc/release_notes/2.16.0.txt000066400000000000000000000027751516720775400223050ustar00rootroot00000000000000= New Features * A type_routing plugin has been added. This plugin allows routing based on the requested type, which can be submitted either via a file extension or Accept header: plugin :type_routing route do |r| r.get 'a' do r.html{ "

This is the HTML response

" } r.json{ '{"json": "ok"}' } r.xml{ "This is the XML response" } end end # /a or /a.html => HTML response # /a.json => JSON response # /a.xml => XML response The response content type is set appropriately when the r.html, r.json, or r.xml block is yielded to. Using plugin options, you can add support for custom types, and choose whether to use only file extensions or only Accept headers for type matching. * A request_headers plugin has been added. This allows easier access to request headers. For example, to access a header called X-My-Header, by default you would need to use the CGI mangled name: r.env['HTTP_X_MY_HEADER'] The request_headers plugin allows the easier to use: r.headers['X-My-Header'] * An unescape_path plugin has been added. By default, Roda does not unescape a URL-encoded PATH_INFO before routing. This plugin allows URL-encoded PATH_INFO to work, supporting %2f as well as / as path separators, and having captures return unescaped values: plugin :unescape_path route do |r| # Assume /b/a URL encoded at %2f%62%2f%61 r.on :x, /(.)/ do |*x| # x => ['b', 'a'] end end jeremyevans-roda-4f30bb3/doc/release_notes/2.17.0.txt000066400000000000000000000044251516720775400223000ustar00rootroot00000000000000= New Plugins * A run_append_slash plugin has been added, which automatically uses "/" as the path instead of an empty path when calling rack applications with r.run: route do |r| r.on "a" do r.run App end end # without run_append_slash: # GET /a => App gets "" as PATH_INFO # GET /a/ => App gets "/" as PATH_INFO # with run_append_slash: # GET /a => App gets "/" as PATH_INFO # GET /a/ => App gets "/" as PATH_INFO By default, the path is modified directly, but if you want to redirect instead, you can pass a :use_redirects option when loading the plugin. = New Features * The assets plugin now supports an :sri option to enable subresource integrity on the link/script tags generated by the assets helper. This option takes either :sha256, :sha384, or :sha512 values, specifying the hash algorithm to use. Roda 3 will default to using :sri => :sha256. * The assets plugin now supports a :postprocessor option, which should be a callable object. If the option is given, it will be called with the filename, type, and rendered asset output of the file (CSS/JS), and should return the postprocessed content to use. This allows any type of custom postprocessing to be done, such as CSS autoprefixing. * The render plugin now supports a :layout_opts=>:merge_locals option, which will automatically merge view template locals into layout template locals. This is useful if you want to use the same local variable for both templates. * The error_handler plugin now supports a :classes option, allowing you to override which exception classes to handle. This allows it to be used with libraries which use exception classes that subclass from Exception instead of StandardError. = Other Improvements * The type_routing plugin now works correctly when using r.run. Previously, if the type routing plugin recognized and removed the file extension used in the requested path, it would not add the file extension back to the path when passing the request to another rack app. = Backwards Compatibility * In the assets plugin, Roda::RodaRequest.assets_matchers now uses symbols instead of strings as the first argument in each entry. This should not affect you unless you were accessing the values directly. jeremyevans-roda-4f30bb3/doc/release_notes/2.18.0.txt000066400000000000000000000044611516720775400223010ustar00rootroot00000000000000= New Plugins * A static_routing plugin has been added, which can give a 3-4x increase in performance for large number of static routes, and makes routing O(1) for static routes. Static routes are routes that match full paths, with no placeholders, and are checked before using the normal routing tree. Static routes are defined via class-level static_* routing methods. There is a static_* routing method for each HTTP verb (e.g. static_get), as well as a static_route method, which will work for any HTTP verb, with the verb-specific method taking priority. By using static_route, you can get significantly faster performance while retaining some of the benefits of Roda's routing tree design (simple shared logic with verb specific behavior). Example: plugin :static_routing static_route '/foo' do |r| @var = :foo r.get do 'Not actually reached' end r.post{'static POST /#{@var}'} end static_get '/foo' do |r| 'static GET /foo' end route do |r| 'Not a static route' end Because static routing routes on the full path instead of by path segment, the methods takes the full path as a string, including the leading slash. * An assets_preloading plugin has been added, which makes it simple to generate HTML link tags or a Link header value to tell the browser to preload assets for performance reasons. # In routes, using the Link header: response.headers['Link'] = preload_assets_link_header(:css) # In templates, using a link tag: <%= preload_assets_link_tags(:css) %> = New Features * RodaRequest#real_remaining_path has been added. This is designed to be overridden by plugins that modify remaining_path for internal routing purposes. RodaRequest#run now uses real_remaining_path when passing requests to other rack applications. * An assets_paths method has been added to the assets plugin. This is similar to the assets method, but it returns an array of paths to the assets, instead of a HTML link/script tag. = Other Improvements * The public plugin now works correctly when used with the type_routing plugin, for paths ending in extensions that type_routing is configured to handle. * The head plugin now works with the not_allowed plugin if it is loaded after the not_allowed plugin. jeremyevans-roda-4f30bb3/doc/release_notes/2.19.0.txt000066400000000000000000000021751516720775400223020ustar00rootroot00000000000000= Improvements * The indifferent_params plugin is now optimized when using Rack 2, using Rack 2's query_parser API, and it no longer needs to do a deep copy of the params. * The Content-Type and Content-Length headers are no longer added for 1xx, 204, 205, and 304 responses. * The assets_paths method in the assets plugin now works correctly when subresource integrity is enabled. * The asset paths are now escaped in tags by the assets and assets_preloading plugins. While it's unlikely a developer would use an asset path that requires escaping, that case is now handled correctly. * The h plugin no longer calls Rack::Utils.escape_html, instead implementing it's own html escaping. * The assets plugin now uses the h plugin, instead of calling Rack::Utils.escape_html. = Backwards Compatibility * The h plugin's html escaping no longer escapes "/", which is a behavior change if you are using any recent version of rack. The security arguments made to escape "/" could be applied to many other characters, so if you want to escape "/", you should probably use a separate method that escapes all \W characters. jeremyevans-roda-4f30bb3/doc/release_notes/2.2.0.txt000066400000000000000000000055511516720775400222130ustar00rootroot00000000000000= New Plugins * A shared_vars plugin has been added, for sharing variables between multiple Roda apps. Example: class API < Roda plugin :shared_vars route do |r| user = shared[:user] # ... end end class App < Roda plugin :shared_vars route do |r| r.on :user_id do |user_id| shared[:user] = User[user_id] r.run API end end end If you pass a hash to shared, it will update the shared variables with the content of the hash: route do |r| r.on :user_id do |user_id| shared(:user => User[user_id]) r.run API end end You can also pass a block to shared, which will set the shared variables only for the given block, restoring the previous shared variables afterward: route do |r| r.on :user_id do |user_id| shared(:user => User[user_id]) do r.run API end end end * The partials method added by the padrino_render plugin has been extracted into a partials plugin, for users who want to use the partials method without having render use a layout by default. = Other New Features * The render plugin when using the :escape option now additionally supports an :escape_safe_classes option, which accepts a class or array of classes that should not be automatically escaped when using <%= %> tags. This makes easier to integrate with helpers or external libraries that return already html escaped strings, without using <%== %> tags. For complete control, an :escaper option is also supported, which will be used for escaping <%= %> strings. The value of this option should be an object that responds to escape_xml with a single argument and returns an output string. * A Roda#delay method has been added to the chunked plugin, which delays execution of the given block until right before the content template is rendered. This allows you to continue to use the routing tree to DRY up your code even when streaming template rendering. Example: r.on 'albums/:d' do |album_id| delay do @album = Album[album_id] end r.get 'info' do chunked(:info) end r.get 'tracks' do chunked(:tracks) end end * The path plugin now supports a :by_name option, which makes lookup of the class be by name as opposed to by reference. This is designed for use in development when doing code reloading. * Roda.path_block has been added to the path plugin for returning the block associated with the given class. = Other Improvements * The default_headers plugin now defaults to the same headers that Roda uses by default (just the Content-Type header with text/html value). Previously, it did not set a Content-Type header if you didn't specify one. * In the path plugin, Roda#path now works correctly in subclasses. jeremyevans-roda-4f30bb3/doc/release_notes/2.20.0.txt000066400000000000000000000003231516720775400222630ustar00rootroot00000000000000= New Features * The render plugin now supports :erubi as an :escape option value, which will change the plugin to use Erubi instead of Erubis as the template processor. Erubi is a simplified Erubis fork. jeremyevans-roda-4f30bb3/doc/release_notes/2.21.0.txt000066400000000000000000000010761516720775400222720ustar00rootroot00000000000000= New Features * The streaming plugin now supports a handle_stream_error method to handle exceptions when using stream(:loop=>true). This method takes the exception and the stream object, and can be used to log errors, output errors into the stream, close the stream, ignore errors, or any other error handling. = Other Improvements * A couple of unused variable assignments have been removed, providing a minor speedup. * The specs no longer produce deprecation warnings when using Minitest 5.10. * Some verbose warnings have been removed from the specs. jeremyevans-roda-4f30bb3/doc/release_notes/2.22.0.txt000066400000000000000000000027601516720775400222740ustar00rootroot00000000000000= New Features * An :unsupported_block_result => :raise option is now supported for Roda applications. This will raise a RodaError if an unsupported value is returned from a match block or the route block. This can make it easier to discover potential problems in the routing tree. This option may become the default behavior in Roda 3. * An :unsupported_matcher => :raise option is now supported for Roda applications. This will raise a RodaError if you use an unsupported value as a matcher. This can make it easier to discover potential problems in the routing tree. This option may become the default behavior in Roda 3. * A :verbatim_string_matcher option is now supported for Roda applications. This will make all string matchers only match the path verbatim, disallowing the use of a colon for placeholders in the string. It's recommended that users switch to using separate symbol arguments for placeholders. If you enable this option, you need to change code such as: r.is "foo/:bar" do |bar| end to: r.is "foo", :bar do |bar| end If you are looking to convert an existing routing tree from using placeholders in strings to separate symbol arguments, you can scan your routing tree for potential usage of placeholders in strings: grep ' r\..*['\''"].*:.*['\''"]' app.rb This option may become the default behavior in Roda 3, with a plugin added to support the current default behavior of allowing placeholders in string matchers. jeremyevans-roda-4f30bb3/doc/release_notes/2.23.0.txt000066400000000000000000000022261516720775400222720ustar00rootroot00000000000000= New Features * An :explicit_cache option has been added to the render plugin. This is similar to the :cache=>false option, but instead of disabling caching completely, this disables caching by default but allows for explicit caching of templates by providing the :cache option to view/render. In development mode, Roda now defaults to :explicit_cache=>true instead of :cache=>false. * An :inherit_cache option has been added to the render plugin, making subclasses of that class start with a dup of the template cache, instead of starting with an empty template cache. This can result in less memory used. * Roda#error_email in the error_email plugin now accepts non-Exception arguments (such as strings). This can be useful in conditions that are errors you may want to notify about, where an exception hasn't been raised. * Roda#error_email_content has been added to the error_email plugin. This can be used to create the email message, which can be delivered via another mechanism, and may make testing easier. = Other Improvements * Roda.freeze in the static_routing plugin now returns self, fixing code such as Roda.freeze.app. jeremyevans-roda-4f30bb3/doc/release_notes/2.24.0.txt000066400000000000000000000046171516720775400223010ustar00rootroot00000000000000= New Features * The middleware plugin now accepts a block that can be used to implement configurable middleware. This allows you to load the Roda application as middleware in another application, and provide options/block that are passed to the block you passed when loading the middleware plugin. Example: class Mid < Roda plugin :middleware do |middleware, *args, &block| middleware.opts[:middleware_args] = args block.call(middleware) end end class App < Roda use Mid, :foo, :bar do |middleware| middleware.opts[:middleware_args] << :baz end end Note that when passing a block when loading the middleware plugin, using the middleware in another rack application will actually load a subclass of the middleware (Mid in the example above). This allows you to use the Roda middleware multiple times in the same process with different configurations. * The cookies plugin now accepts options that are used as the default options when setting and deleting cookies: plugin :cookies, :path => '/foo', :domain => 'example.com' * A strip_path_prefix plugin has been added, which can be used to strip prefixes from internal paths. Internally Sequel stores most paths as absolute paths, but there are cases where this doesn't work well, such as symlink changes and chroot. This plugin supports those scenarios. * A disallow_file_uploads plugin has been added, which raises an exception if the user attempts a multipart file upload. More exactly, it makes the application raise an exception if attempting to parse a request body when the user has submitted a multipart file upload. This is useful if you don't need to support file uploads in your application, or cases where no paths are writable by the application (due to chroot or system call filtering). * The :freeze_middleware option has been added, which freezes all middleware instances when building the rack application. This can help find thread safety issues in middleware. = Other Improvements * The h plugin is now about 6x faster on ruby 2.3+ due to the use of cgi/escape. * The static_routing plugin now works with the hooks plugin if the hooks plugin is loaded first. * The render plugin's render cache is no longer cleared when loading the plugin multiple times. = Backwards Compatibility * The h plugin now escapes ' as ' instead of '. jeremyevans-roda-4f30bb3/doc/release_notes/2.25.0.txt000066400000000000000000000010261516720775400222710ustar00rootroot00000000000000= New Features * An error_mail plugin has been added for reporting exceptions raised via email. This is similar to the existing error_email plugin, but uses the mail library instead of net/smtp directly. If you are already using the mail library and the error_email plugin in your application, it's recommended to switch to the error_mail plugin. Example: plugin :error_mail, :to=>'to@example.com', :from=>'from@example.com' plugin :error_handler do |e| error_mail(e) 'Internal Server Error' end jeremyevans-roda-4f30bb3/doc/release_notes/2.26.0.txt000066400000000000000000000007551516720775400223020ustar00rootroot00000000000000= New Features * The csrf plugin now supports a :skip_middleware option, which adds the methods without adding the middleware. This is designed for cases where you are using multiple rack apps, where the rack_csrf middleware is loaded in an earlier rack app, and you want to avoid the duplicate CSRF checks. = Other Improvements * The type_routing plugin now supports using multiple extensions where one extension is a suffix of another extension, such as using gz and tar.gz. jeremyevans-roda-4f30bb3/doc/release_notes/2.27.0.txt000066400000000000000000000035101516720775400222730ustar00rootroot00000000000000= New Features * String and Integer class matchers have been added. The String class matches any non-empty segment and yields it as a string. This is the same as the behavior of the symbol matchers, but without the duplication. So instead of: r.is "album", :album_name do |album_name| end you can now do: r.is "album", String do |album_name| end This makes it a bit more intuitive that you want to match any string, and avoids the redundancy between the symbol name and block argument name. The Integer class matches any integer segment (\d+) and yields it as an integer: r.is "album", Integer do |album_id| # does not match "/albums/foo" # matches "/albums/1", yielding 1 (not "1") end Previously, the :d matcher in the symbol_matchers plugin could be used to only match integer segments, but it yielded results as strings and not integers, so you still needed to convert the type manually. Using Integer is a bit more intuitive than using :d, and it handles the type conversion for you. * A class_matchers plugin has been added for matching additional classes, with user-specified regexps and type conversion. For example, if you want to match YYYY-MM-DD segments and yield them to the match blocks as ruby Date objects, you can do: plugin :class_matchers class_matcher(Date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| [Date.new(y.to_i, m.to_i, d.to_i)] end and then in your routing tree, you can do: r.on "posts", Date do |date| # does not match "/posts/foo" or "/posts/2017-01" # matches "/posts/2017-01-13", yielding Date.new(2017, 1, 13) end = Backwards Compatibility * If you were using the Integer and String classes as matchers before and expected them to always match, you'll need to change your code to use true instead. jeremyevans-roda-4f30bb3/doc/release_notes/2.28.0.txt000066400000000000000000000010551516720775400222760ustar00rootroot00000000000000= New Features * A status_303 plugin has been added, which changes the default redirect status from 302 to 303 if the HTTP version is 1.1 and the request is not a GET request. = Other Improvements * Roda is now optimized for ruby 2.3+ using frozen string literals instead of constant references. This improves performance on ruby 2.3+, and decreases performance on ruby <2.3. = Backwards Compatibility * Many now unused internal constants are now deprecated, and attempting to access them will result in deprecation warnings on ruby 2.3+. jeremyevans-roda-4f30bb3/doc/release_notes/2.29.0.txt000066400000000000000000000145531516720775400223060ustar00rootroot00000000000000= Deprecated Features Roda 2.29.0 will be the last minor release of Roda 2. Roda 3.0.0 will be released next month and will remove support for the following deprecated features. All of these features will have deprecation warnings if used in Roda 2.29.0. * The use of placeholders in string matchers is now deprecated. So code such as: r.get "users/:user_id" do |id| end should be switched to using a class matcher such as String or Integer: r.get "users", Integer do |id| end or a symbol matcher: r.get "users", :user_id do |id| end If you really want to keep support for placeholders in string matchers, the support is available in the new placeholder_string_matchers plugin. * The :format, :opt, and :optd default symbol matchers are now deprecated in the symbol_matchers plugin. These matchers only made sense when placeholder string matchers are used, which will no longer be the default behavior in Roda 3. These methods can be defined manually if you are going to use the placeholder_string_matchers plugin and still want to use these symbol matchers: symbol_matcher(:format, /(?:\.(\w+))?/) symbol_matcher(:opt, /(?:\/([^\/]+))?/) symbol_matcher(:optd, /(?:\/(\d+))?/) * Ignoring unsupported match block return values is now deprecated. Doing so can hide errors and make debugging more difficult. If you get a deprecation warning related to this, just make sure the match block returns nil or false to specify the match block return value should be ignored. * Treating unsupported matchers as always matching is now deprecated. Doing so can hide errors and make debugging more difficult. If you get a deprecation warning related to this, switch the matcher to true instead of an unsupported object. * The render plugin's handling of plugin level locals and merging of template and layout locals is now deprecated. Users of these features should switch to the new render_locals plugin. * The view_options plugin's handling of per-branch view and layout locals is now deprecated. Users of these feature should switch to the new branch_locals plugin. * The render plugin's support for Erubis escaping is now deprecated. In Roda 3, the render plugin :escape option will use Erubi escaping. Switch to using :escape=>:erubi temporarily to avoid the deprecation warning. * Using the render plugin to render a template that is outside one of the allowed paths is now deprecated unless the :check_paths option has been set to false. In Roda 3, the default behavior will change to checking that template files are in one of the allowed paths. * The :ext option in the render plugin is now deprecated, users should switch to using the :engine option, which has always had priority. * Using the :cache=>true option to the view/render method in the render plugin is now deprecated if the :cache=>nil/false option was given when loading the plugin. In Roda 3, the default behavior will change so that the :cache=>nil/false plugin option still allows caching via the :cache=>true method option. Users can use the :explicit_cache=>true render plugin option instead of the :cache=>nil render plugin option to work around the deprecation warning. * Attempting to use multi_route while routing with a namespace that hasn't yet been defined is now deprecated. The previous behavior was to ignore undefined namespaces, but that is more likely to hide an error than be desired behavior. In Roda 3, using an undefined namespace will raise an error. * The streaming plugin's support for EventMachine is now deprecated, as is related support for Stream#callback. The streaming plugin will be much simpler in Roda 3 by dropping this support. * Calling content_for in the content_for plugin multiple times with the same argument is now deprecated unless the content_for plugin :append option is used to specify behavior. The default behavior in Roda 3 will change to appending to the existing content instead of overwriting the existing content. * The :host matcher in the header_matchers plugin is now deprecated when using a regexp value unless the :host_matcher_captures app option is used. In Roda 3, the :host matcher will automatically yield any regexp captures to the match block. * The :header matcher in the header_matchers plugin is now deprecated unless the :header_matcher_prefix app option is used. In Roda 3, the :header matcher will always prefix the argument given with HTTP_. * The websockets plugin is now deprecated. It was one of the less commonly used plugins, and the tests for it were subject to race conditions and failed occassionally, and even when they worked they almost doubled the testing time. Anyone wanting to use it should consider maintaining it as an external plugin. * The per_thread_caching, static_path_info, and view_subdirs plugins are now deprecated. static_path_info has been a no-op since Roda 3, view_subdirs is just an alias for view_options, and per_thread_caching doesn't change behavior and is unlikely to significantly increase performance. * Additional internal constants are now deprecated. Deprecation warnings for accessing these constants will only be displayed on ruby 2.3+. = Forward Compatibility Roda 3.0.0 will also include some behavior changes which will not have deprecation warnings: * Ruby 1.8.7 support will be dropped. Ruby 1.9.2 will be the new minimum supported version. * Subclassing a Roda app that uses the render plugin will always use a copy of the superclass's template cache. * The assets plugin will default to using subresource integrity using SHA256 for compiled assets, and using SHA256 instead of SHA1 for compiled asset hashes. * Using an Roda app as middleware will now always use a subclass of the app for the middleware. * public_send will be used instead of send internally unless it is expected that private methods will be called. * The match methods added by the symbol_matchers and hash_matchers plugins will be private instead of public. = New Features * The render plugin now has the :layout_opts=>:views plugin option respect the :root app option. * RodaPlugins::OPTS and RodaPlugins::EMPTY_ARRAY have been added. These are a frozen empty hash and a frozen empty array, and they are designed for use in plugins so that similar objects are not needed to be defined separately in each plugin. jeremyevans-roda-4f30bb3/doc/release_notes/2.3.0.txt000066400000000000000000000106771516720775400222210ustar00rootroot00000000000000= New Plugins * A json_parser plugin has been added, for parsing request bodies in JSON format. This is faster than using a middleware to perform the same task. This plugin supports a :parser option to use a custom JSON parser, an :include_request option to include the request when calling the parser, and a :error_handler option for a proc to call with the request if there is an error when parsing. Example: plugin :json_parser, :parser=>JSON.method(:parse), :include_request=>false, :error_handler=>proc{|r| r.halt [400, {}, []]} * A path_rewriter plugin has been added, allowing for the rewriting of paths before routing. This allows you to rewrite just the routing path (the default), or PATH_INFO as well as the routing path (if the :path_info option is used). This is useful if you want to internally treat one path exactly the same as another path. By default, path rewriting is done on prefixes, so any path that starts with the prefix will be rewritten. You can pass a Regexp when rewriting the path for more complete control. Examples: plugin :path_rewriter rewrite_path '/a', '/b' # GET /a treated as GET /b # GET /a/c treated as GET /b/c rewrite_path /\A\/c\z/, '/d' # GET /c treated as GET /d # GET /c/e no change * A precompiled_templates plugin has been added, for precompiling templates before starting the application. This can save a substantial amount of memory if you are using large templates or a large number of small templates in conjunction when using application preloading with a forking webserver. Example: plugin :precompile_templates precompile_templates "views/\*\*/*.erb" precompile_templates "views/users/_*.erb", :locals=>[:user] * A heartbeat plugin has been added, for easily handling heartbeat/status requests. If a heartbeat/status request comes in, it will get a 200 response with a body of "OK". This is designed for automated systems that check if the application is functioning. The default heartbeat path is /heartbeat, but you can choose a different one using the :path option. plugin :heartbeat, :path=>'/heartbeat' = Other New Features * The json plugin now supports a :serializer option to use a custom serializer. Additionally, it now supports a :include_request option to include the request when calling the serializer. * In the render plugin, the render/view methods now support a :cache=>false option to not cache the template. This can be useful for large but rarely used templates, or where a new template object is created for every render/view call. * In the render plugin, the render/view methods now support a :cache_key option to force a specific cache key. Manually setting cache keys can result in improved performance, as automatically determining the cache key can be a relatively expensive operation. * The render plugin now supports a :engine_opts option, to specify per-template engine options. :engine options should be a hash keyed by render engine strings, with values being hashes of template options. * In the mailer plugin, a no_mail! method is now supported when mailing, which will skip the current mail. This makes it easier to delay the decision about actually sending the email till it is time to send the email, which makes it easier to avoid race conditions if you are using a job queue for mailing. = Other Improvements * Roda avoids rehashing hashes at runtime in some places, for a minor speedup. * If the :template_block is given to render/view, default to not caching the template, since it is likely the template block is specific to the request. Allow for the :cache=>true option to be used to force the caching of the template. * Roda now returns a 404 response for unmatched GET requests when using the assets and json plugins where r.assets is the last method called in a route block. = Backwards Compatibility * In the render plugin, the :ext option to the plugin and to the render/view methods is now replaced by the :engine option. Previously, :engine was used by default if :ext was not given. In general, there is no need for two separate options, the engine is used as the extension by Tilt. In general, this is a backwards compatible change, except when both :ext and :engine were specified differently as plugin options, and an inline template is used with render or view without either the :ext or :engine options being specified. jeremyevans-roda-4f30bb3/doc/release_notes/2.4.0.txt000066400000000000000000000031521516720775400222100ustar00rootroot00000000000000= New Plugins * A websocket plugin has been added, for websocket support using faye-websocket. Here's an example of a simple echo service using websockets: plugin :websockets route do |r| r.get "echo" do r.websocket do |ws| # Routing block taken for a websocket request to /echo # ws is a Faye::WebSocket instance, so you can use the # Faye::WebSocket API ws.on(:message) do |event| ws.send(event.data) end end # View rendered if a regular GET request to /echo view "echo" end end * A status_handler plugin has been added, which allows Roda to specially handle arbitrary status codes. Usage is similar to the not_found plugin (which now uses status_handler internally): plugin :status_handler status_handler 403 do "You are forbidden from seeing that!" end status_handler 404 do "Where did it go?" end = Other New Features * The assets plugin now supports a :gzip option, which will save gzipped versions when compiling assets. When serving compiled assets, if the request accepts gzip encoding, it will serve the gzipped version. This also plays nicely with nginx's gzip_static support. * The assets plugin now supports Google Closure Compiler, Uglifier, and MinJS for minifying javascript. You can now specify which css and js compressors to use via the :css_compressor and :js_compressor options. = Backwards Compatibility * Roda.plugin now always returns nil. Previously the return value could be non-nil if the plugin used a configure method. jeremyevans-roda-4f30bb3/doc/release_notes/2.5.0.txt000066400000000000000000000015571516720775400222200ustar00rootroot00000000000000= New Features * The assets plugin now supports a :compiled_asset_host option, which specifies a hostname used to serve compiled assets. * The render plugin now supports a :cache_class option, which specificies a class to use for the thread-safe template cache. This can be used to setup LRU caching or caching that checks modify times on the underlying template files. * r.multi_run in the multi_run plugin now accepts a block, and calls the block before dispatching to the related rack application. This can be used to modify the environment before dispatching. Example: r.multi_run do |prefix| env['authenticated'] = true end = Backwards Compatibility * The :by_name option to the path plugin now defaults to true in development mode. This should only negatively affect applications that register anonymous classes with the path plugin. jeremyevans-roda-4f30bb3/doc/release_notes/2.5.1.txt000066400000000000000000000001541516720775400222110ustar00rootroot00000000000000= Improvements * The multi_route plugin now works correctly if the middleware plugin is loaded after it. jeremyevans-roda-4f30bb3/doc/release_notes/2.6.0.txt000066400000000000000000000013721516720775400222140ustar00rootroot00000000000000= New Features * :params and :params! matchers have been added to the param_matchers plugin, allowing you to match multiple params at the same time: r.on :params=>%w'foo bar' do |foo, bar| end # instead of r.on({:param=>'foo'}, :param=>'bar') do |foo, bar| end = Other Improvements * When loading the csrf plugin multiple times, instead of loading the middleware multiple times with different settings, merge options in later plugin calls into a single middleware option hash, and only load the middleware once. This allows plugins to depend on the csrf plugin, while also allowing the application to use the csrf plugin with options. * request.halt now works correctly when used inside a before hook when using the hooks plugin. jeremyevans-roda-4f30bb3/doc/release_notes/2.7.0.txt000066400000000000000000000052131516720775400222130ustar00rootroot00000000000000= New Features * A default_status plugin has been added for changing the default status for responses. Previously, the default status was hard coded to 200, this plugin allows you to change it. The plugin takes a block which is instance_evaled in the context of the response: plugin :default_status do headers['Content-Type'] == 'foo' ? 201 : 200 end Note that the default status for empty responses (used when no route handles the response) is still 404, this just changes the default for non-empty responses. * A response_request plugin has been added for giving the response instance access to the related request. This can be useful in conjunction with the default_status plugin, if you want the default status of the response to depend on the request, such as using a different status for different request methods: plugin :response_request plugin :default_status do request.post? ? 201 : 200 end * A run_handler plugin has been added, for modifying rack response arrays before returning them when using r.run. Additionally, it allows for continuing with routing if the response returned by r.run is a 404 response, using the :not_found=>:pass option: plugin :run_handler route do |r| # Keep running code if RackAppFoo returns a 404 response r.run RackAppFoo, :not_found=>:pass # Change response status codes before returning. r.run(RackAppBar) do |response| response[0] = 200 if response[0] == 201 end end * Roda.rewite_path in the path_rewriter extension now accepts a block to allow for dynamic replacements. The block is yielded a MatchData instance: rewrite_path(/\A\/a/(\w+)/){|match| match[1].capitalize} # PATH_INFO '/a/moo' => remaining_path '/a/Moo' rewrite_path(/\A\/a/(\w+)/, :path_info => true) do |match| match[1].capitalize end # PATH_INFO '/a/moo' => PATH_INFO '/a/Moo' * The :host matcher in the header_matchers plugin will now yield the regexp captures to the block if given a regexp when the :host_matcher_captures application option is set. This behavior will become the default behavior in Roda 3. This will allow for code like: opts[:host_matcher_captures] = true route do |r| r.on :host=>/\A(\w+).example.com\z/ do |subdomain| # ... end end = Other Improvements * RodaCache now uses a mutex to synchronize access on MRI. Previously, it relied on the global interpreter lock, but testing has shown that is not reliable in all cases. RodaCache has always used a mutex for synchronization on other ruby implementations, this just extends that code to MRI as well. jeremyevans-roda-4f30bb3/doc/release_notes/2.8.0.txt000066400000000000000000000013401516720775400222110ustar00rootroot00000000000000= New Features * A multi_view plugin has been added, for easily setting up routing for rendering multiple views: plugin :multi_view route do |r| r.multi_view(['foo', 'bar', 'baz']) end # or: route do |r| r.multi_view(/(foo|bar|baz)/) end # or: regexp = multi_view_compile(['foo', 'bar', 'baz']) route do |r| r.multi_view(regexp) end # all are equivalent to: route do |r| r.get 'foo' do view('foo') end r.get 'bar' do view('bar') end r.get 'baz' do view('baz') end end = Other Improvements * The content_for plugin now supports haml templates. Previous only erb templates were supported. jeremyevans-roda-4f30bb3/doc/release_notes/2.9.0.txt000066400000000000000000000002411516720775400222110ustar00rootroot00000000000000= New Features * The content_for plugin now supports passing the content as a string argument instead of a block: <% content_for :foo, "Some content" %> jeremyevans-roda-4f30bb3/doc/release_notes/3.0.0.txt000066400000000000000000000053771516720775400222200ustar00rootroot00000000000000= Major Changes * String matchers now match literally by default, for simplicity, understandability, and performance. Use the String class matcher or a symbol matcher to match arbitrary segments. # Before r.is "artists/:name" do |artist_name| end # Now r.is "artists", String do |artist_name| end # or r.is "artists", :name do |artist_name| end You can use the placeholder_string_matchers plugin to restore the historical behavior if you don't want to modify your routes. * Using an unsupported matcher now raises an error, making it more likely to detect using a unexpected value as a matcher (which previously matched everything). * Have a route/match block return an unsupported value now raises an error if nothing has been written to the body, making it more likely to detect using an unexpected value as a block result (which previously was ignored). = Backwards Compatibility * Deprecated plugins, features, and constants have been removed. Before upgrading to 3.0.0, please upgrade to 2.29.0 first and fix any deprecation warnings. * Ruby 1.8.7 support has been dropped. Ruby 1.9.2 is the new minimum supported version. * The :check_paths render plugin option now defaults to true so that generated template paths are checked by default, reducing the risk of rendering arbitrary files. * The assets plugin now defaults to using subresource integrity with SHA256 for compiled assets, and using SHA256 instead of SHA1 for compiled asset hashes. * Subclassing a Roda app that uses the render plugin now always uses a copy of the superclass's template cache. * Using a Roda app as middleware now always uses a subclass of the app for the middleware. * public_send is now used instead of send internally unless it is expected that private methods will be called. * The match methods added by the symbol_matchers and hash_matchers plugins are now private instead of public. = Other Improvements * The streaming plugin has been greatly simplified, by dropping deprecated compatibility for EventMachine. * It is now possible to reset the :include_request option to false in the json and json_parser plugins by loading the plugin a second time with the option set. * The precompile_templates plugin now always sorts locals. This plugin should now be used with Tilt 2.0.1+ (which also sorts locals), though it will still work with earlier Tilt versions. * The multi_run plugin now recomputes the regexp when freezing the app. = Deprecated Features These features will be removed in Roda 3.1.0: * Roda.thread_safe_cache is now deprecated. RodaCache is now used as the thread-safe cache class. * RodaRequest#placeholder_string_matcher? (private method) is now deprecated and always returns false. jeremyevans-roda-4f30bb3/doc/release_notes/3.1.0.txt000066400000000000000000000015661516720775400222150ustar00rootroot00000000000000= New Features * A :timestamp_paths option has been added to the assets plugin to include timestamps in paths in non-compiled mode. This can fix asset staleness issues when using a caching proxy. This is not needed in compiled mode, as the asset file names include the hash of the asset. It is not the default in non-compiled mode, as few people would use a caching proxy in non-compiled mode. = Other Improvements * Make set_layout_locals and set_view_locals in branch_locals plugin work when the other is not called. * When testing support for uglifier usability as a JS asset compressor, handle case where uglifier is installed but there is no available javascript runtime. = Backwards Compatibility * The deprecated Roda.thread_safe_cache method has been removed. * The deprecated private RodaRequest#placeholder_string_matcher? method has been removed. jeremyevans-roda-4f30bb3/doc/release_notes/3.10.0.txt000066400000000000000000000141641516720775400222730ustar00rootroot00000000000000= New Features * A sessions plugin has been added that supports encrypted and signed sessions. This plugin is now the recommended way to implement sessions, replacing the previously recommended Rack::Session::Cookie middleware. The sessions plugin encrypts session data using the AES-256-CTR cipher, and then signs the encrypted data with HMAC-SHA-256. By doing this, attackers must be able to forge a valid HMAC before they can try to exploit possible weaknesses in the encryption, such as timing attacks during decryption that are dependent on attacker chosen initialization vectors or ciphertext. In addition to encryption and a stronger default signature algorithm compared to Rack::Session::Cookie, the sessions plugin has the following benefits: * Built in session expiration enabeld by default, to mitigate possible session replay issues (default: 30 days since session creation, 7 days since last update). * Padding by default to minimize information leakage due to differing session data sizes (session data padded to a multiple of 32 bytes by default before encryption). * Automatic deflate compression of large sessions before encryption (by default if session data is over 128 bytes). * JSON is used for serialization instead of Marshal, preventing remote code execution vulnerabilities if the session secret is disclosed. Note that this means that many ruby types do not round trip in the session, such as Symbol and Time instances. This will probably be the largest barrier to adoption, as you need to make sure your application only uses types that round-trip through JSON before you start using the sessions plugin. * A plain hash is used for the session, instead of a hash-like object. One consequence of this is that keys in the session are not automatically converted to strings. Rack::Session::Cookie converts session keys to strings for keys at the top level, but not for keys in subhashes. * In general sessions are smaller even if deflate compression is not used, despite requiring 16 bytes for the cipher initialization vector. The main reason for this is that the sessions plugin does not set a session id, since one is not needed for cookie sessions. * The sessions plugin requires a :secret option be set that is at least 64 bytes, so that users have to make a determined effort to use weak secrets. * The HMAC calculation considers the cookie key, so that if the same session secret is used for multiple applications with different cookie keys, an attacker cannot use the session from one application in a different application. The sessions plugin ties into the Roda#session method instead of being a rack middleware. This makes it about twice as fast as Rack::Session::Cookie if the session is not accessed. If the session is accessed, the sessions plugin is roughly as fast as Rack::Session::Cookie, even though it uses a stronger HMAC and has to encrypt and decrypt the session. Because the sessions plugin is not a middleware, it does not offer session support to other middleware, only to the app itself. If you would like to use the same approach as the sessions plugin uses but would like support for middleware to access the sessions, a roda/session_middleware file has been added. This file contains RodaSessionMiddleware, which is a middleware that can be used by any other Rack app for session support, and which uses a SessionHash class similar to the one used by Rack::Session::Cookie. To integrate with other plugins that can optionally use symbols or strings in sessions, the sessions plugin sets the :sessions_convert_symbols application option to true. Other plugins can check for this application option, and if set, should use strings instead of symbols in the session. The sessions plugin should be loaded after the flash plugin if both are used in the same application, so that the flash is rotated correctly in the session. * The middleware plugin now supports a :handle_result option, which can be any callable object. If set, this object is called with the environment of the request and the rack response after either the Roda app or next middleware returns the rack response. The rack response can be modified by the callable object, and the response (after possible modification) will be returned to the previous middleware. Example: plugin :middleware, :handle_result=>(proc do |env, res| res[1]['MyHeader'] = 'HeaderValue' end) * The :json_parser and :json_serializer application options are now supported. If set, these options are used for parsing and serializing JSON instead of the default of JSON.parse and .to_json. = Other Improvements * RodaRequest initialization is now faster by avoiding 1-2 method calls. * typecast_params.Integer in the typecast_params plugin now handles numeric input as long the numeric input does not have fractional parts. This makes it more usable when handling JSON input. * If the flash is empty after the request is processed, the flash session key is removed from the session instead of being left as an empty hash. If addition to making the session smaller, this makes the session appear empty if there are no other keys in the session, which works better with the sessions plugin as empty sessions will remove the session cookie completely. = Backwards Compatibility * The flash plugin now uses '_flash' instead of :_flash as the session key. When using session middleware that uses Rack::Session::Abstract::SessionHash to store the session (e.g. Rack::Session::Cookie), session keys are converted internally to strings, so this change will not affect you unless you are using alternative session support. Even if your session does treat :_flash different than '_flash' in keys, the plugin will still work because it will try :_flash if there is no value for '_flash'. This change was made to support the sessions plugin, which doesn't convert keys to strings. * This DEFAULT_PARSER and DEFAULT_SERIALIZER constants from the the json_parser and json plugins have been removed. jeremyevans-roda-4f30bb3/doc/release_notes/3.100.0.txt000066400000000000000000000024151516720775400223470ustar00rootroot00000000000000= New Features * A sec_fetch_site_csrf plugin has been implemented, which implements CSRF protection using the Sec-Fetch-Site header. This offers weaker CSRF protection than the route_csrf plugin, but doesn't require CSRF tokens in forms. Other caveats when using the plugin: * Not all browsers set the Sec-Fetch-Site header. Some popular browsers did not add support until 2023. * Sec-Fetch-Site is only set on HTTPS requests, not on HTTP requests, so if you need to support HTTP requests, you cannot rely on it. * There is no support for cross-site secure CSRF protection by sharing the token used. Like the route_csrf plugin, the sec_fetch_site_csrf plugin exposes a method (check_sec_fetch_site!) that you can call at the appropriate point in your routing tree to enforce the CSRF protection. By default, only same-origin requests are allowed by default. Using plugin options, you can support same-site or none requests, or support requests where the header is not present. For CSRF violations, the default is to raise an exception. You can use plugin options to either return a blank 403 page or clear the current session. You can also pass a block to either the plugin or to the check_sec_fetch_site! method for custom handling. jeremyevans-roda-4f30bb3/doc/release_notes/3.101.0.txt000066400000000000000000000003251516720775400223460ustar00rootroot00000000000000= New Features * A bearer_token plugin has been added for retrieving a bearer token (if provided) from the HTTP Authorization header: # HTTP Header: Authorization: Bearer foo r.bearer_token # => "foo" jeremyevans-roda-4f30bb3/doc/release_notes/3.102.0.txt000066400000000000000000000012731516720775400223520ustar00rootroot00000000000000= New Features * A response_attachment plugin has been extracted from the sinatra_helpers plugin, allowing the use of the response.attachment method without having to include the rest of sinatra_helpers: response.attachment "a.csv" # content-disposition: attachment; filename="a.csv" # content-type: text/csv * A send_file plugin has been extracted from the sinatra_helpers plugin, allowing the use of send_file without having to include the rest of sinatra_helpers. This allows you to return the content of a file as the response. It also sets the content-disposition and content-type headers as appropriate based on the file extension: send_file 'path/to/file.txt' jeremyevans-roda-4f30bb3/doc/release_notes/3.103.0.txt000066400000000000000000000012331516720775400223470ustar00rootroot00000000000000= New Features * An ip_from_header plugin has been added, which makes request.ip pull the IP address from a header if it is present. If you know that all requests are coming from a proxy, this is simpler and faster than attempting to parse the information out of the Forwarded or X-Forwarded-For headers. * A hash_public plugin has been added, which operates similarly to the timestamp plugin plugin, but does cache busting based on the hash of the file content instead of the modification time of the file. This is useful in containerized environments where the modification time of the file may change even if the content of the file does not. jeremyevans-roda-4f30bb3/doc/release_notes/3.11.0.txt000066400000000000000000000037711516720775400222760ustar00rootroot00000000000000= Improvements * The order in which internal plugin before and after hooks are run when multiple plugins are loaded is now fixed and does not depend on the order in which the plugins are loaded. This can prevent some issues in cases the plugins were not loaded in the order previously recommended in the documentation. Internal plugin before hooks are now run in the following order: * hooks * heartbeat * static_routing and internal plugin after hooks are now run in the following order: * class_level_routing * status_handler * head * flash * session * hooks * Default compression of sessions over 128 bytes in length has been disabled in the sessions plugin. Compression of sessions must now be manually enabled if it is desired by setting :gzip_over to an integer. This change is being made to avoid possible compression ratio attacks if both sensitive data and user-submitted data are stored in the session. Such attacks were mitigated by the sessions plugin's default use of padding after compression, and the JSON serialization format used, but disabling compression avoids the possibility. This does not affect backwards compatibility, as compressed sessions will still be decompressed correctly, unless the size of the session cookie when not using compression is over 4096 bytes. = Backwards Compatibility * When using the error_handler plugin, if routing raises an exception that is handled by the error handler, but an exception is raised by a plugin internal after hook after the error handler has been run, the exception will be logged to the rack.errors entry in the environment, but it will be otherwise ignored. Exceptions raised inside the error handler will continue to be be raised to the application's caller. Additionally, the error_handler plugin no longers call before hooks during error handling. * A private Roda#_call method has been added. This could potentially cause issues for applications that add their own _call method. jeremyevans-roda-4f30bb3/doc/release_notes/3.12.0.txt000066400000000000000000000014251516720775400222710ustar00rootroot00000000000000= New Features * A common_logger plugin has been added for common log support. This offers about 30% better performance than Rack::CommonLogger, with the following differences: * When timing requests, doesn't consider middleware or proxy the body, so timing information is just the time that Roda takes to process the request. * Only looks for "Content-Length" as a header, not different capitalizations (Roda only uses "Content-Length" internally). * Logs to $stderr instead of rack.errors in request environment if a logger object is not explicitly passed. = Other Improvements * Internal before/after hook methods now use more descriptive names for easier debugging, with a naming format designed to not conflict with hook methods in external plugins. jeremyevans-roda-4f30bb3/doc/release_notes/3.13.0.txt000066400000000000000000000031341516720775400222710ustar00rootroot00000000000000= New Features * An exception_page plugin has been added for displaying debugging information for a given exception. It is based on Rack::ShowExceptions, with the following differences: * Not a middleware, so it doesn't handle exceptions itself, and has no effect on the callstack unless the exception_page method is called. * Supports external javascript and stylesheets, allowing context toggling to work in applications that use a content security policy to restrict inline javascript and stylesheets (:assets, :css_file, and :js_file options). * Has fewer dependencies (does not require ostruct and erb). * Sets the Content-Type for the response, and returns the body string, but does not modify other headers or the response status. * Supports a configurable amount of context lines in backtraces (:context option). * Supports optional JSON formatted output, if used with the json plugin (:json option). Because this plugin just adds a method you can call, you can selectively choose when to display a debugging page and when not to, as well as customize the debugging parameters on a per-call basis (such as returning JSON formatted debugging information for JSON requests, and HTML formatted debugging information for normal requests). = Other Improvements * The common_logger plugin now correctly handles cases where an exception is being raised and there is no rack response to introspect. = Backwards Compatibility * Stream#write in the streaming plugin now returns the number of bytes written instead of self, so it works with IO.copy_stream. jeremyevans-roda-4f30bb3/doc/release_notes/3.14.0.txt000066400000000000000000000021211516720775400222650ustar00rootroot00000000000000= New Features * The convert! and convert_each! methods in the typecast_params plugin now support a :raise option for handling missing parameters specified as arguments to the methods. If the :raise option is set to false for convert! and the parameter argument is missing, then no conversion is done and an empty hash is returned: typecast_params.convert!('missing', raise: false) do |tp| # ... end # => {} If the :raise option is set to false for convert_each! and a :keys option is given, any key not present is ignored and nil will be returned for the converted value typecast_params.convert_each!(:keys=>['present', 'missing'], raise: false) do |tp| tp.int('b') end # => [{'b'=>1}, nil] = Other Improvements * The :symbolize setting to the convert! and convert_each! methods in the typecast_params plugin is no longer persisted beyond the call to the method. This fixes unexpected behavior if you do: typecast_params.convert!(:symbolize=>true) do |tp| # ... end typecast_params.convert! do |tp| # ... end jeremyevans-roda-4f30bb3/doc/release_notes/3.14.1.txt000066400000000000000000000040321516720775400222710ustar00rootroot00000000000000= Security Fix * Do not post-process content_for block result with template engine Since 2.8.0, the content_for block result was post-processed with the template engine. There is no actual need to do so, as content_for is not designed to render output, it is designed to store already rendered output. This post-processing was introduced when support for haml templates was added in 2.8.0. Post-processing the output with the template engine is generally a no-op for most usage as most output does not contain template metaprogramming characters, which is why this went undetected for so long. However, if a content_for block return value contained unescaped user input, it was probably vulnerable to remote code execution if the default ERB template engine is used, the same as if the user input was passed directly to the render or view method. Example of a vulnerable usage (assuming automatic escaping is not enabled) would be: <% content_for :foo do %> User name: <%= request.params['user_name'] %> <% end %> Such usage is likely vulnerable to cross site scripting unless the content_for output is escaped before being displayed, even without the content_for template post-processing. However, the post-processing turned it from a cross site scripting vulnerability into a remote code execution vulnerability. For non-ERB template engines, whether the post-processing introduced a vulnerability depends on the template engine. Note that if you were correctly escaping user input in your ERB templates (either automatically or manually), you are unlikely to be vulnerable as the escaping escaped the ERB template metacharacters (< and >). For non-ERB templates, escaping the output may not have mitigated the vulnerability, depending on what metacharacters the template engine uses and whether the escaping will modify them. Calling content_for with an argument was not vulnerable as no post-processing was done on the argument, it was only done on the block result. jeremyevans-roda-4f30bb3/doc/release_notes/3.15.0.txt000066400000000000000000000013311516720775400222700ustar00rootroot00000000000000= New Features * The render plugin :escape option value can now be a string or an array of strings, and then the plugin will will only add the :escape template option for those specific template engines given. By default, the :escape plugin option adds the :escape template option for all engines, which breaks the usage with some engines (such as the rcsv engine). * The convert! and convert_each! methods in the typecast_params plugin now support a :skip_missing option to support not storing missing parameters: typecast_params.convert! do |tp| tp.int('missing') end # => {'missing'=>nil} typecast_params.convert!(skip_missing: false) do |tp| tp.int('missing') end # => {} jeremyevans-roda-4f30bb3/doc/release_notes/3.16.0.txt000066400000000000000000000034761516720775400223050ustar00rootroot00000000000000= New Features * A mail_processor plugin has been added for processing mail using a routing tree. Quick example: class MailProcessor < Roda plugin :mail_processor route do |r| # Match based on the To header, extracting the ticket_id r.to /ticket\+(\d+)@example.com/ do |ticket_id| if ticket = Ticket[ticket_id.to_i] # Mark the mail as handled if there is a valid ticket # associated r.handle do ticket.add_note(text: mail_text, from: from) end end end end end You can submit mail for processing by calling the process_mail method with a Mail instance: MailProcessor.process_mail(Mail.read('/path/to/message.eml')) The mail_processor routing tree uses routing methods specific to mail: r.from :: match on the mail From address r.to :: match on the mail To address r.cc :: match on the mail CC address r.rcpt :: match on the mail recipients (To and CC addresses by default) r.subject :: match on the mail subject r.body :: match on the mail body r.text :: match on text extracted from the message (same as mail body by default) r.header :: match on a mail header To mark a mail as having been handled, you call the r.handle method with a block, or one of the above methods prefixed by handle_ (e.g. r.handle_text). The mail_processor plugin supports hooks that are called for handled mail, unhandled mail, and all mail (for archiving). It also supports the ability to configure how reply text is parsed out of mail, who to consider as the recipients of the email, and the ability to have separate routing blocks per recipient email address (with O(1) delegation to the appropriate block if the recipient addresses to match is a string). jeremyevans-roda-4f30bb3/doc/release_notes/3.17.0.txt000066400000000000000000000041101516720775400222700ustar00rootroot00000000000000= New Features * A route_block_args plugin has been added, allowing you to customize which objects are yielded to the the route block. You call the plugin with a block, which is evaluated in the context of the instance and should return an array of arguments for the instance to yield to the route block. To yield both the request and response objects, you can do: plugin :route_block_args do |r| [r, response] end route do |r, response| # ... end In addition to the main route block, using this plugin also affects the arguments passed to routing blocks in the following plugins: * class_level_routing * mailer * mail_processor * multi_route * static_routing = Other Improvements * The set_layout_opts method in the view_options plugin can now override the layout template even if the render plugin :layout option is given. * The mailer and mail_processor plugin now integrate with the hooks plugin to support before/after hooks. * Dispatching to the route block and RodaResponse#finish are both slightly faster. * Internal before hook handling has been moved from an internal plugin into the core, and modified so that if you are not using the internal before hook in any plugin, there is no runtime cost. * The core now recognizes when plugins are using the internal after hook, and automatically loads the internal plugin supporting the after hook. = Backwards Compatibility * When using the render plugin with a :layout option, the render_opts :layout option will be set to true if the layout is enabled. Previously, the render_opts :layout option would retain the value given as the plugin option. Options for the layout (including the template) are still available in the render_opts :layout_opts option. This change was made to fix the set_layout_opts bug in the view_options plugin. * RodaResponse#initialize no longer sets the response status to nil if it was already set. * RodaResponse#finish no longer sets the status on the receiver, it just uses the receiver's status to set the rack response status. jeremyevans-roda-4f30bb3/doc/release_notes/3.18.0.txt000066400000000000000000000161071516720775400223020ustar00rootroot00000000000000= New Features * A direct_call plugin has been added. This plugin makes Roda.call call the app directly, skipping any middleware. This plugin can be used for performance reasons, as the class itself can be used as the base rack app, instead of using a lambda as the base rack app. Roda.app.call will still call all middleware when using this plugin. = Other Improvements * Blocks that are given during application configuration, and previously executed with instance_exec, instead now define methods, and Roda now calls these methods. This is a much faster approach. This new approach, combined with the direct_call plugin and the Roda.freeze optimizations, can be over 80% faster for trivial applications, with measureable improvements in most applications. As methods are strict in regards to arity and instance_exec is not, Roda now checks all such blocks for arity mismatches, and attempts to compensate for arity mismatches. In case of an arity mismatch, Roda will define a method that will call instance_exec, in which case there will not be a performance improvement. For some methods, Roda may not know the expected arity until runtime. In that case, Roda will check the arity at runtime and try to call the method with the arity that it supports if there is an arity mismatch. You can control the checking of arity via two options: :check_arity :: Set to false to turn off all arity checking. Set to :warn to issue a warning when defining the method if there is an arity mismatch (for methods where the expected arity is known in advance). :check_dynamic_arity :: Set to false to turn off arity checking for methods defined where the arity is not known at compile time. Set to :warn to issue a warning at runtime every time the method is called and there is an arity mismatch (for methods where the expected arity is not known in advance). Note that checking the arity at runtime has a performance cost, so for maximum performance this should be set to false. Note that this arity checking is only done to keep backwards compatibility. Since lambdas already used strict arity, no arity checking is done if the block is a lambda and not a regular proc. Roda has a new dispatch API that works with these defined methods. The new dispatch API uses the following methods: * _roda_handle_main_route: Entry point for normal request dispatch. * _roda_handle_route: Yields to the routing block, catching any halts inside the block, treating the block as a routing block. * _roda_main_route: Roda.route defines this method using the block provided, it accepts the request as an argument. * _roda_run_main_route: Calls _roda_main_route with the request, allowing for plugins to execute code around the main routing, while still being able to throw :halt to return a response. All instance methods defined by Roda use the _roda_ prefix. * When deleting the session cookie in the sessions plugin, the Set-Cookie response header now uses the same path and domain that was originally used to set the cookie. This can fix cases where the cookie was not being cleared as expected. * Freezing a Roda app now can add performance improvements in addition to reliability improvements. When freezing the class, if certain methods in the class have not been overridden, Roda now defines aliases or more optimized methods to improve performance. * Roda now warns if the Roda#call method is overridden in a module, without the module also overridding _roda_handle_main_route or _roda_run_main_route. This indicates that the module needs to be updated to use Roda's new dispatch API. Roda will continue to work in this case, but it will be slower than the Roda's now default behavior, as it will force usage of the old dispatch API. This check will be removed in Roda 4, which will remove support for Roda#call (and Roda#_call). * When there is only a single internal before or after hook defined, the hook is now faster by using a method alias. * The route_csrf plugin block or :csrf_failure option proc now integrates with the route_block_args plugin. * The default_status plugin is now faster by defining the default_status method directly. * The default_headers plugin is now faster by defining an optimized set_default_headers method directly. * The hooks plugin is now faster by defining methods for each hook block, with a main hook method that dispatches to each of the hook block methods. If only a single hook block is used, the main hook method is an alias to the hook block method to avoid an extra method call. * The following plugins now use define_method instead of instance_exec for better performance: * defaults_setter * mail_processor * multi_route * named_templates * path * route_block_args * route_csrf * static_routing * status_handler * The internal after hook implementation has now been merged into the error_handler plugin. This is faster in cases where the error_handler plugin is used, and slower in cases where the internal after hook plugin was used without the error_handler plugin. * The route_block_args plugin now handles cases where Roda.convert_route_block has already been overridden. * Performance of routing methods that can yield captures has been improved. * Hash#merge is now used in preference to Hash[].merge! in cases where the receiver of Hash#merge would not be provided by the user. This is because Hash#merge is faster than Hash[].merge! in recent ruby versions. If the receiver of #merge is provided by the user, then Hash[].merge! is still used to ensure that the resulting value is plain hash. * The static_routing plugin no longer removes existing static routes if loaded more than once. * Roda now warns when calling Roda.route without a block. = Backwards Compatibility * The route_block_args plugin no longer affects the class_level_routing plugin. Support for this was added in Roda 3.17.0 when the route_block_args plugin was added, but this was a mistake as class_level_routing blocks should be called with the captures for their matchers, not with the route block args. * Some of the internal state was changed in the following plugins: * class_level_routing * mail_processor * multi_route * named_templates * static_routing * status_handler This only affects you if you were accessing the internal state via the opts hash. * The static_routing plugin no longer defines the r.static_route method. * The mailer plugin was switched to use the new dispatch API, and will no longer handle cases where the old dispatch API (Roda#call) was overrridden. * The static_route method in the static_routing plugin must now be called with a block. Previously, that would not cause a failure until runtime, where it would fail when you tried to execute the route. jeremyevans-roda-4f30bb3/doc/release_notes/3.19.0.txt000066400000000000000000000162041516720775400223010ustar00rootroot00000000000000= New Features * A hash_routes plugin has been added for O(1) route dispatching at any level of the routing tree. By default, Roda uses a linear search of possible branches at each level of the routing tree, which results in roughly O(log(n)) routing behavior in most applications (where n is the total number of routes in the application). Assume you have the following routing tree: route do |r| r.on "a" do # ... end r.on "b" do # ... end r.is "c" do # ... end # ... end With this routing tree, a request for /c will first check /a and /b. This is not normally a performance issue, but if you have a large number of routes at a particular level, it can be. The hash_routes plugin allows you to convert this routing tree to: plugin :hash_routes hash_routes do on "a" do |r| # ... end on "b" do |r| # ... end is "c" do |r| # ... end # ... end route do |r| r.hash_routes end This routing tree looks similar to Roda's standard routing tree, and will have the same behavior as the previous example, but dispatching to the routes inside the hash_routes block by the r.hash_routes method will be an O(1) operation, instead of a linear search. This can significantly improve performance in cases where you have a large number of branches at any point in the routing tree. In order to support O(1) route dispatching at any level of the tree, the hash_routes plugin supports namespaces. You can use this namespace support to keep the primary advantage of Roda when using the hash_routes plugin, which is the ability to operate on a request at any point during routing. Assume you have this routing tree: hash_routes :root do on "foo" do |r| r.on Integer do |foo_id| next unless @foo = Foo[foo_id] r.hash_routes(:foo) end end on "bar" do |r| r.on Integer do |bar_id| next unless @bar = Bar[bar_id] r.hash_routes(:bar) end end # ... end hash_routes :foo do get "show" do @page_title = @foo.name view('foo/show') end # ... end hash_routes :bar do post "edit" do @bar.update(:name=>request.params['name']) r.redirect "/" end # ... end route do |r| r.hash_routes(:root) end With this routing tree, a GET /foo/123/show request will first get dispatched to the on "foo" block in the :root namespace. That will extract the 123 segment from the path, and use it to find the Foo object with id 123 and set that to the instance variable @foo. If there is no matching foo, the rest of the block will be skipped, which will result in a 404 response. If there is a matching foo, after setting the instance variable, it will dispatch to routes in the :foo namespace, one of which is show, which will be able to use the @foo variable, both in the route and in the view. Similarly, a POST /bar/321/edit request would dispatch to the on "bar" block in the :root namespace, will look up the matching bar, then will dispatch to the edit route in the :bar namespace. The hash_routes plugin can be used as a faster version of the multi_route plugin's r.multi_route method. It can also be used as a faster replacement for the multi_view plugin. Please see the hash_routes plugin documentation for additional methods and configuration styles supported by the plugin. * A match_hook plugin has been added, which is called for each successful match, before yielding to the match block. For example, with the following routing tree: plugin :match_hook match_hook do puts "#{r.matched_path}|#{r.remaining_path}" end route do |r| r.on "a" do r.is "b" do r.get do end r.post do end end end end A GET request for /a/b would call the match hook three times, and output the following: /a|/b # When the r.on block matches /a/b| # When the r.is block matches /a/b| # When the r.get block matches A GET request for /a/c would call the match hook once, and output the following: /a|/b # When the r.on block matches This plugin can be used to make debugging easier, as well as for metrics. = Other Improvements * Per-cookie cipher secrets are now supported and used automatically by default in the sessions plugin. This can prevent issues where the cipher secret can be leaked if the random initialization vector turns out not to be so random and ends up being reused. This makes the session cookies slightly larger and about 10-20% slower. Note that because of the way the sessions plugin is designed, even if the cipher secret was leaked and you are not using per-cookie cipher secrets, it would not allow an attacker to forge a session, it would only allow them to read the contents of an existing session. If you are currently using the sessions plugin, and performing rolling restarts, you should temporarily disable per-cookie session secrets until all processes have been restarted and are able to support per-cookie session secrets. You can do so by setting the :per_cookie_cipher_secret sessions plugin option to false temporarily until all processes have restarted and are running Roda 3.19.0+. * When passing route blocks to Roda that have 0 arity instead of the expected arity of 1, emulate an arity of 1 using an approach that is about 2.75-8x faster. This emulation is still about 20% slower than using the expected arity. * Fix emulation of route blocks that have >1 arity but where the expected arity is 1. Such blocks were not handled correctly in Roda 3.18.0. * String matching performance has been improved by 10-20%. * Symbol and String class matching performance has improved by 10-20%. * Terminal matching performance has improved by about 4x. * Roda will now automatically load the direct_call plugin when freezing the application if there is no middleware used and the application has not been subclassed, for improved performance. * Roda no longer builds the rack application until the app class method is called. This can fix O(n^2) issues when building applications with a lot of middleware. One consequence of this is that a Roda.route block is no longer required. If Roda.route is not called, then the default routing tree will return a 404 response for all requests. The delay_build plugin used to support delaying building the rack application until a build! method is called. Now that Roda delays building the rack application until the app method is called, there is no reason to use this plugin, and it is now a no-op. * The assets plugin :timestamp_paths option now supports a string value to use a custom separator. A slash separator is still used by default. = Backwards Compatibility * The static_routing plugin internals have changed, as the static_routing is now implemented via the hash_routes plugin. If you were depending on the internals, you will need to update your code. jeremyevans-roda-4f30bb3/doc/release_notes/3.2.0.txt000066400000000000000000000011671516720775400222130ustar00rootroot00000000000000= New Features * A timestamp_public plugin has been added for serving static files with paths that change based on the modification timestamp of the file. By using a new path, cached versions of the file will not be used, fixing staleness issues. Example: plugin :timestamp_public route do |r| # serves requests for /static/\d+/.* r.timestamp_public # /static/1234567890/path/to/file timestamp_path("path/to/file") end = Other Improvements * When using the assets plugin :timestamp_paths option, the timestamps now include microseconds, to make cache poisoning more difficult. jeremyevans-roda-4f30bb3/doc/release_notes/3.20.0.txt000066400000000000000000000004441516720775400222700ustar00rootroot00000000000000= Improvements * For empty responses with status code 205, a Content-Length header is now added with a value of 0, for better conformance to RFC 7232. Similarly, when using the drop_body plugin, responses with status code 205 now have a Content-Length header added with a value of 0. jeremyevans-roda-4f30bb3/doc/release_notes/3.21.0.txt000066400000000000000000000002631516720775400222700ustar00rootroot00000000000000= Improvements * View rendering speed is significantly improved in development mode by caching file-based templates until there has been a modification to the template file. jeremyevans-roda-4f30bb3/doc/release_notes/3.22.0.txt000066400000000000000000000021071516720775400222700ustar00rootroot00000000000000= Improvements * The render/view methods in the render plugin, when called with a single string/symbol argument (the most common case), are now up to 2.5x/4x faster by directly calling compiled template methods. This works by extracting the UnboundMethod objects that Tilt creates, and defining real methods for them, then calling those methods using send. This avoids most of the overhead of the render and view methods. The compiled template methods are defined inside a module included in the Roda app's class, so this support works even if the Roda app itself is frozen. Some plugins, such as render_locals, do not work with this optimization, and disable the use of it. The view_options plugin does work with this optimization if you are using set_view_subdir or append_view_subdir, but not if using set_view_options or set_layout_options. This optimization depends on Ruby 2.3+ and Tilt 1.2+, and will not be used on earlier versions, or if an API change in Tilt is detected. * Session deserialization is now slightly faster in the sessions plugin. jeremyevans-roda-4f30bb3/doc/release_notes/3.23.0.txt000066400000000000000000000025351516720775400222760ustar00rootroot00000000000000= Improvements * The render/view methods in the render plugin, when called with a single string/symbol argument (the most common case), are now up to 2x faster in cache: false mode by directly calling compiled template methods. This takes the performance increase in 3.22.0 and applies it to cache: false mode in addition to cache: true mode. If the template file has changed, the compiled method is removed, and a new compiled method replaces it. * Template modification detection in the render plugin now uses a faster check for modification, which also avoids a race condition. * The type_routing plugin now handles requests with nothing but the extension in the request path. This fixes cases when you have one app partially route a request, and send the request to another app, and that app uses the type_routing plugin and has an r.is call at the root level. * The roda/session_middleware middleware now works correctly if the type_routing plugin is loaded into Roda itself (as opposed to a Roda subclass). * The exception_page plugin now always shows the line number for each line. Previously, it only showed the line number if it was showing the content of the line, which complicated debugging in cases where the content of the line was no longer retrievable due to file system permissions or restrictions (e.g. chroot). jeremyevans-roda-4f30bb3/doc/release_notes/3.24.0.txt000066400000000000000000000011551516720775400222740ustar00rootroot00000000000000= Improvements * The performance of the render_each plugin has been dramatically improved by calling compiled template methods directly. For a simple template, render_each performance with a single object can be about 2x faster, and render_each performance for 100 objects can be 3x (cache: false) to 9x (cache: true) faster. This optimization can be used if no options are provided to render_each, or if :local and/or :locals options are provided. Use of other options will disable this optimization. * The module_include plugin no longer calls Proc.new without a block, fixing a warning on Ruby 2.7. jeremyevans-roda-4f30bb3/doc/release_notes/3.25.0.txt000066400000000000000000000010301516720775400222650ustar00rootroot00000000000000= Improvements * The new tilt 2.0.10 private API is now supported when using compiled template methods, with up to a 33% performance increase. The older tilt private API (back to tilt 1.2) is still supported. * The performance of the render and view methods in the render plugin when called with only the :locals option are now about 75% faster by calling compiled template methods directly. * Keyword argument separation issues are now handled on Ruby 2.7+ when defining methods with blocks that accept keyword arguments. jeremyevans-roda-4f30bb3/doc/release_notes/3.26.0.txt000066400000000000000000000012401516720775400222710ustar00rootroot00000000000000= New Features * Asynchronous streaming is now supported in the streaming plugin, using the :async option. When using this option, streaming responses are temporarily buffered in a queue. By default, the queue is a sized queue with a maximum of 10 elements, but the queue can be specified manually via the :queue option, which can be used with async libraries that support non-blocking queues. This option is currently only supported on Ruby 2.3+. = Other Improvements * When combining multiple compiled assets into a single file, the files are now separated by a newline, fixing issues when a single line comment is used as the last line of a file. jeremyevans-roda-4f30bb3/doc/release_notes/3.27.0.txt000066400000000000000000000010211516720775400222670ustar00rootroot00000000000000= New Features * A multibyte_string_matcher plugin has been added that supports multibyte characters in strings used as matchers. It uses a slower string matching implementation that supports multibyte characters. As multibyte strings in paths must be escaped, this also loads the unescape_path plugin. = Other Improvements * The json_parser plugin now returns expected results for invalid JSON if the params_capturing plugin is used. * lib/roda.rb has been split into multiple files for easier code navigation. jeremyevans-roda-4f30bb3/doc/release_notes/3.28.0.txt000066400000000000000000000006321516720775400222770ustar00rootroot00000000000000= New Features * The sessions plugin now supports RodaRequest#session_created_at and RodaRequest#session_updated_at for the times of session creation and last update. = Other Improvements * The json_parser plugin now correctly parses the request body even if the request body has already been read. * The sessions plugin now correctly handles upgrading rack cookie sessions when using rack 2.0.8+. jeremyevans-roda-4f30bb3/doc/release_notes/3.29.0.txt000066400000000000000000000007751516720775400223100ustar00rootroot00000000000000= Improvements * The common_logger plugin now includes the SCRIPT_NAME when logging, for greater compatibility with typical web server logs. * The exception_page plugin now handles invalid POST data. Previously, invalid POST data would cause the exception page display to raise an exception. * An error is now raised if trying to load a plugin that is not a module or a recognized plugin symbol. * Specs and older release notes are no longer shipped in the gem, reducing gem size by over 35%. jeremyevans-roda-4f30bb3/doc/release_notes/3.3.0.txt000066400000000000000000000260101516720775400222060ustar00rootroot00000000000000= New Features * A typecast_params plugin has been added for handling the conversion of params to the expected type. This plugin is recommended for all applications that deal with submitted parameters. Submitted parameters should be considered untrusted input, and in standard use with browsers, parameters are submitted as strings (or a hash/array containing strings). In most cases it makes sense to explicitly convert the parameter to the desired type. While this can be done via manual conversion: key = request.params['key'].to_i key = nil unless key > 0 the typecast_params plugin adds a friendlier interface: key = typecast_params.pos_int('key') As typecast_params is a fairly long method name, you may want to consider aliasing it to something more terse in your application, such as tp. One advantage of using typecast_params is that access or conversion errors are raised as a specific exception class (Roda::RodaPlugins::TypecastParams::Error). This allows you to handle this specific exception class globally and return an appropriate 4xx response to the client. You can use the Error#param_name and Error#reason methods to get more information about the error. typecast_params offers support for default values: key = typecast_params.pos_int('key', 1) The default value is only used if no value has been submitted for the parameter, or if the conversion of the value results in nil. Handling defaults for parameter conversion manually is more difficult, since the parameter may not be present at all, or it may be present but an empty string because the user did not enter a value on the related form. Use of typecast_params for the conversion handles both cases. In many cases, parameters should be required, and if they aren't submitted, that should be considered an error. typecast_params handles this with ! methods: key = typecast_params.pos_int!('key') These ! methods raise an error instead of returning nil, and do not allow defaults. To make it easy to handle cases where many parameters need the same conversion done, you can pass an array of keys to a conversion method, and it will return an array of converted values: key1, key2 = typecast_params.pos_int(['key1', 'key2']) This is equivalent to: key1 = typecast_params.pos_int('key1') key2 = typecast_params.pos_int('key2') The ! methods also support arrays of keys, ensuring that all parameters have a value: key1, key2 = typecast_params.pos_int!(['key1', 'key2']) For handling of array parameters, where all entries in the array use the same conversion, there is an array method which takes the type as the first argument and the keys to convert as the second argument: keys = typecast_params.array(:pos_int, 'keys') If you want to ensure that all entries in the array are converted successfully and that there is a value for the array itself, you can use array!: keys = typecast_params.array!(:pos_int, 'keys') This will raise an exception if any of the values in the array for parameter keys cannot be converted to a positive integer. Both array and array! support default values which are used if no value is present for the parameter: keys = typecast_params.array(:pos_int, 'keys', []) keys = typecast_params.array!(:pos_int, 'keys', []) You can also pass an array of keys to array or array!, if you would like to perform the same conversion on multiple arrays: foo_ids, bar_ids = typecast_params.array!(:pos_int, ['foo_ids', 'bar_ids']) The previous examples have shown use of the pos_int method, which uses to_i to convert the value to an integer, but returns nil if the resulting integer is not positive. Unless you need to handle negative numbers, it is recommended to use pos_int instead of int as int will convert invalid values to 0 (since that is how String#to_i works). There are many built in methods for type conversion: any :: Returns the value as is without conversion str :: Raises if value is not already a string nonempty_str :: Raises if value is not already a string, and converts the empty string or string containing only whitespace to nil bool :: Converts entry to boolean if in one of the recognized formats (case insensitive for strings): nil :: nil, '' true :: true, 1, '1', 't', 'true', 'yes', 'y', 'on' false :: false, 0, '0', 'f', 'false', 'no', 'n', 'off' If not in one of those formats, raises an error. int :: Converts value to integer using to_i (note that invalid input strings will be converted to 0) pos_int :: Converts value using to_i, but non-positive values are converted to nil Integer :: Converts value to integer using Kernel::Integer(value, 10) float :: Converts value to float using to_f (note that invalid input strings will be converted to 0.0) Float :: Converts value to float using Kernel::Float(value) Hash :: Raises if value is not already a hash date :: Converts value to Date using Date.parse(value) time :: Converts value to Time using Time.parse(value) datetime :: Converts value to DateTime using DateTime.parse(value) file :: Raises if value is not already a hash with a :tempfile key whose value responds to read (this is the format rack uses for uploaded files). All of these methods also support ! methods (e.g. pos_int!), and all of them can be used in the array and array! methods to support arrays of values. Since parameter hashes can be nested, the [] method can be used to access nested hashes: # params: {'key'=>{'sub_key'=>'1'}} typecast_params['key'].pos_int!('sub_key') # => 1 This works to an arbitrary depth: # params: {'key'=>{'sub_key'=>{'sub_sub_key'=>'1'}}} typecast_params['key']['sub_key'].pos_int!('sub_sub_key') # => 1 And also works with arrays at any depth, if those arrays contain hashes: # params: {'key'=>[{'sub_key'=>{'sub_sub_key'=>'1'}}]} typecast_params['key'][0]['sub_key'].pos_int!('sub_sub_key') # => 1 # params: {'key'=>[{'sub_key'=>['1']}]} typecast_params['key'][0].array!(:pos_int, 'sub_key') # => [1] To allow easier access to nested data, there is a dig method: typecast_params.dig(:pos_int, 'key', 'sub_key') typecast_params.dig(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key') dig will return nil if any access while looking up the nested value returns nil. There is also a dig! method, which will raise an Error if dig would return nil: typecast_params.dig!(:pos_int, 'key', 'sub_key') typecast_params.dig!(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key') Note that none of these conversion methods modify request.params. They purely do the conversion and return the converted value. However, in some cases it is useful to do all the conversion up front, and then pass a hash of converted parameters to an internal method that expects to receive values in specific types. The convert! method does this, and there is also a convert_each! method designed for converting multiple values using the same block: converted_params = typecast_params.convert! do |tp| tp.int('page') tp.pos_int!('artist_id') tp.array!(:pos_int, 'album_ids') tp.convert!('sales') do |stp| tp.pos_int!(['num_sold', 'num_shipped']) end tp.convert!('members') do |mtp| mtp.convert_each! do |stp| stp.str!(['first_name', 'last_name']) end end end # converted_params: # { # 'page' => 1, # 'artist_id' => 2, # 'album_ids' => [3, 4], # 'sales' => { # 'num_sold' => 5, # 'num_shipped' => 6 # }, # 'members' => [ # {'first_name' => 'Foo', 'last_name' => 'Bar'}, # {'first_name' => 'Baz', 'last_name' => 'Quux'} # ] # } convert! and convert_each! only return values you explicitly specify for conversion inside the passed block. You can specify the :symbolize option to convert! or convert_each!, which will symbolize the resulting hash keys: converted_params = typecast_params.convert!(symbolize: true) do |tp| tp.int('page') tp.pos_int!('artist_id') tp.array!(:pos_int, 'album_ids') tp.convert!('sales') do |stp| tp.pos_int!(['num_sold', 'num_shipped']) end tp.convert!('members') do |mtp| mtp.convert_each! do |stp| stp.str!(['first_name', 'last_name']) end end end # converted_params: # { # :page => 1, # :artist_id => 2, # :album_ids => [3, 4], # :sales => { # :num_sold => 5, # :num_shipped => 6 # }, # :members => [ # {:first_name => 'Foo', :last_name => 'Bar'}, # {:first_name => 'Baz', :last_name => 'Quux'} # ] # } Using the :symbolize option makes it simpler to transition from untrusted external data (string keys), to trusted data that can be used internally (trusted in the sense that the expected types are used). Note that if there are multiple conversion errors raised inside a convert! or convert_each! block, they are recorded and a single Roda::RodaPlugins::TypecastParams::Error instance is raised after processing the block. TypecastParams::Error#param_names can be called on the exception to get an array of all parameter names with conversion issues, and TypecastParams::Error#all_errors can be used to get an array of all Error instances. Because of how convert! and convert_each! work, you should avoid calling TypecastParams::Params#[] inside the block you pass to these methods, because if the #[] call fails, it will skip the reminder of the block. Be aware that when you use convert! and convert_each!, the conversion methods called inside the block may return nil if there is a error raised, and nested calls to convert! and convert_each! may not return values. When loading the typecast_params plugin, a subclass of TypecastParams::Params is created specific to the Roda application. You can add support for custom types by passing a block when loading the typecast_params plugin. This block is executed in the context of the subclass, and calling handle_type in the block can be used to add conversion methods. handle_type accepts a type name and the block used to convert the type: plugin :typecast_params do handle_type(:album) do |value| if id = convert_pos_int(val) Album[id] end end end By default, the typecast_params conversion procs are passed the parameter value directly from request.params without modification. In some cases, it may be beneficial to strip leading and trailing whitespace from parameter string values before processing, which you can do by passing the strip: :all> option when loading the plugin. By design, typecast_params only deals with string keys, it is not possible to use symbol keys as arguments to the conversion methods and have them converted. jeremyevans-roda-4f30bb3/doc/release_notes/3.30.0.txt000066400000000000000000000007711516720775400222740ustar00rootroot00000000000000= New Features * A :relative_paths plugin option has been added to the assets plugin. This option makes the paths to the asset files in the link and script tags relative paths instead of absolute paths. = Other Improvements * The :header matcher in the header_matchers plugin now works correctly for the Content-Type and Content-Length headers, which are not prefixed with HTTP_ in the rack environment. * The run_append_slash and run_handler plugins now work correctly when used together. jeremyevans-roda-4f30bb3/doc/release_notes/3.31.0.txt000066400000000000000000000007451516720775400222760ustar00rootroot00000000000000= New Features * A relative_path plugin has been added, adding a relative_path method that will take an absolute path and make it relative to the current request by prepending an appropriate prefix. This is helpful when using Roda as a static site generator to generate a site that can be hosted at any subpath or directly from the filesystem. * In the path plugin, the path method now accepts a :relative option for generating relative paths instead of absolute paths. jeremyevans-roda-4f30bb3/doc/release_notes/3.32.0.txt000066400000000000000000000024321516720775400222720ustar00rootroot00000000000000= New Features * render_each in the render_each plugin now automatically handles template names with subdirectories and extensions. Previously, these caused issues unless the :local option was provided. So now you can use: render_each(foos, "items/foo") instead of: render_each(foos, "items/foo", :local=>:foo) * each_partial has been added to the partials plugin. It operates similarly to render_each, but uses the convention for partial template naming. So this: each_partial(foos, "items/foo") is the same as: render_each(foos, "items/_foo", :local=>:foo) = Other Improvements * The :dependencies option in the assets plugin now works correctly with compiled templates in the render plugin in uncached mode (the default in development). Previously, modifying a dependency file would not result in recompiling the asset template when requesting the main file. * Method visibility issues in the following plugins have been fixed: * content_security_policy * default_headers * indifferent_params * placeholder_string_matchers * symbol_matchers Previously, these plugins made private methods public by mistake when overriding them. Additionally, Roda.freeze no longer changes the visibility of the set_default_headers private method. jeremyevans-roda-4f30bb3/doc/release_notes/3.33.0.txt000066400000000000000000000005521516720775400222740ustar00rootroot00000000000000= New Features * The path plugin now supports a url method, allowing for returning the entire URL instead of just the path for class-based paths. * The public plugin now supports a :brotli option that will directly serve brotli-compressed files (with .br extension) similar to how the :gzip option directly serves gzipped files (with the .gz extension). jeremyevans-roda-4f30bb3/doc/release_notes/3.34.0.txt000066400000000000000000000011041516720775400222670ustar00rootroot00000000000000= Improvements * Multiple unneeded conditionals have been removed. * pre_content and post_context sections in backtraces are no longer included in the exception_page plugin output if they would be empty. * The match_affix plugin can be loaded again with a single argument. It was originally designed to accept a single argument, but a bug introduced in 2.29.0 made it require two arguments. * Core Roda and all plugins that ship with Roda now have 100% branch coverage. * The sinatra_helpers plugin no longer emits statement not reached warnings in verbose mode. jeremyevans-roda-4f30bb3/doc/release_notes/3.35.0.txt000066400000000000000000000007111516720775400222730ustar00rootroot00000000000000= New Features * An r plugin has been added. This plugin adds an r method for the request, useful for allowing the use of r.halt and r.redirect even in methods where the r local variable is not in scope. = Other Improvements * Attempting to load a plugin with an argument or block when the plugin does not accept arguments or a block now warns. This is because a future update to support a block or an optional argument could break the call. jeremyevans-roda-4f30bb3/doc/release_notes/3.36.0.txt000066400000000000000000000011551516720775400222770ustar00rootroot00000000000000= New Features * A multi_public plugin has been added, which allows serving static files from multiple separate directories. This is especially useful when there are different access control requirements per directory. * The content_security_policy now supports a content_security_policy.report_to method to set the report-to directive. = Other Improvements * When using the type_routing plugin and performing type routing using the Accept request header, the Vary response header will be added or updated so that http caches do not cache a response for one type and serve it for a different type. jeremyevans-roda-4f30bb3/doc/release_notes/3.37.0.txt000066400000000000000000000024151516720775400223000ustar00rootroot00000000000000= New Features * A custom_matchers plugin has been added, which allows using arbitrary objects as matchers, as long as the matcher has been registered. You can register matchers using the custom_matcher class method, which takes the class of the matcher, and a block which is yielded the matcher object. The block should return nil or false if the matcher doesn't match, and any other value if the matcher does match. Example: plugin :custom_matchers method_segment = Struct.new(:request_method, :next_segment) custom_matcher(method_segment) do |matcher| # self is the request instance ("r" yielded in the route block below) if matcher.request_method == self.request_method match(matcher.next_segment) end end get_foo = method_segment.new('GET', 'foo') post_any = method_segment.new('POST', String) route do |r| r.on('baz') do r.on(get_foo) do # GET method, /baz/foo prefix end r.is(post_any) do |seg| # for POST /baz/bar, seg is "bar" end end r.on('quux') do r.is(get_foo) do # GET method, /quux/foo route end r.on(post_any) do |seg| # for POST /quux/xyz, seg is "xyz" end end end jeremyevans-roda-4f30bb3/doc/release_notes/3.38.0.txt000066400000000000000000000003131516720775400222740ustar00rootroot00000000000000= Improvements * The error_email and error_mail plugins now rescue invalid parameter errors when preparing the email body, because you generally don't want your error handler to raise an exception. jeremyevans-roda-4f30bb3/doc/release_notes/3.39.0.txt000066400000000000000000000012251516720775400223000ustar00rootroot00000000000000= Improvements * The relative_path plugin is now faster if you are calling relative_path or relative_prefix more than once when handling a request. * The typecast_params.convert! method in the typecast_params plugin now handles explicit nil values the same as missing values. Explicit nil values do not generally occur in normal Rack parameter parsing, but they can occur when using the json_parser plugin to parse JSON requests. * Roda now avoids method redefinition warnings in verbose mode by using a self alias. As Ruby 3 is dropping uninitialized instance variable warnings, Roda will be verbose warning free if you are using Ruby 3. jeremyevans-roda-4f30bb3/doc/release_notes/3.4.0.txt000066400000000000000000000014501516720775400222100ustar00rootroot00000000000000= New Features * A middleware_stack plugin has been added for more detailed control over middleware, allowing for the removal of middleware and the insertion of middleware before existing middleware. Example: plugin :middleware_stack # Remove csrf middleware middleware_stack.remove{|m, *args| m == Rack::Csrf} # Insert csrf middleware before logger middleware middleware_stack.before{|m, *args| m == Rack::CommonLogger}. use(Rack::Csrf, raise: true) # Insert csrf middleware after logger middleware middleware_stack.after{|m, *args| m == Rack::CommonLogger}. use(Rack::Csrf, raise: true) = Other Improvements * The head plugin now calls close on the response body if the body responds to close. Previously an existing response body was just ignored. jeremyevans-roda-4f30bb3/doc/release_notes/3.40.0.txt000066400000000000000000000017741516720775400223010ustar00rootroot00000000000000= New Features * A precompile_views method has been added to the precompile_templates plugin. This method works with Roda's optimized compiled view methods, allowing additional memory sharing between parent and child processes. * A freeze_template_caches! method has been added to the precompile_templates plugin. This freezes the template caches, preventing the compilation of additional templates, useful for enforcing that only precompiled templates are used. Additionally, this speeds up access to the template caches. * RodaCache#freeze now returns the frozen internal hash, which can then be accessed without a mutex. Previously, freeze only froze the receiver and not the internal hash, so it didn't have the expected effect. = Other Improvements * The view method in the render plugin is now faster in most cases when a single argument is used. When freezing the application, an additional optimization is performed to increase the performance of the view method even further. jeremyevans-roda-4f30bb3/doc/release_notes/3.41.0.txt000066400000000000000000000006251516720775400222740ustar00rootroot00000000000000= Improvements * The performance of the render plugin's view method when passed the :content option and no other options or arguments has been improved by about 3x, by calling compiled template methods directly. * The compiled template method for the layout is cleared when the render plugin is loaded again, which can fix issues when it is loaded with different options that affect the layout. jeremyevans-roda-4f30bb3/doc/release_notes/3.42.0.txt000066400000000000000000000014261516720775400222750ustar00rootroot00000000000000= New Features * A recheck_precompiled_assets plugin has been added, which allows for checking for updates to the precompiled asset metadata file, and automatically using the updated data. * The common_logger plugin now supports a :method plugin option to specify the method to call on the logger. = Other Improvements * Plugins and middleware that use keyword arguments are now supported in Ruby 3. * The compile_assets class method in the assets plugin now uses an atomic approach to writing the precompiled asset metadata file. * Minor method visibility issues have been fixed. The custom_matchers plugin no longer makes the unsupported_matcher request method public, and the render plugin no longer makes the _layout_method public when the application is frozen. jeremyevans-roda-4f30bb3/doc/release_notes/3.43.0.txt000066400000000000000000000021071516720775400222730ustar00rootroot00000000000000= New Features * A host_authorization plugin has been added to verify the requested Host header is authorized. Using it can prevent DNS rebinding attacks in cases where the application can receive requests for arbitrary hosts. To check for authorized hosts in your routing tree, you call the check_host_authorization! method. For example, if you want to check for authorized hosts after serving requests for public files, you could do: plugin :public plugin :host_authorization, 'my-domain-name.example.com' route do |r| r.public check_host_authorization! # ... rest of routing tree end In addition to handling single domain names via a string, you can provide an array of domain names, a regexp to match again, or a proc. By default, requests using unauthorized hosts receive an empty 403 response. If you would like to customize the response, you can pass a block when loading the plugin: plugin :host_authorization, 'my-domain-name.example.com' do |r| response.status = 403 "Response Body Here" end jeremyevans-roda-4f30bb3/doc/release_notes/3.44.0.txt000066400000000000000000000020111516720775400222660ustar00rootroot00000000000000= New Features * An optimized_segment_matchers plugin has been added that offers very fast matchers for arbitrary segments (the same segments that would be matched by the String class matcher). The on_segment method it offers accepts no arguments and yields the next segment if there is a segment. The is_segment method is similar, but only yields if the next segment is the final segment. = Other Improvements * The send_file and attachment methods in the sinatra_helpers plugin now support RFC 5987 UTF-8 and ISO-8859-1 encoded filenames, allowing modern browsers to save files with encoded chracters. For older browsers that do not support RFC 5987, unsupported characters in filenames are replaced with dashes. This is considered to be an improvement over the previous behavior of using Ruby's inspect output for the filename, which could contain backslashes (backslash is not an allowed chracter in Windows filenames). * The performance of the String class matcher has been slightly improved. jeremyevans-roda-4f30bb3/doc/release_notes/3.45.0.txt000066400000000000000000000016441516720775400223020ustar00rootroot00000000000000= Improvements * The typecast_params plugin checks now checks for null bytes by default before typecasting. If null bytes are present, it raises an error. Most applications do not require null bytes in parameters, and in some cases allowing them can lead to security issues, especially when parameters are passed to C extensions. In general, the benefit of forbidding null bytes in parameters is greater than the cost. If you would like to continue allowing null bytes, use the :allow_null_bytes option when loading the plugin. Note that this change does not affect uploaded files, since those are expected to contain null bytes. = Backwards Compatibility * The change to the typecast_params plugin to raise an error for null bytes can break applications that are expecting null bytes to be passed in parameters. Such applications should use the :allow_null_bytes option when loading the plugin. jeremyevans-roda-4f30bb3/doc/release_notes/3.46.0.txt000066400000000000000000000014571516720775400223050ustar00rootroot00000000000000= Improvements * The r.on, r.is, r.get and r.post methods (and other verb methods if using the all_verbs plugin) have now been optimized when using a single string or regexp matcher, or the String or Integer class matcher. Since those four matchers are the most common types of matchers passed to the methods, this can significantly improve routing performance (about 50% in the r10k benchmark). This optimization is automatically applied when freezing applications, if the related methods have not been modified by plugins. This optimization does come at the expense of a small decrease in routing performance (3-4%) for unoptimized cases, but the majority of applications will see a overall performance benefit from this change. * Other minor performance improvements have been made. jeremyevans-roda-4f30bb3/doc/release_notes/3.47.0.txt000066400000000000000000000005631516720775400223030ustar00rootroot00000000000000= Improvements * The r.on optimization added in 3.46.0 has been extended to optimize all single argument calls. This results in the following speedups based on argument type: * Hash matching: 10% * Array/Symbol/Class matching: 15% * Proc matching: 25% * true matching: 45% * false/nil matching: 65% * Other minor performance improvements have been made. jeremyevans-roda-4f30bb3/doc/release_notes/3.48.0.txt000066400000000000000000000010551516720775400223010ustar00rootroot00000000000000= New Features * A named_routes plugin has been added, for defining named route blocks that you can dispatch to with r.route. This feature was previously available as part of the multi_route plugin, but there are cases where the r.route method and support for named routes is helpful even when the multi_route plugin is not used (such as when the hash_routes plugin is used instead of the multi_route plugin). The multi_route plugin now depends on the named_routes plugin, so this change should not cause any backwards compatibility issues. jeremyevans-roda-4f30bb3/doc/release_notes/3.49.0.txt000066400000000000000000000013171516720775400223030ustar00rootroot00000000000000= Improvements * The r.is optimization added in 3.46.0 has been extended to optimize all single argument calls. This results in the following speedups based on argument type: * Hash/Class matching: 20% * Symbol matching: 25% * Array matching: 35% * Proc matching: 50% * false/nil matching: 65% * Roda now uses defined?(yield) instead of block_given? internally for better performance on CRuby. defined?(yield) is faster as it is built into the VM, while block_given? is a regular method and has the overhead of calling a regular method. Note that defined?(yield) is not implemented correctly on JRuby before 9.0.0.0, so this release of Roda drops support for JRuby versions before 9.0.0.0. jeremyevans-roda-4f30bb3/doc/release_notes/3.5.0.txt000066400000000000000000000022111516720775400222050ustar00rootroot00000000000000= New Features * A request_aref plugin has been added for configuring the behavior of the [] and []= request methods. These methods are deprecated in the current version of Rack, but Rack will only print a deprecation warning in verbose mode. With this plugin, you can choose to never warn, always warn, or raise an exception: # Don't emit a warning, allowing for the historical Rack # behavior plugin :request_aref, :allow # Always emit a warning when the method is called plugin :request_aref, :warn # Raise an exception if the method is called plugin :request_aref, :raise = Other Improvements * When using the content_for plugin and calling content_for with a block, convert the result of the block to a string before passing the result to Tilt. This can fix issues when the template class that Tilt uses does not handle non-String input. * When using the public plugin with the :gzip option, do not add the Content-Type or Content-Encoding headers if a 304 response is returned. * Add the spec/views/about directory to the gem, allowing the specs to run correctly using just the files in the gem. jeremyevans-roda-4f30bb3/doc/release_notes/3.50.0.txt000066400000000000000000000021121516720775400222650ustar00rootroot00000000000000= New Features * An inject_erb plugin has been added, adding an inject_erb method that allows for injecting content directly into the template output for the template currently being rendered. This allows you to more easily wrap blocks in templates, by calling methods that accept template blocks and injecting content before and after the block. * A capture_erb plugin has been added, adding a capture_erb method for capturing a template block in an erb template and returning the content appended during the block as a string, instead of having the content of the template block be included directly into the template output. This can be combined with the inject_erb plugin to inject modified versions of captured blocks into template output. * The hash_routes plugin now allows calling hash_branch and hash_path without a block in order to remove the existing route handler. This is designed to be used with code reloading libraries, so that if a route file is deleted, the related hash branches/paths are also removed, without having to reload all route files. jeremyevans-roda-4f30bb3/doc/release_notes/3.51.0.txt000066400000000000000000000013061516720775400222720ustar00rootroot00000000000000= New Features * The named_routes plugin now allows calling route without a block to remove the existing route handler. The multi_run plugin now allows calling run without an app to remove an existing handler. These changes are designed to better support code reloading libraries, so that if the related file is deleted, the related handlers are also removed, without having to reload the entire application. = Other Improvements * The error_handler plugin now avoids a method redefinition warning in verbose warning mode. = Other * Roda's primary discussion forum is now GitHub Discussions. The ruby-roda Google Group is still available for users who would prefer to use that instead. jeremyevans-roda-4f30bb3/doc/release_notes/3.52.0.txt000066400000000000000000000015021516720775400222710ustar00rootroot00000000000000= New Features * The typecast_params plugin now supports a :date_parse_input_handler option that will be called with all input that will be passed to the date parsing methods. You can use this option to automatically truncate input, if that is perferable to raising an error (which is how recent versions of Ruby handle too-long input). = Other Improvements * The path helper methods added by the path plugin now support blocks that use keyword arguments on Ruby 3+. * The assets plugin now uses OpenSSL::Digest instead of Digest (if available) for calculating SRI digests. This is faster on Ruby 3+, where Digest no longer uses the faster OpenSSL::Digest automatically if available. * Roda.freeze now returns self when the multi_route plugin is used. This was broken (not returning self) starting in 3.48.0. jeremyevans-roda-4f30bb3/doc/release_notes/3.53.0.txt000066400000000000000000000010421516720775400222710ustar00rootroot00000000000000= New Features * An additional_view_directories plugin has been added, which allows you to specify additional directories to look in for templates. If the template path does not exist when using the default view directory, then each additional view directory will be checked, returning the first path that exists: plugin :additional_view_directories, ['admin_views', 'public_views'] = Other Improvements * The indifferent_params plugin now avoids a deprecation warning when using the rack main branch, which will become Rack 3. jeremyevans-roda-4f30bb3/doc/release_notes/3.54.0.txt000066400000000000000000000037101516720775400222760ustar00rootroot00000000000000= New Features * You can now override the type attribute for script tags produced by the assets plugin, by providing a :type attribute when calling the assets method. = Other Improvements * Reloading the render plugin after the additional_view_directories plugin no longer removes the additional view directories from the allowed paths for templates. * When using Rack 3, Roda will now use an instance of Rack::Headers instead of a plain hash for the headers, allowing for compliance with the Rack 3 SPEC (which will require lowercase header keys). * The public, multi_public, and sinatra_helpers plugin now use Rack::Files instead of Rack::File if available, as Rack::File will be deprecated in Rack 3.0. * The json_parser plugin no longer rewinds the request body before and after reading it when used with Rack 3.0, as Rack 3.0 has dropped the requirement for rewindable input. * The run_handler plugin now closes bodies for upstream 404 responses when using the not_found: :pass option. * The chunked plugin no longer uses Transfer-Encoding: chunked by default. Requiring the use of Transfer-Encoding: chunked made the plugin only work on HTTP 1.1, and not older or newer versions. The plugin still allows for streaming template bodies as they are being rendered. To get the previous behavior of forcing the use of Transfer-Encoding: chunked, you can use the :force_chunked_encoding plugin option * Roda now supports testing with Rack::Lint. This found multiple violations of the Rack SPEC which are fixed in this version, and should ensure that Roda stays in compliance with the Rack SPEC going forward. = Backwards Compatibility * Roda will no longer set the Content-Length header for 205 responses when using Rack <2.0.2, as doing so violates the Rack SPEC for those Rack versions. * The drop_body plugin now drops response bodies for all 1xx responses, not just for 100 and 101 responses, in compliance with the Rack SPEC. jeremyevans-roda-4f30bb3/doc/release_notes/3.55.0.txt000066400000000000000000000011401516720775400222720ustar00rootroot00000000000000= New Features * A :forward_response_headers option has been added to the middleware plugin, which uses the response headers added by the middleware as default response headers even if the middleware does not handle the response. Response headers set by the underlying application take precedence over response headers set by the middleware. * The render plugin view method now accepts a block and will pass the block to the underlying render method call. This is useful for rendering a template that yields inside of an existing layout. Previously, you had to nest render calls to do that. jeremyevans-roda-4f30bb3/doc/release_notes/3.56.0.txt000066400000000000000000000027311516720775400223020ustar00rootroot00000000000000= New Features * RodaRequest#http_version has been added for determining the HTTP version the request was submitted with. This will be a string such as "HTTP/1.0", "HTTP/1.1", "HTTP/2", etc. This will use the SERVER_PROTOCOL and HTTP_VERSION entries from the environment to determine which HTTP version is in use. * The status_handler method in the status_handler plugin now supports a :keep_headers option. The value for this option should be an array of header names to keep. All other headers are removed. The default behavior without the option is still to remove all headers. * A run_require_slash plugin has been added, which will skip dispatching to another rack application if the remaining path is not empty and does not start with a slash. = Other Improvements * The status_303 plugin will use 303 as the default redirect status for non-GET requests for HTTP/2 and higher HTTP versions. Previously, it only used 303 for HTTP/1.1. * The not_allowed plugin now overrides the r.root method to return 405 responses to non-GET requests to the root. * The not_allowed plugin no longer sets the body when returning 405 responses using methods such as r.get and r.post. Previously, the body was unintentionally set to the same value as the Allow header. * When using the Rack master branch (what will become Rack 3), Roda only requires the parts of rack that it uses, instead of requiring rack and relying on autoload to load the parts of rack in use. jeremyevans-roda-4f30bb3/doc/release_notes/3.57.0.txt000066400000000000000000000024531516720775400223040ustar00rootroot00000000000000= New Features * hash_branches and hash_paths plugins have been split off from the hash_routes plugin, allowing you to use only those parts instead of all of hash_routes. The hash_branches plugin supports the hash_branch class method and r.hash_branches routing method. The hash_paths plugin supports the hash_path class method and r.hash_paths routing method. The hash_routes plugin functions as it did previously by requiring the hash_branches and hash_paths plugins. It adds the hash_routes DSL and r.hash_routes routing method. * A hash_branch_view_subdir has been added. It builds on the view_options plugin and new hash_branches plugin, automatically appending a view subdirectory for each successful hash branch. This can DRY up code that uses a separate view subdirectory for each branch. = Other Improvements * Unprintable characters are now hex escaped in the output of the common_logger plugin. This can protect users who use software that respects shell escape sequences to view the logs. = Backwards Compatibility * The static_routing plugin now depends on the hash_paths plugin instead of the hash_routes plugin, so you will need to update your application to explicitly load the hash_routes plugin if you were relying on static_routing to implicitly load it. jeremyevans-roda-4f30bb3/doc/release_notes/3.58.0.txt000066400000000000000000000011421516720775400222770ustar00rootroot00000000000000= New Features * A filter_common_logger plugin has been added, allowing you to skip logging of certain requests in the common_logger plugin. This allows you to only log requests for certain paths, or only log requests for certain types of responses. = Other Improvements * The heartbeat plugin is now compatible with recent changes in the rack master branch (what will be rack 3). * The exception_page plugin will now use Exception#detailed_message on Ruby 3.2+, preserving the did_you_mean and error_highlight information. Additionally, the display of exception messages has been improved. jeremyevans-roda-4f30bb3/doc/release_notes/3.59.0.txt000066400000000000000000000012011516720775400222740ustar00rootroot00000000000000= New Features * An additional_render_engines plugin has been added, for considering multiple render engines for templates. If the template path does not exist for the default render engine, then each additional render engine will be checked, returning the first path that exists: plugin :additional_render_engines, ['haml', 'str'] This is similar to the additional_view_directories plugin added in 3.53.0. Both plugins can be used if you want to consider multiple view directories and multiple render engines. = Other Improvements * A typo in a private method name in the delete_empty_headers plugin has been fixed. jeremyevans-roda-4f30bb3/doc/release_notes/3.6.0.txt000066400000000000000000000017601516720775400222160ustar00rootroot00000000000000= New Features * An early_hints plugin has been added for senting 103 Early Hint responses. This is currently only supported on puma 3.11+, and can allow for improved performance by letting the requestor know which related files will be needed by the request. * An :early_hints option has been added to the assets plugin. If given, calling the assets method will also issue an early hint for the related assets. * A :wrap option has been added to the json_parser plugin. If set to :always, all uploaded json data will be stored using a hash with a "_json" key. If set to :unless_hash, uploaded json data will only be wrapped in such a matter if it is not already a hash. Using the :wrap option can fix problems when using r.params when the uploaded JSON data is an array and not a hash. However, it does change the behavior of r.POST. It is possible to handle uploaded JSON array data without the :wrap option by using r.GET and r.POST directly instead of using r.params. jeremyevans-roda-4f30bb3/doc/release_notes/3.60.0.txt000066400000000000000000000027331516720775400222770ustar00rootroot00000000000000= New Features * A link_to plugin has been added with a link_to method for creating HTML links. The simplest usage of link_to is passing the body and the location to link to as strings: # Instance level link_to("body", "/path") # => "body" The link_to plugin depends on the path plugin, and allows you to pass symbols for named paths: # Class level path :foo, "/path/to/too" # Instance level link_to("body", :foo) # => "body" It also allows you to pass instances of classes that you have registered with the path plugin: # Class level A = Struct.new(:id) path A do "/path/to/a/#{id}" end # Instance level link_to("body", A.new(1)) # => "body" To set additional HTML attributes on the tag, you can pass them as an options hash: link_to("body", "/path", foo: "bar") # => "body" If the body is nil, it will be set to the same as the path: link_to(nil, "/path") # => "/path" The plugin will automatically HTML escape the path and any HTML attribute values, using the h plugin: link_to("body", "/path?a=1&b=2", foo: '"bar"') # => "body" = Other Improvements * Coverage testing has been expanded to multiple rack versions, instead of just the current rack release. jeremyevans-roda-4f30bb3/doc/release_notes/3.61.0.txt000066400000000000000000000017331516720775400222770ustar00rootroot00000000000000= Improvements * The typecast_params plugin now limits input bytesize for integer, float, and date/time typecasts. If the input is over the allowed bytesize, typecasting will fail. This prevents issues with trying to typecast arbitrarily large input. * The default Integer class matcher now limits integer segments to 100 characters by default, also to prevent issues with typecasting arbitrarily large input. Segments larger than 100 characters will no longer be matched by the Integer class matcher. = Backwards Compatibility * If the input bytesize limits in the typecast_params plugin cause issues in your application, you can use the :skip_bytesize_checking option when loading the plugin to disable the checks. * If the default Integer class matcher limit causes problems in your application, you can use the class_matchers plugin to override the matcher to not use a limit: plugin :class_matchers class_matcher(Integer, /(\d+)/){|a| [a.to_i]} jeremyevans-roda-4f30bb3/doc/release_notes/3.62.0.txt000066400000000000000000000034271516720775400223020ustar00rootroot00000000000000= New Features * An Integer_matcher_max plugin has been added for setting the maximum value matched by the Integer matcher (the minimum is always 0, since the Integer matcher does not match negative integers). The default maximum value when using the plugin is 2**63-1, the maximum value for a signed 64-bit integer. You can specify a different maximum value by passing an argument when loading the plugin. * A typecast_params_sized_integers plugin has been added for converting parameters to integers only if the integer is within a specific size. By default, the plugin supports 8-bit, 16-bit, 32-bit, and 64-bit signed and unsigned integer types, with the following typecast_params methods added by the plugin: * int8, uint8, pos_int8, pos_uint8, Integer8, Integeru8 * int16, uint16, pos_int16, pos_uint16, Integer16, Integeru16 * int32, uint32, pos_int32, pos_uint32, Integer32, Integeru32 * int64, uint64, pos_int64, pos_uint64, Integer64, Integeru64 You can override what sizes are added by default by using the :sizes option. You can also specify a :default_size option, in which case the default int, pos_int, and Integer conversions will use the given size. So if you want to change the default typecast_params integer conversion behavior to only support integer values that can fit in 64-bit signed integers, you can use: plugin :typecast_params_sized_integers, sizes: [64], default_size: 64 = Other Improvements * The block passed to the class_matcher method in the class_matchers plugin can now return nil/false to signal that it should not match. This is useful when the regexp argument provided matches segments not valid for the class. * RodaRequest#matched_path now works correctly when using the unescape_path plugin. jeremyevans-roda-4f30bb3/doc/release_notes/3.63.0.txt000066400000000000000000000027651516720775400223070ustar00rootroot00000000000000= New Features * An autoload_hash_branches plugin has been added for autoloading route files for each hash branch, instead of requiring the route files be loaded up front. For example, to automatically load a route file for a hash branch on the first request to that branch: plugin :autoload_hash_branches autoload_hash_branch('branch_name', '/path/to/file') autoload_hash_branch('namespace', 'branch_name', '/path/to/file') The route file loaded should define the expected hash branch. It is common to have route files stored in a directory, with the file name matching the branch name. In that case, you can set autoloading for all route files in a given directory: plugin :autoload_hash_branches autoload_hash_branch_dir('/path/to/dir') autoload_hash_branch_dir('namespace', '/path/to/dir') Note that autoloading hash branches does not work if the application is frozen. This plugin should only be used in development mode for faster startup, or when running tests on a subset of the application in order to avoid loading parts of the application unrelated to what is being tested. * The mailer plugin now supports a :terminal plugin option to make the r.mail method force a terminal match, similar to how r.get and other HTTP verb methods work in standard Roda. This behavior will become the default in Roda 4. = Other Improvements * The mailer plugin now correctly sets the content_type of the body for emails with attachments when using mail 2.8.0+. jeremyevans-roda-4f30bb3/doc/release_notes/3.64.0.txt000066400000000000000000000021351516720775400222770ustar00rootroot00000000000000= New Features * An erb_h plugin has been added for faster HTML escaping using erb/escape. erb 4 added erb/escape and it is included in Ruby 3.2. The erb_h plugin is added as a separate plugin because it changes the behavior of the h method. The h method added by the h plugin will always return a new string, but the h method added by the erb_h plugin will return the argument if the argument is a string that does not need escaping. By avoiding unnecessary string allocations, use of the erb_h plugin can speed up HTML escaping. = Other Improvements * The autoload_hash_branches plugin added in Roda 3.63.0 will now eagerly load the hash branches when freezing the application, allowing the application to continue to work after being frozen. Additionally, file paths for the hash branches will now be automatically expanded, allowing the use of relative file paths. = Backwards Compatibility * The expanding of file paths in the autoload_hash_branches plugin can break applications that were providing relative paths and expecting them to be looked up using the Ruby load path. jeremyevans-roda-4f30bb3/doc/release_notes/3.65.0.txt000066400000000000000000000007061516720775400223020ustar00rootroot00000000000000= New Features * An autoload_named_routes plugin has been added for autoloading files for a named route setup by the named_routes plugin when there is a request for that route. = Other Improvements * The path method in the path plugin now supports a :class_name option. You can set this option to true and use a class name String/Symbol to register paths for classes without referencing the related class, useful when autoloading the class. jeremyevans-roda-4f30bb3/doc/release_notes/3.66.0.txt000066400000000000000000000016141516720775400223020ustar00rootroot00000000000000= New Features * A render_coverage plugin has been added, which will cause compiled template code to be saved to a folder and loaded using load instead of eval. This allows for coverage to work for the compiled template code in Ruby versions before 3.2. It can also allow for verbose syntax warnings in compiled template code (ignored by eval), and can also be useful for static analysis of compiled template code. This plugin requires tilt 2.1+. * The exception_page plugin now supports exception_page_{css,js} instance methods for overriding the CSS and JavaScript on the generated exception page. = Other Improvements * Using inline templates (render/view :inline option) no longer keeps a reference to the Roda instance that caches the template. = Backwards Compatibility * The Render::TemplateMtimeWrapper API has changed. Any external use of this class needs to be updated. jeremyevans-roda-4f30bb3/doc/release_notes/3.67.0.txt000066400000000000000000000014411516720775400223010ustar00rootroot00000000000000= New Feature * A custom_block_results plugin has been added for custom handling of block results. This allows routing blocks to return arbitrary objects instead of just String, nil, and false, and to have custom handling for them. For example, if you want to be able to have your routing blocks return the status code to use, you could do: plugin :custom_block_results handle_block_result Integer do |result| response.status_code = result end route do |r| 200 end While the expected use of the handle_block_result method is with class arguments, you can use any argument that implements an appropriate === method. The symbol_views and json plugins, which support additional block results, now use the custom_block_results plugin internally. jeremyevans-roda-4f30bb3/doc/release_notes/3.68.0.txt000066400000000000000000000011601516720775400223000ustar00rootroot00000000000000= New Feature * Roda.run in the multi_run plugin now accepts blocks, to allow autoloading of apps to dispatch to: class App < Roda plugin :multi_run run("other_app"){OtherApp} route do |r| r.multi_run end end With the above example, the block is not evaluated until a request for the /other_app branch is received. If OtherApp is autoloaded, this can speed up application startup and partial testing. When freezing the application (for production use), the block is eagerly loaded, so that requests to the /other_app branch do not call the block on every request. jeremyevans-roda-4f30bb3/doc/release_notes/3.69.0.txt000066400000000000000000000017761516720775400223160ustar00rootroot00000000000000= New Feature * The symbol_matcher method in the symbol_matchers plugin now supports a block to allow for type conversion of matched segments: symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| [Date.new(y.to_i, m.to_i, d.to_i)] end route do |r| r.on :date do |date| # date is an instance of Date end end As shown above, the block should return an array of objects to yield to the match block. If you have a segment match the passed regexp, but decide during block processing that you do not want to treat it as a match, you can have the block return nil or false. This is useful if you want to make sure you are using valid data: symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| y = y.to_i m = m.to_i d = d.to_i [Date.new(y, m, d)] if Date.valid_date?(y, m, d) end When providing a block when using the symbol_matchers method, that symbol may not work with the params_capturing plugin. jeremyevans-roda-4f30bb3/doc/release_notes/3.7.0.txt000066400000000000000000000101401516720775400222070ustar00rootroot00000000000000= New Features * A content_security_policy plugin has been added for setting up an appropriate Content-Security-Policy header. To configure the default policy, load the plugin with a block: plugin :content_security_policy do |csp| csp.default_src :none csp.img_src :self csp.style_src :self, 'fonts.googleapis.com' csp.script_src :self csp.font_src :self, 'fonts.gstatic.com' csp.form_action :self csp.base_uri :none csp.frame_ancestors :none csp.block_all_mixed_content end It's recommended that use use a default_src of :none at the top of the policy, then explicitly change other settings (e.g. img_src) when you want to allow content. Anywhere in the routing tree, you can use the content_security_policy method to override the default policy. You can pass this method a block: r.get 'foo' do content_security_policy do |csp| csp.object_src :self csp.add_style_src 'bar.com' end # ... end Or just call a method on it: r.get 'foo' do content_security_policy.script_src :self, 'example.com', [:nonce, 'foobarbaz'] # ... end The following methods exist for configuring the content security policy, they set the appropriate directive, with the underscores replaced by a dash. * base_uri * child_src * connect_src * default_src * font_src * form_action * frame_ancestors * frame_src * img_src * manifest_src * media_src * object_src * plugin_types * report_uri * require_sri_for * sandbox * script_src * style_src * worker_src All of these methods support any number of arguments, and each argument should be one of the following types: String :: used verbatim Symbol :: Substitutes underscore with dash and surrounds with single quotes Array :: only accepts 2 element arrays, joins elements with a dash and surrounds them with single quotes Example: content_security_policy.script_src :self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz'] # script-src 'self' 'unsafe-eval' example.com 'nonce-foobarbaz'; When calling a method with no arguments, the setting is removed from the policy instead of being left empty, since all of these setting require at least one value. Likewise, if the policy does not have any settings, the header will not be added. Calling the method overrides any previous setting. Each of the methods has a add_* method (e.g. add_script_src) for appending to the current setting, and a get_* method (e.g. get_script_src) for retrieving the current value of the setting, or nil if it is not defined. content_security_policy.script_src :self, :unsafe_eval # script-src 'self' 'unsafe-eval'; content_security_policy.add_script_src 'example.com', [:nonce, 'foobarbaz'] # script-src 'self' 'unsafe-eval' example.com 'nonce-foobarbaz'; content_security_policy.get_script_src 'example.com', [:nonce, 'foobarbaz'] # => [:self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz']] The clear method can be used to remove all settings from the policy. The following methods to set boolean directives are also defined: * block_all_mixed_content * upgrade_insecure_requests Calling these methods will turn on the related setting. To turn the setting off again, you can call them with a false argument (e.g. block_all_mixed_content(false)). Each method also an *? method (e.g. block_all_mixed_content?) for returning whether the setting is currently enabled. Likewise there is also a report_only method for turning on report only mode (the default is enforcement mode), or turning off report only mode if a false argument is given. Also, there is a report_only? method for returning whether report only mode is enabled. In report only mode, the Content-Security-Policy-Report-Only header is used. = Other Improvements * The response_request plugin now integrates with the error_handler and class_level_routing plugins. Those plugins now reinitialize the current response object instead of creating a new response object. jeremyevans-roda-4f30bb3/doc/release_notes/3.70.0.txt000066400000000000000000000015671516720775400223040ustar00rootroot00000000000000= New Features * A plain_hash_response_headers plugin has been added. On Rack 3, this changes Roda to use a plain hash for response headers (as it does on Rack 2), instead of using Rack::Headers (the default on Rack 3). For a minimal app, using this plugin can almost double the performance on Rack 3. Before using this plugin, you should make sure that all response headers set explictly in your application are already lower-case. = Improvements * Roda now natively uses lower-case for all response headers set implicitly when using Rack 3. Previously, Roda used mixed-case response headers and had Rack::Headers handle the conversion to lower-case (Rack 3 requires lower-case response headers). Note that Rack::Headers is still used for response headers by default on Rack 3, as applications may not have converted to using lower-case response headers. jeremyevans-roda-4f30bb3/doc/release_notes/3.71.0.txt000066400000000000000000000017521516720775400223010ustar00rootroot00000000000000= New Feature * A match_hook_args plugin has been added. This is similar to the existing match_hook plugin, but passes through the matchers and block arguments (values yielded to the match block). Example: plugin :match_hook_args add_match_hook do |matchers, block_args| logger.debug("matchers: #{matchers.inspect}. #{block_args.inspect} yielded.") end # Term is an implicit matcher used for terminating matches, and # will be included in the array of matchers yielded to the match hook # if a terminating match is used. term = self.class::RodaRequest::TERM route do |r| r.root do # for a request for / # matchers: nil, block_args: nil end r.on 'a', ['b', 'c'], Integer do |segment, id| # for a request for /a/b/1 # matchers: ["a", ["b", "c"], Integer], block_args: ["b", 1] end r.get 'd' do # for a request for /d # matchers: ["d", term], block_args: [] end end jeremyevans-roda-4f30bb3/doc/release_notes/3.72.0.txt000066400000000000000000000036441516720775400223040ustar00rootroot00000000000000= New Features * An invalid_request_body plugin has been added for allowing custom handling of invalid request bodies. Roda uses Rack's request body parsing, and by default invalid request bodies can result in different exceptions based on how the body is invalid and which version of Rack is in use. If you want to treat an invalid request body as the submission of no parameters, you can use the :empty_hash argument when loading the plugin: plugin :invalid_request_body, :empty_hash If you want to return a empty 400 (Bad Request) response if an invalid request body is submitted, you can use the :empty_400 argument when loading the plugin: plugin :invalid_request_body, :empty_400 If you want to raise a Roda::RodaPlugins::InvalidRequestBody::Error exception if an invalid request body is submitted (which makes it easier to handle these exceptions when using the error_handler plugin), you can use the :raise argument when loading the plugin: plugin :invalid_request_body, :raise For custom behavior, you can pass a block when loading the plugin The block is called with the exception Rack raised when parsing the body. The block will be used to define a method in the application's RodaRequest class. It can either return a hash of parameters, or you can raise a different exception, or you can halt processing and return a response: plugin :invalid_request_body do |exception| # To treat the exception raised as a submitted parameter {body_error: exception} end = Other Improvements * When using the check_arity: :warn Roda option, Roda now correctly warns when defining a method that expects a single argument when the provided block requires multiple arguments. * The match_hooks plugin is now implemented using the match_hook_args plugin, simplifying the implementation. This change should be transparent unless you were reaching into the internals. jeremyevans-roda-4f30bb3/doc/release_notes/3.73.0.txt000066400000000000000000000020401516720775400222720ustar00rootroot00000000000000= New Features * The middleware plugin now accepts a :next_if_not_found option. This allows the middleware plugin to pass the request to the next application if the current application handles the request but ends up calling the not_found handler. With the following middleware: class Mid < Roda plugin :middleware route do |r| r.on "foo" do r.get "bar" do 'bar' end end end end Requests for /x would be forwarded to the next application, since the application doesn't handle the request, but requests for /foo/x would not be, because the middleware is partially handling the request in the r.on "foo" block. With the :next_if_not_found option, only requests for /foo/bar would be handled by the middleware, and all other requests would be forwarded to the next application. = Other Improvements * The sessions and route_csrf plugins no longer depend on the base64 library. base64 will be removed from Ruby's standard library starting in Ruby 3.4. jeremyevans-roda-4f30bb3/doc/release_notes/3.74.0.txt000066400000000000000000000017601516720775400223030ustar00rootroot00000000000000= New Features * A redirect_http_to_https plugin has been added, redirecting HTTP requests to the same path on an HTTPS site. Using the routing tree, you can control where to do the redirection, which allows you to easily have part of your site accessible via HTTP, with sensitive sections requiring HTTPS: plugin :redirect_http_to_https route do |r| # routes available via both HTTP and HTTPS r.redirect_http_to_https # routes available only via HTTPS end If you want to redirect to HTTPS for all routes in the routing tree, you can have r.redirect_http_to_https as the very first method call in the routing tree. Note that in Roda it is possible to handle routing before the normal routing tree using before hooks. The static_routing and heartbeat plugins use this feature. If you would like to handle routes before the normal routing tree, you can setup a before hook: plugin :hooks before do request.redirect_http_to_https end jeremyevans-roda-4f30bb3/doc/release_notes/3.75.0.txt000066400000000000000000000017621516720775400223060ustar00rootroot00000000000000= New Features * A cookie_flags plugin has been added, for overriding, warning, or raising for incorrect cookie flags. The plugin by default checks whether the secure, httponly, and samesite=strict flags are set. The default behavior is to add the appropriate flags if they are not set, and change the samesite flag to strict if it is set to something else. You can configure the flag checking behavior via the :httponly, :same_site, and :secure options. You can configure the action the plugin takes via the :action option. The default action is to modify the flags, but the :action option can be set to :raise, :warn, or :warn_and_modify to override the behavior. The recommended way to use the plugin is to use it during testing, and specify action: :raise, so you can catch places where cookies are set with the wrong flags. Then you can fix those places to use the correct flags, which is better than relying on the plugin at runtime in production to fix incorrect flags. jeremyevans-roda-4f30bb3/doc/release_notes/3.76.0.txt000066400000000000000000000014461516720775400223060ustar00rootroot00000000000000= New Features * A break plugin has been added, allowing you to use break from inside a routing block and continue routing after the block. This offers the same feature as the pass plugin, but using the standard break keyword instead of the r.pass method. * The error_mail and error_email features now both accept a :filter plugin option. The value should respond to call with two arguments. The first arguments is the key, and the second is the value, and should return a truthy value if the value should be filtered. This will be used for filtering parameter values, ENV values, and session values in the generated emails. = Other Improvements * On Ruby 3.3+, the middleware plugin sets a temporary class name for the created middleware, based on the class name of the Roda app. jeremyevans-roda-4f30bb3/doc/release_notes/3.77.0.txt000066400000000000000000000006411516720775400223030ustar00rootroot00000000000000= New Features * The route_csrf plugin now supports formaction/formmethod attributes in forms. A csrf_formaction_tag method has been added for creating a hidden input for a particular path and method. When a form is submitted, the check_csrf! method will fix check for a path-specific csrf token (set by the hidden tag added by the csrf_formaction_tag method), before checking for the default csrf token. jeremyevans-roda-4f30bb3/doc/release_notes/3.78.0.txt000066400000000000000000000063641516720775400223140ustar00rootroot00000000000000= New Features * A permissions_policy plugin has been added that allows you to easily set a Permissions-Policy header for the application, which browsers can use to determine whether to allow specific functionality on the returned page (mainly related to which JavaScript APIs the page is allowed to use). You would generally call the plugin with a block to set the default policy: plugin :permissions_policy do |pp| pp.camera :none pp.fullscreen :self pp.clipboard_read :self, 'https://example.com' end Then, anywhere in the routing tree, you can customize the policy for just that branch or action using the same block syntax: r.get 'foo' do permissions_policy do |pp| pp.camera :self end # ... end In addition to using a block, you can also call methods on the object returned by the method: r.get 'foo' do permissions_policy.camera :self # ... end You can use the :default plugin option to set the default for all settings. For example, to disallow all access for each setting by default: plugin :permissions_policy, default: :none The following methods are available for configuring the permissions policy, which specify the setting (substituting _ with -): * accelerometer * ambient_light_sensor * autoplay * bluetooth * camera * clipboard_read * clipboard_write * display_capture * encrypted_media * fullscreen * geolocation * gyroscope * hid * idle_detection * keyboard_map * magnetometer * microphone * midi * payment * picture_in_picture * publickey_credentials_get * screen_wake_lock * serial * sync_xhr * usb * web_share * window_management All of these methods support any number of arguments, and each argument should be one of the following values: :all :: Grants permission to all domains (must be only argument) :none :: Does not allow permission at all (must be only argument) :self :: Allows feature in current document and any nested browsing contexts that use the same domain as the current document. :src :: Allows feature in current document and any nested browsing contexts that use the same domain as the src of the iframe. String :: Specifies origin domain where access is allowed When calling a method with no arguments, the setting is removed from the policy instead of being left empty, since all of these setting require at least one value. Likewise, if the policy does not have any settings, the header will not be added. Calling the method overrides any previous setting. Each of the methods has +add_*+ and +get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method returns the current value for the setting (this will be +:all+ if all domains are allowed, or any array of strings/:self/:src). permissions_policy.fullscreen :self, 'https://example.com' # fullscreen (self "https://example.com") permissions_policy.add_fullscreen 'https://*.example.com' # fullscreen (self "https://example.com" "https://*.example.com") permissions_policy.get_fullscreen # => [:self, "https://example.com", "https://*.example.com"] The clear method can be used to remove all settings from the policy. jeremyevans-roda-4f30bb3/doc/release_notes/3.79.0.txt000066400000000000000000000150021516720775400223020ustar00rootroot00000000000000= New Features * The hmac_paths plugin allows protection of paths using an HMAC. This can be used to prevent users enumerating paths, since only paths with valid HMACs will be respected. To use the plugin, you must provide a :secret option. This sets the secret for the HMACs. Make sure to keep this value secret, as this plugin does not provide protection against users who know the secret value. The secret must be at least 32 bytes. plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes' To generate a valid HMAC path, you call the hmac_path method: hmac_path('/widget/1') # => "/0c2feaefdfc80cc73da19b060c713d4193c57022815238c6657ce2d99b5925eb/0/widget/1" The first segment in the returned path is the HMAC. The second segment is flags for the type of paths (see below), and the rest of the path is as given. To protect a path or any subsection in the routing tree, you wrap the related code in an +r.hmac_path+ block. route do |r| r.hmac_path do r.get 'widget', Integer do |widget_id| # ... end end end If first segment of the remaining path contains a valid HMAC for the rest of the path (considering the flags), then r.hmac_path will match and yield to the block, and routing continues inside the block with the HMAC and flags segments removed. In the above example, if you provide a user a link for widget with ID 1, there is no way for them to guess the valid path for the widget with ID 2, preventing a user from enumerating widgets, without relying on custom access control. Users can only access paths that have been generated by the application and provided to them, either directly or indirectly. In the above example, r.hmac_path is used at the root of the routing tree. If you would like to call it below the root of the routing tree, it works correctly, but you must pass hmac_path the :root option specifying where r.hmac_paths will be called from. Consider this example: route do |r| r.on 'widget' do r.hmac_path do r.get Integer do |widget_id| # ... end end end r.on 'foobar' do r.hmac_path do r.get Integer do |foobar_id| # ... end end end end For security reasons, the hmac_path plugin does not allow an HMAC path designed for widgets to be a valid match in the r.hmac_path call inside the "r.on 'foobar'" block, preventing users who have a valid HMAC for a widget from looking at the page for a foobar with the same ID. When generating HMAC paths where the matching r.hmac_path call is not at the root of the routing tree, you must pass the :root option: hmac_path('/1', root: '/widget') # => "/widget/daccafce3ce0df52e5ce774626779eaa7286085fcbde1e4681c74175ff0bbacd/0/1" hmac_path('/1', root: '/foobar') # => "/foobar/c5fdaf482771d4f9f38cc13a1b2832929026a4ceb05e98ed6a0cd5a00bf180b7/0/1" Note how the HMAC changes even though the path is the same. In addition to the +:root+ option, there are additional options that further constrain use of the generated paths. The :method option creates a path that can only be called with a certain request method: hmac_path('/widget/1', method: :get) # => "/d38c1e634ecf9a3c0ab9d0832555b035d91b35069efcbf2670b0dfefd4b62fdd/m/widget/1" Note how this results in a different HMAC than the original hmac_path('/widget/1') call. This sets the flags segment to "m", which means r.hmac_path will consider the request mehod when checking the HMAC, and will only match if the provided request method is GET. This allows you to provide a user the ability to submit a GET request for the underlying path, without providing them the ability to submit a POST request for the underlying path, with no other access control. The :params option accepts a hash of params, converts it into a query string, and includes the query string in the returned path. It sets the flags segment to +p+, which means r.hmac_path will check for that exact query string. Requests with an empty query string or a different string will not match. hmac_path('/widget/1', params: {foo: 'bar'}) # => "/fe8d03f9572d5af6c2866295bd3c12c2ea11d290b1cbd016c3b68ee36a678139/p/widget/1?foo=bar" For GET requests, which cannot have request bodies, that is sufficient to ensure that the submitted params are exactly as specified. However, POST requests can have request bodies, and request body params override query string params in r.params. So if you are using this for POST requests (or other HTTP verbs that can have request bodies), use r.GET instead of r.params to specifically check query string parameters. You can use +:root+, +:method+, and +:params+ at the same time: hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'}) # => "/widget/9169af1b8f40c62a1c2bb15b1b377c65bda681b8efded0e613a4176387468c15/mp/1?foo=bar" This gives you a path only valid for a GET request with a root of "/widget" and a query string of "foo=bar". To handle secret rotation, you can provide an :old_secret option when loading the plugin. plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes', old_secret: 'previous-secret-value-with-at-least-32-bytes' This will use :secret for constructing new paths, but will respect paths generated by :old_secret. = Other Improvements * When not using cached templates in the render plugin, the render plugin now has better handling when a template is modified and results in an error. Previously, the error would be raised on the first request after the template modification, but subsequent requests would use the previous template value. The render plugin will no longer update the last modified time in this case, so if a template is modified and introduces an error (e.g. SyntaxError in an erb template), all future requests that use the template will result in the error being raised, until the template is fixed. = Backwards Compatibility * The internal TemplateMtimeWrapper API has been modified. As documented, this is an internal class and the API can change in any Roda version. However, if any code was relying on the previous implementation of TemplateMtimeWrapper#modified?, it will need to be modified, as that method has been replaced with TemplateMtimeWrapper#if_modified. Additionally, the TemplateMtimeWrapper#compiled_method_lambda API has also changed. jeremyevans-roda-4f30bb3/doc/release_notes/3.8.0.txt000066400000000000000000000017671516720775400222270ustar00rootroot00000000000000= New Features * The convert_each! method in the typecast_params plugin now accepts a Proc or Method value for the :keys option. The proc or method is called with the current array or hash that typecast params is operating on, and should return an array of keys to use for the conversion. * The convert_each! method in the typecast_params plugin will now automatically handle hashes with keys from '0'..'N', without a :keys option being provided. This makes it possible to handle parameter names such as foo[0][bar], foo[0][baz], foo[1][bar], and foo[1][baz], if you want to avoid the issues related to rack's issues when parsing array parameters. = Other Improvements * The Roda::RodaVersionNumber constant has been added for easier version comparisons. It is 30080 for version 3.8.0. = Backwards Compatibility * When an unsupported type is given as value of the :keys option to the convert_each! method in the typecast_params plugin, a ProgrammerError exception is now raised. jeremyevans-roda-4f30bb3/doc/release_notes/3.80.0.txt000066400000000000000000000030371516720775400222770ustar00rootroot00000000000000= New Features * The hmac_paths plugin now supports a :namespace option for both hmac_path and r.hmac_path. The :namespace option makes the generated HMAC values unique per namespace, allowing easy use of per user/group HMAC paths. This can be useful if the same path will show different information to different users/groups, and you want to prevent path enumeration for each user/group (not allow paths enumerated by one user/group to be valid for a different user/group). Example: hmac_path('/widget/1', namespace: '1') # => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1" hmac_path('/widget/1', namespace: '2') # => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1" The HMAC path created with namespace: '1' will only be valid when calling r.hmac_path with namespace: '1' (similar for namespace: '2'). It is expected that the most common use of the :namespace option is to reference session values, so the value of each path depends on the logged in user. You can use the :namespace_session_key plugin option to set the default namespace for both hmac_path and r.hmac_path: plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes', namespace_session_key: 'account_id' This will use session['account_id'] (converted to a string) as the namespace for both hmac_path and r.hmac_path, unless a specific :namespace option is given, making it simple to implement per user/group HMAC paths across an application. jeremyevans-roda-4f30bb3/doc/release_notes/3.81.0.txt000066400000000000000000000016401516720775400222760ustar00rootroot00000000000000= New Features * The hmac_paths plugin now supports :until and :seconds options for hmac_path, to create a path that is only valid for a specific amount of time. :until sets a specific time that the path will be valid until, and :seconds makes the path only valid for the given number of seconds. hmac_path('/widget/1', until: Time.utc(2100)) # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1" Requests for the path after the given time will not be matched by r.hmac_path. = Other Improvements * The early_hints plugin now correctly follows the Rack 3 SPEC when using Rack 3. This was not caught previously because Rack only added official support for early_hints in the last month. * Ruby 3.4 backtraces are now parsed correctly in the exception_page plugin. * Some plugins that accept a block no longer issue an unused block warning on Ruby 3.4. jeremyevans-roda-4f30bb3/doc/release_notes/3.82.0.txt000066400000000000000000000034151516720775400223010ustar00rootroot00000000000000= New Features * A :zstd option has been added to the public and multi_public plugins to support serving zstd-compressed files with a .zst extension. This option is similar to the existing :gzip and :brotli plugin options. Chrome started supporting zstd encoding in March. * An :encodings option has been added to the public and multi_public plugins, for more control over how encodings are handled. This allows for changing the order in which encodings are attempted, the use of custom encodings, and the use of different file extensions for encodings. Example: plugin :public, encodings: {'zstd'=>'.zst', 'deflate'=>'.deflate'} If the :encodings option is not provided, the :zstd, :brotli, and :gzip options are used to build an equivalent :encodings option. = Other Improvements * The capture_erb plugin now integrates better when using erubi/capture_block for <%= method do %> support in ERB templates, using the native capture method provided by the buffer object. * Encoding handling has been more optimized in the public plugin. Regexps for the encodings are precomputed, avoiding a regexp allocation per request per encoding attempted. On Ruby 2.4+ Regexp#match? is used for better performance. If the Accept-Encoding header is not present, no encoding matching is attemped. = Backwards Compatibility * The private public_serve_compressed request method in the public plugin now assumes it is called after the encoding is already valid. If you are calling this method in your own code, you now need to perform checks to make sure the client can accept the encoding before calling this method. * The :public_gzip and :public_brotli application options are no longer set by the public plugin. The :public_encodings option is now set. jeremyevans-roda-4f30bb3/doc/release_notes/3.83.0.txt000066400000000000000000000004221516720775400222750ustar00rootroot00000000000000= New Features * An assume_ssl plugin has been added. This plugin is designed for cases where the application is being fronted by an SSL-terminating reverse proxy that does not set the X-Forwarded-Proto or similar header to indicate it is forwarding an SSL request. jeremyevans-roda-4f30bb3/doc/release_notes/3.84.0.txt000066400000000000000000000006161516720775400223030ustar00rootroot00000000000000= New Features * An hsts plugin has been added to easily add an appropriate Strict-Transport-Security header: plugin :hsts # Strict-Transport-Security: max-age=63072000; includeSubDomains plugin :hsts, preload: true # Strict-Transport-Security: max-age=63072000; includeSubDomains; preload = Other Improvements * The gem size has been reduced 25% by removing documentation. jeremyevans-roda-4f30bb3/doc/release_notes/3.85.0.txt000066400000000000000000000067161516720775400223130ustar00rootroot00000000000000= New Features * The class_matchers and symbol_matchers plugins now allow building on top of existing class and symbol matchers. This allows you to simplify code such as: r.on "employee", Integer do |emp_id| next unless employee = Employee[emp_id] # ... end by defining an appropriate class matcher: class_matcher Employee, Integer do |emp_id| Employee[emp_id] end and then changing the matcher in the route code: r.on "employee", Employee do |employee| # ... end This avoids the need to check for a valid employee in each route, by having the check in the class_matcher block. If a request comes in with a valid integer segment, but there is no employee assigned with that integer, then the Employee matcher will not match. Symbol matchers can build upon class matchers (and vice-versa): symbol_matcher :ActiveEmployee, Employee do |employee| employee if employee.active? end With the above :ActiveEmployee matcher, segments will only match if they are an integer that is related to an employee, and that employee is active. = Other Improvements * As shown in the above examples, class_matcher and symbol_matcher blocks can now return non-arrays. This can reduce the number of unnecessary allocations, and result in simpler code. * The blocks passed to class_matcher and symbol_matcher are now evaluated in route block context. That allows you to have the matchers depend on request or session specific state. For example, a Post class matcher such as: class_matcher Post, Integer do |id| Post.where(user_id: session['user_id']).with_pk(id) end will only match if the user for the related Post matches the logged in user. * Symbol matchers based on regexps are now faster by caching the regexp at a higher level, avoiding the need to look up the cached regexp for every request. * The public plugin now avoids a deprecation warning when using Ruby 3.4.0-preview2. * The capture_erb plugin no longer breaks if ActiveSupport 4 is loaded. ActiveSupport 4 defines Kernel#capture, which broke the capture_erb plugin's assumption that calling capture was safe if the method was defined. capture_erb does not call capture on the buffer object if the buffer object is a String instance. The use of capture is designed for usage with erubi/capture_block, which does not use a String instance as a buffer object. = Backwards Compatibility * Changing the class_matcher and symbol_matcher blocks to be evaluated in route block context can break code that assumes they were evaluated in the context in which they were called. Generally, that context is application class context. For example, the following type of code would break: class App < Roda plugin :class_matchers def self.get_class(klass) const_get(klass) end class_matcher Employee, Integer do |emp_id| get_class(:Employee)[emp_id] end end This worked previously, because get_class was defined as a class method, and the block was evaluated in class context, as that is the context in which it was defined. You would have to define a get_class instance method to allow the example to continue to work. * The internals of the Integer_matcher_max plugin have been updated, to integrate with the class_matchers and symbol_matchers changes. The _match_class_convert_Integer and _match_class_max_Integer private request methods have been removed. jeremyevans-roda-4f30bb3/doc/release_notes/3.86.0.txt000066400000000000000000000024741516720775400223110ustar00rootroot00000000000000= New Features * A conditional_sessions plugin has been added. This allows you to only support sessions for a subset of the application's requests. You pass a block when loading the plugin, and sessions are only supported if the block returns truthy. The block is evaluated in request scope. As an example, if you do not want to support sessions for request paths starting with /static, you could use: plugin :conditional_sessions, secret: ENV["SECRET"] do !path_info.start_with?('/static') end With this example, if the request path starts with /static: * The request methods +session+, +session_created_at+, and +session_updated_at+ all raise an exception. * The request +persist_session+ and route scope +clear_session+ methods do nothing and return nil. Options passed when loading the plugin are passed to the sessions plugin. * In the content_security_policy plugin, you can now call response.skip_content_security_policy! to skip the setting of the response header. * In the permissions_policy plugin, you can now call response.skip_permissions_policy! to skip the setting of the response header. = Other Improvements * When using the autoload_hash_branches and/or autoload_named_routes plugins, Roda.freeze now works correctly if the Roda class is already frozen. jeremyevans-roda-4f30bb3/doc/release_notes/3.87.0.txt000066400000000000000000000027551516720775400223140ustar00rootroot00000000000000= New Features * A host_routing plugin has been added, for easier routing based on the request host. Example: plugin :host_routing do |hosts| hosts.to :api, "api.example.com", "api2.example.com" hosts.default :www end route do |r| r.api do # requests to api.example.com or api2.example.com end r.www do # requests to other domains end end The plugin also adds request predicate methods: route do |r| r.api? # true if the request is to api.example.com or api2.example.com r.www? # true for request for other domains end If the :scope_predicates plugin option is given, these predicate methods are also supported directly in block scope (no "r."). For more advanced cases, such as prefix matches on the host, the hosts.default method accepts a block. In this case, you should also call hosts.register to notify the plugin about what hosts the block could return: plugin :host_routing do |hosts| hosts.register :api hosts.default :www do |host| :api if host.end_with?(".api.example.com") end end = Other Improvements * In the custom_block_results plugin, if the block passed to handle_block_result returns an object that is not a String, nil, or false, Roda no longer attempts to write it to the response body. Doing so is undesirable and would be a violation of the rack spec. * Minor performance improvements have been made to the header_matchers plugin. jeremyevans-roda-4f30bb3/doc/release_notes/3.88.0.txt000066400000000000000000000046511516720775400223120ustar00rootroot00000000000000= New Features * Fixed locals are now supported in templates when using Tilt 2.6+. Without fixed locals, templates that support local variables can be called with any locals, and a separate template method is compiled for each combination of local variable names. This causes multiple issues: * It is inefficient, especially for large templates that are called with many combinations of locals. * It hides issues if unused local variable names are passed to the template * It does not support default values for local variables * It does not support required local variables * It does not support cases where you want to pass values via a keyword splat * It does not support named blocks Fixed locals solve these problems by having the compiled methods use keyword arguments instead of a single positional hash argument. This allows you to use required keyword arguments, provide default values for optional keyword arguments, and use keyword splats and named blocks. See https://github.com/jeremyevans/tilt#fixed-locals for details. You can enable embedded fixed locals in templates using the `:extract_fixed_locals` template option. The recommended template options when creating new Roda applications that use the render plugin are now: plugin :render, template_opts: { scope_class: self, # Always uses current class as scope class for compiled templates freeze: true, # Freeze string literals in templates extract_fixed_locals: true, # Support embedded fixed locals in templates default_fixed_locals: '()', # Default to templates not supporting local variables escape: true, # For Erubi templates, escapes <%= by default (use <%== for unescaped chain_appends: true, # For Erubi templates, improves performance skip_compiled_encoding_detection: true, # Unless you need encodings explicitly specified } = Other Improvements * The json_parser plugin now handles the case where Rack::Request#POST has already been called on the env hash, when using Rack 3+. * The default_headers plugin now handles a mixed/upper case Content-Type header, when using Rack 3+ (which requires lower case headers). * The render_coverage plugin now handles the case where both :scope_class template option and fixed locals are used. * Roda now avoids warnings when the -W:strict_unused_block Ruby option is used. jeremyevans-roda-4f30bb3/doc/release_notes/3.89.0.txt000066400000000000000000000022371516720775400223110ustar00rootroot00000000000000= New Features * The render plugin now supports an :assume_fixed_locals option, which allows for better caching when all templates use fixed locals, by using a simplified cache key, and avoiding duplicate cache entries for templates rendered both with and without locals. Additionally, when this plugin option is set, calling template methods is now faster if the following are true: * The application is frozen * Template caching is enabled * Ruby version is 3+ * A part plugin has been added, which simplifies rendering a template with locals: # render plugin render(:template, locals: {foo: 'bar'}) # part plugin part(:template, foo: 'bar') In addition to offering a nicer API if you only need to provide locals, the part method can also be faster if all of the following are true: * The application is frozen * The :assume_fixed_locals render plugin option is set * Template caching is enabled * Ruby version is 3+ (even faster on Ruby 3.4+) = Other Improvements * The mailer plugin's mail and sendmail class methods now support keyword arguments and pass them as keywords to the r.mail blocks in the routing tree. jeremyevans-roda-4f30bb3/doc/release_notes/3.9.0.txt000066400000000000000000000056671516720775400222330ustar00rootroot00000000000000= New Features * A route_csrf plugin has been added. This plugin allows for more control over CSRF protection, since the user can choose where in the routing tree to enforce the protection. Additionally, the route_csrf plugin offers better security than the CSRF protection used by the csrf plugin (which uses the rack_csrf library). The route_csrf plugin defaults to allowing only CSRF tokens specific to a given request method and request path, and not allowing generic CSRF tokens (though it does offer optional support for such tokens). Both request-specific and generic CSRF tokens are designed to never leak the CSRF secret key, making it more difficult to forge valid CSRF tokens. Additionally, the plugin offers optional support for accepting rack_csrf tokens, which should only be enabled during a short transition period. Some differences between the route_csrf plugin and the older csrf plugin: * route_csrf supports and by default only allows CSRF tokens specific to request method and request path, as mentioned above. You can use the require_request_specific_tokens: false option to allow generic CSRF tokens. * route_csrf does not check the HTTP header by default, it only checks the header if the :check_header option is set. The :check_header option can be set to true to check both the parameter and the header, or set to :only to only check the header. * route_csrf raises by default for invalid CSRF tokens. rack_csrf returns an empty 403 response in that case. You can use the error_handler plugin to handle the Roda::RodaPlugins::RouteCsrf::InvalidToken exceptions, or you can use the csrf_failure: :empty_403 option if you would like the csrf plugin default behavior. The plugin also accepts a block for configurable failure behavior. * route_csrf does not use a middleware, as it is designed to give more control. In order to enforce the CSRF protection, you need to call check_csrf! in your routing tree at the appropriate place. If you are not sure where to add it, add it to the top of the routing tree, after the public or assets routes if you are using those plugins: route do r.public r.assets check_csrf! # ... end The check_csrf! method accepts an options hash, which can be used to override the plugin options on a per-call basis. * The csrf_token/csrf_tag methods take an optional path and method arguments. If a path is given, the method defaults to POST, and the resulting CSRF token can only be used to submit forms for the path and method. If a path is not given, the resulting CSRF token will be generic, but it will only work if the plugin has been configured to allow generic CSRF tokens. * A csrf_path method is available for easily taking a form action string and returning an appropriate path to pass to the csrf_token or csrf_tag methods. jeremyevans-roda-4f30bb3/doc/release_notes/3.90.0.txt000066400000000000000000000006021516720775400222730ustar00rootroot00000000000000= Improvements * The send_file method in the sinatra_helpers plugin now returns a response body that implements to_path. * Roda now sets a temporary name for the remaining anonymous modules and classes on Ruby 3.3+. * The common_logger plugin now escapes embedded newlines. These should only be present if the server is broken and including newlines in things it shouldn't. jeremyevans-roda-4f30bb3/doc/release_notes/3.91.0.txt000066400000000000000000000022101516720775400222710ustar00rootroot00000000000000= New Features * The render_each method in the render_each plugin now accepts a block. If passed a block, instead of returning a concatenation of the rendered template output, it yields each rendered template output, and returns nil. This allows for use in the case where you want to wrap the template output: <% render_each([1,2,3], :foo) do |text| %>

<%= text %>

<% end %> If can also be used to reduce memory usage even in the case where you are not wrapping template output. Instead of: <%= render_each([1,2,3], :foo) %> You can do: <% render_each([1,2,3], :foo) %><%= body %><% end %> This will avoid building a potentially large unnecessary intermediate string. * The capture_erb plugin now supports a returns: :buffer method and plugin option. When this option is provided, the capture_erb method returns the buffer instead of the return value of the block passed to it. This better handles cases where the template ends in a conditional: <% value = capture_erb do %> Some content here. <% if something %> Some more content here. <% end %> <% end %> jeremyevans-roda-4f30bb3/doc/release_notes/3.92.0.txt000066400000000000000000000011601516720775400222750ustar00rootroot00000000000000= New Features * An each_part plugin has been added, offering a simpler method for using render_each with locals: # With render_each: render_each(array_of_foos, :foo, locals: {bar: 1}) # With each_part: each_part(array_of_foos, :foo, bar: 1) The each_part provides similar benefits to the render_each plugin that the part plugin provides to the render plugin. The each_part method has been optimized to work with the render plugin's :assume_fixed_locals option. = Other Improvements * The render_each plugin has been optimized to work with the render plugin's :assume_fixed_locals option. jeremyevans-roda-4f30bb3/doc/release_notes/3.93.0.txt000066400000000000000000000034261516720775400223050ustar00rootroot00000000000000= New Features * The typecast_params plugin handle_type method now supports an :invalid_value_message option, for a custom error message for the type, explaining why the input is invalid. This error message is used when there is a parameter given, but it cannot be converted to the desired type: plugin :typecast_params do handle_type(:single_char, invalid_value_message: \ "value not a single character") do |v| v if v.is_a?(String) && v.length == 1 end end Previously, the error message in this case was the same as when no parameter was provided, which was misleading. The types natively supported by the typecast_params and typecast_params_sized_integers plugins now use :invalid_value_message for better error reporting. You can override the invalid value messages for these types using the invalid_value_message method: plugin :typecast_params do invalid_value_message(:pos_int, "value must be greater than 0 for parameter") end = Other Improvements * Many minor performance improvements, mostly from rubocop-performance: * flat_map instead of map.flatten(1) * tr/delete instead of gsub * symbol instead of string argument to method_defined? * hoist literal arrays outside blocks * end_with?/include? instead of =~ = Backwards Compatibility * The fourth parameter in the process and process_arg private methods in the typecast_params plugin has changed from being the max input bytesize of the type, to the type symbol, and is now a required parameter. External callers of these private methods will need to be updated. * Code rescuing Roda::RodaPlugins::TypecastParams::Error and handling the reason may need to adjust to handling :invalid_value in cases where it was handling :missing. jeremyevans-roda-4f30bb3/doc/release_notes/3.94.0.txt000066400000000000000000000014121516720775400222770ustar00rootroot00000000000000= New Features * A view_subdir_leading_slash plugin has been added, for using the current view subdirectory for all templates that do not start with a slash. The default behavior when using view subdirectories remains to use the current view subdirectory unless the template name includes a slash. This makes it easier to use nested view subdirectories, at the expense of making it slightly more difficult to use templates outside the current view subdirectory. = Other Improvements * The render_each and each_part plugin template selection code is now more optimized when the application is frozen and using the :assume_fixed_locals render plugin option. * The render_each and each_part plugin default local generation is now more optimized on Ruby 3+. jeremyevans-roda-4f30bb3/doc/release_notes/3.95.0.txt000066400000000000000000000033451516720775400223070ustar00rootroot00000000000000= New Features * A response_content_type plugin has been added for more easily setting the content type of responses: When setting the content-type, you can pass either a string, which is used directly: response.content_type = "text/html" Or, if you have registered mime types when loading the plugin: plugin :response_content_type, mime_types: { plain: "text/plain", html: "text/html", pdf: "application/pdf" } You can use a symbol: response.content_type = :html If you would like to load all mime types supported by rack/mime, you can use the mime_types: :from_rack_mime option: plugin :response_content_type, mime_types: :from_rack_mime Note that you are unlikely to be using all of these mime types, so doing this will likely result in unnecessary memory usage. It is recommended to use a hash with only the mime types your application actually uses. To prevent silent failures, if you attempt to set the response type with a symbol, and the symbol is not recognized, a KeyError is raised. * The typecast_params plugin now includes typecast_query_params and typecast_body_params methods in addition to typecast_params. typecast_query_params deals with query string parameters (r.GET), and typecast_body_params deals with request body parameters (r.POST). = Other Improvements * The sessions plugin now raises Roda::RodaPlugins::Sessions::CookieTooLarge if the total cookie size is over 4K. Previously, it only raised the exception if the cookie value was over 4K. Browsers enforce the limit on the total cookie size, not just the value, so this change prevents Roda from setting a cookie that a browser would ignore due to size restrictions. jeremyevans-roda-4f30bb3/doc/release_notes/3.96.0.txt000066400000000000000000000007271516720775400223110ustar00rootroot00000000000000= New Features * A redirect_path plugin has been added, which integrates the path plugin with r.redirect: Foo = Struct.new(:id) foo = Foo.new(1) plugin :redirect_path path Foo do |foo| "/foo/#{foo.id}" end route do |r| r.get "example" do # redirects to /foo/1 r.redirect(foo) end r.get "suffix-example" do # redirects to /foo/1/status r.redirect(foo, "/status") end end jeremyevans-roda-4f30bb3/doc/release_notes/3.97.0.txt000066400000000000000000000021201516720775400222770ustar00rootroot00000000000000= New Features * A map_matcher plugin has been added, for matching the next segment in the request path to a hash key, yielding the hash value. This allows for a better approach for metaprogramming routes. When dealing with many similar routes, it is common to use a hash keyed by route segment, and then match against an array of the keys: map = { 'album' => Album, 'artist' => Artist, # ... }.freeze keys = map.keys.freeze route do |r| r.on "type", keys do |key| value = map[key] # ... end end For large maps, this approach is suboptimal, since the array matcher will iterate over each element in the array, checking whether it matches. The map_matcher plugin allows for: plugin :map_matcher route do |r| r.on "type", map: map do |value| # ... end end This gets the next route segment and checks whether it is a key in the map, instead of iterating over an array of hash keys, so it is more efficient for large maps. It also results in simpler code. jeremyevans-roda-4f30bb3/doc/release_notes/3.98.0.txt000066400000000000000000000012601516720775400223040ustar00rootroot00000000000000= New Features * The sessions plugin now supports an :env_key option, which allows the use of a non-default env key. This allows you to maintain the session for the Roda application separately from the default Rack session. This can be useful in all of the following cases when you want to have the Roda application use a separate session from other applications or middleware: * Using middleware in the Roda application. * Using the Roda application as middleware in another Rack application. * Using r.run in the Roda application to call another Rack application. * Calling the Roda application from another Rack application using the same env hash. jeremyevans-roda-4f30bb3/doc/release_notes/3.99.0.txt000066400000000000000000000006411516720775400223070ustar00rootroot00000000000000= New Features * Set instances are now supported as matchers by default, as Set will be a core class in Ruby 4.0. A Set matcher operates similarly to an array matcher if all of the array elements are strings, with the difference that the Set matcher will perform better for large numbers of elements, since it will look for a matching entry in the set, instead of iterating over the elements of the set. jeremyevans-roda-4f30bb3/lib/000077500000000000000000000000001516720775400161565ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/lib/roda.rb000066400000000000000000000547121516720775400174410ustar00rootroot00000000000000# frozen-string-literal: true require "thread" require_relative "roda/request" require_relative "roda/response" require_relative "roda/plugins" require_relative "roda/cache" require_relative "roda/version" # The main class for Roda. Roda is built completely out of plugins, with the # default plugin being Roda::RodaPlugins::Base, so this class is mostly empty # except for some constants. class Roda # Error class raised by Roda class RodaError < StandardError; end @app = nil @inherit_middleware = true @middleware = [] @opts = {} @raw_route_block = nil @route_block = nil @rack_app_route_block = nil module RodaPlugins # The base plugin for Roda, implementing all default functionality. # Methods are put into a plugin so future plugins can easily override # them and call super to get the default behavior. module Base # Class methods for the Roda class. module ClassMethods # The rack application that this class uses. def app @app || build_rack_app end # Whether middleware from the current class should be inherited by subclasses. # True by default, should be set to false when using a design where the parent # class accepts requests and uses run to dispatch the request to a subclass. attr_accessor :inherit_middleware # The settings/options hash for the current class. attr_reader :opts # The route block that this class uses. attr_reader :route_block # Call the internal rack application with the given environment. # This allows the class itself to be used as a rack application. # However, for performance, it's better to use #app to get direct # access to the underlying rack app. def call(env) app.call(env) end # Clear the middleware stack def clear_middleware! @middleware.clear @app = nil end # Define an instance method using the block with the provided name and # expected arity. If the name is given as a Symbol, it is used directly. # If the name is given as a String, a unique name will be generated using # that string. The expected arity should be either 0 (no arguments), # 1 (single argument), or :any (any number of arguments). # # If the :check_arity app option is not set to false, Roda will check that # the arity of the block matches the expected arity, and compensate for # cases where it does not. If it is set to :warn, Roda will warn in the # cases where the arity does not match what is expected. # # If the expected arity is :any, Roda must perform a dynamic arity check # when the method is called, which can hurt performance even in the case # where the arity matches. The :check_dynamic_arity app option can be # set to false to turn off the dynamic arity checks. The # :check_dynamic_arity app option can be to :warn to warn if Roda needs # to adjust arity dynamically. # # Roda only checks arity for regular blocks, not lambda blocks, as the # fixes Roda uses for regular blocks would not work for lambda blocks. # # Roda does not support blocks with required keyword arguments if the # expected arity is 0 or 1. def define_roda_method(meth, expected_arity, &block) if meth.is_a?(String) meth = roda_method_name(meth) end call_meth = meth # RODA4: Switch to false # :warn in last Roda 3 version if (check_arity = opts.fetch(:check_arity, true)) && !block.lambda? required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(block) if keyword == :required && (expected_arity == 0 || expected_arity == 1) raise RodaError, "cannot use block with required keyword arguments when calling define_roda_method with expected arity #{expected_arity}" end case expected_arity when 0 unless required_args == 0 if check_arity == :warn RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 0, but arguments required for #{block.inspect}" end b = block block = lambda{instance_exec(&b)} # Fallback end when 1 if required_args == 0 && optional_args == 0 && !rest if check_arity == :warn RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but no arguments accepted for #{block.inspect}" end temp_method = roda_method_name("temp") class_eval("def #{temp_method}(_) #{meth =~ /\A\w+\z/ ? "#{meth}_arity" : "send(:\"#{meth}_arity\")"} end", __FILE__, __LINE__) alias_method meth, temp_method undef_method temp_method private meth alias_method meth, meth meth = :"#{meth}_arity" elsif required_args > 1 if check_arity == :warn RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but multiple arguments required for #{block.inspect}" end b = block block = lambda{|r| instance_exec(r, &b)} # Fallback end when :any if check_dynamic_arity = opts.fetch(:check_dynamic_arity, check_arity) if keyword # Complexity of handling keyword arguments using define_method is too high, # Fallback to instance_exec in this case. b = block block = if RUBY_VERSION >= '2.7' eval('lambda{|*a, **kw| instance_exec(*a, **kw, &b)}', nil, __FILE__, __LINE__) # Keyword arguments fallback else # :nocov: lambda{|*a| instance_exec(*a, &b)} # Keyword arguments fallback # :nocov: end else arity_meth = meth meth = :"#{meth}_arity" end end else raise RodaError, "unexpected arity passed to define_roda_method: #{expected_arity.inspect}" end end define_method(meth, &block) private meth alias_method meth, meth if arity_meth required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(instance_method(meth)) max_args = required_args + optional_args define_method(arity_meth) do |*a| arity = a.length if arity > required_args if arity > max_args && !rest if check_dynamic_arity == :warn RodaPlugins.warn "Dynamic arity mismatch in block passed to define_roda_method. At most #{max_args} arguments accepted, but #{arity} arguments given for #{block.inspect}" end a = a.slice(0, max_args) end elsif arity < required_args if check_dynamic_arity == :warn RodaPlugins.warn "Dynamic arity mismatch in block passed to define_roda_method. #{required_args} args required, but #{arity} arguments given for #{block.inspect}" end a.concat([nil] * (required_args - arity)) end send(meth, *a) end private arity_meth alias_method arity_meth, arity_meth end call_meth end # Expand the given path, using the root argument as the base directory. def expand_path(path, root=opts[:root]) ::File.expand_path(path, root) end # Freeze the internal state of the class, to avoid thread safety issues at runtime. # It's optional to call this method, as nothing should be modifying the # internal state at runtime anyway, but this makes sure an exception will # be raised if you try to modify the internal state after calling this. # # Note that freezing the class prevents you from subclassing it, mostly because # it would cause some plugins to break. def freeze return self if frozen? unless opts[:subclassed] # If the _roda_run_main_route instance method has not been overridden, # make it an alias to _roda_main_route for performance if instance_method(:_roda_run_main_route).owner == InstanceMethods class_eval("alias _roda_run_main_route _roda_main_route") end self::RodaResponse.class_eval do if instance_method(:set_default_headers).owner == ResponseMethods && instance_method(:default_headers).owner == ResponseMethods private alias set_default_headers set_default_headers def set_default_headers @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html' end end end if @middleware.empty? && use_new_dispatch_api? plugin :direct_call end if ([:on, :is, :_verb, :_match_class_String, :_match_class_Integer, :_match_string, :_match_regexp, :empty_path?, :if_match, :match, :_match_class]).all?{|m| self::RodaRequest.instance_method(m).owner == RequestMethods} plugin :_optimized_matching end end build_rack_app @opts.freeze @middleware.freeze super end # Rebuild the _roda_before and _roda_after methods whenever a plugin might # have added a _roda_before_* or _roda_after_* method. def include(*a) res = super def_roda_before def_roda_after res end # When inheriting Roda, copy the shared data into the subclass, # and setup the request and response subclasses. def inherited(subclass) raise RodaError, "Cannot subclass a frozen Roda class" if frozen? # Mark current class as having been subclassed, as some optimizations # depend on the class not being subclassed opts[:subclassed] = true super subclass.instance_variable_set(:@inherit_middleware, @inherit_middleware) subclass.instance_variable_set(:@middleware, @inherit_middleware ? @middleware.dup : []) subclass.instance_variable_set(:@opts, opts.dup) subclass.opts.delete(:subclassed) subclass.opts.to_a.each do |k,v| if (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen? subclass.opts[k] = v.dup end end if block = @raw_route_block subclass.route(&block) end request_class = Class.new(self::RodaRequest) request_class.roda_class = subclass request_class.match_pattern_cache = RodaCache.new subclass.const_set(:RodaRequest, request_class) response_class = Class.new(self::RodaResponse) response_class.roda_class = subclass subclass.const_set(:RodaResponse, response_class) end # Load a new plugin into the current class. A plugin can be a module # which is used directly, or a symbol representing a registered plugin # which will be required and then used. Returns nil. # # Note that you should not load plugins into a Roda class after the # class has been subclassed, as doing so can break the subclasses. # # Roda.plugin PluginModule # Roda.plugin :csrf def plugin(plugin, *args, &block) raise RodaError, "Cannot add a plugin to a frozen Roda class" if frozen? plugin = RodaPlugins.load_plugin(plugin) if plugin.is_a?(Symbol) raise RodaError, "Invalid plugin type: #{plugin.class.inspect}" unless plugin.is_a?(Module) if !plugin.respond_to?(:load_dependencies) && !plugin.respond_to?(:configure) && (!args.empty? || block) # RODA4: switch from warning to error RodaPlugins.warn("Plugin #{plugin} does not accept arguments or a block, but arguments or a block was passed when loading this. This will raise an error in Roda 4.") end plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies) include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods) extend(plugin::ClassMethods) if defined?(plugin::ClassMethods) self::RodaRequest.send(:include, plugin::RequestMethods) if defined?(plugin::RequestMethods) self::RodaRequest.extend(plugin::RequestClassMethods) if defined?(plugin::RequestClassMethods) self::RodaResponse.send(:include, plugin::ResponseMethods) if defined?(plugin::ResponseMethods) self::RodaResponse.extend(plugin::ResponseClassMethods) if defined?(plugin::ResponseClassMethods) plugin.configure(self, *args, &block) if plugin.respond_to?(:configure) @app = nil end # :nocov: ruby2_keywords(:plugin) if respond_to?(:ruby2_keywords, true) # :nocov: # Setup routing tree for the current Roda application, and build the # underlying rack application using the stored middleware. Requires # a block, which is yielded the request. By convention, the block # argument should be named +r+. Example: # # Roda.route do |r| # r.root do # "Root" # end # end # # This should only be called once per class, and if called multiple # times will overwrite the previous routing. def route(&block) unless block RodaPlugins.warn "no block passed to Roda.route" return end @raw_route_block = block @route_block = block = convert_route_block(block) @rack_app_route_block = block = rack_app_route_block(block) public define_roda_method(:_roda_main_route, 1, &block) @app = nil end # Add a middleware to use for the rack application. Must be # called before calling #route to have an effect. Example: # # Roda.use Rack::ShowExceptions def use(*args, &block) @middleware << [args, block].freeze @app = nil end # :nocov: ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true) # :nocov: private # Return the number of required argument, optional arguments, # whether the callable accepts any additional arguments, # and whether the callable accepts keyword arguments (true, false # or :required). def _define_roda_method_arg_numbers(callable) optional_args = 0 rest = false keyword = false callable.parameters.map(&:first).each do |arg_type, _| case arg_type when :opt optional_args += 1 when :rest rest = true when :keyreq keyword = :required when :key, :keyrest keyword ||= true end end arity = callable.arity if arity < 0 arity = arity.abs - 1 end required_args = arity arity -= 1 if keyword == :required if callable.is_a?(Proc) && !callable.lambda? optional_args -= arity end [required_args, optional_args, rest, keyword] end # The base rack app to use, before middleware is added. def base_rack_app_callable(new_api=true) if new_api lambda{|env| new(env)._roda_handle_main_route} else block = @rack_app_route_block lambda{|env| new(env).call(&block)} end end # Build the rack app to use def build_rack_app app = base_rack_app_callable(use_new_dispatch_api?) @middleware.reverse_each do |args, bl| mid, *args = args app = mid.new(app, *args, &bl) app.freeze if opts[:freeze_middleware] end @app = app end # Modify the route block to use for any route block provided as input, # which can include route blocks that are delegated to by the main route block. # Can be modified by plugins. def convert_route_block(block) block end # Build a _roda_before method that calls each _roda_before_* method # in order, if any _roda_before_* methods are defined. Also, rebuild # the route block if a _roda_before method is defined. def def_roda_before meths = private_instance_methods.grep(/\A_roda_before_\d\d/).sort unless meths.empty? plugin :_before_hook unless private_method_defined?(:_roda_before) if meths.length == 1 class_eval("alias _roda_before #{meths.first}", __FILE__, __LINE__) else class_eval("def _roda_before; #{meths.join(';')} end", __FILE__, __LINE__) end private :_roda_before alias_method :_roda_before, :_roda_before end end # Build a _roda_after method that calls each _roda_after_* method # in order, if any _roda_after_* methods are defined. Also, use # the internal after hook plugin if the _roda_after method is defined. def def_roda_after meths = private_instance_methods.grep(/\A_roda_after_\d\d/).sort unless meths.empty? plugin :error_handler unless private_method_defined?(:_roda_after) if meths.length == 1 class_eval("alias _roda_after #{meths.first}", __FILE__, __LINE__) else class_eval("def _roda_after(res); #{meths.map{|s| "#{s}(res)"}.join(';')} end", __FILE__, __LINE__) end private :_roda_after alias_method :_roda_after, :_roda_after end end # The route block to use when building the rack app (or other initial # entry point to the route block). # By default, modifies the rack app route block to support before hooks # if any before hooks are defined. # Can be modified by plugins. def rack_app_route_block(block) block end # Whether the new dispatch API should be used. def use_new_dispatch_api? # RODA4: remove this method ancestors.each do |mod| break if mod == InstanceMethods meths = mod.instance_methods(false) if meths.include?(:call) && !(meths.include?(:_roda_handle_main_route) || meths.include?(:_roda_run_main_route)) RodaPlugins.warn < 'GET' def env @_request.env end # The class-level options hash. This should probably not be # modified at the instance level. Example: # # Roda.plugin :render # Roda.route do |r| # opts[:render_opts].inspect # end def opts self.class.opts end attr_reader :_request # :nodoc: alias request _request remove_method :_request attr_reader :_response # :nodoc: alias response _response remove_method :_response # The session hash for the current request. Raises RodaError # if no session exists. Example: # # session # => {} def session @_request.session end private # Convert the segment matched by the Integer matcher to an integer. def _convert_class_Integer(value) value.to_i end end end end extend RodaPlugins::Base::ClassMethods plugin RodaPlugins::Base end jeremyevans-roda-4f30bb3/lib/roda/000077500000000000000000000000001516720775400171035ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/lib/roda/cache.rb000066400000000000000000000017601516720775400204770ustar00rootroot00000000000000# frozen-string-literal: true require "thread" class Roda # A thread safe cache class, offering only #[] and #[]= methods, # each protected by a mutex. class RodaCache # Create a new thread safe cache. def initialize @mutex = Mutex.new @hash = {} end # Make getting value from underlying hash thread safe. def [](key) @mutex.synchronize{@hash[key]} end # Make setting value in underlying hash thread safe. def []=(key, value) @mutex.synchronize{@hash[key] = value} end # Return the frozen internal hash. The internal hash can then # be accessed directly since it is frozen and there are no # thread safety issues. def freeze @hash.freeze end private # Create a copy of the cache with a separate mutex. def initialize_copy(other) @mutex = Mutex.new other.instance_variable_get(:@mutex).synchronize do @hash = other.instance_variable_get(:@hash).dup end end end end jeremyevans-roda-4f30bb3/lib/roda/plugins.rb000066400000000000000000000037011516720775400211120ustar00rootroot00000000000000# frozen-string-literal: true require_relative "cache" class Roda # Module in which all Roda plugins should be stored. Also contains logic for # registering and loading plugins. module RodaPlugins OPTS = {}.freeze EMPTY_ARRAY = [].freeze # Stores registered plugins @plugins = RodaCache.new class << self # Make warn a public method, as it is used for deprecation warnings. # Roda::RodaPlugins.warn can be overridden for custom handling of # deprecation warnings. public :warn end # If the registered plugin already exists, use it. Otherwise, # require it and return it. This raises a LoadError if such a # plugin doesn't exist, or a RodaError if it exists but it does # not register itself correctly. def self.load_plugin(name) h = @plugins unless plugin = h[name] require "roda/plugins/#{name}" raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = h[name] end plugin end # Register the given plugin with Roda, so that it can be loaded using #plugin # with a symbol. Should be used by plugin files. Example: # # Roda::RodaPlugins.register_plugin(:plugin_name, PluginModule) def self.register_plugin(name, mod) @plugins[name] = mod end # Deprecate the constant with the given name in the given module, # if the ruby version supports it. def self.deprecate_constant(mod, name) # :nocov: if RUBY_VERSION >= '2.3' mod.deprecate_constant(name) end # :nocov: end if RUBY_VERSION >= '3.3' # Create a new module using the block, and set the temporary name # on it using the given a containing module and name. def self.set_temp_name(mod) mod.set_temporary_name(yield) mod end # :nocov: else def self.set_temp_name(mod) mod end end # :nocov: end end jeremyevans-roda-4f30bb3/lib/roda/plugins/000077500000000000000000000000001516720775400205645ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/lib/roda/plugins/Integer_matcher_max.rb000066400000000000000000000030171516720775400250570ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The Integer_matcher_max plugin sets the maximum integer value # value that the Integer class matcher will match by default. # By default, loading this plugin sets the maximum value to # 2**63-1, the largest signed 64-bit integer value: # # plugin :Integer_matcher_max # route do |r| # r.is Integer do # # Matches /9223372036854775807 # # Does not match /9223372036854775808 # end # end # # To specify a different maximum value, you can pass a different # maximum value when loading the plugin: # # plugin :Integer_matcher_max, 2**64-1 module IntegerMatcherMax def self.configure(app, max=nil) if max app.class_eval do meth = :_max_value_convert_class_Integer define_method(meth){max} alias_method meth, meth private meth end end end module InstanceMethods private # Do not have the Integer matcher max when over the maximum # configured Integer value. def _convert_class_Integer(value) value = super value if value <= _max_value_convert_class_Integer end # Use 2**63-1 as the default maximum value for the Integer # matcher. def _max_value_convert_class_Integer 9223372036854775807 end end end register_plugin(:Integer_matcher_max, IntegerMatcherMax) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/_after_hook.rb000066400000000000000000000002611516720775400233700ustar00rootroot00000000000000# frozen-string-literal: true require_relative 'error_handler' # class Roda module RodaPlugins # RODA4: Remove register_plugin(:_after_hook, ErrorHandler) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/_base64.rb000066400000000000000000000011561516720775400223370ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins module Base64_ class << self if RUBY_VERSION >= '2.4' def decode64(str) str.unpack1("m0") end # :nocov: else def decode64(str) str.unpack("m0")[0] end # :nocov: end def urlsafe_encode64(bin) str = [bin].pack("m0") str.tr!("+/", "-_") str end def urlsafe_decode64(str) decode64(str.tr("-_", "+/")) end end end register_plugin(:_base64, Base64_) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/_before_hook.rb000066400000000000000000000021211516720775400235260ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # Internal before hook module, not for external use. # Allows for plugins to configure the order in which # before processing is done by using _roda_before_* # private instance methods that are called in sorted order. # Loaded automatically by the base library if any _roda_before_* # methods are defined. module BeforeHook # :nodoc: module InstanceMethods # Run internal before hooks - Old Dispatch API. def call(&block) # RODA4: Remove super do _roda_before instance_exec(@_request, &block) # call Fallback end end # Run internal before hooks before running the main # roda route. def _roda_run_main_route(r) _roda_before super end private # Default empty implementation of _roda_before, usually # overridden by Roda.def_roda_before. def _roda_before end end end register_plugin(:_before_hook, BeforeHook) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/_optimized_matching.rb000066400000000000000000000161261516720775400251340ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The _optimized_matching plugin is automatically used internally to speed # up matching when a single argument String instance, String class, Integer # class, or Regexp matcher is passed to +r.on+, +r.is_+, or a verb method # such as +r.get+ or +r.post+. # # The optimization works by avoiding the +if_match+ method if possible. # Instead of clearing the captures array on every call, and having the # matching append to the captures, it checks directly for the match, # and on succesful match, it yields directly to the block without using # the captures array. module OptimizedMatching TERM = Base::RequestMethods::TERM module RequestMethods # Optimize the r.is method handling of a single string, String, Integer, # regexp, or true, argument. def is(*args, &block) case args.length when 1 _is1(args, &block) when 0 always(&block) if @remaining_path.empty? else if_match(args << TERM, &block) end end # Optimize the r.on method handling of a single string, String, Integer, # or regexp argument. Inline the related matching code to avoid the # need to modify @captures. def on(*args, &block) case args.length when 1 case matcher = args[0] when String always{yield} if _match_string(matcher) when Class if matcher == String rp = @remaining_path if rp.getbyte(0) == 47 if last = rp.index('/', 1) @remaining_path = rp[last, rp.length] always{yield rp[1, last-1]} elsif (len = rp.length) > 1 @remaining_path = "" always{yield rp[1, len]} end end elsif matcher == Integer if (matchdata = /\A\/(\d{1,100})(?=\/|\z)/.match(@remaining_path)) && (value = scope.send(:_convert_class_Integer, matchdata[1])) @remaining_path = matchdata.post_match always{yield(value)} end else path = @remaining_path captures = @captures.clear meth = :"_match_class_#{matcher}" if respond_to?(meth, true) # Allow calling private methods, as match methods are generally private if send(meth, &block) block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end else unsupported_matcher(matcher) end end when Regexp if matchdata = self.class.cached_matcher(matcher){matcher}.match(@remaining_path) @remaining_path = matchdata.post_match always{yield(*matchdata.captures)} end when true always(&block) when false, nil # nothing else path = @remaining_path captures = @captures.clear matched = case matcher when Array _match_array(matcher) when Hash _match_hash(matcher) when Symbol _match_symbol(matcher) when Proc matcher.call else unsupported_matcher(matcher) end if matched block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end end when 0 always(&block) else if_match(args, &block) end end private # Optimize the r.get/r.post method handling of a single string, String, Integer, # regexp, or true, argument. def _verb(args, &block) case args.length when 0 always(&block) when 1 _is1(args, &block) else if_match(args << TERM, &block) end end # Internals of r.is/r.get/r.post optimization. Inline the related matching # code to avoid the need to modify @captures. def _is1(args, &block) case matcher = args[0] when String rp = @remaining_path if _match_string(matcher) if @remaining_path.empty? always{yield} else @remaining_path = rp nil end end when Class if matcher == String rp = @remaining_path if rp.getbyte(0) == 47 && !rp.index('/', 1) && (len = rp.length) > 1 @remaining_path = '' always{yield rp[1, len]} end elsif matcher == Integer if (matchdata = /\A\/(\d{1,100})\z/.match(@remaining_path)) && (value = scope.send(:_convert_class_Integer, matchdata[1])) @remaining_path = '' always{yield(value)} end else path = @remaining_path captures = @captures.clear meth = :"_match_class_#{matcher}" if respond_to?(meth, true) # Allow calling private methods, as match methods are generally private if send(meth, &block) && @remaining_path.empty? block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end else unsupported_matcher(matcher) end end when Regexp if (matchdata = self.class.cached_matcher(matcher){matcher}.match(@remaining_path)) && matchdata.post_match.empty? @remaining_path = '' always{yield(*matchdata.captures)} end when true always(&block) if @remaining_path.empty? when false, nil # nothing else path = @remaining_path captures = @captures.clear matched = case matcher when Array _match_array(matcher) when Hash _match_hash(matcher) when Symbol _match_symbol(matcher) when Proc matcher.call else unsupported_matcher(matcher) end if matched && @remaining_path.empty? block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end end end end end register_plugin(:_optimized_matching, OptimizedMatching) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/_symbol_class_matchers.rb000066400000000000000000000070251516720775400256340ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins module SymbolClassMatchers_ module ClassMethods private # Backend of symbol_matcher and class_matcher. def _symbol_class_matcher(expected_class, obj, matcher, block, &request_class_block) unless obj.is_a?(expected_class) raise RodaError, "Invalid type passed to class_matcher or symbol_matcher: #{matcher.inspect}" end if obj.is_a?(Symbol) type = "symbol" meth = :"match_symbol_#{obj}" else type = "class" meth = :"_match_class_#{obj}" end case matcher when Regexp regexp = matcher consume_regexp = self::RodaRequest.send(:consume_pattern, regexp) when Symbol unless opts[:symbol_matchers] raise RodaError, "cannot provide Symbol matcher to class_matcher unless using symbol_matchers plugin: #{matcher.inspect}" end regexp, consume_regexp, matcher_block = opts[:symbol_matchers][matcher] unless regexp raise RodaError, "unregistered symbol matcher given to #{type}_matcher: #{matcher.inspect}" end block = _merge_matcher_blocks(type, obj, block, matcher_block) when Class unless opts[:class_matchers] raise RodaError, "cannot provide Class matcher to symbol_matcher unless using class_matchers plugin: #{matcher.inspect}" end regexp, consume_regexp, matcher_block = opts[:class_matchers][matcher] unless regexp raise RodaError, "unregistered class matcher given to #{type}_matcher: #{matcher.inspect}" end block = _merge_matcher_blocks(type, obj, block, matcher_block) else raise RodaError, "unsupported matcher given to #{type}_matcher: #{matcher.inspect}" end if block.is_a?(Symbol) convert_meth = block elsif block convert_meth = :"_convert_#{type}_#{obj}" define_method(convert_meth, &block) private convert_meth end array = opts[:"#{type}_matchers"][obj] = [regexp, consume_regexp, convert_meth].freeze self::RodaRequest.class_eval do class_exec(meth, array, &request_class_block) private meth end nil end # If both block and matche_meth are given, # define a method for block, and then return a # proc that calls matcher_meth first, and only calls # the newly defined method with the return values of matcher_meth # if matcher_method returns a truthy value. # Otherwise, return matcher_meth or block. def _merge_matcher_blocks(type, obj, block, matcher_meth) if matcher_meth if block convert_meth = :"_convert_merge_#{type}_#{obj}" define_method(convert_meth, &block) private convert_meth proc do |*a| if captures = send(matcher_meth, *a) if captures.is_a?(Array) send(convert_meth, *captures) else send(convert_meth, captures) end end end else matcher_meth end else block end end end end register_plugin(:_symbol_class_matchers, SymbolClassMatchers_) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/_symbol_regexp_matchers.rb000066400000000000000000000011571516720775400260210ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The _symbol_regexp_matchers plugin is designed for internal use by other plugins, # for the historical behavior of a symbol matching an arbitrary segment by default # using a regexp. module SymbolRegexpMatchers module RequestMethods private # The regular expression to use for matching symbols. By default, any non-empty # segment matches. def _match_symbol_regexp(s) "([^\\/]+)" end end end register_plugin(:_symbol_regexp_matchers, SymbolRegexpMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/additional_render_engines.rb000066400000000000000000000040321516720775400262670ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The additional_render_engines plugin allows for specifying additional render # engines to consider for templates. When rendering a template, it will # first try the default template engine specified in the render plugin. If the # template file to be rendered does not exist, it will try each additional render # engine specified in this plugin, in order, using the path to the first # template file that exists in the file system. If no such path is found, it # uses the default path specified by the render plugin. # # Example: # # plugin :render # default engine is 'erb' # plugin :additional_render_engines, ['haml', 'str'] # # route do |r| # # Will check the following in order, using path for first # # template file that exists: # # * views/t.erb # # * views/t.haml # # * views/t.str # render :t # end module AdditionalRenderEngines def self.load_dependencies(app, render_engines) app.plugin :render end # Set the additional render engines to consider. def self.configure(app, render_engines) app.opts[:additional_render_engines] = render_engines.dup.freeze end module InstanceMethods private # If the template path does not exist, try looking for the template # using each of the render engines, in order, returning # the first path that exists. If no template path exists for the # default any or any additional engines, return the original path. def template_path(opts) orig_path = super unless File.file?(orig_path) self.opts[:additional_render_engines].each do |engine| path = super(opts.merge(:engine=>engine)) return path if File.file?(path) end end orig_path end end end register_plugin(:additional_render_engines, AdditionalRenderEngines) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/additional_view_directories.rb000066400000000000000000000045701516720775400266550ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The additional_view_directories plugin allows for specifying additional view # directories to look in for templates. When rendering a template, it will # first try the :views directory specified in the render plugin. If the template # file to be rendered does not exist in that directory, it will try each additional # view directory specified in this plugin, in order, using the path to the first # template file that exists in the file system. If no such path is found, it # uses the default path specified by the render plugin. # # Example: # # plugin :render, :views=>'dir' # plugin :additional_view_directories, ['dir1', 'dir2', 'dir3'] # # route do |r| # # Will check the following in order, using path for first # # template file that exists: # # * dir/t.erb # # * dir1/t.erb # # * dir2/t.erb # # * dir3/t.erb # render :t # end module AdditionalViewDirectories # Depend on the render plugin, since this plugin only makes # sense when the render plugin is used. def self.load_dependencies(app, view_dirs) app.plugin :render end # Set the additional view directories to look in. Each additional view directory # is also added as an allowed path. def self.configure(app, view_dirs) view_dirs = app.opts[:additional_view_directories] = view_dirs.map{|f| app.expand_path(f, nil)}.freeze app.plugin :render, :allowed_paths=>(app.opts[:render][:allowed_paths] + view_dirs).uniq.freeze end module InstanceMethods private # If the template path does not exist, try looking for the template # in each of the additional view directories, in order, returning # the first path that exists. If no additional directory includes # the template, return the original path. def template_path(opts) orig_path = super unless File.file?(orig_path) self.opts[:additional_view_directories].each do |view_dir| path = super(opts.merge(:views=>view_dir)) return path if File.file?(path) end end orig_path end end end register_plugin(:additional_view_directories, AdditionalViewDirectories) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/all_verbs.rb000066400000000000000000000025701516720775400230660ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The all_verbs plugin adds methods for http verbs other than # get and post. The following verbs are added, assuming # rack handles them: delete, head, options, link, patch, put, # trace, unlink. # # These methods operate just like Roda's default get and post # methods, so using them without any arguments just checks for # the request method, while using them with any arguments also # checks that the arguments match the full path. # # Example: # # plugin :all_verbs # # route do |r| # r.delete do # # Handle DELETE # end # r.put do # # Handle PUT # end # r.patch do # # Handle PATCH # end # end # # The verb methods are defined via metaprogramming, so there # isn't documentation for the individual methods created. module AllVerbs module RequestMethods %w'delete head options link patch put trace unlink'.each do |verb| if ::Rack::Request.method_defined?("#{verb}?") class_eval(<<-END, __FILE__, __LINE__+1) def #{verb}(*args, &block) _verb(args, &block) if #{verb}? end END end end end end register_plugin(:all_verbs, AllVerbs) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/assets.rb000066400000000000000000001072001516720775400224130ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The assets plugin adds support for rendering your CSS and javascript # asset files on the fly in development, and compiling them # to a single, compressed file in production. # # This uses the render plugin for rendering the assets, and the render # plugin uses tilt internally, so you can use any template engine # supported by tilt for your assets. Tilt ships with support for # the following asset template engines, assuming the necessary libraries # are installed: # # css :: Less, Sass, Scss # js :: CoffeeScript # # You can also use opal as a javascript template engine, assuming it is # installed. # # == Usage # # When loading the plugin, use the :css and :js options # to set the source file(s) to use for CSS and javascript assets: # # plugin :assets, css: 'some_file.scss', js: 'some_file.coffee' # # This will look for the following files: # # assets/css/some_file.scss # assets/js/some_file.coffee # # The values for the :css and :js options can be arrays to load multiple # files. If you want to change the paths where asset files are stored, see the # Options section below. # # === Serving # # In your routes, call the +r.assets+ method to add a route to your assets, # which will make your app serve the rendered assets: # # route do |r| # r.assets # end # # You should generally call +r.assets+ inside the route block itself, and not # under any branches of the routing tree. # # === Views # # In your layout view, use the assets method to add links to your CSS and # javascript assets: # # <%= assets(:css) %> # <%= assets(:js) %> # # You can add attributes to the tags by using an options hash: # # <%= assets(:css, media: 'print') %> # # The assets method will respect the application's +:add_script_name+ option, # if it is set it will automatically prefix the path with the +SCRIPT_NAME+ for # the request. # # == Asset Paths # # If you just want the paths rather than the full tags, you can use # assets_paths instead. This will return an array of the sources that # the assets function would have put into tags: # # assets_paths(:css) # # => ["/assets/css/foo.css", "/assets/css/app.css"] # # If compilation is turned on, it will return the path to the compiled # asset: # # assets_paths(:css) # # => ["/assets/app.5e7b06baa1a514d8473b0eca514b806c201073b9.css"] # # == Asset Groups # # The asset plugin supports groups for the cases where you have different # css/js files for your front end and back end. To use asset groups, you # pass a hash for the :css and/or :js options: # # plugin :assets, css: {frontend: 'some_frontend_file.scss', # backend: 'some_backend_file.scss'} # # This expects the following directory structure for your assets: # # assets/css/frontend/some_frontend_file.scss # assets/css/backend/some_backend_file.scss # # If you do not want to force that directory structure when using # asset groups, you can use the group_subdirs: false option. # # In your view code use an array argument in your call to assets: # # <%= assets([:css, :frontend]) %> # # === Nesting # # Asset groups also support nesting, though that should only be needed # in fairly large applications. You can use a nested hash when loading # the plugin: # # plugin :assets, # css: {frontend: {dashboard: 'some_frontend_file.scss'}} # # and an extra entry per nesting level when creating the tags. # # <%= assets([:css, :frontend, :dashboard]) %> # # == Caching # # The assets plugin uses the caching plugin internally, and will set the # Last-Modified header to the modified timestamp of the asset source file # when rendering the asset. # # If you have assets that include other asset files, such as using @import # in a sass file, you need to specify the dependencies for your assets so # that the assets plugin will correctly pick up changes. You can do this # using the :dependencies option to the plugin, which takes a hash where # the keys are paths to asset files, and values are arrays of paths to # dependencies of those asset files: # # app.plugin :assets, # dependencies: {'assets/css/bootstrap.scss'=>Dir['assets/css/bootstrap/' '**/*.scss']} # # == Asset Compilation # # In production, you are generally going to want to compile your assets # into a single file, with you can do by calling compile_assets after # loading the plugin: # # plugin :assets, css: 'some_file.scss', js: 'some_file.coffee' # compile_assets # # After calling compile_assets, calls to assets in your views will default # to a using a single link each to your CSS and javascript compiled asset # files. By default the compiled files are written to the public directory, # so that they can be served by the webserver. # # === Asset Compression # # If you have the yuicompressor gem installed and working, it will be used # automatically to compress your javascript and css assets. For javascript # assets, if yuicompressor is not available, the plugin will check for # closure-compiler, uglifier, and minjs and use the first one that works. # If no compressors are available, the assets will just be concatenated # together and not compressed during compilation. You can use the # :css_compressor and :js_compressor options to specify the compressor to use. # # It is also possible to use the built-in compression options in the CSS or JS # compiler, assuming the compiler supports such options. For example, with # sass/sassc, you can use: # # plugin :assets, # css_opts: {style: :compressed} # # === Source Maps (CSS) # # The assets plugin does not have direct support for source maps, so it is # recommended you use embedded source maps if supported by the CSS compiler. # For sass/sassc, you can use: # # plugin :assets, # css_opts: {:source_map_embed=>true, source_map_contents: true, source_map_file: "."} # # === With Asset Groups # # When using asset groups, a separate compiled file will be produced per # asset group. # # === Unique Asset Names # # When compiling assets, a unique name is given to each asset file, using the # a SHA1 hash of the content of the file. This is done so that clients do # not attempt to use cached versions of the assets if the asset has changed. # # === Serving # # When compiling assets, +r.assets+ will serve the compiled asset # files. However, it is recommended to have the main webserver (e.g. nginx) # serve the compiled files, instead of relying on the application. # # Assuming you are using compiled assets in production mode that are served # by the webserver, you can remove the serving of them by the application: # # route do |r| # r.assets unless ENV['RACK_ENV'] == 'production' # end # # If you do have the application serve the compiled assets, it will use the # Last-Modified header to make sure that clients do not redownload compiled # assets that haven't changed. # # === Asset Precompilation # # If you want to precompile your assets, so they do not need to be compiled # every time you boot the application, you can provide a :precompiled option # when loading the plugin. The value of this option should be the filename # where the compiled asset metadata is stored. # # If the compiled asset metadata file does not exist when the assets plugin # is loaded, the plugin will run in non-compiled mode. However, when you call # compile_assets, it will write the compiled asset metadata file after # compiling the assets. # # If the compiled asset metadata file already exists when the assets plugin # is loaded, the plugin will read the file to get the compiled asset metadata, # and it will run in compiled mode, assuming that the compiled asset files # already exist. # # ==== On Heroku # # Heroku supports precompiling the assets when using Roda. You just need to # add an assets:precompile task, similar to this: # # namespace :assets do # desc "Precompile the assets" # task :precompile do # require './app' # App.compile_assets # end # end # # == Postprocessing # # If you pass a callable object to the :postprocessor option, it will be called # before an asset is served. # If the assets are to be compiled, the object will be called at compilation time. # # It is passed three arguments; the name of the asset file, the type of the # asset file (which is a symbol, either :css or :js), and the asset contents. # # It should return the new content for the asset. # # You can use this to call Autoprefixer on your CSS: # # plugin :assets, { # css: [ 'style.scss' ], # postprocessor: lambda do |file, type, content| # type == :css ? AutoprefixerRails.process(content).css : content # end # } # # == External Assets/Assets from Gems # # The assets plugin only supports loading assets files underneath the assets # path. You cannot pass an absolute path to an asset file and have it # work. If you would like to reference asset files that are outside the assets # path, you have the following options: # # * Copy, hard link, or symlink the external assets files into the assets path. # * Use tilt-indirect or another method of indirection (such as an erb template that loads # the external asset file) so that a file inside the assets path can reference files # outside the assets path. # # == Plugin Options # # :add_suffix :: Whether to append a .css or .js extension to asset routes in non-compiled mode # (default: false) # :compiled_asset_host :: The asset host to use for compiled assets. Should include the protocol # as well as the host (e.g. "https://cdn.example.com", "//cdn.example.com") # :compiled_css_dir :: Directory name in which to store the compiled css file, # inside :compiled_path (default: nil) # :compiled_css_route :: Route under :prefix for compiled css assets (default: :compiled_css_dir) # :compiled_js_dir :: Directory name in which to store the compiled javascript file, # inside :compiled_path (default: nil) # :compiled_js_route :: Route under :prefix for compiled javscript assets (default: :compiled_js_dir) # :compiled_name :: Compiled file name prefix (default: 'app') # :compiled_path:: Path inside public folder in which compiled files are stored (default: :prefix) # :concat_only :: Whether to just concatenate instead of concatenating # and compressing files (default: false) # :css_compressor :: Compressor to use for compressing CSS, either :yui, :none, or nil (the default, which will try # :yui if available, but not fail if it is not available) # :css_dir :: Directory name containing your css source, inside :path (default: 'css') # :css_headers :: A hash of additional headers for your rendered css files # :css_opts :: Template options to pass to the render plugin (via :template_opts) when rendering css assets # :css_route :: Route under :prefix for css assets (default: :css_dir) # :dependencies :: A hash of dependencies for your asset files. Keys should be paths to asset files, # values should be arrays of paths your asset files depends on. This is used to # detect changes in your asset files. # :early_hints :: Automatically send early hints for all assets. Requires the early_hints plugin. # :group_subdirs :: Whether a hash used in :css and :js options requires the assets for the # related group are contained in a subdirectory with the same name (default: true) # :gzip :: Store gzipped compiled assets files, and serve those to clients who accept gzip encoding. # :headers :: A hash of additional headers for both js and css rendered files # :js_compressor :: Compressor to use for compressing javascript, either :yui, :closure, :uglifier, :minjs, # :none, or nil (the default, which will try :yui, :closure, :uglifier, then :minjs, but # not fail if any of them is not available) # :js_dir :: Directory name containing your javascript source, inside :path (default: 'js') # :js_headers :: A hash of additional headers for your rendered javascript files # :js_opts :: Template options to pass to the render plugin (via :template_opts) when rendering javascript assets # :js_route :: Route under :prefix for javascript assets (default: :js_dir) # :path :: Path to your asset source directory (default: 'assets'). Relative # paths will be considered relative to the application's :root option. # :postprocessor :: A block which should accept three arguments (asset name, asset type, # content). This block can be used to hook into the asset system and # make your own modifications before the asset is served. If the asset # is to be compiled, the block is called at compile time. # :prefix :: Prefix for assets path in your URL/routes (default: 'assets') # :precompiled :: Path to the compiled asset metadata file. If the file exists, will use compiled # mode using the metadata in the file. If the file does not exist, will use # non-compiled mode, but will write the metadata to the file if compile_assets is called. # :public :: Path to your public folder, in which compiled files are placed (default: 'public'). Relative # paths will be considered relative to the application's :root option. # :relative_paths :: Use relative paths instead of absolute paths when setting up link and script tags for # assets. # :sri :: Enables subresource integrity when setting up references to compiled assets. The value should be # :sha256, :sha384, or :sha512 depending on which hash algorithm you want to use. This changes the # hash algorithm that Roda will use when naming compiled asset files. The default is :sha256, you # can use nil to disable subresource integrity. # :timestamp_paths :: Include the timestamp of assets in asset paths in non-compiled mode. Doing this can # slow down development requests due to additional requests to get last modified times, # but it will make sure the paths change in development when there are modifications, # which can fix issues when using a caching proxy in non-compiled mode. This can also # be specified as a string to use that string to separate the timestamp from the asset. # By default, / is used as the separator if timestamp paths are enabled. module Assets DEFAULTS = { :compiled_name => 'app'.freeze, :js_dir => 'js'.freeze, :css_dir => 'css'.freeze, :prefix => 'assets'.freeze, :concat_only => false, :compiled => false, :add_suffix => false, :early_hints => false, :timestamp_paths => false, :group_subdirs => true, :compiled_css_dir => nil, :compiled_js_dir => nil, :sri => :sha256 }.freeze # Internal exception raised when a compressor cannot be found CompressorNotFound = Class.new(RodaError) # Load the render, caching, and h plugins, since the assets plugin # depends on them. def self.load_dependencies(app, opts = OPTS) app.plugin :render app.plugin :caching app.plugin :h if opts[:relative_paths] app.plugin :relative_path end end # Setup the options for the plugin. See the Assets module RDoc # for a description of the supported options. def self.configure(app, opts = {}) if app.assets_opts prev_opts = app.assets_opts[:orig_opts] orig_opts = app.assets_opts[:orig_opts].merge(opts) [:headers, :css_headers, :js_headers, :css_opts, :js_opts, :dependencies].each do |s| if prev_opts[s] if opts[s] orig_opts[s] = prev_opts[s].merge(opts[s]) else orig_opts[s] = prev_opts[s].dup end end end app.opts[:assets] = orig_opts.dup app.opts[:assets][:orig_opts] = orig_opts else app.opts[:assets] = opts.dup app.opts[:assets][:orig_opts] = opts end opts = app.opts[:assets] opts[:path] = app.expand_path(opts[:path]||"assets").freeze opts[:public] = app.expand_path(opts[:public]||"public").freeze # Combine multiple values into a path, ignoring trailing slashes j = lambda do |*v| opts.values_at(*v). reject{|s| s.to_s.empty?}. map{|s| s.chomp('/')}. join('/').freeze end # Same as j, but add a trailing slash if not empty sj = lambda do |*v| s = j.call(*v) s.empty? ? s : (s + '/').freeze end if opts[:precompiled] && !opts[:compiled] && ::File.exist?(opts[:precompiled]) require 'json' opts[:compiled] = app.send(:_precompiled_asset_metadata, opts[:precompiled]) end if opts[:early_hints] app.plugin :early_hints end if opts[:timestamp_paths] && !opts[:timestamp_paths].is_a?(String) opts[:timestamp_paths] = '/' end DEFAULTS.each do |k, v| opts[k] = v unless opts.has_key?(k) end [ [:compiled_path, :prefix], [:js_route, :js_dir], [:css_route, :css_dir], [:compiled_js_route, :compiled_js_dir], [:compiled_css_route, :compiled_css_dir] ].each do |k, v| opts[k] = opts[v] unless opts.has_key?(k) end [:css_headers, :js_headers, :css_opts, :js_opts, :dependencies].each do |s| opts[s] ||= {} end expanded_deps = opts[:expanded_dependencies] = {} opts[:dependencies].each do |file, deps| expanded_deps[File.expand_path(file)] = Array(deps) end if headers = opts[:headers] opts[:css_headers] = headers.merge(opts[:css_headers]) opts[:js_headers] = headers.merge(opts[:js_headers]) end opts[:css_headers][RodaResponseHeaders::CONTENT_TYPE] ||= "text/css; charset=UTF-8".freeze opts[:js_headers][RodaResponseHeaders::CONTENT_TYPE] ||= "application/javascript; charset=UTF-8".freeze [:css_headers, :js_headers, :css_opts, :js_opts, :dependencies, :expanded_dependencies].each do |s| opts[s].freeze end [:headers, :css, :js].each do |s| opts[s].freeze if opts[s] end # Used for reading/writing files opts[:js_path] = sj.call(:path, :js_dir) opts[:css_path] = sj.call(:path, :css_dir) opts[:compiled_js_path] = j.call(:public, :compiled_path, :compiled_js_dir, :compiled_name) opts[:compiled_css_path] = j.call(:public, :compiled_path, :compiled_css_dir, :compiled_name) # Used for URLs/routes opts[:js_prefix] = sj.call(:prefix, :js_route) opts[:css_prefix] = sj.call(:prefix, :css_route) opts[:compiled_js_prefix] = j.call(:prefix, :compiled_js_route, :compiled_name) opts[:compiled_css_prefix] = j.call(:prefix, :compiled_css_route, :compiled_name) opts[:js_suffix] = (opts[:add_suffix] ? '.js' : '').freeze opts[:css_suffix] = (opts[:add_suffix] ? '.css' : '').freeze opts.freeze end module ClassMethods # Return the assets options for this class. def assets_opts opts[:assets] end # Compile options for the given asset type. If no asset_type # is given, compile both the :css and :js asset types. You # can specify an array of types (e.g. [:css, :frontend]) to # compile assets for the given asset group. def compile_assets(type=nil) require 'fileutils' unless assets_opts[:compiled] opts[:assets] = assets_opts.merge(:compiled => _compiled_assets_initial_hash).freeze end if type == nil _compile_assets(:css) _compile_assets(:js) else _compile_assets(type) end if precompile_file = assets_opts[:precompiled] require 'json' ::FileUtils.mkdir_p(File.dirname(precompile_file)) tmp_file = "#{precompile_file}.tmp" ::File.open(tmp_file, 'wb'){|f| f.write((opts[:json_serializer] || :to_json.to_proc).call(assets_opts[:compiled]))} ::File.rename(tmp_file, precompile_file) end assets_opts[:compiled] end private # The initial hash to use to store compiled asset metadata. def _compiled_assets_initial_hash {} end # Internals of compile_assets, handling recursive calls for loading # all asset groups under the given type. def _compile_assets(type) type, *dirs = type if type.is_a?(Array) dirs ||= [] files = assets_opts[type] dirs.each{|d| files = files[d]} case files when Hash files.each_key{|dir| _compile_assets([type] + dirs + [dir])} else files = Array(files) compile_assets_files(files, type, dirs) unless files.empty? end end # The precompiled asset metadata stored in the given file def _precompiled_asset_metadata(file) (opts[:json_parser] || ::JSON.method(:parse)).call(::File.read(file)) end # Compile each array of files for the given type into a single # file. Dirs should be an array of asset group names, if these # are files in an asset group. def compile_assets_files(files, type, dirs) dirs = nil if dirs && dirs.empty? o = assets_opts app = allocate content = files.map do |file| file = "#{dirs.join('/')}/#{file}" if dirs && o[:group_subdirs] file = "#{o[:"#{type}_path"]}#{file}" app.read_asset_file(file, type) end.join("\n") unless o[:concat_only] content = compress_asset(content, type) end suffix = ".#{dirs.join('.')}" if dirs key = "#{type}#{suffix}" unique_id = o[:compiled][key] = asset_digest(content) path = "#{o[:"compiled_#{type}_path"]}#{suffix}.#{unique_id}.#{type}" ::FileUtils.mkdir_p(File.dirname(path)) ::File.open(path, 'wb'){|f| f.write(content)} if o[:gzip] require 'zlib' Zlib::GzipWriter.open("#{path}.gz") do |gz| gz.write(content) end end nil end # Compress the given content for the given type by using the # configured compressor, or trying the supported compressors. def compress_asset(content, type) case compressor = assets_opts[:"#{type}_compressor"] when :none return content when nil # default, try different compressors else # Allow calling private compress methods return send("compress_#{type}_#{compressor}", content) end compressors = if type == :js [:yui, :closure, :uglifier, :minjs] else [:yui] end compressors.each do |comp| begin # Allow calling private compress methods if c = send("compress_#{type}_#{comp}", content) return c end rescue LoadError, CompressorNotFound end end content end # Compress the CSS using YUI Compressor, requires java runtime def compress_css_yui(content) compress_yui(content, :compress_css) end # Compress the JS using Google Closure Compiler, requires java runtime def compress_js_closure(content) require 'closure-compiler' begin ::Closure::Compiler.new.compile(content) rescue ::Closure::Error => e raise CompressorNotFound, "#{e.class}: #{e.message}", e.backtrace end end # Compress the JS using MinJS, a pure ruby compressor def compress_js_minjs(content) require 'minjs' Minjs::Compressor::Compressor.new(:debug => false).compress(content).to_js end # Compress the JS using Uglifier, requires javascript runtime def compress_js_uglifier(content) begin require 'uglifier' rescue => e # :nocov: raise CompressorNotFound, "#{e.class}: #{e.message}", e.backtrace # :nocov: end Uglifier.compile(content) end # Compress the CSS using YUI Compressor, requires java runtime def compress_js_yui(content) compress_yui(content, :compress_js) end # Compress the CSS/JS using YUI Compressor, requires java runtime def compress_yui(content, meth) require 'yuicompressor' ::YUICompressor.public_send(meth, content, :munge => true) rescue ::Errno::ENOENT => e raise CompressorNotFound, "#{e.class}: #{e.message}", e.backtrace end # Return a unique id for the given content. By default, uses the # SHA256 hash of the content. This method can be overridden to use # a different digest type or to return a static string if you don't # want to use a unique value. def asset_digest(content) algo = assets_opts[:sri] || :sha256 digest = begin require 'openssl' ::OpenSSL::Digest # :nocov: rescue LoadError require 'digest/sha2' ::Digest # :nocov: end digest.const_get(algo.to_s.upcase).hexdigest(content) end end module InstanceMethods # Return an array of paths for the given asset type and optionally # asset group. See the assets function documentation for details. def assets_paths(type) o = self.class.assets_opts if type.is_a?(Array) ltype, *dirs = type else ltype = type end stype = ltype.to_s url_prefix = request.script_name if self.class.opts[:add_script_name] relative_paths = o[:relative_paths] paths = if o[:compiled] relative_paths = false if o[:compiled_asset_host] if ukey = _compiled_assets_hash(type, true) ["#{o[:compiled_asset_host]}#{url_prefix}/#{o[:"compiled_#{stype}_prefix"]}.#{ukey}.#{stype}"] else [] end else asset_dir = o[ltype] if dirs && !dirs.empty? dirs.each{|f| asset_dir = asset_dir[f]} prefix = "#{dirs.join('/')}/" if o[:group_subdirs] end Array(asset_dir).map do |f| if ts = o[:timestamp_paths] mtime = asset_last_modified(File.join(o[:"#{stype}_path"], *[prefix, f].compact)) mtime = "#{sprintf("%i%06i", mtime.to_i, mtime.usec)}#{ts}" end "#{url_prefix}/#{o[:"#{stype}_prefix"]}#{mtime}#{prefix}#{f}#{o[:"#{stype}_suffix"]}" end end if relative_paths paths.map! do |path| "#{relative_prefix}#{path}" end end paths end # Return a string containing html tags for the given asset type. # This will use a script tag for the :js type and a link tag for # the :css type. # # To return the tags for a specific asset group, use an array for # the type, such as [:css, :frontend]. # # You can specify custom attributes for the tag by passing a hash # as the attrs argument. # # When the assets are not compiled, this will result in a separate # tag for each asset file. When the assets are compiled, this will # result in a single tag to the compiled asset file. def assets(type, attrs = OPTS) ltype = type.is_a?(Array) ? type[0] : type o = self.class.assets_opts if o[:compiled] && (algo = o[:sri]) && (hash = _compiled_assets_hash(type)) attrs = Hash[attrs] attrs[:integrity] = "#{algo}-#{h([[hash].pack('H*')].pack('m').tr("\n", ''))}" end attributes = attrs.map{|k,v| "#{k}=\"#{h(v)}\""}.join(' ') if ltype == :js tag_start = "" else tag_start = "" end paths = assets_paths(type) if o[:early_hints] early_hint_as = ltype == :js ? 'script' : 'style' early_hints = paths.map{|p| "<#{p}>; rel=preload; as=#{early_hint_as}"} early_hints = early_hints.join("\n") if Rack.release < '3' send_early_hints(RodaResponseHeaders::LINK=>early_hints) end paths.map{|p| "#{tag_start}#{h(p)}#{tag_end}"}.join("\n") end # Render the asset with the given filename. When assets are compiled, # or when the file is already of the given type (no rendering necessary), # this returns the contents of the compiled file. # When assets are not compiled and the file is not already in the same format, # this will render the asset using the render plugin. # In both cases, if the file has not been modified since the last request, # this will return a 304 response. def render_asset(file, type) o = self.class.assets_opts if o[:compiled] file = "#{o[:"compiled_#{type}_path"]}#{file}" if o[:gzip] && env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/ @_response[RodaResponseHeaders::CONTENT_ENCODING] = 'gzip' file += '.gz' end check_asset_request(file, type, ::File.stat(file).mtime) ::File.read(file) else file = "#{o[:"#{type}_path"]}#{file}" check_asset_request(file, type, asset_last_modified(file)) read_asset_file(file, type) end end # Return the content of the file if it is already of the correct type. # Otherwise, render the file using the render plugin. +file+ should be # the relative path to the file from the current directory. def read_asset_file(file, type) o = self.class.assets_opts content = if file.end_with?(".#{type}") ::File.read(file) else render_asset_file(file, :template_opts=>o[:"#{type}_opts"], :dependencies=>o[:expanded_dependencies][file]) end o[:postprocessor] ? o[:postprocessor].call(file, type, content) : content end private def _compiled_assets_hash(type, return_ukey=false) compiled = self.class.assets_opts[:compiled] type, *dirs = type if type.is_a?(Array) stype = type.to_s if dirs && !dirs.empty? key = dirs.join('.') ckey = "#{stype}.#{key}" if hash = ukey = compiled[ckey] ukey = "#{key}.#{ukey}" end else hash = ukey = compiled[stype] end return_ukey ? ukey : hash end # Return when the file was last modified. If the file depends on any # other files, check the modification times of all dependencies and # return the maximum. def asset_last_modified(file) if deps = self.class.assets_opts[:expanded_dependencies][file] ([file] + Array(deps)).map{|f| ::File.stat(f).mtime}.max else ::File.stat(file).mtime end end # If the asset hasn't been modified since the last request, return # a 304 response immediately. Otherwise, add the appropriate # type-specific headers. def check_asset_request(file, type, mtime) @_request.last_modified(mtime) @_response.headers.merge!(self.class.assets_opts[:"#{type}_headers"]) end # Render the given asset file using the render plugin, with the given options. # +file+ should be the relative path to the file from the current directory. def render_asset_file(file, options) render_template({:path => file}, options) end end module RequestClassMethods # An array of asset type strings and regexps for that type, for all asset types # handled. def assets_matchers @assets_matchers ||= [:css, :js].map do |t| if regexp = assets_regexp(t) [t, regexp].freeze end end.compact.freeze end private # A string for the asset filename for the asset type, key, and digest. def _asset_regexp(type, key, digest) "#{key.sub(/\A#{type}/, '')}.#{digest}.#{type}" end # The regexp matcher to use for the given type. This handles any asset groups # for the asset types. def assets_regexp(type) o = roda_class.assets_opts if compiled = o[:compiled] assets = compiled. select{|k,_| k =~ /\A#{type}/}. map{|k, md| _asset_regexp(type, k, md)} return if assets.empty? /#{o[:"compiled_#{type}_prefix"]}(#{Regexp.union(assets)})/ else return unless assets = o[type] assets = unnest_assets_hash(assets) ts = o[:timestamp_paths] /#{o[:"#{type}_prefix"]}#{"\\d+#{ts}" if ts}(#{Regexp.union(assets.uniq)})#{o[:"#{type}_suffix"]}/ end end # Recursively unnested the given assets hash, returning a single array of asset # files for the given. def unnest_assets_hash(h) case h when Hash h.flat_map do |k,v| assets = unnest_assets_hash(v) assets = assets.map{|x| "#{k}/#{x}"} if roda_class.assets_opts[:group_subdirs] assets end else Array(h) end end end module RequestMethods # Render the matching asset if this is a GET request for a supported asset. def assets if is_get? self.class.assets_matchers.each do |type, matcher| is matcher do |file| scope.render_asset(file, type) end end nil end end end end register_plugin(:assets, Assets) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/assets_preloading.rb000066400000000000000000000061741516720775400246270ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The assets_preloading plugin generates html tags or a header value # to facilitate browser preloading of your assets. This allows # compatible browsers to fetch assets before they are required, # streamlining page rendering. # # For a list of compatible browsers, see # http://caniuse.com/#search=link-rel-preload # # The plugin provides two functions - preload_assets_link_header and # preload_assets_link_tags. The resulting preloading should be # identical, it is up to you which system you prefer. # # preload_assets_link_header returns a string suitable for populating # the response Link header: # # response.headers['Link'] = preload_assets_link_header(:css) # # Link header will now contain something like: # # ;rel="preload";as="style" # # preload_assets_link_tags returns a string to drop into your # templates containing link tags: # # preload_assets_link_tags(:css) # # returns # # Note that these link tags are different to the usual asset # declarations in markup; this will only instruct a compatible browser # to fetch the file and cache it for later; the browser will not parse # the asset until it encounters a traditional link or script tag. # # You must still setup and link to your assets as you did previously. # # Both functions can be passed any combination of asset types or # asset groups, as multiple arguments: # # # generate tags for css assets and the app js asset group # preload_assets_link_tags(:css, [:js, :app], [:js, :bar]) # # # generate Link header for css assets and js asset groups app and bar # preload_assets_link_header(:css, [:js, :app]) # module AssetsPreloading TYPE_AS = { :css => 'style'.freeze, :js => 'script'.freeze, }.freeze # Depend on the assets plugin, as we'll be calling some functions in it. def self.load_dependencies(app) app.plugin :assets end module InstanceMethods # Return a string of tags for the given asset # types/groups. def preload_assets_link_tags(*args) _preload_assets_array(args).map{|path, as| ""}.join("\n") end # Return a string suitable for a Link header for the # given asset types/groups. def preload_assets_link_header(*args) _preload_assets_array(args).map{|path, as| "<#{path}>;rel=preload;as=#{as}"}.join(",") end private # Return an array of paths/as pairs for the given asset # types and/or groups. def _preload_assets_array(assets) assets.flat_map do |type| paths = assets_paths(type) type = type[0] if type.is_a?(Array) as = TYPE_AS[type] paths.map{|path| [path, as]} end end end end register_plugin(:assets_preloading, AssetsPreloading) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/assume_ssl.rb000066400000000000000000000015511516720775400232710ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The assume_ssl plugin makes the request ssl? method always return # true. This is useful when using an SSL-terminating reverse proxy # that doesn't set the X-Forwarded-Proto or similar header to notify # Rack that it is forwarding an SSL request. # # The sessions and sinatra_helpers plugins that ship with Roda both # use the ssl? method internally and can be affected by use of the # plugin. It's recommended that you use this plugin if you are # using either plugin and an SSL-terminating proxy as described above. # # plugin :assume_ssl module AssumeSSL module RequestMethods # Assume all requests are protected by SSL. def ssl? true end end end register_plugin(:assume_ssl, AssumeSSL) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/autoload_hash_branches.rb000066400000000000000000000062321516720775400255740ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The autoload_hash_branches plugin builds on the hash_branches plugin and allows for # delaying loading of a file containing a hash branch for an application until there # is a request that uses the hash branch. This can be useful in development # to improvement startup time by not loading all branches up front. It can also be # useful in testing subsets of an application by only loading the hash branches being # tested. # # You can specify a single hash branch for autoloading: # # plugin :autoload_hash_branches # autoload_hash_branch('branch_name', '/absolute/path/to/file') # autoload_hash_branch('namespace', 'branch_name', 'relative/path/to/file') # # You can also set the plugin to autoload load all hash branch files in a given directory. # This will look at each .rb file in the directory, and add an autoload for it, using the # filename without the .rb as the branch name: # # autoload_hash_branch_dir('/path/to/dir') # autoload_hash_branch_dir('namespace', '/path/to/dir') # # In both cases, when the autoloaded file is required, it should redefine the same # hash branch. If it does not, requests to the hash branch will result in a 404 error. # # When freezing an application, all hash branches are automatically loaded, because # autoloading hash branches does not work for frozen applications. module AutoloadHashBranches def self.load_dependencies(app) app.plugin :hash_branches end def self.configure(app) app.opts[:autoload_hash_branch_files] ||= [] end module ClassMethods # Autoload the given file when there is request for the hash branch. # The given file should configure the hash branch specified. def autoload_hash_branch(namespace='', segment, file) segment = "/#{segment}" file = File.expand_path(file) opts[:autoload_hash_branch_files] << file routes = opts[:hash_branches][namespace] ||= {} meth = routes[segment] = define_roda_method(routes[segment] || "hash_branch_#{namespace}_#{segment}", 1) do |r| loc = method(routes[segment]).source_location require file # Avoid infinite loop in case method is not overridden if method(meth).source_location != loc send(meth, r) end end nil end # For each .rb file in the given directory, add an autoloaded hash branch # based on the file name. def autoload_hash_branch_dir(namespace='', dir) Dir.new(dir).entries.each do |file| if file =~ /\.rb\z/i autoload_hash_branch(namespace, file.sub(/\.rb\z/i, ''), File.join(dir, file)) end end end # Eagerly load all hash branches when freezing the application. def freeze opts.delete(:autoload_hash_branch_files).each{|file| require file} unless opts.frozen? super end end end register_plugin(:autoload_hash_branches, AutoloadHashBranches) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/autoload_named_routes.rb000066400000000000000000000051121516720775400254650ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The autoload_named_routes plugin builds on the named_routes plugin and allows for # delaying loading of a file containing a named route for an application until there # is a request that uses the named route. This can be useful in development # to improvement startup time by not loading all named routes up front. It can also be # useful in testing subsets of an application by only loading the named routes being # tested. # # You can specify a single hash branch for autoloading: # # plugin :autoload_named_route # autoload_named_route(:route_name, '/absolute/path/to/file') # autoload_named_route(:namespace, :route_name, 'relative/path/to/file') # # Note that unlike the +route+ method defined by the named_routes plugin, when providing # a namespace, the namespace comes before the route name and not after. # # When the autoloaded file is required, it should redefine the same # named route. If it does not, requests to the named route will be ignored (as if the # related named route block was empty). # # When freezing an application, all named routes are automatically loaded, because # autoloading named routes does not work for frozen applications. module AutoloadNamedRoutes def self.load_dependencies(app) app.plugin :named_routes end def self.configure(app) app.opts[:autoload_named_route_files] ||= [] end module ClassMethods # Autoload the given file when there is request for the named route. # The given file should configure the named route specified. def autoload_named_route(namespace=nil, name, file) file = File.expand_path(file) opts[:autoload_named_route_files] << file routes = opts[:namespaced_routes][namespace] ||= {} meth = routes[name] = define_roda_method(routes[name] || "named_routes_#{namespace}_#{name}", 1) do |r| loc = method(routes[name]).source_location require file # Avoid infinite loop in case method is not overridden if method(meth).source_location != loc send(meth, r) end end nil end # Eagerly load all autoloaded named routes when freezing the application. def freeze opts.delete(:autoload_named_route_files).each{|file| require file} unless opts.frozen? super end end end register_plugin(:autoload_named_routes, AutoloadNamedRoutes) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/backtracking_array.rb000066400000000000000000000053501516720775400247350ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The backtracking_array plugin changes the handling of array # matchers such that if one of the array entries matches, but # a later match argument fails, it will backtrack and try the # next entry in the array. For example, the following match # block does not match +/a/b+ by default: # # r.is ['a', 'a/b'] do |path| # # ... # end # # This is because the 'a' entry in the array matches, which # makes the array match. However, the next matcher is the # terminal matcher (since +r.is+ was used), and since the # path is not terminal as it still contains +/b+ after # matching 'a'. # # With the backtracking_array plugin, when the terminal matcher # fails, matching will go on to the next entry in the array, # 'a/b', which will also match. Since 'a/b' # matches the path fully, the terminal matcher also matches, # and the match block yields. module BacktrackingArray module RequestMethods private # When matching for a single array, after a successful # array element match, attempt to match all remaining # elements. If the remaining elements could not be # matched, reset the state and continue to the next # entry in the array. def _match_array(arg, rest) path = @remaining_path captures = @captures caps = captures.dup arg.each do |v| if match(v, rest) if v.is_a?(String) captures.push(v) end if match_all(rest) return true end # Matching all remaining elements failed, reset state captures.replace(caps) @remaining_path = path end end false end # If any of the args are an array, handle backtracking such # that if a later matcher fails, we roll back to the current # matcher and proceed to the next entry in the array. def match_all(args) args = args.dup until args.empty? arg = args.shift if match(arg, args) return true if arg.is_a?(Array) else return end end true end # When matching an array, include the remaining arguments, # otherwise, just match the single argument. def match(v, rest = nil) if v.is_a?(Array) _match_array(v, rest) else super(v) end end end end register_plugin(:backtracking_array, BacktrackingArray) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/bearer_token.rb000066400000000000000000000015061516720775400235530ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The bearer_token plugin adds an +r.bearer_token+ method for retrieving # a bearer token from the +Authorization+ HTTP header. Bearer tokens will # in the authorization header will be recognized as long as they start # with the case insensitive string "bearer ". module BearerToken # :nocov: METHOD = RUBY_VERSION >= "2.4" ? :match? : :match # :nocov: module RequestMethods # Return the bearer token for the request if there is one in the # authorization HTTP header. def bearer_token if (auth = @env["HTTP_AUTHORIZATION"]) && auth.send(METHOD, /\Abearer /i) auth[7, 100000000] end end end end register_plugin(:bearer_token, BearerToken) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/branch_locals.rb000066400000000000000000000041611516720775400237050ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The branch_locals plugin allows you to override view and layout # locals for specific branches and routes. # # plugin :render # plugin :render_locals, render: {footer: 'Default'}, layout: {title: 'Main'} # plugin :branch_locals # # route do |r| # r.on "users" do # set_layout_locals title: 'Users' # set_view_locals footer: '(c) Roda' # end # end # # The locals you specify in the set_layout_locals and set_view_locals methods # have higher precedence than the render_locals plugin options, but lower precedence # than options you directly pass to the view/render methods. module BranchLocals # Load the render_locals plugin before this plugin, since this plugin # works by overriding methods in the render_locals plugin. def self.load_dependencies(app) app.plugin :render_locals end module InstanceMethods # Update the default layout locals to use in this branch. def set_layout_locals(opts) if locals = @_layout_locals @_layout_locals = locals.merge(opts) else @_layout_locals = opts end end # Update the default view locals to use in this branch. def set_view_locals(opts) if locals = @_view_locals @_view_locals = locals.merge(opts) else @_view_locals = opts end end private # Make branch specific view locals override render_locals plugin defaults. def render_locals locals = super if @_view_locals locals = Hash[locals].merge!(@_view_locals) end locals end # Make branch specific layout locals override render_locals plugin defaults. def layout_locals locals = super if @_layout_locals locals = Hash[locals].merge!(@_layout_locals) end locals end end end register_plugin(:branch_locals, BranchLocals) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/break.rb000066400000000000000000000020301516720775400221700ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The break plugin supports calling break inside a match block, to # return from the block and continue in the routing tree, restoring # the remaining path so that future matchers operating on the path # operate as expected. # # plugin :break # # route do |r| # r.on "foo", :bar do |bar| # break if bar == 'baz' # "/foo/#{bar} (not baz)" # end # # r.on "foo/baz" do # "/foo/baz" # end # end # # This provides the same basic feature as the pass plugin, but # uses Ruby's standard control flow primative instead of a # separate method. module Break module RequestMethods private # Handle break inside match blocks, restoring remaining path. def if_match(_) rp = @remaining_path super ensure @remaining_path = rp end end end register_plugin(:break, Break) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/caching.rb000066400000000000000000000175031516720775400225130ustar00rootroot00000000000000# frozen-string-literal: true require 'time' # class Roda module RodaPlugins # The caching plugin adds methods related to HTTP caching. # # For proper caching, you should use either the +last_modified+ or # +etag+ request methods. # # r.get 'albums', Integer do |album_id| # @album = Album[album_id] # r.last_modified @album.updated_at # view('album') # end # # # or # # r.get 'albums', Integer do |album_id| # @album = Album[album_id] # r.etag @album.sha1 # view('album') # end # # Both +last_modified+ or +etag+ will immediately halt processing # if there have been no modifications since the last time the # client requested the resource, assuming the client uses the # appropriate HTTP 1.1 request headers. # # This plugin also includes the +cache_control+ and +expires+ # response methods. The +cache_control+ method sets the # Cache-Control header using the given hash: # # response.cache_control public: true, max_age: 60 # # Cache-Control: public, max-age=60 # # The +expires+ method is similar, but in addition # to setting the HTTP 1.1 Cache-Control header, it also sets # the HTTP 1.0 Expires header: # # response.expires 60, public: true # # Cache-Control: public, max-age=60 # # Expires: Mon, 29 Sep 2014 21:25:47 GMT # # The implementation was originally taken from Sinatra, # which is also released under the MIT License: # # Copyright (c) 2007, 2008, 2009 Blake Mizerany # Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. module Caching module RequestMethods # Set the last modified time of the resource using the Last-Modified header. # The +time+ argument should be a Time instance. # # If the current request includes an If-Modified-Since header that is # equal or later than the time specified, immediately returns a response # with a 304 status. # # If the current request includes an If-Unmodified-Since header that is # before than the time specified, immediately returns a response # with a 412 status. def last_modified(time) return unless time res = response e = env res[RodaResponseHeaders::LAST_MODIFIED] = time.httpdate return if e['HTTP_IF_NONE_MATCH'] status = res.status if (!status || status == 200) && (ims = time_from_header(e['HTTP_IF_MODIFIED_SINCE'])) && ims >= time.to_i res.status = 304 halt end if (!status || (status >= 200 && status < 300) || status == 412) && (ius = time_from_header(e['HTTP_IF_UNMODIFIED_SINCE'])) && ius < time.to_i res.status = 412 halt end end # Set the response entity tag using the ETag header. # # The +value+ argument is an identifier that uniquely # identifies the current version of the resource. # Options: # :weak :: Use a weak cache validator (a strong cache validator is the default) # :new_resource :: Whether this etag should match an etag of * (true for POST, false otherwise) # # When the current request includes an If-None-Match header with a # matching etag, immediately returns a response with a 304 or 412 status, # depending on the request method. # # When the current request includes an If-Match header with a # etag that doesn't match, immediately returns a response with a 412 status. def etag(value, opts=OPTS) # Before touching this code, please double check RFC 2616 14.24 and 14.26. weak = opts[:weak] new_resource = opts.fetch(:new_resource){post?} res = response e = env res[RodaResponseHeaders::ETAG] = etag = "#{'W/' if weak}\"#{value}\"" status = res.status if (!status || (status >= 200 && status < 300) || status == 304) if etag_matches?(e['HTTP_IF_NONE_MATCH'], etag, new_resource) res.status = (request_method =~ /\AGET|HEAD|OPTIONS|TRACE\z/i ? 304 : 412) halt end if ifm = e['HTTP_IF_MATCH'] unless etag_matches?(ifm, etag, new_resource) res.status = 412 halt end end end end private # Helper method checking if a ETag value list includes the current ETag. def etag_matches?(list, etag, new_resource) return unless list return !new_resource if list == '*' list.to_s.split(/\s*,\s*/).include?(etag) end # Helper method parsing a time value from an HTTP header, returning the # time as an integer. def time_from_header(t) Time.httpdate(t).to_i if t rescue ArgumentError end end module ResponseMethods # Specify response freshness policy for using the Cache-Control header. # Options can can any non-value directives (:public, :private, :no_cache, # :no_store, :must_revalidate, :proxy_revalidate), with true as the value. # Options can also contain value directives (:max_age, :s_maxage). # # response.cache_control public: true, max_age: 60 # # => Cache-Control: public, max-age=60 # # See RFC 2616 / 14.9 for more on standard cache control directives: # http://tools.ietf.org/html/rfc2616#section-14.9.1 def cache_control(opts) values = [] opts.each do |k, v| next unless v k = k.to_s.tr('_', '-') values << (v == true ? k : "#{k}=#{v}") end @headers[RodaResponseHeaders::CACHE_CONTROL] = values.join(', ') unless values.empty? end # Set Cache-Control header with the max_age given. max_age should # be an integer number of seconds that the current request should be # cached for. Also sets the Expires header, useful if you have # HTTP 1.0 clients (Cache-Control is an HTTP 1.1 header). def expires(max_age, opts=OPTS) cache_control(Hash[opts].merge!(:max_age=>max_age)) @headers[RodaResponseHeaders::EXPIRES] = (Time.now + max_age).httpdate end # Remove Content-Type and Content-Length for 304 responses. def finish a = super if a[0] == 304 h = a[1] h.delete(RodaResponseHeaders::CONTENT_TYPE) h.delete(RodaResponseHeaders::CONTENT_LENGTH) end a end end end register_plugin(:caching, Caching) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/capture_erb.rb000066400000000000000000000070631516720775400234120ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The capture_erb plugin allows you to capture the content of a block # in an ERB template, and return it as a value, instead of # injecting the template block into the template output. # # <% value = capture_erb do %> # Some content here. # <% end %> # # +capture_erb+ can be used inside other methods that are called # inside templates. It can be combined with the inject_erb plugin # to wrap template blocks with arbitrary output and then inject the # wrapped output into the template. # # If the output buffer object responds to +capture+ and is not # an instance of String (e.g. when +erubi/capture_block+ is being # used as the template engine), this will call +capture+ on the # output buffer object, instead of setting the output buffer object # temporarily to a new object. # # By default, capture_erb returns the value of the block, converted # to a string. However, that can cause issues with code such as: # # <% value = capture_erb do %> # Some content here. # <% if something %> # Some more content here. # <% end %> # <% end %> # # In this case, the block may return nil, instead of the content of # the template. To handle this case, you can provide the # returns: :buffer option when calling the method (to handle # that specific call, or when loading the plugin (to default to that # behavior). Note that if the output buffer object responds to # +capture+ and is not an instance of String, the returns: :buffer # behavior is the default and cannot be changed. module CaptureERB def self.load_dependencies(app, opts=OPTS) app.plugin :render end # Support returns: :buffer to default to returning buffer # object. def self.configure(app, opts=OPTS) # RODA4: make returns: :buffer the default behavior app.opts[:capture_erb_returns] = opts[:returns] if opts.has_key?(:returns) end module InstanceMethods # Temporarily replace the ERB output buffer # with an empty string, and then yield to the block. # Return the value of the block, converted to a string. # Restore the previous ERB output buffer before returning. # # Options: # :returns :: If set to :buffer, returns the value of the # template output variable, instead of the return # value of the block converted to a string. This # is the default behavior if the template output # variable supports the +capture+ method and is not # a String instance. def capture_erb(opts=OPTS, &block) outvar = render_opts[:template_opts][:outvar] buf_was = instance_variable_get(outvar) if buf_was.respond_to?(:capture) && !buf_was.instance_of?(String) buf_was.capture(&block) else returns = opts.fetch(:returns) { self.opts[:capture_erb_returns] } begin instance_variable_set(outvar, String.new) if returns == :buffer yield instance_variable_get(outvar).to_s else yield.to_s end ensure instance_variable_set(outvar, buf_was) if outvar && buf_was end end end end end register_plugin(:capture_erb, CaptureERB) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/chunked.rb000066400000000000000000000267411516720775400225440ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The chunked plugin allows you to stream rendered views to clients. # This can significantly improve performance of page rendering on the # client, as it flushes the headers and top part of the layout template # (generally containing references to the stylesheet and javascript assets) # before rendering the content template. # # This allows the client to fetch the assets while the template is still # being rendered. Additionally, this plugin makes it easy to defer # executing code required to render the content template until after # the top part of the layout has been flushed, so the client can fetch the # assets while the application is still doing the necessary processing in # order to render the content template, such as retrieving values from a # database. # # There are a couple disadvantages of streaming. First is that the layout # must be rendered before the content, so any state changes made in your # content template will not affect the layout template. Second, error # handling is reduced, since if an error occurs while rendering a template, # a successful response code has already been sent. # # To use chunked encoding for a response, just call the chunked method # instead of view: # # r.root do # chunked(:index) # end # # If you want to execute code after flushing the top part of the layout # template, but before rendering the content template, pass a block to # chunked: # # r.root do # chunked(:index) do # # expensive calculation here # end # end # # You can also call delay manually with a block, and the execution of the # block will be delayed until rendering the content template. This is # useful if you want to delay execution for all routes under a branch: # # r.on 'albums', Integer do |album_id| # delay do # @album = Album[album_id] # end # r.get 'info' do # chunked(:info) # end # r.get 'tracks' do # chunked(:tracks) # end # end # # If you want to chunk all responses, pass the :chunk_by_default option # when loading the plugin: # # plugin :chunked, chunk_by_default: true # # then you can just use the normal view method: # # r.root do # view(:index) # end # # and it will chunk the response. Note that you still need to call # chunked if you want to pass a block of code to be executed after flushing # the layout and before rendering the content template. Also, before you # enable chunking by default, you need to make sure that none of your # content templates make state changes that affect the layout template. # Additionally, make sure nowhere in your app are you doing any processing # after the call to view. # # If you use :chunk_by_default, but want to turn off chunking for a view, # call no_chunk!: # # r.root do # no_chunk! # view(:index) # end # # Inside your layout or content templates, you can call the flush method # to flush the current result of the template to the user, useful for # streaming large datasets. # # <% (1..100).each do |i| %> # <%= i %> # <% sleep 0.1 %> # <% flush %> # <% end %> # # Note that you should not call flush from inside subtemplates of the # content or layout templates, unless you are also calling flush directly # before rendering the subtemplate, and also directly injecting the # subtemplate into the current template without modification. So if you # are using the above template code in a subtemplate, in your content # template you should do: # # <% flush %><%= render(:subtemplate) %> # # If you want to use chunked encoding when rendering a template, but don't # want to use a layout, pass the layout: false option to chunked. # # r.root do # chunked(:index, layout: false) # end # # In order to handle errors in chunked responses, you can override the # handle_chunk_error method: # # def handle_chunk_error(e) # env['rack.logger'].error(e) # end # # It is possible to set @_out_buf to an error notification and call # flush to output the message to the client inside handle_chunk_error. # # In order for chunking to work, you must make sure that no proxies between # the application and the client buffer responses. # # If you are using nginx and have it set to buffer proxy responses by # default, you can turn this off on a per response basis using the # X-Accel-Buffering header. To set this header or similar headers for # all chunked responses, pass a :headers option when loading the plugin: # # plugin :chunked, headers: {'X-Accel-Buffering'=>'no'} # # By default, this plugin does not use Transfer-Encoding: chunked, it only # returns a body that will stream the response in chunks. If you would like # to force the use of Transfer-Encoding: chunked, you can use the # :force_chunked_encoding plugin option. If using the # :force_chunked_encoding plugin option, chunking will only be used for # HTTP/1.1 requests since Transfer-Encoding: chunked is only supported # in HTTP/1.1 (non-HTTP/1.1 requests will have behavior similar to # calling no_chunk!). # # The chunked plugin requires the render plugin, and only works for # template engines that store their template output variable in # @_out_buf. Also, it only works if the content template is directly # injected into the layout template without modification. # # If using the chunked plugin with the flash plugin, make sure you # call the flash method early in your route block. If the flash # method is not called until template rendering, the flash may not be # rotated. module Chunked # Depend on the render plugin def self.load_dependencies(app, opts=OPTS) app.plugin :render end # Set plugin specific options. Options: # :chunk_by_default :: chunk all calls to view by default # :headers :: Set default additional headers to use when calling view def self.configure(app, opts=OPTS) app.opts[:chunk_by_default] = opts[:chunk_by_default] app.opts[:force_chunked_encoding] = opts[:force_chunked_encoding] if opts[:headers] app.opts[:chunk_headers] = (app.opts[:chunk_headers] || {}).merge(opts[:headers]).freeze end end # Rack response body instance for chunked responses using # Transfer-Encoding: chunked. class Body # Save the scope of the current request handling. def initialize(scope) @scope = scope end # For each response chunk yielded by the scope, # yield it it to the caller in chunked format, starting # with the size of the request in ASCII hex format, then # the chunk. After all chunks have been yielded, yield # a 0 sized chunk to finish the response. def each @scope.each_chunk do |chunk| next if !chunk || chunk.empty? yield("%x\r\n" % chunk.bytesize) yield(chunk) yield("\r\n") end ensure yield("0\r\n\r\n") end end # Rack response body instance for chunked responses not # using Transfer-Encoding: chunked. class StreamBody # Save the scope of the current request handling. def initialize(scope) @scope = scope end # Yield each non-empty chunk as the body. def each(&block) @scope.each_chunk do |chunk| yield chunk if chunk && !chunk.empty? end end end module InstanceMethods # Disable chunking for the current request. Mostly useful when # chunking is turned on by default. def no_chunk! @_chunked = false end # If chunking by default, call chunked if it hasn't yet been # called and chunking is not specifically disabled. def view(*a) if opts[:chunk_by_default] && !defined?(@_chunked) && !defined?(yield) chunked(*a) else super end end # Render a response to the user in chunks. See Chunked for # an overview. If a block is given, it is passed to #delay. def chunked(template, opts=OPTS, &block) unless defined?(@_chunked) @_chunked = !self.opts[:force_chunked_encoding] || @_request.http_version == "HTTP/1.1" end if block delay(&block) end unless @_chunked # If chunking is disabled, do a normal rendering of the view. run_delayed_blocks return view(template, opts) end if template.is_a?(Hash) if opts.empty? opts = template else opts = Hash[opts].merge!(template) end end # Hack so that the arguments don't need to be passed # through the response and body objects. @_each_chunk_args = [template, opts] res = response headers = res.headers if chunk_headers = self.opts[:chunk_headers] headers.merge!(chunk_headers) end if self.opts[:force_chunked_encoding] res[RodaResponseHeaders::TRANSFER_ENCODING] = 'chunked' body = Body.new(self) else body = StreamBody.new(self) end throw :halt, res.finish_with_body(body) end # Delay the execution of the block until right before the # content template is to be rendered. def delay(&block) raise RodaError, "must pass a block to Roda#delay" unless block (@_delays ||= []) << block end # Yield each chunk of the template rendering separately. def each_chunk response.body.each{|s| yield s} template, opts = @_each_chunk_args # Use a lambda for the flusher, so that a call to flush # by a template can result in this method yielding a chunk # of the response. @_flusher = lambda do yield @_out_buf @_out_buf = String.new end if layout_opts = view_layout_opts(opts) @_out_buf = render_template(layout_opts) do flush run_delayed_blocks yield opts[:content] || render_template(template, opts) nil end else run_delayed_blocks yield view(template, opts) end flush rescue => e handle_chunk_error(e) end # By default, raise the exception. def handle_chunk_error(e) raise e end # Call the flusher if one is defined. If one is not defined, this # is a no-op, so flush can be used inside views without breaking # things if chunking is not used. def flush @_flusher.call if @_flusher end private # Run all delayed blocks def run_delayed_blocks return unless @_delays @_delays.each(&:call) end end end register_plugin(:chunked, Chunked) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/class_level_routing.rb000066400000000000000000000100741516720775400251560ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The class_level_routing plugin adds routing methods at the class level, which can # be used instead of or in addition to using the normal +route+ method to start the # routing tree. If a request is not matched by the normal routing tree, the class # level routes will be tried. This offers a more Sinatra-like API, while # still allowing you to use a routing tree inside individual actions. # # Here's the first example from the README, modified to use the class_level_routing # plugin: # # class App < Roda # plugin :class_level_routing # # # GET / request # root do # request.redirect "/hello" # end # # # GET /hello/world request # get "hello/world" do # "Hello world!" # end # # # /hello request # is "hello" do # # Set variable for both GET and POST requests # @greeting = 'Hello' # # # GET /hello request # request.get do # "#{@greeting}!" # end # # # POST /hello request # request.post do # puts "Someone said #{@greeting}!" # request.redirect # end # end # end # # When using the class_level_routing plugin with nested routes, you may also want to use the # delegate plugin to delegate certain instance methods to the request object, so you don't have # to continually use +request.+ in your routing blocks. # # Note that class level routing is implemented via a simple array of routes, so routing performance # will degrade linearly as the number of routes increases. For best performance, you should use # the normal +route+ class method to define your routing tree. This plugin does make it simpler to # add additional routes after the routing tree has already been defined, though. module ClassLevelRouting # Initialize the class_routes array when the plugin is loaded. Also, if the application doesn't # currently have a routing block, setup an empty routing block so that things will still work if # a routing block isn't added. def self.configure(app) app.opts[:class_level_routes] ||= [] end module ClassMethods # Define routing methods that will store class level routes. [:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |request_meth| define_method(request_meth) do |*args, &block| meth = define_roda_method("class_level_routing_#{request_meth}", :any, &block) opts[:class_level_routes] << [request_meth, args, meth].freeze end end # Freeze the class level routes so that there can be no thread safety issues at runtime. def freeze opts[:class_level_routes].freeze super end end module InstanceMethods def initialize(_) super @_original_remaining_path = @_request.remaining_path end private # If the normal routing tree doesn't handle an action, try each class level route # to see if it matches. def _roda_after_10__class_level_routing(result) if result && result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty? # Reset the response so it doesn't inherit the status or any headers from # the original response. @_response.send(:initialize) @_response.status = nil result.replace(_roda_handle_route do r = @_request opts[:class_level_routes].each do |request_meth, args, meth| r.instance_variable_set(:@remaining_path, @_original_remaining_path) r.public_send(request_meth, *args) do |*a| send(meth, *a) end end nil end) end end end end register_plugin(:class_level_routing, ClassLevelRouting) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/class_matchers.rb000066400000000000000000000117111516720775400241050ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The class_matchers plugin allows you do define custom regexps and # conversion procs to use for specific classes. For example, if you # have multiple routes similar to: # # r.on /(\d\d\d\d)-(\d\d)-(\d\d)/ do |y, m, d| # date = Date.new(y.to_i, m.to_i, d.to_i) # # ... # end # # You can register a Date class matcher for that regexp: # # class_matcher(Date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| # Date.new(y.to_i, m.to_i, d.to_i) # end # # And then use the Date class as a matcher, and it will yield a Date object: # # r.on Date do |date| # # ... # end # # This is useful to DRY up code if you are using the same type of pattern and # type conversion in multiple places in your application. You can have the # block return an array to yield multiple captures. # # If you have a segment match the passed regexp, but decide during block # processing that you do not want to treat it as a match, you can have the # block return nil or false. This is useful if you want to make sure you # are using valid data: # # class_matcher(Date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| # y = y.to_i # m = m.to_i # d = d.to_i # Date.new(y, m, d) if Date.valid_date?(y, m, d) # end # # The second argument to class_matcher can be a class already registered # as a class matcher. This can DRY up code that wants a conversion # performed by an existing class matcher: # # class_matcher Employee, Integer do |id| # Employee[id] # end # # With the above example, the Integer matcher performs the conversion to # integer, so +id+ is yielded as an integer. The block then looks up the # employee with that id. If there is no employee with that id, then # the Employee matcher will not match. # # If using the symbol_matchers plugin, you can provide a recognized symbol # matcher as the second argument to class_matcher, and it will work in # a similar manner: # # symbol_matcher(:employee_id, /E-(\d{6})/) do |employee_id| # employee_id.to_i # end # class_matcher Employee, :employee_id do |id| # Employee[id] # end # # Blocks passed to the class_matchers plugin are evaluated in route # block context. # # This plugin does not work with the params_capturing plugin, as it does not # offer the ability to associate block arguments with named keys. module ClassMatchers def self.load_dependencies(app) app.plugin :_symbol_class_matchers end def self.configure(app) app.opts[:class_matchers] ||= { Integer=>[/(\d{1,100})/, /\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer].freeze, String=>[/([^\/]+)/, nil, nil].freeze } end module ClassMethods # Set the matcher and block to use for the given class. # The matcher can be a regexp, registered class matcher, or registered symbol # matcher (if using the symbol_matchers plugin). # # If providing a regexp, the block given will be called with all regexp captures. # If providing a registered class or symbol, the block will be called with the # captures returned by the block for the registered class or symbol, or the regexp # captures if no block was registered with the class or symbol. In either case, # if a block is given, it should return an array with the captures to yield to # the match block. def class_matcher(klass, matcher, &block) _symbol_class_matcher(Class, klass, matcher, block) do |meth, (_, regexp, convert_meth)| if regexp define_method(meth){consume(regexp, convert_meth)} else define_method(meth){_consume_segment(convert_meth)} end end end # Freeze the class_matchers hash when freezing the app. def freeze opts[:class_matchers].freeze super end end module RequestMethods # Use faster approach for segment matching. This is used for # matchers based on the String class matcher, and avoids the # use of regular expressions for scanning. def _consume_segment(convert_meth) rp = @remaining_path if _match_class_String if convert_meth if captures = scope.send(convert_meth, @captures.pop) if captures.is_a?(Array) @captures.concat(captures) else @captures << captures end else @remaining_path = rp nil end else true end end end end end register_plugin(:class_matchers, ClassMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/common_logger.rb000066400000000000000000000055221516720775400237440ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The common_logger plugin adds common logger support to Roda # applications, similar to Rack::CommonLogger, with the following # differences: # # * Better performance # * Doesn't include middleware timing # * Doesn't proxy the body # * Doesn't support different capitalization of the Content-Length response header # * Logs to +$stderr+ instead of env['rack.errors'] if explicit logger not passed # # Example: # # plugin :common_logger # plugin :common_logger, $stdout # plugin :common_logger, Logger.new('filename') # plugin :common_logger, Logger.new('filename'), method: :debug module CommonLogger MUTATE_LINE = RUBY_VERSION < '2.3' || RUBY_VERSION >= '3' private_constant :MUTATE_LINE def self.configure(app, logger=nil, opts=OPTS) app.opts[:common_logger] = logger || app.opts[:common_logger] || $stderr app.opts[:common_logger_meth] = app.opts[:common_logger].method(opts.fetch(:method){logger.respond_to?(:write) ? :write : :<<}) end if RUBY_VERSION >= '2.1' # A timer object for calculating elapsed time. def self.start_timer Process.clock_gettime(Process::CLOCK_MONOTONIC) end else # :nocov: def self.start_timer # :nodoc: Time.now end # :nocov: end module InstanceMethods private # Log request/response information in common log format to logger. def _roda_after_90__common_logger(result) return unless result && (status = result[0]) && (headers = result[1]) elapsed_time = if timer = @_request_timer '%0.4f' % (CommonLogger.start_timer - timer) else '-' end env = @_request.env line = "#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{status} #{((length = headers[RodaResponseHeaders::CONTENT_LENGTH]) && (length unless length == '0')) || '-'} #{elapsed_time} " if MUTATE_LINE line.gsub!(/[^[:print:]]/){|c| sprintf("\\x%x", c.ord)} # :nocov: else line = line.gsub(/[^[:print:]]/){|c| sprintf("\\x%x", c.ord)} # :nocov: end line[-1] = "\n" opts[:common_logger_meth].call(line) end # Create timer instance used for timing def _roda_before_05__common_logger @_request_timer = CommonLogger.start_timer end end end register_plugin(:common_logger, CommonLogger) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/conditional_sessions.rb000066400000000000000000000045721516720775400253520ustar00rootroot00000000000000# frozen-string-literal: true class Roda module RodaPlugins # The conditional_sessions plugin loads the sessions plugin. However, # it only allows sessions if the block passed to the plugin returns # truthy. The block is evaluated in request context. This is designed for # use in applications that want to use sessions for some requests, # and want to be sure that sessions are not used for other requests. # For example, if you want to make sure that sessions are not used for # requests with paths starting with /static, you could do: # # plugin :conditional_sessions, secret: ENV["SECRET"] do # !path_info.start_with?('/static') # end # # The the request session, session_created_at, and session_updated_at methods # raise a RodaError exception when sessions are not allowed. The request # persist_session and route scope clear_session methods do nothing when # sessions are not allowed. module ConditionalSessions # Pass all options to the sessions block, and use the block to define # a request method for whether sessions are allowed. def self.load_dependencies(app, opts=OPTS, &block) app.plugin :sessions, opts app::RodaRequest.class_eval do define_method(:use_sessions?, &block) alias use_sessions? use_sessions? end end module InstanceMethods # Do nothing if not using sessions. def clear_session super if @_request.use_sessions? end end module RequestMethods # Raise RodaError if not using sessions. def session raise RodaError, "session called on request not using sessions" unless use_sessions? super end # Raise RodaError if not using sessions. def session_created_at raise RodaError, "session_created_at called on request not using sessions" unless use_sessions? super end # Raise RodaError if not using sessions. def session_updated_at raise RodaError, "session_updated_at called on request not using sessions" unless use_sessions? super end # Do nothing if not using sessions. def persist_session(headers, session) super if use_sessions? end end end register_plugin(:conditional_sessions, ConditionalSessions) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/content_for.rb000066400000000000000000000064011516720775400234320ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The content_for plugin is designed to be used with the # render plugin, allowing you to store content inside one # template, and retrieve that content inside a separate # template. Most commonly, this is so view templates # can set content for the layout template to display outside # of the normal content pane. # # In the template in which you want to store content, call # content_for with a block: # # <% content_for :foo do %> # Some content here. # <% end %> # # or: # # <% content_for :foo do "Some content here." end %> # # You can also set the raw content as the second argument, # instead of passing a block: # # <% content_for :foo, "Some content" %> # # In the template in which you want to retrieve content, # call content_for without the block or argument: # # <%= content_for :foo %> # # Note that when storing content by calling content_for # with a block and embedding template code, the return # value of the block is used as the content (after being # converted to a string). This can cause issues in some # cases, such as: # # <% content_for :foo do %> # <% [1,2,3].each do |i| %> # Content <%= i %> # <% end %> # <% end %> # # In the above example, the return value of the block is # [1,2,3], as Array#each returns the receiver. # If whitespace is not important, you can work around this by # adding an empty line before the end of the content_for block. # # If content_for is used multiple times with the same key, # by default, the last call will append previous calls. # If you want to overwrite the previous content, pass the # append: false option when loading the plugin: # # plugin :content_for, append: false module ContentFor # Depend on the capture_erb plugin, since it uses capture_erb # to capture the content. def self.load_dependencies(app, _opts = OPTS) app.plugin :capture_erb end # Configure whether to append or overwrite if content_for # is called multiple times with the same key. def self.configure(app, opts = OPTS) app.opts[:append_content_for] = opts.fetch(:append, true) end module InstanceMethods # If called with a block, store content enclosed by block # under the given key. If called without a block, retrieve # stored content with the given key, or return nil if there # is no content stored with that key. def content_for(key, value=nil, &block) append = opts[:append_content_for] if block || value if block value = capture_erb(&block) end @_content_for ||= {} if append (@_content_for[key] ||= []) << value else @_content_for[key] = value end elsif @_content_for && (value = @_content_for[key]) if append value = value.join end value end end end end register_plugin(:content_for, ContentFor) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/content_security_policy.rb000066400000000000000000000245261516720775400261020ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The content_security_policy plugin allows you to easily set a Content-Security-Policy # header for the application, which modern browsers will use to control access to specific # types of page content. # # You would generally call the plugin with a block to set the default policy: # # plugin :content_security_policy do |csp| # csp.default_src :none # csp.img_src :self # csp.style_src :self # csp.script_src :self # csp.font_src :self # csp.form_action :self # csp.base_uri :none # csp.frame_ancestors :none # csp.block_all_mixed_content # end # # Then, anywhere in the routing tree, you can customize the policy for just that # branch or action using the same block syntax: # # r.get 'foo' do # content_security_policy do |csp| # csp.object_src :self # csp.add_style_src 'bar.com' # end # # ... # end # # In addition to using a block, you can also call methods on the object returned # by the method: # # r.get 'foo' do # content_security_policy.script_src :self, 'example.com', [:nonce, 'foobarbaz'] # # ... # end # # The following methods are available for configuring the content security policy, # which specify the setting (substituting _ with -): # # * base_uri # * child_src # * connect_src # * default_src # * font_src # * form_action # * frame_ancestors # * frame_src # * img_src # * manifest_src # * media_src # * object_src # * plugin_types # * report_to # * report_uri # * require_sri_for # * sandbox # * script_src # * style_src # * worker_src # # All of these methods support any number of arguments, and each argument should # be one of the following types: # # String :: used verbatim # Symbol :: Substitutes +_+ with +-+ and surrounds with ' # Array :: only accepts 2 element arrays, joins elements with +-+ and # surrounds the result with ' # # Example: # # content_security_policy.script_src :self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz'] # # script-src 'self' 'unsafe-eval' example.com 'nonce-foobarbaz'; # # When calling a method with no arguments, the setting is removed from the policy instead # of being left empty, since all of these setting require at least one value. Likewise, # if the policy does not have any settings, the header will not be added. # # Calling the method overrides any previous setting. Each of the methods has +add_*+ and # +get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method # returns the current value for the setting. # # content_security_policy.script_src :self, :unsafe_eval # content_security_policy.add_script_src 'example.com', [:nonce, 'foobarbaz'] # # script-src 'self' 'unsafe-eval' example.com 'nonce-foobarbaz'; # # content_security_policy.get_script_src # # => [:self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz']] # # The clear method can be used to remove all settings from the policy. Empty policies # do not set any headers. You can use +response.skip_content_security_policy!+ to skip # setting a policy. This is faster than calling +content_security_policy.clear+, since # it does not duplicate the default policy. # # The following methods to set boolean settings are also defined: # # * block_all_mixed_content # * upgrade_insecure_requests # # Calling these methods will turn on the related setting. To turn the setting # off again, you can call them with a +false+ argument. There is also a *? method # for each setting for returning whether the setting is currently enabled. # # Likewise there is also a +report_only+ method for turning on report only mode (the # default is enforcement mode), or turning off report only mode if a false argument # is given. Also, there is a +report_only?+ method for returning whether report only # mode is enabled. module ContentSecurityPolicy # Represents a content security policy. class Policy ' base-uri child-src connect-src default-src font-src form-action frame-ancestors frame-src img-src manifest-src media-src object-src plugin-types report-to report-uri require-sri-for sandbox script-src style-src worker-src '.split.each(&:freeze).each do |setting| meth = setting.tr('-', '_').freeze # Setting method name sets the setting value, or removes it if no args are given. define_method(meth) do |*args| if args.empty? @opts.delete(setting) else @opts[setting] = args.freeze end nil end # add_* method name adds to the setting value, or clears setting if no values # are given. define_method("add_#{meth}") do |*args| unless args.empty? @opts[setting] ||= EMPTY_ARRAY @opts[setting] += args @opts[setting].freeze end nil end # get_* method always returns current setting value. define_method("get_#{meth}") do @opts[setting] end end %w'block-all-mixed-content upgrade-insecure-requests'.each(&:freeze).each do |setting| meth = setting.tr('-', '_').freeze # Setting method name turns on setting if true or no argument given, # or removes setting if false is given. define_method(meth) do |arg=true| if arg @opts[setting] = true else @opts.delete(setting) end nil end # *? method returns true or false depending on whether setting is enabled. define_method("#{meth}?") do !!@opts[setting] end end def initialize clear end # Clear all settings, useful to remove any inherited settings. def clear @opts = {} end # Do not allow future modifications to any settings. def freeze @opts.freeze header_value.freeze super end # The header name to use, depends on whether report only mode has been enabled. def header_key @report_only ? RodaResponseHeaders::CONTENT_SECURITY_POLICY_REPORT_ONLY : RodaResponseHeaders::CONTENT_SECURITY_POLICY end # The header value to use. def header_value return @header_value if @header_value s = String.new @opts.each do |k, vs| s << k unless vs == true vs.each{|v| append_formatted_value(s, v)} end s << '; ' end @header_value = s end # Set whether the Content-Security-Policy-Report-Only header instead of the # default Content-Security-Policy header. def report_only(report=true) @report_only = report end # Whether this policy uses report only mode. def report_only? !!@report_only end # Set the current policy in the headers hash. If no settings have been made # in the policy, does not set a header. def set_header(headers) return if @opts.empty? headers[header_key] ||= header_value end private # Handle three types of values when formatting the header: # String :: used verbatim # Symbol :: Substitutes _ with - and surrounds with ' # Array :: only accepts 2 element arrays, joins them with - and # surrounds them with ' def append_formatted_value(s, v) case v when String s << ' ' << v when Array case v.length when 2 s << " '" << v.join('-') << "'" else raise RodaError, "unsupported CSP value used: #{v.inspect}" end when Symbol s << " '" << v.to_s.tr('_', '-') << "'" else raise RodaError, "unsupported CSP value used: #{v.inspect}" end end # Make object copy use copy of settings, and remove cached header value. def initialize_copy(_) super @opts = @opts.dup @header_value = nil end end # Yield the current Content Security Policy to the block. def self.configure(app) policy = app.opts[:content_security_policy] = if policy = app.opts[:content_security_policy] policy.dup else Policy.new end yield policy if defined?(yield) policy.freeze end module InstanceMethods # If a block is given, yield the current content security policy. Returns the # current content security policy. def content_security_policy policy = @_response.content_security_policy yield policy if defined?(yield) policy end end module ResponseMethods # Unset any content security policy when reinitializing def initialize super @content_security_policy &&= nil end # The current content security policy to be used for this response. def content_security_policy @content_security_policy ||= roda_class.opts[:content_security_policy].dup end # Do not set a content security policy header for this response. def skip_content_security_policy! @skip_content_security_policy = true end private # Set the appropriate content security policy header. def set_default_headers super unless @skip_content_security_policy (@content_security_policy || roda_class.opts[:content_security_policy]).set_header(headers) end end end end register_plugin(:content_security_policy, ContentSecurityPolicy) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/cookie_flags.rb000066400000000000000000000134111516720775400235360ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The cookie_flags plugin allows users to force specific cookie flags for # all cookies set by the application. It can also be used to warn or # raise for unexpected cookie flags. # # The cookie_flags plugin deals with the following cookie flags: # # httponly :: Disallows access to the cookie from client-side scripts. # samesite :: Restricts to which domains the cookie is sent. # secure :: Instructs the browser to only transmit the cookie over HTTPS. # # This plugin ships in secure-by-default mode, where it enforces # secure, httponly, samesite=strict cookies. You can disable enforcing # specific flags using the following options: # # :httponly :: Set to false to not enforce httponly flag. # :same_site :: Set to symbol or string to enforce a different samesite # setting, or false to not enforce a specific samesite setting. # :secure :: Set to false to not enforce secure flag. # # For example, to enforce secure cookies and enforce samesite=lax, but not enforce # an httponly flag: # # plugin :cookie_flags, httponly: false, same_site: 'lax' # # In general, overriding cookie flags using this plugin should be considered a # stop-gap solution. Instead of overriding cookie flags, it's better to fix # whatever is setting the cookie flags incorrectly. You can use the :action # option to modify the behavior: # # # Issue warnings when modifying cookie flags # plugin :cookie_flags, action: :warn_and_modify # # # Issue warnings for incorrect cookie flags without modifying cookie flags # plugin :cookie_flags, action: :warn # # # Raise errors for incorrect cookie flags # plugin :cookie_flags, action: :raise # # The recommended way to use the plugin is to use it only during testing with # action: :raise. Then as long as you have fully covering tests, you # can be sure the cookies set by your application use the correct flags. # # Note that this plugin only affects cookies set by the application, and does not # affect cookies set by middleware the application is using. module CookieFlags # :nocov: MATCH_METH = RUBY_VERSION >= '2.4' ? :match? : :match # :nocov: private_constant :MATCH_METH DEFAULTS = {:secure=>true, :httponly=>true, :same_site=>'strict', :action=>:modify}.freeze private_constant :DEFAULTS # Error class raised for action: :raise when incorrect cookie flags are used. class Error < RodaError end def self.configure(app, opts=OPTS) previous = app.opts[:cookie_flags] || DEFAULTS opts = app.opts[:cookie_flags] = previous.merge(opts) case opts[:same_site] when String, Symbol opts[:same_site] = opts[:same_site].to_s.downcase.freeze opts[:same_site_string] = "; samesite=#{opts[:same_site]}".freeze opts[:secure] = true if opts[:same_site] == 'none' end opts.freeze end module InstanceMethods private def _handle_cookie_flags_array(cookies) opts = self.class.opts[:cookie_flags] needs_secure = opts[:secure] needs_httponly = opts[:httponly] if needs_same_site = opts[:same_site] same_site_string = opts[:same_site_string] same_site_regexp = /;\s*samesite\s*=\s*(\S+)\s*(?:\z|;)/i end action = opts[:action] cookies.map do |cookie| if needs_secure add_secure = !/;\s*secure\s*(?:\z|;)/i.send(MATCH_METH, cookie) end if needs_httponly add_httponly = !/;\s*httponly\s*(?:\z|;)/i.send(MATCH_METH, cookie) end if needs_same_site has_same_site = same_site_regexp.match(cookie) unless add_same_site = !has_same_site update_same_site = needs_same_site != has_same_site[1].downcase end end next cookie unless add_secure || add_httponly || add_same_site || update_same_site case action when :raise, :warn, :warn_and_modify message = "Response contains cookie with unexpected flags: #{cookie.inspect}." \ "Expecting the following cookie flags: "\ "#{'secure ' if add_secure}#{'httponly ' if add_httponly}#{same_site_string[2..-1] if add_same_site || update_same_site}" if action == :raise raise Error, message else warn(message) next cookie if action == :warn end end if update_same_site cookie = cookie.gsub(same_site_regexp, same_site_string) else cookie = cookie.dup cookie << same_site_string if add_same_site end cookie << '; secure' if add_secure cookie << '; httponly' if add_httponly cookie end end if Rack.release >= '3' def _handle_cookie_flags(cookies) cookies = [cookies] if cookies.is_a?(String) _handle_cookie_flags_array(cookies) end else def _handle_cookie_flags(cookie_string) _handle_cookie_flags_array(cookie_string.split("\n")).join("\n") end end # Handle cookie flags in response def _roda_after_85__cookie_flags(res) return unless res && (headers = res[1]) && (value = headers[RodaResponseHeaders::SET_COOKIE]) headers[RodaResponseHeaders::SET_COOKIE] = _handle_cookie_flags(value) end end end register_plugin(:cookie_flags, CookieFlags) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/cookies.rb000066400000000000000000000035051516720775400225500ustar00rootroot00000000000000# frozen-string-literal: true require 'rack/utils' # class Roda module RodaPlugins # The cookies plugin adds response methods for handling cookies. # Currently, you can set cookies with +set_cookie+ and delete cookies # with +delete_cookie+: # # response.set_cookie('foo', 'bar') # response.delete_cookie('foo') # # Pass a hash of cookie options when loading the plugin to set some # defaults for all cookies upon setting and deleting. This is particularly # useful for configuring the +domain+ and +path+ of all cookies. # # plugin :cookies, domain: 'example.com', path: '/api' module Cookies # Allow setting default cookie options when loading the cookies plugin. def self.configure(app, opts={}) app.opts[:cookies_opts] = (app.opts[:cookies_opts]||{}).merge(opts).freeze end module ResponseMethods # Modify the headers to include a Set-Cookie value that # deletes the cookie. A value hash can be provided to # override the default one used to delete the cookie. # Example: # # response.delete_cookie('foo') # response.delete_cookie('foo', domain: 'example.org') def delete_cookie(key, value = {}) ::Rack::Utils.delete_cookie_header!(@headers, key, roda_class.opts[:cookies_opts].merge(value)) end # Set the cookie with the given key in the headers. # # response.set_cookie('foo', 'bar') # response.set_cookie('foo', value: 'bar', domain: 'example.org') def set_cookie(key, value) value = { :value=>value } unless value.respond_to?(:keys) ::Rack::Utils.set_cookie_header!(@headers, key, roda_class.opts[:cookies_opts].merge(value)) end end end register_plugin(:cookies, Cookies) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/csrf.rb000066400000000000000000000047131516720775400220530ustar00rootroot00000000000000# frozen-string-literal: true require 'rack/csrf' class Roda module RodaPlugins # This plugin is no longer recommended for use, it exists only for # backwards compatibility. Consider using the route_csrf plugin # instead, as that provides stronger CSRF protection. # # The csrf plugin adds CSRF protection using rack_csrf, along with # some csrf helper methods to use in your views. To use it, load # the plugin, with the options hash passed to Rack::Csrf: # # plugin :csrf, raise: true # # Optionally you can choose not to setup rack_csrf middleware on the # roda app if you already have one configured: # # plugin :csrf, skip_middleware: true # # This adds the following instance methods: # # csrf_field :: The field name to use for the hidden/meta csrf tag. # csrf_header :: The http header name to use for submitting csrf token via # headers (useful for javascript). # csrf_metatag :: An html meta tag string containing the token, suitable # for placing in the page header # csrf_tag :: An html hidden input tag string containing the token, suitable # for placing in an html form. # csrf_token :: The value of the csrf token, in case it needs to be accessed # directly. module Csrf CSRF = ::Rack::Csrf # Load the Rack::Csrf middleware into the app with the given options. def self.configure(app, opts={}) return if opts[:skip_middleware] app.instance_exec do @middleware.each do |(mid, *rest), _| if mid.equal?(CSRF) rest[0].merge!(opts) build_rack_app return end end use CSRF, opts end end module InstanceMethods # The name of the hidden/meta csrf tag. def csrf_field CSRF.field end # The http header name to use for submitting csrf token via headers. def csrf_header CSRF.header end # An html meta tag string containing the token. def csrf_metatag(opts={}) CSRF.metatag(env, opts) end # An html hidden input tag string containing the token. def csrf_tag CSRF.tag(env) end # The value of the csrf token. def csrf_token CSRF.token(env) end end end register_plugin(:csrf, Csrf) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/custom_block_results.rb000066400000000000000000000054471516720775400253700ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The custom_block_results plugin allows you to specify handling # for different block results. By default, Roda only supports # nil, false, and string block results, but using this plugin, # you can support other block results. # # For example, if you wanted to support returning Integer # block results, and have them set the response status code, # you could do: # # plugin :custom_block_results # # handle_block_result Integer do |result| # response.status_code = result # end # # route do |r| # 200 # end # # The expected use case for this is to customize behavior by # class, but matching uses ===, so it is possible to use non-class # objects that respond to === appropriately. # # Note that custom block result handling only occurs if the types # are not handled by Roda itself. You cannot use this to modify # the handling of nil, false, or string results. Additionally, # if the response body has already been written to before the the # route block exits, then the result of the block is ignored, # and the related +handle_block_result+ block will not be called # (this is standard Roda behavior). # # The return value of the +handle_block_result+ block is written # to the body if the block return value is a String, similar to # standard Roda handling of block results. Non-String return # values are ignored. module CustomBlockResults def self.configure(app) app.opts[:custom_block_results] ||= {} end module ClassMethods # Freeze the configured custom block results when freezing the app. def freeze opts[:custom_block_results].freeze super end # Specify a block that will be called when an instance of klass # is returned as a block result. The block defines a method. def handle_block_result(klass, &block) opts[:custom_block_results][klass] = define_roda_method(opts[:custom_block_results][klass] || "custom_block_result_#{klass}", 1, &block) end end module RequestMethods private # Try each configured custom block result, and call the related method # to get the block result. def unsupported_block_result(result) roda_class.opts[:custom_block_results].each do |klass, meth| if klass === result result = scope.send(meth, result) if String === result return result else return end end end super end end end register_plugin(:custom_block_results, CustomBlockResults) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/custom_matchers.rb000066400000000000000000000052561516720775400243210ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The custom_matchers plugin supports using arbitrary objects # as matchers, as long as the application has been configured # to accept such objects. # # After loading the plugin, support for custom matchers can be # configured using the +custom_matcher+ class method. This # method is generally passed the class of the object you want # to use as a custom matcher, as well as a block. The block # will be called in the context of the request instance # with the specific matcher used in the match method. # # Blocks can append to the captures in order to yield the appropriate # values to match blocks, or call request methods that append to the # captures. # # Example: # # plugin :custom_matchers # method_segment = Struct.new(:request_method, :next_segment) # custom_matcher(method_segment) do |matcher| # # self is the request instance ("r" yielded in the route block below) # if matcher.request_method == self.request_method # match(matcher.next_segment) # end # end # # get_foo = method_segment.new('GET', 'foo') # post_any = method_segment.new('POST', String) # route do |r| # r.on('baz') do # r.on(get_foo) do # # GET method, /baz/foo prefix # end # # r.is(post_any) do |seg| # # for POST /baz/bar, seg is "bar" # end # end # # r.on('quux') do # r.is(get_foo) do # # GET method, /quux/foo route # end # # r.on(post_any) do |seg| # # for POST /quux/xyz, seg is "xyz" # end # end # end module CustomMatchers def self.configure(app) app.opts[:custom_matchers] ||= OPTS end module ClassMethods def custom_matcher(match_class, &block) custom_matchers = Hash[opts[:custom_matchers]] meth = custom_matchers[match_class] = custom_matchers[match_class] || :"_custom_matcher_#{match_class}" opts[:custom_matchers] = custom_matchers.freeze self::RodaRequest.send(:define_method, meth, &block) nil end end module RequestMethods private # Try custom matchers before calling super def unsupported_matcher(matcher) roda_class.opts[:custom_matchers].each do |match_class, meth| if match_class === matcher return send(meth, matcher) end end super end end end register_plugin(:custom_matchers, CustomMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/default_headers.rb000066400000000000000000000037571516720775400242440ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The default_headers plugin accepts a hash of headers, # and overrides the default_headers method in the # response class to be a copy of the headers. # # Note that when using this module, you should not # attempt to mutate of the values set in the default # headers hash. # # Example: # # plugin :default_headers, 'Content-Type'=>'text/csv' # # You can modify the default headers later by loading the # plugin again: # # plugin :default_headers, 'Foo'=>'bar' # plugin :default_headers, 'Bar'=>'baz' module DefaultHeaders # Merge the given headers into the existing default headers, if any. def self.configure(app, headers={}) app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze end module ClassMethods # The default response headers to use for the current class. def default_headers opts[:default_headers] end # Optimize the response class set_default_headers method if it hasn't been # overridden and all default headers are strings. def freeze if (headers = opts[:default_headers]).all?{|k, v| k.is_a?(String) && v.is_a?(String)} && (self::RodaResponse.instance_method(:set_default_headers).owner == Base::ResponseMethods) self::RodaResponse.class_eval(<<-END, __FILE__, __LINE__+1) private def set_default_headers h = @headers #{headers.map{|k,v| "h[#{k.inspect}] ||= #{v.inspect}"}.join('; ')} end END end super end end module ResponseMethods # Get the default headers from the related roda class. def default_headers roda_class.default_headers end end end register_plugin(:default_headers, DefaultHeaders) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/default_status.rb000066400000000000000000000022321516720775400241370ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The default_status plugin accepts a block which should # return a response status integer. This integer will be used as # the default response status (usually 200) if the body has been # written to, and you have not explicitly set a response status. # # Example: # # # Use 201 default response status for all requests # plugin :default_status do # 201 # end module DefaultStatus def self.configure(app, &block) raise RodaError, "default_status plugin requires a block" unless block if check_arity = app.opts.fetch(:check_arity, true) unless block.arity == 0 if check_arity == :warn RodaPlugins.warn "Arity mismatch in block passed to plugin :default_status. Expected Arity 0, but arguments required for #{block.inspect}" end b = block block = lambda{instance_exec(&b)} # Fallback end end app::RodaResponse.send(:define_method, :default_status, &block) end end register_plugin(:default_status, DefaultStatus) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/delay_build.rb000066400000000000000000000005451516720775400233720ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins module DelayBuild module ClassMethods # No-op for backwards compatibility def build! end end end # RODA4: Remove plugin # Only available for backwards compatibility, no longer needed register_plugin(:delay_build, DelayBuild) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/delegate.rb000066400000000000000000000043001516720775400226600ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The delegate plugin allows you to easily setup instance methods in # the scope of the route block that call methods on the related # request, response, or class which may offer a simpler API in some cases. # Roda doesn't automatically setup such delegate methods because # it pollutes the application's method namespace, but this plugin # allows the user to do so. # # Here's an example based on the README's initial example, using the # request_delegate method to simplify the DSL: # # plugin :delegate # request_delegate :root, :on, :is, :get, :post, :redirect # # route do |r| # # GET / request # root do # redirect "/hello" # end # # # /hello branch # on "hello" do # # Set variable for all routes in /hello branch # @greeting = 'Hello' # # # GET /hello/world request # get "world" do # "#{@greeting} world!" # end # # # /hello request # is do # # GET /hello request # get do # "#{@greeting}!" # end # # # POST /hello request # post do # puts "Someone said #{@greeting}!" # redirect # end # end # end # end module Delegate module ClassMethods # Delegate the given methods to the class def class_delegate(*meths) meths.each do |meth| define_method(meth){|*a, &block| self.class.public_send(meth, *a, &block)} end end # Delegate the given methods to the request def request_delegate(*meths) meths.each do |meth| define_method(meth){|*a, &block| @_request.public_send(meth, *a, &block)} end end # Delegate the given methods to the response def response_delegate(*meths) meths.each do |meth| define_method(meth){|*a, &block| @_response.public_send(meth, *a, &block)} end end end end register_plugin(:delegate, Delegate) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/delete_empty_headers.rb000066400000000000000000000020421516720775400252620ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The delete_empty_headers plugin deletes any headers whose # value is set to the empty string. Because of how default headers are # set in Roda, if you have a default header but don't want # to set it for a specific request, you need to use this plugin # and set the header value to the empty string, and Roda will automatically # delete the header. module DeleteEmptyHeaders module ResponseMethods # Delete any empty headers when calling finish def finish delete_empty_headers(super) end # Delete any empty headers when calling finish_with_body def finish_with_body(_) delete_empty_headers(super) end private # Delete any empty headers from response def delete_empty_headers(res) res[1].delete_if{|_, v| v.is_a?(String) && v.empty?} res end end end register_plugin(:delete_empty_headers, DeleteEmptyHeaders) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/direct_call.rb000066400000000000000000000016411516720775400233600ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The direct_call plugin makes the call class method skip the middleware stack # (app.call will still call the middleware). # This can be used as an optimization, as the Roda class itself can be used # as the callable, which is faster than using a lambda. module DirectCall def self.configure(app) app.send(:build_rack_app) end module ClassMethods # Call the application without middlware. def call(env) new(env)._roda_handle_main_route end private # If new_api is true, use the receiver as the base rack app for better # performance. def base_rack_app_callable(new_api=true) if new_api self else super end end end end register_plugin(:direct_call, DirectCall) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/disallow_file_uploads.rb000066400000000000000000000027011516720775400254550ustar00rootroot00000000000000# frozen-string-literal: true raise LoadError, "disallow_file_uploads plugin not supported on Rack <1.6" if Rack.release < '1.6' # class Roda module RodaPlugins # The disallow_file_uploads plugin raises a Roda::RodaPlugins::DisallowFileUploads::Error # if there is an attempt to upload a file. This plugin is useful for applications where # multipart file uploads are not expected and you want to remove the ability for rack # to create temporary files. Example: # # plugin :disallow_file_uploads # # This plugin is only supported on Rack 1.6+. This plugin does not technically # block users from uploading files, it only blocks the parsing of request bodies containing # multipart file uploads. So if you do not call +r.POST+ (or something that calls it such as # +r.params+), then Roda will not attempt to parse the request body, and an exception will not # be raised. module DisallowFileUploads # Exception class used when a multipart file upload is attempted. class Error < RodaError; end NO_TEMPFILE = lambda{|_,_| raise Error, "Support for uploading files has been disabled"} module RequestMethods # HTML escape the input and return the escaped version. def initialize(_, env) env['rack.multipart.tempfile_factory'] = NO_TEMPFILE super end end end register_plugin(:disallow_file_uploads, DisallowFileUploads) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/drop_body.rb000066400000000000000000000025361516720775400231000ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The drop_body plugin automatically drops the body and # Content-Type/Content-Length headers from the response if # the response status indicates that the response should # not include a body (response statuses 100, 101, 102, 204, # and 304). For response status 205, the body and Content-Type # headers are dropped, but the Content-length header is set to # '0' instead of being dropped. module DropBody module ResponseMethods DROP_BODY_STATUSES = [100, 101, 102, 204, 205, 304].freeze RodaPlugins.deprecate_constant(self, :DROP_BODY_STATUSES) DROP_BODY_RANGE = 100..199 private_constant :DROP_BODY_RANGE # If the response status indicates a body should not be # returned, use an empty body and remove the Content-Length # and Content-Type headers. def finish r = super case r[0] when DROP_BODY_RANGE, 204, 304 r[2] = EMPTY_ARRAY h = r[1] h.delete(RodaResponseHeaders::CONTENT_LENGTH) h.delete(RodaResponseHeaders::CONTENT_TYPE) when 205 r[2] = EMPTY_ARRAY empty_205_headers(r[1]) end r end end end register_plugin(:drop_body, DropBody) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/each_part.rb000066400000000000000000000052771516720775400230520ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The each_part plugin adds an each_part method, which is a # render_each-like method that treats all keywords as locals. # # # Can replace this: # render_each(enum, :template, locals: {foo: 'bar'}) # # # With this: # each_part(enum, :template, foo: 'bar') # # On Ruby 2.7+, the part method takes a keyword splat, so you # must pass keywords and not a positional hash for the locals. # # If you are using the :assume_fixed_locals render plugin option, # template caching is enabled, you are using Ruby 3+, and you # are freezing your Roda application, in addition to providing a # simpler API, this also provides a performance improvement. module EachPart def self.load_dependencies(app) app.plugin :render_each end module ClassMethods # When freezing, optimize the part method if assuming fixed locals # and caching templates. def freeze if render_opts[:assume_fixed_locals] && !render_opts[:check_template_mtime] include AssumeFixedLocalsInstanceMethods end super end end module InstanceMethods if RUBY_VERSION >= '2.7' class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def each_part(enum, template, **locals, &block) render_each(enum, template, :locals=>locals, &block) end RUBY # :nocov: else def each_part(enum, template, locals=OPTS, &block) render_each(enum, template, :locals=>locals, &block) end end # :nocov: end module AssumeFixedLocalsInstanceMethods # :nocov: if RUBY_VERSION >= '3.0' # :nocov: class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def each_part(enum, template, **locals, &block) if optimized_method = _cached_render_each_template_method(template) optimized_method = optimized_method[0] as = render_each_default_local(template) if defined?(yield) enum.each do |v| locals[as] = v yield send(optimized_method, **locals) end nil else enum.map do |v| locals[as] = v send(optimized_method, **locals) end.join end else render_each(enum, template, :locals=>locals, &block) end end RUBY end end end register_plugin(:each_part, EachPart) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/early_hints.rb000066400000000000000000000014551516720775400234370ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The early_hints plugin allows sending 103 Early Hints responses # using the rack.early_hints environment variable. # Early hints allow clients to preload necessary files before receiving # the response. module EarlyHints module InstanceMethods # Send given hash of Early Hints using the rack.early_hints environment variable, # currenly only supported by puma. hash given should generally have the single # key 'Link', and a string or array of strings for each of the early hints. def send_early_hints(hash) if eh_proc = env['rack.early_hints'] eh_proc.call(hash) end end end end register_plugin(:early_hints, EarlyHints) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/empty_root.rb000066400000000000000000000025001516720775400233070ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The empty_root plugin makes +r.root+ match both on +/+ and # on the empty string. This is mostly useful when using multiple # rack applications, where the initial PATH_INFO has been moved # to SCRIPT_NAME. For example, if you have the following # applications: # # class App1 < Roda # on "albums" do # run App2 # end # end # # class App2 < Roda # plugin :empty_root # # route do |r| # r.root do # "root" # end # end # end # # Then requests for both +/albums/+ and +/albums+ will return # "root". Without this plugin loaded into App2, only requests # for +/albums/+ will return "root", since by default, +r.root+ # matches only when the current PATH_INFO is +/+ and not when # it is empty. module EmptyRoot module RequestMethods # Match when the remaining path is the empty string, # in addition to the default behavior of matching when # the remaining path is +/+. def root(&block) super if remaining_path.empty? && is_get? always(&block) end end end end register_plugin(:empty_root, EmptyRoot) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/environments.rb000066400000000000000000000046141516720775400236450ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The environments plugin adds a environment class accessor to get # the environment for the application, 3 predicate class methods # to check for the current environment (development?, test? and # production?), and a class configure method that takes environment(s) # and yields to the block if the given environment(s) match the # current environment. # # The default environment for the application is based on # ENV['RACK_ENV']. # # Example: # # class Roda # plugin :environments # # environment # => :development # development? # => true # test? # => false # production? # => false # # # Set the environment for the application # self.environment = :test # test? # => true # # configure do # # called, as no environments given # end # # configure :development, :production do # # not called, as no environments match # end # # configure :test do # # called, as environment given matches current environment # end # end module Environments # Set the environment to use for the app. Default to ENV['RACK_ENV'] # if no environment is given. If ENV['RACK_ENV'] is not set and # no environment is given, assume the development environment. def self.configure(app, env=ENV["RACK_ENV"]) app.environment = (env || 'development').to_sym end module ClassMethods # If no environments are given or one of the given environments # matches the current environment, yield the receiver to the block. def configure(*envs) if envs.empty? || envs.any?{|s| s == environment} yield self end end # The current environment for the application, which should be stored # as a symbol. def environment opts[:environment] end # Override the environment for the application, instead of using # RACK_ENV. def environment=(v) opts[:environment] = v end [:development, :test, :production].each do |env| define_method("#{env}?"){environment == env} end end end register_plugin(:environments, Environments) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/erb_h.rb000066400000000000000000000024011516720775400221650ustar00rootroot00000000000000# frozen-string-literal: true require 'erb/escape' # class Roda module RodaPlugins # The erb_h plugin adds an +h+ instance method that will HTML # escape the input and return it. This is similar to the h # plugin, but it uses erb/escape to implement the HTML escaping, # which offers faster performance. # # To make sure that this speeds up applications using the h # plugin, this depends on the h plugin, and overrides the # h method. # # The following example will return "<foo>" as the body. # # plugin :erb_h # # route do |r| # h('') # end # # The faster performance offered by the erb_h plugin is due # to erb/escape avoiding allocations if not needed (returning the # input object if no escaping is needed). That behavior change # can cause problems if you mutate the result of the h method # (which can mutate the input), or mutate the input of the h # method after calling it (which can mutate the result). module ErbH def self.load_dependencies(app) app.plugin :h end module InstanceMethods define_method(:h, ERB::Escape.instance_method(:html_escape)) end end register_plugin(:erb_h, ErbH) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/error_email.rb000066400000000000000000000120161516720775400234110ustar00rootroot00000000000000# frozen-string-literal: true require 'net/smtp' class Roda module RodaPlugins # The error_email plugin adds an +error_email+ instance method that # send an email related to the exception. This is most useful if you are # also using the error_handler plugin: # # plugin :error_email, to: 'to@example.com', from: 'from@example.com' # plugin :error_handler do |e| # error_email(e) # 'Internal Server Error' # end # # It is similar to the error_mail plugin, except that it uses net/smtp # directly instead of using the mail library. If you are not already using the # mail library in your application, it makes sense to use error_email # instead of error_mail. # # Options: # # :filter :: Callable called with the key and value for each parameter, environment # variable, and session value. If it returns true, the value of the # parameter is filtered in the email. # :from :: The From address to use in the email (required) # :headers :: A hash of additional headers to use in the email (default: empty hash) # :host :: The SMTP server to use to send the email (default: localhost) # :prefix :: A prefix to use in the email's subject line (default: no prefix) # :to :: The To address to use in the email (required) # # The subject of the error email shows the exception class and message. # The body of the error email shows the backtrace of the error and the # request environment, as well the request params and session variables (if any). # You can also call error_email with a plain string instead of an exception, # in which case the string is used as the subject, and no backtrace is included. # # Note that emailing on every error as shown above is only appropriate # for low traffic web applications. For high traffic web applications, # use an error reporting service instead of this plugin. module ErrorEmail DEFAULTS = { :filter=>lambda{|k,v| false}, :headers=>OPTS, :host=>'localhost', # :nocov: :emailer=>lambda{|h| Net::SMTP.start(h[:host]){|s| s.send_message(h[:message], h[:from], h[:to])}}, # :nocov: :default_headers=>lambda do |h, e| subject = if e.respond_to?(:message) "#{e.class}: #{e.message}" else e.to_s end {'From'=>h[:from], 'To'=>h[:to], 'Subject'=>"#{h[:prefix]}#{subject}"} end, :body=>lambda do |s, e| filter = s.opts[:error_email][:filter] format = lambda do |h| h = h.map{|k, v| "#{k.inspect} => #{filter.call(k, v) ? 'FILTERED' : v.inspect}"} h.sort! h.join("\n") end begin params = s.request.params params = (format[params] unless params.empty?) rescue params = 'Invalid Parameters!' end message = String.new message << <env['rack.errors'] but # otherwise ignored. This avoids recursive calls into the # error_handler. Note that if the error_handler itself raises # an exception, the exception will be raised without normal after # processing. This can cause some after processing to run twice # (once before the error_handler is called and once after) if # later after processing raises an exception. # # By default, this plugin handles StandardError and ScriptError. # To override the exception classes it will handle, pass a :classes # option to the plugin: # # plugin :error_handler, classes: [StandardError, ScriptError, NoMemoryError] module ErrorHandler DEFAULT_ERROR_HANDLER_CLASSES = [StandardError, ScriptError].freeze # If a block is given, automatically call the +error+ method on # the Roda class with it. def self.configure(app, opts={}, &block) app.opts[:error_handler_classes] = (opts[:classes] || app.opts[:error_handler_classes] || DEFAULT_ERROR_HANDLER_CLASSES).dup.freeze if block app.error(&block) end end module ClassMethods # Install the given block as the error handler, so that if routing # the request raises an exception, the block will be called with # the exception in the scope of the Roda instance. def error(&block) define_method(:handle_error, &block) alias_method(:handle_error, :handle_error) private :handle_error end end module InstanceMethods # If an error occurs, set the response status to 500 and call # the error handler. Old Dispatch API. def call # RODA4: Remove begin res = super ensure _roda_after(res) end rescue *opts[:error_handler_classes] => e _handle_error(e) end # If an error occurs, set the response status to 500 and call # the error handler. def _roda_handle_main_route begin res = super ensure _roda_after(res) end rescue *opts[:error_handler_classes] => e _handle_error(e) end private # Default empty implementation of _roda_after, usually # overridden by Roda.def_roda_before. def _roda_after(res) end # Handle the given exception using handle_error, using a default status # of 500. Run after hooks on the rack response, but if any error occurs # when doing so, log the error using rack.errors and return the response. def _handle_error(e) res = @_response res.send(:initialize) res.status = 500 res = _roda_handle_route{handle_error(e)} begin _roda_after(res) rescue => e2 if errors = env['rack.errors'] errors.puts "Error in after hook processing of error handler: #{e2.class}: #{e2.message}" e2.backtrace.each{|line| errors.puts(line)} end end res end # By default, have the error handler reraise the error, so using # the plugin without installing an error handler doesn't change # behavior. def handle_error(e) raise e end end end register_plugin(:error_handler, ErrorHandler) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/error_mail.rb000066400000000000000000000105561516720775400232530ustar00rootroot00000000000000# frozen-string-literal: true require 'mail' class Roda module RodaPlugins # The error_mail plugin adds an +error_mail+ instance method that # send an email related to the exception. This is most useful if you are # also using the error_handler plugin: # # plugin :error_mail, to: 'to@example.com', from: 'from@example.com' # plugin :error_handler do |e| # error_mail(e) # 'Internal Server Error' # end # # It is similar to the error_email plugin, except that it uses the mail # library instead of net/smtp directly. If you are already using the # mail library in your application, it makes sense to use error_mail # instead of error_email. # # Options: # # :filter :: Callable called with the key and value for each parameter, environment # variable, and session value. If it returns true, the value of the # parameter is filtered in the email. # :from :: The From address to use in the email (required) # :headers :: A hash of additional headers to use in the email (default: empty hash) # :prefix :: A prefix to use in the email's subject line (default: no prefix) # :to :: The To address to use in the email (required) # # The subject of the error email shows the exception class and message. # The body of the error email shows the backtrace of the error and the # request environment, as well the request params and session variables (if any). # You can also call error_mail with a plain string instead of an exception, # in which case the string is used as the subject, and no backtrace is included. # # Note that emailing on every error as shown above is only appropriate # for low traffic web applications. For high traffic web applications, # use an error reporting service instead of this plugin. module ErrorMail DEFAULT_FILTER = lambda{|k,v| false} private_constant :DEFAULT_FILTER # Set default opts for plugin. See ErrorEmail module RDoc for options. def self.configure(app, opts=OPTS) app.opts[:error_mail] = email_opts = (app.opts[:error_mail] || {:filter=>DEFAULT_FILTER}).merge(opts).freeze unless email_opts[:to] && email_opts[:from] raise RodaError, "must provide :to and :from options to error_mail plugin" end end module InstanceMethods # Send an email for the given error. +exception+ is usually an exception # instance, but it can be a plain string which is used as the subject for # the email. def error_mail(exception) _error_mail(exception).deliver! end # The content of the email to send, include the headers and the body. # Takes the same argument as #error_mail. def error_mail_content(exception) _error_mail(exception).to_s end private def _error_mail(e) email_opts = self.class.opts[:error_mail] subject = if e.respond_to?(:message) "#{e.class}: #{e.message}" else e.to_s end subject = "#{email_opts[:prefix]}#{subject}" filter = email_opts[:filter] format = lambda do |h| h = h.map{|k, v| "#{k.inspect} => #{filter.call(k, v) ? 'FILTERED' : v.inspect}"} h.sort! h.join("\n") end begin params = request.params params = (format[params] unless params.empty?) rescue params = 'Invalid Parameters!' end message = String.new message << < # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL # THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # The HTML template used in Rack::ShowExceptions was based on Django's # template and is under the following license: # # adapted from Django # Copyright (c) Django Software Foundation and individual contributors. # Used under the modified BSD license: # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 module ExceptionPage def self.load_dependencies(app) app.plugin :h end # Stylesheet used by the HTML exception page def self.css <div { border-bottom:1px solid #ddd; } h1 { font-weight:normal; } h2 { margin-bottom:.8em; } h2 span { font-size:80%; color:#666; font-weight:normal; } h3 { margin:1em 0 .5em 0; } h4 { margin:0 0 .5em 0; font-weight: normal; } table { border:1px solid #ccc; border-collapse: collapse; background:white; } tbody td, tbody th { vertical-align:top; padding:2px 3px; } thead th { padding:1px 6px 1px 3px; background:#fefefe; text-align:left; font-weight:normal; font-size:11px; border:1px solid #ddd; } tbody th { text-align:right; color:#666; padding-right:.5em; } table.vars { margin:5px 0 2px 40px; } table.vars td, table.req td { font-family:monospace; } table td.code { width:100%;} table td.code div { overflow:hidden; } table.source th { color:#666; } table.source td { font-family:monospace; white-space:pre; border-bottom:1px solid #eee; } ul.traceback { list-style-type:none; } ul.traceback li.frame { margin-bottom:1em; } div.context { margin: 10px 0; } div.context ol { padding-left:30px; margin:0 10px; list-style-position: inside; } div.context ol li { font-family:monospace; white-space:pre; color:#666; cursor:pointer; } div.context ol.context-line li { color:black; background-color:#ccc; } div.context ol.context-line li span { float: right; } div.commands { margin-left: 40px; } div.commands a { color:black; text-decoration:none; } #summary { background: #ffc; } #summary h2 { font-weight: normal; color: #666; font-family: monospace; white-space: pre-wrap;} #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; } #summary ul#quicklinks li { float: left; padding: 0 1em; } #summary ul#quicklinks>li+li { border-left: 1px #666 solid; } #explanation { background:#eee; } #traceback { background:#eee; } #requestinfo { background:#f6f6f6; padding-left:120px; } #summary table { border:none; background:transparent; } #requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; } #requestinfo h3 { margin-bottom:-1em; } .error { background: #ffc; } .specific { color:#cc3300; font-weight:bold; } END end # Javascript used by the HTML exception page for context toggling def self.js <{ "class"=>exception.class.to_s, "message"=>message, "backtrace"=>exception.backtrace.map(&:to_s) } } elsif env['HTTP_ACCEPT'] =~ /text\/html/ @_response[RodaResponseHeaders::CONTENT_TYPE] = "text/html" context = opts[:context] || 7 css_file = opts[:css_file] js_file = opts[:js_file] case prefix = opts[:assets] when false css_file = false if css_file.nil? js_file = false if js_file.nil? when nil # nothing else prefix = '' if prefix == true css_file ||= "#{prefix}/exception_page.css" js_file ||= "#{prefix}/exception_page.js" end css = case css_file when nil "" when false # :nothing else "" end js = case js_file when nil "" when false # :nothing else "" end frames = exception.backtrace.map.with_index do |line, i| frame = {:id=>i} if line =~ /\A(.*?):(\d+)(?::in [`'](.*)')?\Z/ filename = frame[:filename] = $1 lineno = frame[:lineno] = $2.to_i frame[:function] = $3 begin lineno -= 1 lines = ::File.readlines(filename) if line = lines[lineno] pre_lineno = [lineno-context, 0].max if (pre_context = lines[pre_lineno...lineno]) && !pre_context.empty? frame[:pre_context_lineno] = pre_lineno frame[:pre_context] = pre_context end post_lineno = [lineno+context, lines.size].min if (post_context = lines[lineno+1..post_lineno]) && !post_context.empty? frame[:post_context_lineno] = post_lineno frame[:post_context] = post_context end frame[:context_line] = line.chomp end rescue end frame end end.compact r = @_request begin post_data = r.POST missing_post = "No POST data" rescue missing_post = "Invalid POST data" end info = lambda do |title, id, var, none| <#{title} #{(var && !var.empty?) ? (<#{none}

" #{var.sort_by{|k, _| k.to_s}.map{|key, val| (< END2 }
Variable Value
#{h key}
#{h val.inspect}
END1 } END end < #{h exception.class} at #{h r.path} #{css}

#{h exception.class} at #{h r.path}

#{h message}

Ruby #{(first = frames.first) ? "#{h first[:filename]}: in #{h first[:function]}, line #{first[:lineno]}" : "unknown location"}
Web #{r.request_method} #{h r.host}#{h r.path}

Jump to:

Traceback (innermost first)

    #{frames.map{|frame| id = frame[:id]; (< #{h frame[:filename]}:#{frame[:lineno]} in #{h frame[:function]} #{frame[:context_line] ? (<'
    #{frame[:pre_context] ? (< #{frame[:pre_context].map{|line| "
  • #{h line}
  • "}.join} END3 }
    1. #{h frame[:context_line]}...
    #{frame[:post_context] ? (< #{frame[:post_context].map{|line| "
  • #{h line}
  • "}.join} END4 }
    END2 } END1 }

Request information

#{info.call('GET', 'get-info', r.GET, 'No GET data')} #{info.call('POST', 'post-info', post_data, missing_post)} #{info.call('Cookies', 'cookie-info', r.cookies, 'No cookie data')} #{info.call('Rack ENV', 'env-info', r.env, 'No Rack env?')}

You're seeing this error because you use the Roda exception_page plugin.

#{js} END else @_response[RodaResponseHeaders::CONTENT_TYPE] = "text/plain" "#{exception.class}: #{message}\n#{exception.backtrace.map{|l| "\t#{l}"}.join("\n")}" end end # The CSS to use on the exception page def exception_page_css ExceptionPage.css end # The JavaScript to use on the exception page def exception_page_js ExceptionPage.js end private if Exception.method_defined?(:detailed_message) def exception_page_exception_message(exception) exception.detailed_message(highlight: false).to_s end # :nocov: else # Return message to use for exception. def exception_page_exception_message(exception) exception.message.to_s end end # :nocov: end module RequestMethods # Serve exception page assets def exception_page_assets get 'exception_page.css' do response[RodaResponseHeaders::CONTENT_TYPE] = "text/css" scope.exception_page_css end get 'exception_page.js' do response[RodaResponseHeaders::CONTENT_TYPE] = "application/javascript" scope.exception_page_js end end end end register_plugin(:exception_page, ExceptionPage) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/filter_common_logger.rb000066400000000000000000000025731516720775400253140ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The skip_common_logger plugin allows for skipping common_logger logging # of some requests. You pass a block when loading the plugin, and the # block will be called before logging each request. The block should return # whether the request should be logged. # # Example: # # # Only log server errors # plugin :filter_common_logger do |result| # result[0] >= 500 # end # # # Don't log requests to certain paths # plugin :filter_common_logger do |_| # # Block is called in the same context as the route block # !request.path.start_with?('/admin/') # end module FilterCommonLogger def self.load_dependencies(app, &_) app.plugin :common_logger end def self.configure(app, &block) app.send(:define_method, :_common_log_request?, &block) app.send(:private, :_common_log_request?) app.send(:alias_method, :_common_log_request?, :_common_log_request?) end module InstanceMethods private # Log request/response information in common log format to logger. def _roda_after_90__common_logger(result) super if result && _common_log_request?(result) end end end register_plugin(:filter_common_logger, FilterCommonLogger) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/flash.rb000066400000000000000000000062241516720775400222120ustar00rootroot00000000000000# frozen-string-literal: true require 'delegate' class Roda module RodaPlugins # The flash plugin adds a +flash+ instance method to Roda, # for typical web application flash handling, where values # set in the current flash hash are available in the next # request. # # With the example below, if a POST request is submitted, # it will redirect and the resulting GET request will # return 'b'. # # plugin :flash # # route do |r| # r.is '' do # r.get do # flash['a'] # end # # r.post do # flash['a'] = 'b' # r.redirect('') # end # end # end # # You can modify the flash for the current request (instead of # the next request) by using the +flash.now+ method: # # r.get do # flash.now['a'] = 'b' # flash['a'] # = >'b' # end module Flash # Simple flash hash, where assiging to the hash updates the flash # used in the following request. class FlashHash < DelegateClass(Hash) # The flash hash for the next request. This # is what gets written to by #[]=. attr_reader :next # The flash hash for the current request alias now __getobj__ # Setup the next hash when initializing, and handle treat nil # as a new empty hash. def initialize(hash={}) super(hash||{}) @next = {} end # Update the next hash with the given key and value. def []=(k, v) @next[k] = v end # Remove given key from the next hash, or clear the next hash if # no argument is given. def discard(key=(no_arg=true)) if no_arg @next.clear else @next.delete(key) end end # Copy the entry with the given key from the current hash to the # next hash, or copy all entries from the current hash to the # next hash if no argument is given. def keep(key=(no_arg=true)) if no_arg @next.merge!(self) else self[key] = self[key] end end # Replace the current hash with the next hash and clear the next hash. def sweep replace(@next) @next.clear self end end module InstanceMethods # Access the flash hash for the current request, loading # it from the session if it is not already loaded. def flash # :_flash to support transparent upgrades from previous key @_flash ||= FlashHash.new(session['_flash'] || (session['_flash'] = session.delete(:_flash))) end private # If the routing doesn't raise an error, rotate the flash # hash in the session so the next request has access to it. def _roda_after_40__flash(_) if f = @_flash f = f.next if f.empty? session.delete('_flash') else session['_flash'] = f end end end end end register_plugin(:flash, Flash) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/h.rb000066400000000000000000000027071516720775400213460ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The h plugin adds an +h+ instance method that will HTML # escape the input and return it. # # The following example will return "<foo>" as the body. # # plugin :h # # route do |r| # h('') # end module H begin require 'cgi/escape' unless CGI.respond_to?(:escapeHTML) # work around for JRuby 9.1 # :nocov: CGI = Object.new CGI.extend(defined?(::CGI::Escape) ? ::CGI::Escape : ::CGI::Util) # :nocov: end module InstanceMethods # HTML escape the input and return the escaped version. def h(string) CGI.escapeHTML(string.to_s) end end rescue LoadError # :nocov: # A Hash of entities and their escaped equivalents, # to be escaped by h(). ESCAPE_HTML = { "&" => "&".freeze, "<" => "<".freeze, ">" => ">".freeze, "'" => "'".freeze, '"' => """.freeze, }.freeze # A Regexp of HTML entities to match for escaping. ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys) module InstanceMethods def h(string) string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] } end end # :nocov: end end register_plugin(:h, H) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/halt.rb000066400000000000000000000067631516720775400220550ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The halt plugin augments the standard request +halt+ method to allow the response # status, body, or headers to be changed when halting. # # After loading the halt plugin: # # plugin :halt # # You can call the halt method with an integer to set the response status and return: # # route do |r| # r.halt(403) # end # # Or set the response body and return: # # route do |r| # r.halt('body') # end # # Or set both: # # route do |r| # r.halt(403, 'body') # end # # Or set response status, headers, and body: # # route do |r| # r.halt(403, {'Content-Type'=>'text/csv'}, 'body') # end # # As supported by default, you can still pass an array which contains a rack response: # # route do |r| # r.halt([403, {'Content-Type'=>'text/csv'}, ['body']]) # end # # Note that there is a difference between providing status, headers, and body as separate # arguments and providing them as a single rack response array. With a rack response array, # the values are used directly, while with 3 arguments, the headers given are merged into # the existing headers and the given body is written to the existing response body. # # If using other plugins that recognize additional types of match block responses, such # as +symbol_views+ and +json+, you can pass those additional types to +r.halt+: # # plugin :halt # plugin :symbol_views # plugin :json # route do |r| # # symbol_views plugin, specifying template file to render as body # r.halt(:template) if r.params['a'] # # # symbol_views plugin, specifying status code, headers, and template file to render as body # r.halt(500, {'header'=>'value'}, :other_template) if r.params['c'] # # # json plugin, specifying status code and JSON body # r.halt(500, [{'error'=>'foo'}]) if r.params['b'] # end # # Note that when using the +json+ plugin with the +halt+ plugin, you cannot return a # array as a single argument and have it be converted to json, since it would be interpreted # as a rack response. You must use call +r.halt+ with either two or three argument forms # in that case. module Halt module RequestMethods # Expand default halt method to handle status codes, headers, and bodies. See Halt. def halt(*res) case res.length when 0 # do nothing when 1 case v = res[0] when Integer response.status = v when Array throw :halt, v else if result = block_result_body(v) response.write(result) else raise Roda::RodaError, "singular argument given to #halt not handled: #{v.inspect}" end end when 2 resp = response resp.status = res[0] resp.write(block_result_body(res[1])) when 3 resp = response resp.status = res[0] resp.headers.merge!(res[1]) resp.write(block_result_body(res[2])) else raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})" end super() end end end register_plugin(:halt, Halt) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hash_branch_view_subdir.rb000066400000000000000000000045441516720775400257620ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The hash_branch_view_subdir plugin builds on the hash_branches and view_options # plugins, automatically appending a view subdirectory for any matching hash branch # taken. In cases where you are using a separate view subdirectory per hash branch, # this can result in DRYer code. Example: # # plugin :hash_branch_view_subdir # # route do |r| # r.hash_branches # end # # hash_branch 'foo' do |r| # # view subdirectory here is 'foo' # r.hash_branches('foo') # end # # hash_branch 'foo', 'bar' do |r| # # view subdirectory here is 'foo/bar' # end module HashBranchViewSubdir def self.load_dependencies(app) app.plugin :hash_branches app.plugin :view_options end def self.configure(app) app.opts[:hash_branch_view_subdir_methods] ||= {} end module ClassMethods # Freeze the hash_branch_view_subdir metadata when freezing the app. def freeze opts[:hash_branch_view_subdir_methods].freeze.each_value(&:freeze) super end # Duplicate hash_branch_view_subdir metadata in subclass. def inherited(subclass) super h = subclass.opts[:hash_branch_view_subdir_methods] opts[:hash_branch_view_subdir_methods].each do |namespace, routes| h[namespace] = routes.dup end end # Automatically append a view subdirectory for a successful hash_branch route, # by modifying the generated method to append the view subdirectory before # dispatching to the original block. def hash_branch(namespace='', segment, &block) meths = opts[:hash_branch_view_subdir_methods][namespace] ||= {} if block meth = meths[segment] = define_roda_method(meths[segment] || "_hash_branch_view_subdir_#{namespace}_#{segment}", 1, &convert_route_block(block)) super do |*_| append_view_subdir(segment) send(meth, @_request) end else if meth = meths.delete(segment) remove_method(meth) end super end end end end register_plugin(:hash_branch_view_subdir, HashBranchViewSubdir) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hash_branches.rb000066400000000000000000000114371516720775400237070ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The hash_branches plugin allows for O(1) dispatch to multiple route tree branches, # based on the next segment in the remaining path: # # class App < Roda # plugin :hash_branches # # hash_branch("a") do |r| # # /a branch # end # # hash_branch("b") do |r| # # /b branch # end # # route do |r| # r.hash_branches # end # end # # With the above routing tree, the +r.hash_branches+ call in the main routing tree # will dispatch requests for the +/a+ and +/b+ branches of the tree to the appropriate # routing blocks. # # In this example, the hash branches for +/a+ and +/b+ are in the same file, but in larger # applications, they are usually stored in separate files. This allows for easily splitting # up the routing tree into a separate file per branch. # # The +hash_branch+ method supports namespaces, which allow for dispatching to sub-branches # any level of the routing tree, fully supporting the needs of applications with large and # deep routing branches: # # class App < Roda # plugin :hash_branches # # # Only one argument used, so the namespace defaults to '', and the argument # # specifies the route name # hash_branch("a") do |r| # # No argument given, so uses the already matched path as the namespace, # # which is '/a' in this case. # r.hash_branches # end # # hash_branch("b") do |r| # # uses :b as the namespace when looking up routes, as that was explicitly specified # r.hash_branches(:b) # end # # # Two arguments used, so first specifies the namespace and the second specifies # # the branch name # hash_branch("/a", "b") do |r| # # /a/b path # end # # hash_branch("/a", "c") do |r| # # /a/c path # end # # hash_branch(:b, "b") do |r| # # /b/b path # end # # hash_branch(:b, "c") do |r| # # /b/c path # end # # route do |r| # # No argument given, so uses '' as the namespace, as no part of the path has # # been matched yet # r.hash_branches # end # end # # With the above routing tree, requests for the +/a+ and +/b+ branches will be # dispatched to the appropriate +hash_branch+ block. Those blocks will the dispatch # to the remaining +hash_branch+ blocks, with the +/a+ branch using the implicit namespace of # +/a+, and the +/b+ branch using the explicit namespace of +:b+. # # It is best for performance to explicitly specify the namespace when calling # +r.hash_branches+. module HashBranches def self.configure(app) app.opts[:hash_branches] ||= {} end module ClassMethods # Freeze the hash_branches metadata when freezing the app. def freeze opts[:hash_branches].freeze.each_value(&:freeze) super end # Duplicate hash_branches metadata in subclass. def inherited(subclass) super h = subclass.opts[:hash_branches] opts[:hash_branches].each do |namespace, routes| h[namespace] = routes.dup end end # Add branch handler for the given namespace and segment. If called without # a block, removes the existing branch handler if it exists. def hash_branch(namespace='', segment, &block) segment = "/#{segment}" routes = opts[:hash_branches][namespace] ||= {} if block routes[segment] = define_roda_method(routes[segment] || "hash_branch_#{namespace}_#{segment}", 1, &convert_route_block(block)) elsif meth = routes.delete(segment) remove_method(meth) end end end module RequestMethods # Checks the matching hash_branch namespace for a branch matching the next # segment in the remaining path, and dispatch to that block if there is one. def hash_branches(namespace=matched_path) rp = @remaining_path return unless rp.getbyte(0) == 47 # "/" if routes = roda_class.opts[:hash_branches][namespace] if segment_end = rp.index('/', 1) if meth = routes[rp[0, segment_end]] @remaining_path = rp[segment_end, 100000000] always{scope.send(meth, self)} end elsif meth = routes[rp] @remaining_path = '' always{scope.send(meth, self)} end end end end end register_plugin(:hash_branches, HashBranches) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hash_matcher.rb000066400000000000000000000021631516720775400235410ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The hash_matcher plugin adds the hash_matcher class method, which # allows for easily defining hash matchers: # # class App < Roda # plugin :hash_matcher # # hash_matcher(:foo) do |v| # params['foo'] == v # end # # route do # r.on foo: 'bar' do # # matches when param foo has value bar # end # end # end module HashMatcher module ClassMethods # Create a match_#{key} method in the request class using the given # block, so that using a hash key in a request match method will # call the block. The block should return nil or false to not # match, and anything else to match. See the HashMatcher module # documentation for an example. def hash_matcher(key, &block) meth = :"match_#{key}" self::RodaRequest.send(:define_method, meth, &block) self::RodaRequest.send(:private, meth) end end end register_plugin(:hash_matcher, HashMatcher) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hash_paths.rb000066400000000000000000000101461516720775400232350ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The hash_paths plugin allows for O(1) dispatch to multiple routes at any point # in the routing tree. It is useful when you have a large number of specific routes # to dispatch to at any point in the routing tree. # # You configure the hash paths to dispatch to using the +hash_path+ class method, # specifying the remaining path, with a block to handle that path. Then you dispatch # to the configured paths using +r.hash_paths+: # # class App < Roda # plugin :hash_paths # # hash_path("/a") do |r| # # /a path # end # # hash_path("/a/b") do |r| # # /a/b path # end # # route do |r| # r.hash_paths # end # end # # With the above routing tree, the +r.hash_paths+ call will dispatch requests for the +/a+ and # +/a/b+ request paths. # # The +hash_path+ class method supports namespaces, which allows +r.hash_paths+ to be used at # any level of the routing tree. Here is an example that uses namespaces for sub-branches: # # class App < Roda # plugin :hash_paths # # # Two arguments provided, so first argument is the namespace # hash_path("/a", "/b") do |r| # # /a/b path # end # # hash_path("/a", "/c") do |r| # # /a/c path # end # # hash_path(:b, "/b") do |r| # # /b/b path # end # # hash_path(:b, "/c") do |r| # # /b/c path # end # # route do |r| # r.on 'a' do # # No argument given, so uses the already matched path as the namespace, # # which is '/a' in this case. # r.hash_paths # end # # r.on 'b' do # # uses :b as the namespace when looking up routes, as that was explicitly specified # r.hash_paths(:b) # end # end # end # # With the above routing tree, requests for the +/a+ branch will be handled by the first # +r.hash_paths+ call, and requests for the +/b+ branch will be handled by the second # +r.hash_paths+ call. Those will dispatch to the configured hash paths for the +/a+ and # +:b+ namespaces. # # It is best for performance to explicitly specify the namespace when calling # +r.hash_paths+. module HashPaths def self.configure(app) app.opts[:hash_paths] ||= {} end module ClassMethods # Freeze the hash_paths metadata when freezing the app. def freeze opts[:hash_paths].freeze.each_value(&:freeze) super end # Duplicate hash_paths metadata in subclass. def inherited(subclass) super h = subclass.opts[:hash_paths] opts[:hash_paths].each do |namespace, routes| h[namespace] = routes.dup end end # Add path handler for the given namespace and path. When the # r.hash_paths method is called, checks the matching namespace # for the full remaining path, and dispatch to that block if # there is one. If called without a block, removes the existing # path handler if it exists. def hash_path(namespace='', path, &block) routes = opts[:hash_paths][namespace] ||= {} if block routes[path] = define_roda_method(routes[path] || "hash_path_#{namespace}_#{path}", 1, &convert_route_block(block)) elsif meth = routes.delete(path) remove_method(meth) end end end module RequestMethods # Checks the matching hash_path namespace for a branch matching the # remaining path, and dispatch to that block if there is one. def hash_paths(namespace=matched_path) if (routes = roda_class.opts[:hash_paths][namespace]) && (meth = routes[@remaining_path]) @remaining_path = '' always{scope.send(meth, self)} end end end end register_plugin(:hash_paths, HashPaths) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hash_public.rb000066400000000000000000000105521516720775400233750ustar00rootroot00000000000000# frozen-string-literal: true require 'digest/sha2' class Roda module RodaPlugins # The hash_public plugin adds a +hash_path+ method for constructing # content-hash-based paths, and a +r.hash_public+ routing method to serve # static files from a directory (using the public plugin). This plugin is # useful when you want to modify the path to static files when the content # of the file changes, ensuring that requests for the static file will not # be cached. # # Unlike the timestamp_public plugin, which uses file modification times, # hash_public uses a SHA256 digest of the file content. This makes paths # stable across different build environments (e.g. Docker images built in # CI/CD pipelines), where file modification times may vary even when the # file content has not changed. # # Note that while this plugin will not serve files outside of the public # directory, for performance reasons it does not check the path of the file # is inside the public directory when computing the content hash. If the # +hash_path+ method is called with untrusted input, it is possible for an # attacker to read the content hash of any file on the file system. # # This plugin caches the digest of file content on first read. That means # if you change the file after that, it will continue to show the old hash. # This can cause problems in development mode if you are modifying the # content of files served by the plugin. # # Examples: # # # Use public folder as location of files, and static as the path prefix # plugin :hash_public # # # Use /path/to/app/static as location of files, and public as the path prefix # opts[:root] = '/path/to/app' # plugin :hash_public, root: 'static', prefix: 'public' # # # Assuming public is the location of files, and static as the path prefix # route do # # Make GET /static/any-string/images/foo.png look for public/images/foo.png # r.hash_public # # r.get "example" do # # "/static/sha256-url-safe-base64-encoded-file-digest-/images/foo.png" # hash_path("images/foo.png") # end # end module HashPublic # Use options given to setup content-hash-based file serving. The # following options are recognized by the plugin: # # :prefix :: The prefix for paths, before the hash segment # :length :: The number of characters of the digest to use in paths # (default: full 43-character SHA256 URL safe base64 digest) # # The options given are also passed to the public plugin. def self.configure(app, opts = {}) app.plugin :public, opts app.opts[:hash_public_prefix] = (opts[:prefix] || app.opts[:hash_public_prefix] || 'static').dup.freeze app.opts[:hash_public_length] = opts[:length] || app.opts[:hash_public_length] app.opts[:hash_public_mutex] ||= Mutex.new app.opts[:hash_public_cache] ||= {} end module InstanceMethods # Return a path to the static file that could be served by r.hash_public. # This does not check the file is inside the directory for performance # reasons, so this should not be called with untrusted input. def hash_path(file) opts = self.opts cache = opts[:hash_public_cache] mutex = opts[:hash_public_mutex] unless digest = mutex.synchronize{cache[file]} digest = ::Digest::SHA256.file(File.join(opts[:public_root], file)).base64digest digest.chomp!("=") digest.tr!("+/", "-_") if length = opts[:hash_public_length] digest = digest[0, length] end digest.freeze mutex.synchronize{cache[file] = digest} end "/#{opts[:hash_public_prefix]}/#{digest}/#{file}" end end module RequestMethods # Serve files from the public directory if the file exists, # it includes the hash_public prefix segment followed by # a string segment for the content hash, and this is a GET request. def hash_public if is_get? on roda_class.opts[:hash_public_prefix], String do |_| public end end end end end register_plugin(:hash_public, HashPublic) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hash_routes.rb000066400000000000000000000247431516720775400234470ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The hash_routes plugin builds on top of the hash_branches and hash_paths plugins, and adds # a DSL for configuring hash branches and paths. It also adds an +r.hash_routes+ method for # first attempting dispatch to the configured hash_paths, then to the configured hash_branches: # # class App < Roda # plugin :hash_routes # # hash_branch("a") do |r| # # /a branch # end # # hash_branch("b") do |r| # # /b branch # end # # hash_path("/a") do |r| # # /a path # end # # hash_path("/a/b") do |r| # # /a/b path # end # # route do |r| # r.hash_routes # end # end # # With the above routing tree, requests for +/a+ and +/a/b+ will be routed to the appropriate # +hash_path+ block. Other requests for the +/a+ branch, and all requests for the +/b+ # branch will be routed to the appropriate +hash_branch+ block. # # It is best for performance to explicitly specify the namespace when calling # +r.hash_routes+. # # Because specifying routes explicitly using the +hash_branch+ and +hash_path+ # class methods can get repetitive, the hash_routes plugin offers a DSL for DRYing # the code up. This DSL is used by calling the +hash_routes+ class method. The # DSL used tries to mirror the standard Roda DSL, but it is not a normal routing # tree (it's not possible to execute arbitrary code between branches during routing). # # class App < Roda # plugin :hash_routes # # # No block argument is used, DSL evaluates block using instance_exec # hash_routes "" do # # on method is used for routing to next segment, # # for similarity to standard Roda # on "a" do |r| # r.hash_routes '/a' # end # # on "b" do |r| # r.hash_routes(:b) # end # end # # # Block argument is used, block is yielded DSL instance # hash_routes "/a" do |hr| # # is method is used for routing to the remaining path, # # for similarity to standard Roda # hr.is "b" do |r| # # /a/b path # end # # hr.is "c" do |r| # # /a/c path # end # end # # hash_routes :b do # is "b" do |r| # # /b/b path # end # # is "c" do |r| # # /b/c path # end # end # # route do |r| # # No change here, DSL only makes setup DRYer # r.hash_branches # end # end # # The +hash_routes+ DSL also offers some additional features to handle additional # cases. It supports verb methods, such as +get+ and +post+, which operate like # +is+, but are only called if the verb matches (and are not yielded the request). # It supports a +view+ method for routes that only render views, as well as a # +views+ method for setting up routes for multiple views in a single call, which # is a good replacement for the +multi_view+ plugin. # +is+, +view+, and the verb methods can use a value of +true+ for the empty # remaining path (as the empty string specifies the "/" remaining path). # It also supports a +dispatch_from+ method, allowing you to setup dispatching to # current group of routes from a higher-level namespace. # The +hash_routes+ class method will return the DSL instance, so you are not # limited to using it with a block. # # Here's the above example modified to use some of these features: # # class App < Roda # plugin :hash_routes # # hash_routes "/a" do # # Dispatch requests for the /a branch from the empty (default) routing # # namespace to this namespace # dispatch_from "a" # # # Handle GET /a path, render "a" template, returning 404 for non-GET requests # view true, "a" # # # Handle /a/b path, returning 404 for non-GET requests # get "b" do # # GET /a/b path # end # # # Handle /a/c path, returning 404 for non-POST requests # post "c" do # # POST /a/c path # end # end # # bhr = hash_routes(:b) # # # Dispatch requests for the /b branch from the empty routing to this namespace, # # but first check routes in the :b_preauth namespace. If there is no # # matching route in the :b_preauth namespace, call the check_authenticated! # # method before dispatching to any of the routes in this namespace # bhr.dispatch_from "", "b" do |r| # r.hash_routes :b_preauth # check_authenticated! # end # # bhr.is true do |r| # # /b path # end # # bhr.is "" do |r| # # /b/ path # end # # # GET /b/d path, render 'd2' template, returning 404 for non-GET requests # bhr.views 'd', 'd2' # # # GET /b/e path, render 'e' template, returning 404 for non-GET requests # # GET /b/f path, render 'f' template, returning 404 for non-GET requests # bhr.views %w'e f' # # route do |r| # r.hash_branches # end # end # # The +view+ and +views+ method depend on the render plugin being loaded, but this # plugin does not load the render plugin. You must load the render plugin separately # if you want to use the +view+ and +views+ methods. # # Certain parts of the +hash_routes+ DSL support do not work with the # route_block_args plugin, as doing so would reduce performance. These are: # # * dispatch_from # * view # * views # * all verb methods (get, post, etc.) module HashRoutes def self.load_dependencies(app) app.plugin :hash_branches app.plugin :hash_paths end def self.configure(app) app.opts[:hash_routes_methods] ||= {} end # Internal class handling the internals of the +hash_routes+ class method blocks. class DSL def initialize(roda, namespace) @roda = roda @namespace = namespace end # Setup the given branch in the given namespace to dispatch to routes in this # namespace. If a block is given, call the block with the request before # dispatching to routes in this namespace. def dispatch_from(namespace='', branch, &block) ns = @namespace if block meth_hash = @roda.opts[:hash_routes_methods] key = [:dispatch_from, namespace, branch].freeze meth = meth_hash[key] = @roda.define_roda_method(meth_hash[key] || "hash_routes_dispatch_from_#{namespace}_#{branch}", 1, &block) @roda.hash_branch(namespace, branch) do |r| send(meth, r) r.hash_routes(ns) end else @roda.hash_branch(namespace, branch) do |r| r.hash_routes(ns) end end end # Use the segment to setup a branch in the current namespace. def on(segment, &block) @roda.hash_branch(@namespace, segment, &block) end # Use the segment to setup a path in the current namespace. # If path is given as a string, it is prefixed with a slash. # If path is +true+, the empty string is used as the path. def is(path, &block) path = path == true ? "" : "/#{path}" @roda.hash_path(@namespace, path, &block) end # Use the segment to setup a path in the current namespace that # will render the view with the given name if the GET method is # used, and will return a 404 if another request method is used. # If path is given as a string, it is prefixed with a slash. # If path is +true+, the empty string is used as the path. def view(path, template) path = path == true ? "" : "/#{path}" @roda.hash_path(@namespace, path) do |r| r.get do view(template) end end end # For each template in the array of templates, setup a path in # the current namespace for the template using the same name # as the template. def views(templates) templates.each do |template| view(template, template) end end [:get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth| define_method(meth) do |path, &block| verb(meth, path, &block) end end private # Setup a path in the current namespace for the given request method verb. # Returns 404 for requests for the path with a different request method. def verb(verb, path, &block) path = path == true ? "" : "/#{path}" meth_hash = @roda.opts[:hash_routes_methods] key = [@namespace, path].freeze meth = meth_hash[key] = @roda.define_roda_method(meth_hash[key] || "hash_routes_#{@namespace}_#{path}", 0, &block) @roda.hash_path(@namespace, path) do |r| r.send(verb) do send(meth) end end end end module ClassMethods # Freeze the hash_routes metadata when freezing the app. def freeze opts[:hash_routes_methods].freeze super end # Invoke the DSL for configuring hash routes, see DSL for methods inside the # block. If the block accepts an argument, yield the DSL instance. If the # block does not accept an argument, instance_exec the block in the context # of the DSL instance. def hash_routes(namespace='', &block) dsl = DSL.new(self, namespace) if block if block.arity == 1 yield dsl else dsl.instance_exec(&block) end end dsl end end module RequestMethods # Check for matches in both the hash_path and hash_branch namespaces for # a matching remaining path or next segment in the remaining path, respectively. def hash_routes(namespace=matched_path) hash_paths(namespace) hash_branches(namespace) end end end register_plugin(:hash_routes, HashRoutes) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/head.rb000066400000000000000000000053371516720775400220220ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The head plugin attempts to automatically handle HEAD requests, # by treating them as GET requests and returning an empty body # without modifying the response status or response headers. # # So for the following routes, # # route do |r| # r.root do # 'root' # end # # r.get 'a' do # 'a' # end # # r.is 'b', method: [:get, :post] do # 'b' # end # end # # HEAD requests for +/+, +/a+, and +/b+ will all return 200 status # with an empty body. # # This plugin also works with the not_allowed plugin if it is loaded # after the not_allowed plugin. In that case, if GET is one of Allow # header options, then HEAD will be as well. # # NOTE: if you have a public facing website it is recommended that # you enable this plugin, or manually handle HEAD anywhere you would # handle GET. Search engines and other bots may send a # HEAD request prior to crawling a page with a GET request. Without # this plugin those HEAD requests will return a 404 status, which # may prevent search engines from crawling your website. module Head # used to ensure proper resource release on HEAD requests # we do not respond to a to_path method, here. class CloseLater def initialize(body) @body = body end # yield nothing def each(&_) end # this should be called by the Rack server def close @body.close end end module InstanceMethods private # Always use an empty response body for head requests, with a # content length of 0. def _roda_after_30__head(res) if res && @_request.head? body = res[2] if body.respond_to?(:close) res[2] = CloseLater.new(body) else res[2] = EMPTY_ARRAY end end end end module RequestMethods # Consider HEAD requests as GET requests. def is_get? super || head? end private # If the current request is a HEAD request, match if one of # the given methods is a GET request. def match_method(method) super || (!method.is_a?(Array) && head? && method.to_s.upcase == 'GET') end # Work with the not_allowed plugin so that if GET is one # of the Allow header options, then HEAD is as well. def method_not_allowed(verbs) verbs = verbs.sub('GET', 'HEAD, GET') super end end end register_plugin(:head, Head) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/header_matchers.rb000066400000000000000000000053371516720775400242370ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The header_matchers plugin adds hash matchers for matching on less-common # HTTP headers. # # plugin :header_matchers # # It adds a +:header+ matcher for matching on arbitrary headers, which matches # if the header is present, and yields the header value: # # r.on header: 'HTTP-X-App-Token' do |header_value| # # Looks for env['HTTP_X_APP_TOKEN'] and yields it # end # # It adds a +:host+ matcher for matching by the host of the request: # # r.on host: 'foo.example.com' do # end # # For regexp values of the +:host+ matcher, any captures are yielded to the block: # # r.on host: /\A(\w+).example.com\z/ do |subdomain| # end # # It adds a +:user_agent+ matcher for matching on a user agent patterns, which # yields the regexp captures to the block: # # r.on user_agent: /Chrome\/([.\d]+)/ do |chrome_version| # end # # It adds an +:accept+ matcher for matching based on the Accept header: # # r.on accept: 'text/csv' do # end # # Note that the +:accept+ matcher is very simple and cannot handle wildcards, # priorities, or anything but a simple comma separated list of mime types. module HeaderMatchers module RequestMethods private # Match if the given mimetype is one of the accepted mimetypes. def match_accept(mimetype) if @env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype} response[RodaResponseHeaders::CONTENT_TYPE] = mimetype end end # Match if the given uppercase key is present inside the environment. def match_header(key) key = key.upcase key.tr!("-","_") unless key == "CONTENT_TYPE" || key == "CONTENT_LENGTH" key = "HTTP_#{key}" end if v = @env[key] @captures << v end end # Match if the host of the request is the same as the hostname. +hostname+ # can be a regexp or a string. def match_host(hostname) if hostname.is_a?(Regexp) if match = hostname.match(host) @captures.concat(match.captures) end else hostname === host end end # Match the submitted user agent to the given pattern, capturing any # regexp match groups. def match_user_agent(pattern) if (user_agent = @env["HTTP_USER_AGENT"]) && (match = pattern.match(user_agent)) @captures.concat(match.captures) end end end end register_plugin(:header_matchers, HeaderMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/heartbeat.rb000066400000000000000000000023141516720775400230500ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The heartbeat handles heartbeat/status requests. If a request for # the heartbeat path comes in, a 200 response with a # text/plain Content-Type and a body of "OK" will be returned. # The default heartbeat path is "/heartbeat", so to use that: # # plugin :heartbeat # # You can also specify a custom heartbeat path: # # plugin :heartbeat, path: '/status' module Heartbeat # Set the heartbeat path to the given path. def self.configure(app, opts=OPTS) app.opts[:heartbeat_path] = (opts[:path] || app.opts[:heartbeat_path] || "/heartbeat").dup.freeze end module InstanceMethods private # If the request is for a heartbeat path, return the heartbeat response. def _roda_before_20__heartbeat if env['PATH_INFO'] == opts[:heartbeat_path] response = @_response response.status = 200 response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain' response.write 'OK' throw :halt, response.finish end end end end register_plugin(:heartbeat, Heartbeat) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hmac_paths.rb000066400000000000000000000434071516720775400232300ustar00rootroot00000000000000# frozen-string-literal: true require 'openssl' # class Roda module RodaPlugins # The hmac_paths plugin allows protection of paths using an HMAC. This can be used # to prevent users enumerating paths, since only paths with valid HMACs will be # respected. # # To use the plugin, you must provide a +secret+ option. This sets the secret for # the HMACs. Make sure to keep this value secret, as this plugin does not provide # protection against users who know the secret value. The secret must be at least # 32 bytes. # # plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes' # # To generate a valid HMAC path, you call the +hmac_path+ method: # # hmac_path('/widget/1') # # => "/0c2feaefdfc80cc73da19b060c713d4193c57022815238c6657ce2d99b5925eb/0/widget/1" # # The first segment in the returned path is the HMAC. The second segment is flags for # the type of paths (see below), and the rest of the path is as given. # # To protect a path or any subsection in the routing tree, you wrap the related code # in an +r.hmac_path+ block. # # route do |r| # r.hmac_path do # r.get 'widget', Integer do |widget_id| # # ... # end # end # end # # If first segment of the remaining path contains a valid HMAC for the rest of the path (considering # the flags), then +r.hmac_path+ will match and yield to the block, and routing continues inside # the block with the HMAC and flags segments removed. # # In the above example, if you provide a user a link for widget with ID 1, there is no way # for them to guess the valid path for the widget with ID 2, preventing a user from # enumerating widgets, without relying on custom access control. Users can only access # paths that have been generated by the application and provided to them, either directly # or indirectly. # # In the above example, +r.hmac_path+ is used at the root of the routing tree. If you # would like to call it below the root of the routing tree, it works correctly, but you # must pass +hmac_path+ the +:root+ option specifying where +r.hmac_paths+ will be called from. # Consider this example: # # route do |r| # r.on 'widget' do # r.hmac_path do # r.get Integer do |widget_id| # # ... # end # end # end # # r.on 'foobar' do # r.hmac_path do # r.get Integer do |foobar_id| # # ... # end # end # end # end # # For security reasons, the hmac_path plugin does not allow an HMAC path designed for # widgets to be a valid match in the +r.hmac_path+ call inside the r.on 'foobar' # block, preventing users who have a valid HMAC for a widget from looking at the page for # a foobar with the same ID. When generating HMAC paths where the matching +r.hmac_path+ # call is not at the root of the routing tree, you must pass the +:root+ option: # # hmac_path('/1', root: '/widget') # # => "/widget/daccafce3ce0df52e5ce774626779eaa7286085fcbde1e4681c74175ff0bbacd/0/1" # # hmac_path('/1', root: '/foobar') # # => "/foobar/c5fdaf482771d4f9f38cc13a1b2832929026a4ceb05e98ed6a0cd5a00bf180b7/0/1" # # Note how the HMAC changes even though the path is the same. # # In addition to the +:root+ option, there are additional options that further constrain # use of the generated paths. # # The +:method+ option creates a path that can only be called with a certain request # method: # # hmac_path('/widget/1', method: :get) # # => "/d38c1e634ecf9a3c0ab9d0832555b035d91b35069efcbf2670b0dfefd4b62fdd/m/widget/1" # # Note how this results in a different HMAC than the original hmac_path('/widget/1') # call. This sets the flags segment to +m+, which means +r.hmac_path+ will consider the # request mehod when checking the HMAC, and will only match if the provided request method # is GET. This allows you to provide a user the ability to submit a GET request for the # underlying path, without providing them the ability to submit a POST request for the # underlying path, with no other access control. # # The +:params+ option accepts a hash of params, converts it into a query string, and # includes the query string in the returned path. It sets the flags segment to +p+, which # means +r.hmac_path+ will check for that exact query string. Requests with an empty query # string or a different string will not match. # # hmac_path('/widget/1', params: {foo: 'bar'}) # # => "/fe8d03f9572d5af6c2866295bd3c12c2ea11d290b1cbd016c3b68ee36a678139/p/widget/1?foo=bar" # # For GET requests, which cannot have request bodies, that is sufficient to ensure that the # submitted params are exactly as specified. However, POST requests can have request bodies, # and request body params override query string params in +r.params+. So if you are using # this for POST requests (or other HTTP verbs that can have request bodies), use +r.GET+ # instead of +r.params+ to specifically check query string parameters. # # The generated paths can be timestamped, so that they are only valid until a given time # or for a given number of seconds after they are generated, using the :until or :seconds # options: # # hmac_path('/widget/1', until: Time.utc(2100)) # # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1" # # hmac_path('/widget/1', seconds: Time.utc(2100).to_i - Time.now.to_i) # # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1" # # The :namespace option, if provided, should be a string, and it modifies the generated HMACs # to only match those in the same namespace. This can be used to provide different paths to # different users or groups of users. # # hmac_path('/widget/1', namespace: '1') # # => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1" # # hmac_path('/widget/1', namespace: '2') # # => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1" # # The +r.hmac_path+ method accepts a :namespace option, and if a :namespace option is # provided, it will only match an hmac path if the namespace given matches the one used # when the hmac path was created. # # r.hmac_path(namespace: '1'){} # # will match "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1" # # will not match "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1" # # The most common use of the :namespace option is to reference session values, so the value of # each path depends on the logged in user. You can use the +:namespace_session_key+ plugin # option to set the default namespace for both +hmac_path+ and +r.hmac_path+: # # plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes', # namespace_session_key: 'account_id' # # This will use session['account_id'] as the default namespace for both +hmac_path+ # and +r.hmac_path+ (if the session value is not nil, it is converted to a string using +to_s+). # You can override the default namespace by passing a +:namespace+ option when calling +hmac_path+ # and +r.hmac_path+. # # You can use +:root+, +:method+, +:params+, and +:namespace+ at the same time: # # hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'}, namespace: '1') # # => "/widget/c14c78a81d34d766cf334a3ddbb7a6b231bc2092ef50a77ded0028586027b14e/mpn/1?foo=bar" # # This gives you a path only valid for a GET request with a root of /widget and # a query string of foo=bar, using namespace +1+. # # To handle secret rotation, you can provide an +:old_secret+ option when loading the # plugin. # # plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes', # old_secret: 'previous-secret-value-with-at-least-32-bytes' # # This will use +:secret+ for constructing new paths, but will respect paths generated by # +:old_secret+. # # = HMAC Construction # # This describes the internals for how HMACs are constructed based on the options provided # to +hmac_path+. In the examples below: # # * +HMAC+ is the raw HMAC-SHA256 output (first argument is secret, second is data) # * +HMAC_hex+ is the hexidecimal version of +HMAC+ # * +secret+ is the plugin :secret option # # The +:secret+ plugin option is never used directly as the HMAC secret. All HMACs are # generated with a root-specific secret. The root will be the empty if no +:root+ option # is given. The hmac path flags are always included in the hmac calculation, prepended to the # path: # # r.hmac_path('/1') # HMAC_hex(HMAC_hex(secret, ''), '/0/1') # # r.hmac_path('/1', root: '/2') # HMAC_hex(HMAC_hex(secret, '/2'), '/0/1') # # The +:method+ option uses an uppercase version of the method prepended to the path. This # cannot conflict with the path itself, since paths must start with a slash. # # r.hmac_path('/1', method: :get) # HMAC_hex(HMAC_hex(secret, ''), 'GET:/m/1') # # The +:params+ option includes the query string for the params in the HMAC: # # r.hmac_path('/1', params: {k: 2}) # HMAC_hex(HMAC_hex(secret, ''), '/p/1?k=2') # # The +:until+ and +:seconds+ option include the timestamp in the HMAC: # # r.hmac_path('/1', until: Time.utc(2100)) # HMAC_hex(HMAC_hex(secret, ''), '/t/4102444800/1') # # If a +:namespace+ option is provided, the original secret used before the +:root+ option is # an HMAC of the +:secret+ plugin option and the given namespace. # # r.hmac_path('/1', namespace: '2') # HMAC_hex(HMAC_hex(HMAC(secret, '2'), ''), '/n/1') module HmacPaths def self.configure(app, opts=OPTS) hmac_secret = opts[:secret] unless hmac_secret.is_a?(String) && hmac_secret.bytesize >= 32 raise RodaError, "hmac_paths plugin :secret option must be a string containing at least 32 bytes" end if hmac_old_secret = opts[:old_secret] unless hmac_old_secret.is_a?(String) && hmac_old_secret.bytesize >= 32 raise RodaError, "hmac_paths plugin :old_secret option must be a string containing at least 32 bytes if present" end end app.opts[:hmac_paths_secret] = hmac_secret app.opts[:hmac_paths_old_secret] = hmac_old_secret if opts[:namespace_session_key] app.opts[:hmac_paths_namespace_session_key] = opts[:namespace_session_key] end end module InstanceMethods # Return a path with an HMAC. Designed to be used with r.hmac_path, to make sure # users can only request paths that they have been provided by the application # (directly or indirectly). This can prevent users of a site from enumerating # valid paths. The given path should be a string starting with +/+. Options: # # :method :: Limits the returned path to only be valid for the given request method. # :namespace :: Make the HMAC value depend on the given namespace. If this is not # provided, the default namespace is used. To explicitly not use a # namespace when there is a default namespace, pass a nil value. # :params :: Includes parameters in the query string of the returned path, and # limits the returned path to only be valid for that exact query string. # :root :: Should be an empty string or string starting with +/+. This will be # the already matched path of the routing tree using r.hmac_path. Defaults # to the empty string, which will returns paths valid for r.hmac_path at # the top level of the routing tree. # :seconds :: Make the given path valid for the given integer number of seconds. # :until :: Make the given path valid until the given Time. def hmac_path(path, opts=OPTS) unless path.is_a?(String) && path.getbyte(0) == 47 raise RodaError, "path must be a string starting with /" end root = opts[:root] || '' unless root.is_a?(String) && ((root_byte = root.getbyte(0)) == 47 || root_byte == nil) raise RodaError, "root must be empty string or string starting with /" end if valid_until = opts[:until] valid_until = valid_until.to_i elsif seconds = opts[:seconds] valid_until = Time.now.to_i + seconds end flags = String.new path = path.dup if method = opts[:method] flags << 'm' end if params = opts[:params] flags << 'p' path << '?' << Rack::Utils.build_query(params) end if hmac_path_namespace(opts) flags << 'n' end if valid_until flags << 't' path = "/#{valid_until}#{path}" end flags << '0' if flags.empty? hmac_path = if method "#{method.to_s.upcase}:/#{flags}#{path}" else "/#{flags}#{path}" end "#{root}/#{hmac_path_hmac(root, hmac_path, opts)}/#{flags}#{path}" end # The HMAC to use in hmac_path, for the given root, path, and options. def hmac_path_hmac(root, path, opts=OPTS) OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, hmac_path_hmac_secret(root, opts), path) end # The namespace to use for the hmac path. If a :namespace option is not # provided, and a :namespace_session_key option was provided, this will # use the value of the related session key, if present. def hmac_path_namespace(opts=OPTS) opts.fetch(:namespace){hmac_path_default_namespace} end private # The secret used to calculate the HMAC in hmac_path. This is itself an HMAC, created # using the secret given in the plugin, for the given root and options. # This always returns a hexidecimal string. def hmac_path_hmac_secret(root, opts=OPTS) secret = opts[:secret] || self.opts[:hmac_paths_secret] if namespace = hmac_path_namespace(opts) secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, namespace) end OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, root) end # The default namespace to use for hmac_path, if a :namespace option is not provided. def hmac_path_default_namespace if (key = opts[:hmac_paths_namespace_session_key]) && (value = session[key]) value.to_s end end end module RequestMethods # Looks at the first segment of the remaining path, and if it contains a valid HMAC for the # rest of the path considering the flags in the second segment and the given options, the # block matches and is yielded to, and the result of the block is returned. Otherwise, the # block does not matches and routing continues after the call. def hmac_path(opts=OPTS, &block) orig_path = remaining_path mpath = matched_path on String do |submitted_hmac| rpath = remaining_path if submitted_hmac.bytesize == 64 on String do |flags| if flags.bytesize >= 1 if flags.include?('n') ^ !scope.hmac_path_namespace(opts).nil? # Namespace required and not provided, or provided and not required. # Bail early to avoid unnecessary HMAC calculation. @remaining_path = orig_path return end if flags.include?('m') rpath = "#{env['REQUEST_METHOD'].to_s.upcase}:#{rpath}" end if flags.include?('p') rpath = "#{rpath}?#{env["QUERY_STRING"]}" end if hmac_path_valid?(mpath, rpath, submitted_hmac, opts) if flags.include?('t') on Integer do |int| if int >= Time.now.to_i always(&block) else # Return from method without matching @remaining_path = orig_path return end end else always(&block) end end end # Return from method without matching @remaining_path = orig_path return end end # Return from method without matching @remaining_path = orig_path return end end private # Determine whether the provided hmac matches. def hmac_path_valid?(root, path, hmac, opts=OPTS) if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac) true elsif old_secret = roda_class.opts[:hmac_paths_old_secret] opts = opts.dup opts[:secret] = old_secret Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac) else false end end end end register_plugin(:hmac_paths, HmacPaths) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hooks.rb000066400000000000000000000055031516720775400222370ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The hooks plugin adds before and after hooks to the request cycle. # # plugin :hooks # # before do # request.redirect('/login') unless logged_in? # @time = Time.now # end # # after do |res| # logger.notice("Took #{Time.now - @time} seconds") # end # # Note that in general, before hooks are not needed, since you can just # run code at the top of the route block: # # route do |r| # r.redirect('/login') unless logged_in? # # ... # end # # However, this code makes it easier to write after hooks, as well as # handle cases where before hooks are added after the route block. # # Note that the after hook is called with the rack response array # of status, headers, and body. If it wants to change the response, # it must mutate this argument, calling response.status= inside # an after block will not affect the returned status. Note that after # hooks can be called with nil if an exception is raised during routing. module Hooks def self.configure(app) app.opts[:after_hooks] ||= [] app.opts[:before_hooks] ||= [] end module ClassMethods # Freeze the array of hook methods when freezing the app. def freeze opts[:after_hooks].freeze opts[:before_hooks].freeze super end # Add an after hook. def after(&block) opts[:after_hooks] << define_roda_method("after_hook", 1, &block) if opts[:after_hooks].length == 1 class_eval("alias _roda_after_80__hooks #{opts[:after_hooks].first}", __FILE__, __LINE__) else class_eval("def _roda_after_80__hooks(res) #{opts[:after_hooks].map{|m| "#{m}(res)"}.join(';')} end", __FILE__, __LINE__) end private :_roda_after_80__hooks def_roda_after nil end # Add a before hook. def before(&block) opts[:before_hooks].unshift(define_roda_method("before_hook", 0, &block)) if opts[:before_hooks].length == 1 class_eval("alias _roda_before_10__hooks #{opts[:before_hooks].first}", __FILE__, __LINE__) else class_eval("def _roda_before_10__hooks; #{opts[:before_hooks].join(';')} end", __FILE__, __LINE__) end private :_roda_before_10__hooks def_roda_before nil end end module InstanceMethods private # Default method if no after hooks are defined. def _roda_after_80__hooks(res) end # Default method if no before hooks are defined. def _roda_before_10__hooks end end end register_plugin(:hooks, Hooks) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/host_authorization.rb000066400000000000000000000137461516720775400250610ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The host_authorization plugin allows configuring an authorized host or # an array of authorized hosts. Then in the routing tree, you can check # whether the request uses an authorized host via the +check_host_authorization!+ # method. # # If the request doesn't match one of the authorized hosts, the # request processing stops at that point. Using this plugin can prevent # DNS rebinding attacks if the application can receive requests for # arbitrary hosts. # # By default, an empty response using status 403 will be returned for requests # with unauthorized hosts. # # Because +check_host_authorization!+ is an instance method, you can easily choose # to only check for authorization in certain routes, or to check it after # other processing. For example, you could check for authorized hosts after # serving static files, since the serving of static files should not be # vulnerable to DNS rebinding attacks. # # = Usage # # In your routing tree, call the +check_host_authorization!+ method at the point you # want to check for authorized hosts: # # plugin :host_authorization, 'www.example.com' # plugin :public # # route do |r| # r.public # check_host_authorization! # # # ... # end # # = Specifying authorized hosts # # For applications hosted on a single domain name, you can use a single string: # # plugin :host_authorization, 'www.example.com' # # For applications hosted on multiple domain names, you can use an array of strings: # # plugin :host_authorization, %w'www.example.com www.example2.com' # # For applications supporting arbitrary subdomains, you can use a regexp. If using # a regexp, make sure you use \A and \z in your regexp, and restrict # the allowed characters to the minimum required, otherwise you can potentionally # introduce a security issue: # # plugin :host_authorization, /\A[-0-9a-f]+\.example\.com\z/ # # For applications with more complex requirements, you can use a proc. Similarly # to the regexp case, the proc should be aware the host contains user-submitted # values, and not assume it is in any particular format: # # plugin :host_authorization, proc{|host| ExternalService.allowed_host?(host)} # # If an array of values is passed as the host argument, the host is authorized if # it matches any value in the array. All host authorization checks use the # === method, which is why it works for strings, regexps, and procs. # It can also work with arbitrary objects that support ===. # # For security reasons, only the +Host+ header is checked by default. If you are # sure that your application is being run behind a forwarding proxy that sets the # X-Forwarded-Host header, you should enable support for checking that # header using the +:check_forwarded+ option: # # plugin :host_authorization, 'www.example.com', check_forwarded: true # # In this case, the trailing host in the X-Forwarded-Host header is checked, # which should be the host set by the forwarding proxy closest to the application. # In cases where multiple forwarding proxies are used that append to the # X-Forwarded-Host header, you should not use this plugin. # # = Customizing behavior # # By default, an unauthorized host will receive an empty 403 response. You can # customize this by passing a block when loading the plugin. For example, for # sites using the render plugin, you could return a page that uses your default # layout: # # plugin :render # plugin :host_authorization, 'www.example.com' do |r| # response.status = 403 # view(:content=>"

Forbidden

") # end # # The block passed to this plugin is treated as a match block. module HostAuthorization def self.configure(app, host, opts=OPTS, &block) app.opts[:host_authorization_host] = host app.opts[:host_authorization_check_forwarded] = opts[:check_forwarded] if opts.key?(:check_forwarded) if block app.define_roda_method(:host_authorization_unauthorized, 1, &block) end end module InstanceMethods # Check whether the host is authorized. If not authorized, return a response # immediately based on the plugin block. def check_host_authorization! r = @_request return if host_authorized?(_convert_host_for_authorization(r.env["HTTP_HOST"].to_s.dup)) if opts[:host_authorization_check_forwarded] && (host = r.env["HTTP_X_FORWARDED_HOST"]) if i = host.rindex(',') host = host[i+1, 10000000].to_s end host = _convert_host_for_authorization(host.strip) if !host.empty? && host_authorized?(host) return end end r.on do host_authorization_unauthorized(r) end end private # Remove the port information from the passed string (mutates the passed argument). def _convert_host_for_authorization(host) host.sub!(/:\d+\z/, "") host end # Whether the host given is one of the authorized hosts for this application. def host_authorized?(host, authorized_host = opts[:host_authorization_host]) case authorized_host when Array authorized_host.any?{|auth_host| host_authorized?(host, auth_host)} else authorized_host === host end end # Action to take for unauthorized hosts. Sets a 403 status by default. def host_authorization_unauthorized(_) @_response.status = 403 nil end end end register_plugin(:host_authorization, HostAuthorization) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/host_routing.rb000066400000000000000000000146621516720775400236460ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The host_routing plugin adds support for more routing requests based on # the requested host. It also adds predicate methods for checking # whether a request was requested with the given host. # # When loading the plugin, you pass a block, which is used for configuring # the plugin. For example, if you want to treat requests to api.example.com # or api2.example.com as api requests, and treat other requests as www # requests, you could use: # # plugin :host_routing do |hosts| # hosts.to :api, "api.example.com", "api2.example.com" # hosts.default :www # end # # With this configuration, in your routing tree, you can call the +r.api+ and # +r.www+ methods for dispatching to routing blocks only for those types of # requests: # # route do |r| # r.api do # # requests to api.example.com or api2.example.com # end # # r.www do # # requests to other domains # end # end # # In addition to the routing methods, predicate methods are also added to the # request object: # # route do |r| # "#{r.api?}-#{r.www?}" # end # # Requests to api.example.com or api2.example.com return "true-false" # # Other requests return "false-true" # # If the +:scope_predicates+ plugin option is given, predicate methods are also # created in route block scope: # # plugin :host_routing, scope_predicates: true do |hosts| # hosts.to :api, "api.example.com" # hosts.default :www # end # # route do |r| # "#{api?}-#{www?}" # end # # To handle hosts that match a certain format (such as all subdomains), # where the specific host names are not known up front, you can provide a block # when calling +hosts.default+. This block is passed the host name, or an empty # string if no host name is provided, and is evaluated in route block scope. # When using this support, you should also call +hosts.register+ # to register host types that could be returned by the block. For example, to # handle api subdomains differently: # # plugin :host_routing do |hosts| # hosts.to :api, "api.example.com" # hosts.register :api_sub # hosts.default :www do |host| # :api_sub if host.end_with?(".api.example.com") # end # end # # This plugin uses the host method on the request to get the hostname (this method # is defined by Rack). module HostRouting # Setup the host routing support. The block yields an object used to # configure the plugin. Options: # # :scope_predicates :: Setup predicate methods in route block scope # in addition to request scope. def self.configure(app, opts=OPTS, &block) hosts, host_hash, default_block, default_host = DSL.new.process(&block) app.opts[:host_routing_hash] = host_hash app.opts[:host_routing_default_host] = default_host app.send(:define_method, :_host_routing_default, &default_block) if default_block app::RodaRequest.class_exec do hosts.each do |host| host_sym = host.to_sym define_method(host_sym){|&blk| always(&blk) if _host_routing_host == host} alias_method host_sym, host_sym meth = :"#{host}?" define_method(meth){_host_routing_host == host} alias_method meth, meth end end if opts[:scope_predicates] app.class_exec do hosts.each do |host| meth = :"#{host}?" define_method(meth){@_request.send(meth)} alias_method meth, meth end end end end class DSL def initialize @hosts = [] @host_hash = {} end # Run the DSL for the given block. def process(&block) instance_exec(self, &block) if !@default_host raise RodaError, "must call default method inside host_routing plugin block to set default host" end @hosts.concat(@host_hash.values) @hosts << @default_host @hosts.uniq! [@hosts.freeze, @host_hash.freeze, @default_block, @default_host].freeze end # Register hosts that can be returned. This is only needed if # calling register with a block, where the block can return # a value that doesn't match a host given to +to+ or +default+. def register(*hosts) @hosts = hosts end # Treat all given hostnames as routing to the give host. def to(host, *hostnames) hostnames.each do |hostname| @host_hash[hostname] = host end end # Register the default hostname. If a block is provided, it is # called with the host if there is no match for one of the hostnames # provided to +to+. If the block returns nil/false, the hostname # given to this method is used. def default(hostname, &block) @default_host = hostname @default_block = block end end private_constant :DSL module InstanceMethods # Handle case where plugin is used without providing a block to # +hosts.default+. This returns nil, ensuring that the hostname # provided to +hosts.default+ will be used. def _host_routing_default(_) nil end end module RequestMethods private # Cache the host to use in the host routing support, so the processing # is only done once per request. def _host_routing_host @_host_routing_host ||= _get_host_routing_host end # Determine the host to use for the host routing support. Tries the # following, in order: # # * An exact match for a hostname given in +hosts.to+ # * The return value of the +hosts.default+ block, if given # * The default value provided in the +hosts.default+ call def _get_host_routing_host host = self.host || "" roda_class.opts[:host_routing_hash][host] || scope._host_routing_default(host) || roda_class.opts[:host_routing_default_host] end end end register_plugin(:host_routing, HostRouting) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/hsts.rb000066400000000000000000000026151516720775400220760ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The hsts plugin allows for easily configuring an appropriate # Strict-Transport-Security response header for the application: # # plugin :hsts # # Strict-Transport-Security: max-age=63072000; includeSubDomains # # plugin :hsts, preload: true # # Strict-Transport-Security: max-age=63072000; includeSubDomains; preload # # plugin :hsts, max_age: 31536000, subdomains: false # # Strict-Transport-Security: max-age=31536000 module Hsts # Ensure default_headers plugin is loaded first def self.load_dependencies(app, opts=OPTS) app.plugin :default_headers end # Configure the Strict-Transport-Security header. Options: # :max_age :: Set max-age in seconds (default is 63072000, two years) # :preload :: Set preload, so the domain can be included in HSTS preload lists # :subdomains :: Set to false to not set includeSubDomains. By default, # includeSubDomains is set to enforce HTTPS for subdomains. def self.configure(app, opts=OPTS) app.plugin :default_headers, RodaResponseHeaders::STRICT_TRANSPORT_SECURITY => "max-age=#{opts[:max_age]||63072000}#{'; includeSubDomains' unless opts[:subdomains] == false}#{'; preload' if opts[:preload]}".freeze end end register_plugin(:hsts, Hsts) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/indifferent_params.rb000066400000000000000000000100431516720775400247470ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The indifferent_params plugin adds a +params+ instance # method which offers indifferent access to the request # params, allowing you to use symbols to lookup values in # a hash where the keys are strings. Note that while this # allows for an easier transition from some other ruby frameworks, # it is a bad idea in general as it makes it more difficult to # separate external data from internal data, and doesn't handle # any typecasting of the data. Consider using the typecast_params # plugin instead of this plugin for accessing parameters. # # Example: # # plugin :indifferent_params # # route do |r| # params[:foo] # end # # The exact behavior depends on the version of Rack in use. # If you are using Rack 2, this plugin uses rack's API # to set the query parser for the request to use indifferent # access. Rack 1 doesn't support indifferent access to # params, so if you are using Rack 1, this plugin will make # a deep copy of the request params hash, where each level # uses indifferent access. On Rack 1, The params hash is # initialized lazily, so you only pay the penalty of # copying the request params if you call the +params+ method. # # Note that there is a rack-indifferent gem that # monkey patches rack to always use indifferent params. If # you are using Rack 1, it is recommended to use # rack-indifferent instead of this plugin, as it is faster # and has some other minor advantages, though # it affects all rack applications instead of just the Roda app that # you load the plugin into. module IndifferentParams INDIFFERENT_PROC = lambda{|h,k| h[k.to_s] if k.is_a?(Symbol)} if Rack.release > '2' require 'rack/query_parser' class QueryParser < Rack::QueryParser # Work around for invalid optimization in rack def parse_nested_query(qs, d=nil) return make_params.to_params_hash if qs.nil? || qs.empty? super end class Params < Rack::QueryParser::Params if Rack.release >= '3' if Params < Hash def initialize super(&INDIFFERENT_PROC) end else def initialize @size = 0 @params = Hash.new(&INDIFFERENT_PROC) end end else def initialize(limit = Rack::Utils.key_space_limit) @limit = limit @size = 0 @params = Hash.new(&INDIFFERENT_PROC) end end end end module RequestMethods query_parser = Rack.release >= '3' ? QueryParser.new(QueryParser::Params, 32) : QueryParser.new(QueryParser::Params, 65536, 32) QUERY_PARSER = Rack::Utils.default_query_parser = query_parser private def query_parser QUERY_PARSER end end module InstanceMethods def params @_request.params end end else module InstanceMethods # A copy of the request params that will automatically # convert symbols to strings. def params @_params ||= indifferent_params(@_request.params) end private # Recursively process the request params and convert # hashes to support indifferent access, leaving # other values alone. def indifferent_params(params) case params when Hash hash = Hash.new(&INDIFFERENT_PROC) params.each{|k, v| hash[k] = indifferent_params(v)} hash when Array params.map{|x| indifferent_params(x)} else params end end end end end register_plugin(:indifferent_params, IndifferentParams) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/inject_erb.rb000066400000000000000000000017041516720775400232170ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The inject_erb plugin allows you to inject content directly # into the template output: # # <% inject_erb("Some HTML Here") %> # # This will inject Some HTML Here into the template output, # even though the tag being used is <% and not <%=. # # This method can be used inside methods, such as to wrap calls to # methods that accept template blocks, to inject code before and after # the template blocks. module InjectERB def self.load_dependencies(app) app.plugin :render end module InstanceMethods # Inject into the template output for the template currently being # rendered. def inject_erb(value) instance_variable_get(render_opts[:template_opts][:outvar]) << value.to_s end end end register_plugin(:inject_erb, InjectERB) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/invalid_request_body.rb000066400000000000000000000102651516720775400253300ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The invalid_request_body plugin allows for custom handling of invalid request # bodies. Roda uses Rack for parsing request bodies, so by default, any # invalid request bodies would result in Rack raising an exception, and the # exception could change for different reasons the request body is invalid. # This plugin overrides RodaRequest#POST (which parses parameters from request # bodies), and if parsing raises an exception, it allows for custom behavior. # # If you want to treat an invalid request body as the submission of no parameters, # you can use the :empty_hash argument when loading the plugin: # # plugin :invalid_request_body, :empty_hash # # If you want to return a empty 400 (Bad Request) response if an invalid request # body is submitted, you can use the :empty_400 argument when loading the plugin: # # plugin :invalid_request_body, :empty_400 # # If you want to raise a Roda::RodaPlugins::InvalidRequestBody::Error exception # if an invalid request body is submitted (which makes it easier to handle these # exceptions when using the error_handler plugin), you can use the :raise argument # when loading the plugin: # # plugin :invalid_request_body, :raise # # For custom behavior, you can pass a block when loading the plugin. The block # is called with the exception Rack raised when parsing the body. The block will # be used to define a method in the application's RodaRequest class. It can either # return a hash of parameters, or you can raise a different exception, or you # can halt processing and return a response: # # plugin :invalid_request_body do |exception| # # To treat the exception raised as a submitted parameter # {body_error: exception} # end module InvalidRequestBody # Exception class raised for invalid request bodies. Error = Class.new(RodaError) # Set the action to use (:empty_400, :empty_hash, :raise) for invalid request bodies, # or use a block for custom behavior. def self.configure(app, action=nil, &block) if action if block raise RodaError, "cannot provide both block and action when loading invalid_request_body plugin" end method = :"handle_invalid_request_body_#{action}" unless RequestMethods.private_method_defined?(method) raise RodaError, "invalid invalid_request_body action provided: #{action}" end app::RodaRequest.send(:alias_method, :handle_invalid_request_body, method) elsif block app::RodaRequest.class_eval do define_method(:handle_invalid_request_body, &block) alias handle_invalid_request_body handle_invalid_request_body end else raise RodaError, "must provide block or action when loading invalid_request_body plugin" end app::RodaRequest.send(:private, :handle_invalid_request_body) end module RequestMethods # Handle invalid request bodies as configured if the default behavior # raises an exception. def POST super rescue => e handle_invalid_request_body(e) end private # Return an empty 400 HTTP response for invalid request bodies. def handle_invalid_request_body_empty_400(e) response.status = 400 headers = response.headers headers.clear headers[RodaResponseHeaders::CONTENT_TYPE] = 'text/html' headers[RodaResponseHeaders::CONTENT_LENGTH] ='0' throw :halt, response.finish_with_body([]) end # Treat invalid request bodies by using an empty hash as the # POST params. def handle_invalid_request_body_empty_hash(e) {} end # Raise a specific error for all invalid request bodies, # to allow for easy rescuing using the error_handler plugin. def handle_invalid_request_body_raise(e) raise Error, e.message end end end register_plugin(:invalid_request_body, InvalidRequestBody) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/ip_from_header.rb000066400000000000000000000022711516720775400240560ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The ip_from_header plugin allows for overriding +request.ip+ to return # the value contained in a specific header. This is useful when the # application is behind a proxy that sets a specific header, especially # when the proxy does not use a fixed IP address range. Example showing # usage with Cloudflare: # # plugin :ip_from_header, "CF-Connecting-IP" # # This plugin assumes that if the header is set, it contains a valid IP # address, it does not check the format of the header value, just as # Rack::Request#ip does not check the IP address it returns is # actually valid. module IPFromHeader def self.configure(app, header) app.opts[:ip_from_header_env_key] = "HTTP_#{header.upcase.tr('-', '_')}".freeze end module RequestMethods # Return the IP address continained in the configured header, if present. # Fallback to the default behavior if not present. def ip @env[roda_class.opts[:ip_from_header_env_key]] || super end end end register_plugin(:ip_from_header, IPFromHeader) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/json.rb000066400000000000000000000076331516720775400220730ustar00rootroot00000000000000# frozen-string-literal: true require 'json' class Roda module RodaPlugins # The json plugin allows match blocks to return # arrays or hashes, and have those arrays or hashes be # converted to json which is used as the response body. # It also sets the response content type to application/json. # So you can take code like: # # r.root do # response['Content-Type'] = 'application/json' # [1, 2, 3].to_json # end # r.is "foo" do # response['Content-Type'] = 'application/json' # {'a'=>'b'}.to_json # end # # and DRY it up: # # plugin :json # r.root do # [1, 2, 3] # end # r.is "foo" do # {'a'=>'b'} # end # # By default, only arrays and hashes are handled, but you # can specifically set the allowed classes to json by adding # using the :classes option when loading the plugin: # # plugin :json, classes: [Array, Hash, Sequel::Model] # # By default objects are serialized with +to_json+, but you # can pass in a custom serializer, which can be any object # that responds to +call(object)+. # # plugin :json, serializer: proc{|o| o.to_json(root: true)} # # If you need the request information during serialization, such # as HTTP headers or query parameters, you can pass in the # +:include_request+ option, which will pass in the request # object as the second argument when calling the serializer. # # plugin :json, include_request: true, serializer: proc{|o, request| ...} # # The default content-type is 'application/json', but you can change that # using the +:content_type+ option: # # plugin :json, content_type: 'application/xml' # # This plugin depends on the custom_block_results plugin, and therefore does # not support treating String, FalseClass, or NilClass values as JSON. module Json # Set the classes to automatically convert to JSON, and the serializer to use. def self.configure(app, opts=OPTS) app.plugin :custom_block_results classes = opts[:classes] || [Array, Hash] app.opts[:json_result_classes] ||= [] app.opts[:json_result_classes] += classes classes = app.opts[:json_result_classes] classes.uniq! classes.freeze classes.each do |klass| app.opts[:custom_block_results][klass] = :handle_json_block_result end app.opts[:json_result_serializer] = opts[:serializer] || app.opts[:json_result_serializer] || app.opts[:json_serializer] || :to_json.to_proc app.opts[:json_result_include_request] = opts[:include_request] if opts.has_key?(:include_request) app.opts[:json_result_content_type] = opts[:content_type] || 'application/json'.freeze end module ClassMethods # The classes that should be automatically converted to json def json_result_classes # RODA4: remove, only used by previous implementation. opts[:json_result_classes] end end module InstanceMethods # Handle a result for one of the registered JSON result classes # by converting the result to JSON. def handle_json_block_result(result) @_response[RodaResponseHeaders::CONTENT_TYPE] ||= opts[:json_result_content_type] @_request.send(:convert_to_json, result) end end module RequestMethods private # Convert the given object to JSON. Uses to_json by default, # but can use a custom serializer passed to the plugin. def convert_to_json(result) opts = roda_class.opts serializer = opts[:json_result_serializer] if opts[:json_result_include_request] serializer.call(result, self) else serializer.call(result) end end end end register_plugin(:json, Json) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/json_parser.rb000066400000000000000000000101251516720775400234350ustar00rootroot00000000000000# frozen-string-literal: true require 'json' class Roda module RodaPlugins # The json_parser plugin parses request bodies in JSON format # if the request's content type specifies json. This is mostly # designed for use with JSON API sites. # # This only parses the request body as JSON if the Content-Type # header for the request includes "json". # # The parsed JSON body will be available in +r.POST+, just as a # parsed HTML form body would be. It will also be available in # +r.params+ (which merges +r.GET+ with +r.POST+). module JsonParser DEFAULT_ERROR_HANDLER = proc{|r| r.halt [400, {}, []]} # Handle options for the json_parser plugin: # :error_handler :: A proc to call if an exception is raised when # parsing a JSON request body. The proc is called # with the request object, and should probably call # halt on the request or raise an exception. # :parser :: The parser to use for parsing incoming json. Should be # an object that responds to +call(str)+ and returns the # parsed data. The default is to call JSON.parse. # :include_request :: If true, the parser will be called with the request # object as the second argument, so the parser needs # to respond to +call(str, request)+. # :wrap :: Whether to wrap uploaded JSON data in a hash with a "_json" # key. Without this, calls to +r.params+ will fail if a non-Hash # (such as an array) is uploaded in JSON format. A value of # :always will wrap all values, and a value of :unless_hash will # only wrap values that are not already hashes. def self.configure(app, opts=OPTS) app.opts[:json_parser_error_handler] = opts[:error_handler] || app.opts[:json_parser_error_handler] || DEFAULT_ERROR_HANDLER app.opts[:json_parser_parser] = opts[:parser] || app.opts[:json_parser_parser] || app.opts[:json_parser] || JSON.method(:parse) app.opts[:json_parser_include_request] = opts[:include_request] if opts.has_key?(:include_request) case opts[:wrap] when :unless_hash, :always app.opts[:json_parser_wrap] = opts[:wrap] when nil # Nothing else raise RodaError, "unsupported option value for json_parser plugin :wrap option: #{opts[:wrap].inspect} (should be :unless_hash or :always)" end end module RequestMethods # If the Content-Type header in the request includes "json", # parse the request body as JSON. Ignore an empty request body. def POST env = @env if post_params = env["roda.json_params"] return post_params end unless (input = env["rack.input"]) && (content_type = self.content_type) && content_type.include?('json') return super end str = _read_json_input(input) return super if str.empty? begin json_params = parse_json(str) rescue roda_class.opts[:json_parser_error_handler].call(self) end wrap = roda_class.opts[:json_parser_wrap] if wrap == :always || (wrap == :unless_hash && !json_params.is_a?(Hash)) json_params = {"_json"=>json_params} end env["roda.json_params"] = json_params json_params end private def parse_json(str) args = [str] args << self if roda_class.opts[:json_parser_include_request] roda_class.opts[:json_parser_parser].call(*args) end # Rack 3 dropped requirement that input be rewindable if Rack.release >= '3' def _read_json_input(input) input.read end else def _read_json_input(input) input.rewind str = input.read input.rewind str end end end end register_plugin(:json_parser, JsonParser) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/link_to.rb000066400000000000000000000046521516720775400225570ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The link_to plugin adds the +link_to+ instance method, which can be used for constructing # HTML links (+a+ tag with +href+ attribute). # # The simplest usage of +link_to+ is passing the body and the location to link to as strings: # # link_to("body", "/path") # # => "body" # # The link_to plugin depends on the path plugin, and allows you to pass symbols for named paths: # # # Class level # path :foo, "/path/to/too" # # # Instance level # link_to("body", :foo) # # => "body" # # It also allows you to pass instances of classes that you have registered with the path plugin: # # # Class level # A = Struct.new(:id) # path A do # "/path/to/a/#{id}" # end # # # Instance level # link_to("body", A.new(1)) # # => "body" # # To set additional HTML attributes on the +a+ tag, you can pass them as an options hash: # # link_to("body", "/path", foo: "bar") # # => "body" # # If the body is nil, it will be set to the same as the path: # # link_to(nil, "/path") # # => "/path" # # The plugin will automatically HTML escape the path and any HTML attribute values, using the h plugin: # # link_to("body", "/path?a=1&b=2", foo: '"bar"') # # => "body" module LinkTo def self.load_dependencies(app) app.plugin :h app.plugin :path end module InstanceMethods # Return a string with an HTML +a+ tag with an +href+ attribute. See LinkTo # module documentation for details. def link_to(body, href, attributes=OPTS) case href when Symbol href = public_send(:"#{href}_path") when String # nothing else href = path(href) end href = h(href) body = href if body.nil? buf = String.new << "" << body << "" end end end register_plugin(:link_to, LinkTo) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/mail_processor.rb000066400000000000000000000603271516720775400241420ustar00rootroot00000000000000# frozen-string-literal: true require 'mail' class Roda module RodaPlugins # The mail_processor plugin allows your Roda application to process mail # using a routing tree. Quick example: # # class MailProcessor < Roda # plugin :mail_processor # # route do |r| # # Match based on the To header, extracting the ticket_id # r.to /ticket\+(\d+)@example.com/ do |ticket_id| # if ticket = Ticket[ticket_id.to_i] # # Mark the mail as handled if there is a valid ticket associated # r.handle do # ticket.add_note(text: mail_text, from: from) # end # end # end # # # Match based on the To or CC header # r.rcpt "post@example.com" do # # Match based on the body, capturing the post id and tag # r.body(/^Post: (\d+)-(\w+)/) do |post_id, tag| # unhandled_mail("no matching post") unless post = Post[post_id.to_i] # unhandled_mail("tag doesn't match for post") unless post.tag == tag # # # Match based on APPROVE somewhere in the mail text, # # marking the mail as handled # r.handle_text /\bAPPROVE\b/i do # post.approve!(from) # end # # # Match based on DENY somewhere in the mail text, # # marking the mail as handled # r.handle_text /\bDENY\b/i do # post.deny!(from) # end # end # end # end # end # # = Processing Mail # # To submit a mail for processing via the mail_processor routing tree, call the +process_mail+ # method with a +Mail+ instance: # # MailProcessor.process_mail(Mail.new do # # ... # end) # # You can use this to process mail messages from the filesystem: # # MailProcessor.process_mail(Mail.read('/path/to/message.eml')) # # If you have a service that delivers mail via an HTTP POST request (for realtime # processing), you can have your web routes convert the web request into a +Mail+ instance # and then call +process_mail+: # # r.post "email" do # # check request is submitted by trusted sender # # # If request body is the raw mail body # r.body.rewind # MailProcessor.process_mail(Mail.new(r.body.read)) # # # If request body is in a parameter named content # MailProcessor.process_mail(Mail.new(r.params['content'])) # # # If the HTTP request requires a specific response status code (such as 204) # response.status = 204 # # nil # end # # Note that when receiving messages via HTTP, you need to make sure you check that the # request is trusted. How to do this depends on the delivery service, but could involve # using HTTP basic authentication, checking for valid API tokens, or checking that a message # includes a signature/hash that matches the expected value. # # If you have setup a default retriever_method for +Mail+, you can call +process_mailbox+, # which will process all mail in the given mailbox (using +Mail.find_and_delete+): # # MailProcessor.process_mailbox # # You can also use a +:retreiver+ option to provide a specific retriever: # # MailProcessor.process_mailbox(retreiver: Mail::POP3.new) # # = Routing Mail # # The mail_processor plugin handles routing similar to Roda's default routing for # web requests, but because mail processing may not return a result, the mail_processor # plugin uses a more explicit approach to consider whether the message has been handled. # If the +r.handle+ method is called during routing, the mail is considered handled, # otherwise the mail is considered not handled. The +unhandled_mail+ method can be # called at any point to stop routing and consider the mail as not handled (even if # inside an +r.handle+ block). # # Here are the mail routing methods and what they use for matching: # # from :: match on the mail From address # to :: match on the mail To address # cc :: match on the mail CC address # rcpt :: match on the mail recipients (To and CC addresses by default) # subject :: match on the mail subject # body :: match on the mail body # text :: match on text extracted from the message (same as mail body by default) # header :: match on a mail header # # All of these routing methods accept a single argument, except for +r.header+, which # can take two arguments. # # Each of these routing methods also has a +r.handle_*+ method # (e.g. +r.handle_from+), which will call +r.handle+ implicitly to mark the # mail as handled if the routing method matches and control is passed to the block. # # The address matchers (from, to, cc, rcpt) perform a case-insensitive match if # given a string or array of strings, and a regular regexp match if given a regexp. # # The content matchers (subject, body, text) perform a case-sensitive substring search # if given a string or array of strings, and a regular regexp match if given a regexp. # # The header matcher should be called with a key and an optional value. If the matcher is # called with a key and not a value, it matches if a header matching the key is present # in the message, yielding the header value. If the matcher is called with a key and a # value, it matches if a header matching the key is present and the header value matches # the value given, using the same criteria as the content matchers. # # In all cases for matchers, if a string is given and matches, the match block is called without # arguments. If an array of strings is given, and one of the strings matches, # the match block is called with the matching string argument. If a regexp is given, # the match block is called with the regexp captures. This is the same behavior for Roda's # general string, array, and regexp matchers. # # = Recipient-Specific Routing # # To allow splitting up the mail processor routing tree based on recipients, you can use # the +rcpt+ class method, which takes any number of string or regexps arguments for recipient # addresses, and a block to handle the routing for those addresses instead of using the # default routing. # # MailProcessor.rcpt('a@example.com') do |r| # r.text /Post: (\d+)-(\h+)/ do |post_id, hmac| # next unless Post[post_id.to_i] # unhandled_mail("no matching Post") unless post = Post[post_id.to_i] # unhandled_mail("HMAC for doesn't match for post") unless hmac == post.hmac_for_address(from.first) # # r.handle_text 'APPROVE' do # post.approved_by(from) # end # # r.handle_text 'DENY' do # post.denied_by(from) # end # end # end # # The +rcpt+ class method does not mark the messages as handled, because in most cases you will # need to do additional matching to extract the information necessary to handle # the mail. You will need to call +r.handle+ or similar method inside the block # to mark the mail as handled. # # Matching on strings provided to the +rcpt+ class method is an O(1) operation as # the strings are stored lowercase in a hash. Matching on regexps provided to the # +rcpt+ class method is an O(n) operation on the number of regexps. # # If you would like to break up the routing tree using something other than the # recipient address, you can use the multi_route plugin. # # = Hooks # # The mail_processor plugin offers hooks for processing mail. # # For mail that is handled successfully, you can use the handled_mail hook: # # MailProcessor.handled_mail do # # nothing by default # end # # For mail that is not handled successfully, either because +r.handle+ was not called # during routing or because the +unhandled_mail+ method was called explicitly, # you can use the unhandled_mail hook. # # The default is to reraise the UnhandledMail exception that was raised during routing, # so that calling code will not be able to ignore errors when processing mail. However, # you may want to save such mails to a special location or forward them as attachments # for manual review, and the unhandled_mail hook allows you to do that: # # MailProcessor.unhandled_mail do # # raise by default # # # Forward the mail as an attachment to an admin # m = Mail.new # m.to 'admin@example.com' # m.subject '[APP] Unhandled Received Email' # m.add_file(filename: 'message.eml', :content=>mail.encoded) # m.deliver # end # # Finally, for all processed mail, regardless of whether it was handled or not, # there is an after_mail hook, which can be used to archive all processed mail: # # MailProcessor.after_mail do # # nothing by default # # # Add it to a received_mail table using Sequel # DB[:received_mail].insert(:message=>mail.encoded) # end # # The after_mail hook is called after the handled_mail or unhandled_mail hook # is called, even if routing, the handled_mail hook, or the unhandled_mail hook # raises an exception. The handled_mail and unhandled_mail hooks are not called # if an exception is raised during routing (other than for UnhandledMail exceptions). # # = Extracting Text from Mail # # The most common use of the mail_processor plugin is to handle replies to mails sent # out by the application, so that recipients can reply to mail to make changes without # having to access the application directly. When handling replies, it is common to want # to extract only the text of the reply, and ignore the text of the message that was # replied to. Because there is no consistent way to format replies in mail, there have # evolved various approaches to do this, with some gems devoted to extracting the reply # text from a message. # # The mail_processor plugin does not choose any particular approach for extracting text from mail, # but it includes the ability to configure how to do that via the +mail_text+ class method. # This method affects the +r.text+ match method, as well as +mail_text+ instance method. # By default, the decoded body of the mail is used as the mail text. # # MailProcessor.mail_text do # # mail.body.decoded by default # # # https://github.com/github/email_reply_parser # EmailReplyParser.parse_reply(mail.body.decoded) # # # https://github.com/fiedl/extended_email_reply_parser # mail.parse # end # # = Security # # Note that due to the way mail delivery works via SMTP, the actual sender and recipient of # the mail (the SMTP envelope MAIL FROM and RCPT TO addresses) may not match the sender and # receiver embedded in the message. Because mail_processor routing relies on parsing the mail, # it does not have access to the actual sender and recipient used at the SMTP level, unless # a mail server adds that information as a header to the mail (and clears any existing header # to prevent spoofing). Keep that in mind when you are setting up your mail routes. If you # have setup your mail server to add the SMTP RCPT TO information to a header, you may want # to only consider that header when looking for the recipients of the message, instead of # looking at the To and CC headers. You can override the default behavior for determining # the recipients (this will affect the +rcpt+ class method, +r.rcpt+ match method, and # +mail_recipients+ instance method): # # MailProcessor.mail_recipients do # # Assuming the information is in the X-SMTP-To header # Array(header['X-SMTP-To'].decoded) # end # # Also note that unlike when handling web requests where you can rely on storing authentication # information in the session, when processing mail, you should manually authenticate each message, # as email is trivially forged. One way to do this is assigning and storing a unique identifier when # sending each message, and checking for a matching identifier when receiving a response. Another # option is including a computable authentication code (e.g. HMAC) in the message, and then # when receiving a response, recomputing the authentication code and seeing if it matches the # authentication code in the message. The unique identifier approach requires storing a large # number of identifiers, but allows you to remove the identifier after a reply is received # (to ensure only one response is handled). The authentication code approach does not # require additional storage, but does not allow you to ensure only a single response is handled. # # = Avoiding Mail Loops # # If processing the mail results in sending out additional mail, be careful not to send a # response to the sender of the email, otherwise if the sender of the email has an # auto-responder, you can end up with a mail loop, where every mail you send results in # a response, which you then process and send out a response to. module MailProcessor # Exception class raised when a mail processed is not handled during routing, # either implicitly because the +r.handle+ method was not called, or via an explicit # call to +unhandled_mail+. class UnhandledMail < StandardError; end module ClassMethods # Freeze the rcpt routes if they are present. def freeze if string_routes = opts[:mail_processor_string_routes].freeze string_routes.freeze opts[:mail_processor_regexp_routes].freeze end super end # Process the given Mail instance, calling the appropriate hooks depending on # whether the mail was handled during processing. def process_mail(mail) scope = new("PATH_INFO"=>'', 'SCRIPT_NAME'=>'', "REQUEST_METHOD"=>"PROCESSMAIL", 'rack.input'=>StringIO.new, 'roda.mail'=>mail) begin begin scope.process_mail rescue UnhandledMail scope.unhandled_mail_hook else scope.handled_mail_hook end ensure scope.after_mail_hook end end # Process all mail in the given mailbox. If the +:retriever+ option is # given, should be an object supporting the Mail retriever API, otherwise # uses the default Mail retriever_method. This deletes retrieved mail from the # mailbox after processing, so that when called multiple times it does # not reprocess the same mail. If mail should be archived and not deleted, # the +after_mail+ method should be used to perform the archiving of the mail. def process_mailbox(opts=OPTS) (opts[:retriever] || Mail).find_and_delete(opts.dup){|m| process_mail(m)} nil end # Setup a routing tree for the given recipient addresses, which can be strings or regexps. # Any messages matching the given recipient address will use these routing trees instead # of the normal routing tree. def rcpt(*addresses, &block) opts[:mail_processor_string_routes] ||= {} opts[:mail_processor_regexp_routes] ||= {} string_meth = nil regexp_meth = nil addresses.each do |address| case address when String unless string_meth string_meth = define_roda_method("mail_processor_string_route_#{address}", 1, &convert_route_block(block)) end opts[:mail_processor_string_routes][address] = string_meth when Regexp unless regexp_meth regexp_meth = define_roda_method("mail_processor_regexp_route_#{address}", :any, &convert_route_block(block)) end opts[:mail_processor_regexp_routes][address] = regexp_meth else raise RodaError, "invalid address format passed to rcpt, should be Array or String" end end nil end %w'after_mail handled_mail unhandled_mail'.each do |meth| class_eval(<<-END, __FILE__, __LINE__+1) def #{meth}(&block) define_method(:#{meth}_hook, &block) nil end END end %w'mail_recipients mail_text'.each do |meth| class_eval(<<-END, __FILE__, __LINE__+1) def #{meth}(&block) define_method(:#{meth}, &block) nil end END end end module InstanceMethods [:to, :from, :cc, :body, :subject, :header].each do |field| class_eval(<<-END, __FILE__, __LINE__+1) def #{field} mail.#{field} end END end # Perform the processing of mail for this request, first considering # routes defined via the class-level +rcpt+ method, and then the # normal routing tree passed in as the block. def process_mail(&block) if string_routes = opts[:mail_processor_string_routes] addresses = mail_recipients addresses.each do |address| if meth = string_routes[address.to_s.downcase] _roda_handle_route{send(meth, @_request)} return end end opts[:mail_processor_regexp_routes].each do |regexp, meth| addresses.each do |address| if md = regexp.match(address) _roda_handle_route{send(meth, @_request, *md.captures)} return end end end end _roda_handle_main_route nil end # Hook called after processing any mail, whether the mail was # handled or not. Does nothing by default. def after_mail_hook nil end # Hook called after processing a mail, when the mail was handled. # Does nothing by default. def handled_mail_hook nil end # Hook called after processing a mail, when the mail was not handled. # Reraises the UnhandledMail exception raised during mail processing # by default. def unhandled_mail_hook raise end # The mail instance being processed. def mail env['roda.mail'] end # The text of the mail instance being processed, uses the # decoded body of the mail by default. def mail_text mail.body.decoded end # The recipients of the mail instance being processed, uses the To and CC # headers by default. def mail_recipients Array(to) + Array(cc) end # Raise an UnhandledMail exception with the given reason, used to mark the # mail as not handled. A reason why the mail was not handled must be # provided, which will be used as the exception message. def unhandled_mail(reason) raise UnhandledMail, reason end end module RequestMethods [:to, :from, :cc, :body, :subject, :rcpt, :text].each do |field| class_eval(<<-END, __FILE__, __LINE__+1) def handle_#{field}(val) #{field}(val) do |*args| handle do yield(*args) end end end def #{field}(address, &block) on(:#{field}=>address, &block) end END case field when :rcpt, :text, :body, :subject next end class_eval(<<-END, __FILE__, __LINE__+1) private def match_#{field}(address) _match_address(:#{field}, address, Array(mail.#{field})) end END end # Same as +header+, but also mark the message as being handled. def handle_header(key, value=nil) header(key, value) do |*args| handle do yield(*args) end end end # Match based on a mail header value. def header(key, value=nil, &block) on(:header=>[key, value], &block) end # Mark the mail as having been handled, so routing will not call # unhandled_mail implicitly. def handle(&block) env['roda.mail_handled'] = true always(&block) end private if RUBY_VERSION >= '2.4.0' # Whether the addresses are the same (case insensitive match). def address_match?(a1, a2) a1.casecmp?(a2) end else # :nocov: def address_match?(a1, a2) a1.downcase == a2.downcase end # :nocov: end # Match if any of the given addresses match the given val, which # can be a string (case insensitive match of the string), array of # strings (case insensitive match of any string), or regexp # (normal regexp match). def _match_address(field, val, addresses) case val when String addresses.any?{|a| address_match?(a, val)} when Array overlap = [] addresses.each do |a| val.each do |v| if address_match?(a, v) overlap << a end end end unless overlap.empty? @captures.concat(overlap) end when Regexp matched = false addresses.each do |v| if md = val.match(v) matched = true @captures.concat(md.captures) end end matched else unsupported_matcher(:field=>val) end end # Match if the content matches the given val, which # can be a string (case sensitive substring match), array of # strings (case sensitive substring match of any string), or regexp # (normal regexp match). def _match_content(field, val, content) case val when String content.include?(val) when Array val.each do |v| if content.include?(v) return @captures << v end end false when Regexp if md = val.match(content) @captures.concat(md.captures) end else unsupported_matcher(field=>val) end end # Match the value against the full mail body. def match_body(val) _match_content(:body, val, mail.body.decoded) end # Match the value against the mail subject. def match_subject(val) _match_content(:subject, val, mail.subject) end # Match the given address against all recipients in the mail. def match_rcpt(address) _match_address(:rcpt, address, scope.mail_recipients) end # Match the value against the extracted mail text. def match_text(val) _match_content(:text, val, scope.mail_text) end # Match against a header specified by key with the given # value (which may be nil). def match_header((key, value)) return unless content = mail.header[key] if value.nil? @captures << content.decoded else _match_content(:header, value, content.decoded) end end # The mail instance being processed. def mail env['roda.mail'] end # If the routing did not explicitly mark the mail as handled # mark it as unhandled. def block_result_body(_) unless env['roda.mail_handled'] scope.unhandled_mail('mail was not handled during mail_processor routing') end end end end register_plugin(:mail_processor, MailProcessor) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/mailer.rb000066400000000000000000000236071516720775400223720ustar00rootroot00000000000000# frozen-string-literal: true require 'stringio' require 'mail' class Roda module RodaPlugins # The mailer plugin allows your Roda application to send emails easily. # # class Mailer < Roda # plugin :render # plugin :mailer # # route do |r| # r.on "albums", Integer do |album_id| # @album = Album[album_id] # # r.mail "added" do # from 'from@example.com' # to 'to@example.com' # cc 'cc@example.com' # bcc 'bcc@example.com' # subject 'Album Added' # add_file "path/to/album_added_img.jpg" # render(:albums_added_email) # body # end # end # end # end # # The default method for sending a mail is +sendmail+: # # Mailer.sendmail("/albums/1/added") # # If you want to return the Mail::Message instance for further modification, # you can just use the +mail+ method: # # mail = Mailer.mail("/albums/1/added") # mail.from 'from2@example.com' # mail.deliver # # The mailer plugin uses the mail gem, so if you want to configure how # email is sent, you can use Mail.defaults (see the mail gem documentation for # more details): # # Mail.defaults do # delivery_method :smtp, address: 'smtp.example.com', port: 587 # end # # You can support multipart emails using +text_part+ and +html_part+: # # r.mail "added" do # from 'from@example.com' # to 'to@example.com' # subject 'Album Added' # text_part render('album_added.txt') # views/album_added.txt.erb # html_part render('album_added.html') # views/album_added.html.erb # end # # In addition to allowing you to use Roda's render plugin for rendering # email bodies, you can use all of Roda's usual routing tree features # to DRY up your code: # # r.on "albums", Integer do |album_id| # @album = Album[album_id] # from 'from@example.com' # to 'to@example.com' # # r.mail "added" do # subject 'Album Added' # render(:albums_added_email) # end # # r.mail "deleted" do # subject 'Album Deleted' # render(:albums_deleted_email) # end # end # # When sending a mail via +mail+ or +sendmail+, a RodaError will be raised # if the mail object does not have a body. This is similar to the 404 # status that Roda uses by default for web requests that don't have # a body. If you want to specifically send an email with an empty body, you # can use the explicit empty string: # # r.mail do # from 'from@example.com' # to 'to@example.com' # subject 'No Body Here' # "" # end # # If while preparing the email you figure out you don't want to send an # email, call +no_mail!+: # # r.mail 'welcome', Integer do |id| # no_mail! unless user = User[id] # # ... # end # # You can pass arguments when calling +mail+ or +sendmail+, and they # will be yielded as additional arguments to the appropriate +r.mail+ block: # # Mailer.sendmail('/welcome/1', 'foo@example.com') # # r.mail 'welcome', Integer do |user_id, mail_from| # from mail_from # to User[user_id].email # # ... # end # # By default, the mailer uses text/plain as the Content-Type for emails. # You can override the default by specifying a :content_type option when # loading the plugin: # # plugin :mailer, content_type: 'text/html' # # For backwards compatibility reasons, the +r.mail+ method does not do # a terminal match by default if provided arguments (unlike +r.get+ and # +r.post+). You can pass the :terminal option to make +r.mail+ enforce # a terminal match if provided arguments. # # The mailer plugin does support being used inside a Roda application # that is handling web requests, where the routing block for mails and # web requests is shared. However, it's recommended that you create a # separate Roda application for emails. This can be a subclass of your main # Roda application if you want your helper methods to automatically be # available in your email views. module Mailer # Error raised when the using the mail class method, but the routing # tree doesn't return the mail object. class Error < ::Roda::RodaError; end # Set the options for the mailer. Options: # :content_type :: The default content type for emails (default: text/plain) def self.configure(app, opts=OPTS) app.opts[:mailer] = (app.opts[:mailer]||OPTS).merge(opts).freeze end module ClassMethods # Return a Mail::Message instance for the email for the given request path # and arguments. Any arguments given are yielded to the appropriate +r.mail+ # block after any usual match block arguments. You can further manipulate the #returned mail object before calling +deliver+ to send the mail. def mail(path, *args) mail = ::Mail.new catch(:no_mail) do unless mail.equal?(new("PATH_INFO"=>path, 'SCRIPT_NAME'=>'', "REQUEST_METHOD"=>"MAIL", 'rack.input'=>StringIO.new, 'roda.mail'=>mail, 'roda.mail_args'=>args)._roda_handle_main_route) raise Error, "route did not return mail instance for #{path.inspect}, #{args.inspect}" end mail end end # :nocov: ruby2_keywords(:mail) if respond_to?(:ruby2_keywords, true) # :nocov: # Calls +mail+ with given arguments and immediately sends the resulting mail. def sendmail(*args) if m = mail(*args) m.deliver end end # :nocov: ruby2_keywords(:sendmail) if respond_to?(:ruby2_keywords, true) # :nocov: end module RequestMethods # Similar to routing tree methods such as +get+ and +post+, this matches # only if the request method is MAIL (only set when using the Roda class # +mail+ or +sendmail+ methods) and the rest of the arguments match # the request. This yields any of the captures to the block, as well as # any arguments passed to the +mail+ or +sendmail+ Roda class methods. def mail(*args) if @env["REQUEST_METHOD"] == "MAIL" # RODA4: Make terminal match the default send(roda_class.opts[:mailer][:terminal] ? :_verb : :if_match, args) do |*vs| yield(*(vs + @env['roda.mail_args'])) end end end end module ResponseMethods # The mail object related to the current request. attr_accessor :mail # If the related request was an email request, add any response headers # to the email, as well as adding the response body to the email. # Return the email unless no body was set for it, which would indicate # that the routing tree did not handle the request. def finish if m = mail header_content_type = @headers.delete(RodaResponseHeaders::CONTENT_TYPE) m.headers(@headers) m.body(@body.join) unless @body.empty? mail_attachments.each do |a, block| m.add_file(*a) block.call if block end if content_type = header_content_type || roda_class.opts[:mailer][:content_type] if mail.multipart? if /multipart\/mixed/ =~ mail.content_type && mail.parts.length >= 2 && (part = mail.parts.find{|p| !p.attachment && (p.encoded; /text\/plain/ =~ p.content_type)}) part.content_type = content_type end else mail.content_type = content_type end end unless m.body.to_s.empty? && m.parts.empty? && @body.empty? m end else super end end # The attachments related to the current mail. def mail_attachments @mail_attachments ||= [] end end module InstanceMethods # Add delegates for common email methods. [:from, :to, :cc, :bcc, :subject].each do |meth| define_method(meth) do |*args| env['roda.mail'].public_send(meth, *args) nil end end [:text_part, :html_part].each do |meth| define_method(meth) do |*args| _mail_part(meth, *args) end end # If this is an email request, set the mail object in the response, as well # as the default content_type for the email. def initialize(env) super if mail = env['roda.mail'] res = @_response res.mail = mail res.headers.delete(RodaResponseHeaders::CONTENT_TYPE) end end # Delay adding a file to the message until after the message body has been set. # If a block is given, the block is called after the file has been added, and you # can access the attachment via response.mail_attachments.last. def add_file(*a, &block) response.mail_attachments << [a, block] nil end # Signal that no mail should be sent for this request. def no_mail! throw :no_mail end private # Set the text_part or html_part (depending on the method) in the related email, # using the given body and optional headers. def _mail_part(meth, body, headers=nil) env['roda.mail'].public_send(meth) do body(body) headers(headers) if headers end nil end end end register_plugin(:mailer, Mailer) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/map_matcher.rb000066400000000000000000000023021516720775400233660ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The map_matcher plugin allows you to provide a string-keyed # hash during route matching, and match any of the keys in the hash # as the next segment in the request path, yielding the corresponding # value in the hash: # # class App < Roda # plugin :map_matcher # # map = { "foo" => "bar", "baz" => "quux" }.freeze # # route do # r.get map: map do |v| # v # # GET /foo => bar # # GET /baz => quux # end # end # end module MapMatcher module RequestMethods private # Match only if the next segment in the path is one of the keys # in the hash, and yield the value of the hash. def match_map(hash) rp = @remaining_path if key = _match_class_String if value = hash[@captures[-1]] @captures[-1] = value true else @remaining_path = rp @captures.pop false end end end end end register_plugin(:map_matcher, MapMatcher) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/match_affix.rb000066400000000000000000000046741516720775400233750ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The match_affix plugin allows changing the default prefix and suffix used for # match patterns. Roda's default behavior for a match pattern like "albums" # is to use the pattern /\A\/(?:albums)(?=\/|\z)/. This prefixes the pattern # with +/+ and suffixes it with (?=\/|\z). With the match_affix plugin, you # can change the prefix and suffix to use. So if you want to be explicit and require # a leading +/+ in patterns, you can set the prefix to "". If you want to # consume a trailing slash instead of leaving it, you can set the suffix to (\/|\z). # # You set the prefix and suffix to use by passing arguments when loading the plugin: # # plugin :match_affix, "" # # will load the plugin and use an empty prefix (instead of a slash). # # plugin :match_affix, "", /(\/|\z)/ # # will use an empty prefix and change the suffix to consume a trailing slash. # # plugin :match_affix, nil, /(?:\/\z|(?=\/|\z))/ # # will not modify the prefix and will change the suffix so that it consumes a trailing slash # at the end of the path only. # # This plugin automatically loads the placeholder_string_matchers plugin. module MatchAffix def self.load_dependencies(app, _prefix, _suffix=nil) app.plugin :placeholder_string_matchers end # Set the default prefix and suffix to use in match patterns, if a non-nil value # is given. def self.configure(app, prefix, suffix=nil) app.opts[:match_prefix] = prefix if prefix app.opts[:match_suffix] = suffix if suffix end module RequestClassMethods private # Use the match prefix and suffix provided when loading the plugin, or fallback # to Roda's default prefix/suffix if one was not provided. def consume_pattern(pattern) /\A#{roda_class.opts[:match_prefix] || "/"}(?:#{pattern})#{roda_class.opts[:match_suffix] || "(?=\/|\z)"}/ end end module RequestMethods private # Use regexps for all string matches, so that the prefix and suffix matches work. def _match_string(str) consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:(\w+)/){|m| _match_symbol_regexp($1)}}) end end end register_plugin(:match_affix, MatchAffix) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/match_hook.rb000066400000000000000000000017741516720775400232360ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The match_hook plugin adds hooks that are called upon a successful match # by any of the matchers. The hooks do not take any arguments. If you would # like hooks that pass the arguments/matchers and values yielded to the route block, # use the match_hook_args plugin. This uses the match_hook_args plugin internally, # but doesn't pass the matchers and values yielded. # # plugin :match_hook # # match_hook do # logger.debug("#{request.matched_path} matched. #{request.remaining_path} remaining.") # end module MatchHook def self.load_dependencies(app) app.plugin :match_hook_args end module ClassMethods # Add a match hook. def match_hook(&block) meth = define_roda_method("match_hook", 0, &block) add_match_hook{|_,_| send(meth)} nil end end end register_plugin :match_hook, MatchHook end end jeremyevans-roda-4f30bb3/lib/roda/plugins/match_hook_args.rb000066400000000000000000000053751516720775400242530ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The match_hook_args plugin adds hooks that are called upon a successful match # by any of the matchers. It is similar to the match_hook plugin, but it allows # for passing the matchers and block arguments for each match method. # # plugin :match_hook_args # # add_match_hook do |matchers, block_args| # logger.debug("matchers: #{matchers.inspect}. #{block_args.inspect} yielded.") # end # # # Term is an implicit matcher used for terminating matches, and # # will be included in the array of matchers yielded to the match hook # # if a terminating match is used. # term = self.class::RodaRequest::TERM # # route do |r| # r.root do # # for a request for / # # matchers: nil, block_args: nil # end # # r.on 'a', ['b', 'c'], Integer do |segment, id| # # for a request for /a/b/1 # # matchers: ["a", ["b", "c"], Integer], block_args: ["b", 1] # end # # r.get 'd' do # # for a request for /d # # matchers: ["d", term], block_args: [] # end # end module MatchHookArgs def self.configure(app) app.opts[:match_hook_args] ||= [] end module ClassMethods # Freeze the array of hook methods when freezing the app def freeze opts[:match_hook_args].freeze super end # Add a match hook that will be called with matchers and block args. def add_match_hook(&block) opts[:match_hook_args] << define_roda_method("match_hook_args", :any, &block) if opts[:match_hook_args].length == 1 class_eval("alias _match_hook_args #{opts[:match_hook_args].first}", __FILE__, __LINE__) else class_eval("def _match_hook_args(v, a); #{opts[:match_hook_args].map{|m| "#{m}(v, a)"}.join(';')} end", __FILE__, __LINE__) end public :_match_hook_args nil end end module InstanceMethods # Default empty method if no match hooks are defined. def _match_hook_args(matchers, block_args) end end module RequestMethods private # Call the match hook with matchers and block args if yielding to the block before yielding to the block. def if_match(v) super do |*a| scope._match_hook_args(v, a) yield(*a) end end # Call the match hook with nil matchers and blocks before yielding to the block def always scope._match_hook_args(nil, nil) super end end end register_plugin :match_hook_args, MatchHookArgs end end jeremyevans-roda-4f30bb3/lib/roda/plugins/middleware.rb000066400000000000000000000171531516720775400232350ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The middleware plugin allows the Roda app to be used as # rack middleware. # # In the example below, requests to /mid will return Mid # by the Mid middleware, and requests to /app will not be # matched by the Mid middleware, so they will be forwarded # to App. # # class Mid < Roda # plugin :middleware # # route do |r| # r.is "mid" do # "Mid" # end # end # end # # class App < Roda # use Mid # # route do |r| # r.is "app" do # "App" # end # end # end # # run App # # By default, when the app is used as middleware and handles the request at # all, it does not forward the request to the next middleware. For the # following setup: # # class Mid < Roda # plugin :middleware # # route do |r| # r.on "foo" do # r.is "mid" do # "Mid" # end # end # end # end # # class App < Roda # use Mid # # route do |r| # r.on "foo" do # r.is "app" do # "App" # end # end # end # end # # run App # # Requests for +/foo/mid will+ return +Mid+, but requests for +/foo/app+ # will return an empty 404 response, because the middleware handles the # +/foo/app+ request in the r.on "foo" do block, but does not # have the block return a result, which Roda treats as an empty 404 response. # If you would like the middleware to forward +/foo/app+ request to the # application, you should use the +:next_if_not_found+ plugin option. # # It is possible to use the Roda app as a regular app even when using # the middleware plugin. Using an app as middleware automatically creates # a subclass of the app for the middleware. Because a subclass is automatically # created when the app is used as middleware, any configuration of the app # should be done before using it as middleware instead of after. # # You can support configurable middleware by passing a block when loading # the plugin: # # class Mid < Roda # plugin :middleware do |middleware, *args, &block| # middleware.opts[:middleware_args] = args # block.call(middleware) # end # # route do |r| # r.is "mid" do # opts[:middleware_args].join(' ') # end # end # end # # class App < Roda # use Mid, :foo, :bar do |middleware| # middleware.opts[:middleware_args] << :baz # end # end # # # Request to App for /mid returns # # "foo bar baz" module Middleware NEXT_PROC = lambda{throw :next, true} private_constant :NEXT_PROC # Configure the middleware plugin. Options: # :env_var :: Set the environment variable to use to indicate to the roda # application that the current request is a middleware request. # You should only need to override this if you are using multiple # roda middleware in the same application. # :handle_result :: Callable object that will be called with request environment # and rack response for all requests passing through the middleware, # after either the middleware or next app handles the request # and returns a response. # :forward_response_headers :: Whether changes to the response headers made inside # the middleware's route block should be applied to the # final response when the request is forwarded to the app. # Defaults to false. # :next_if_not_found :: If the middleware handles the request but returns a not found # result (404 with no body), forward the result to the next middleware. def self.configure(app, opts={}, &block) app.opts[:middleware_env_var] = opts[:env_var] if opts.has_key?(:env_var) app.opts[:middleware_env_var] ||= 'roda.forward_next' app.opts[:middleware_configure] = block if block app.opts[:middleware_handle_result] = opts[:handle_result] app.opts[:middleware_forward_response_headers] = opts[:forward_response_headers] app.opts[:middleware_next_if_not_found] = opts[:next_if_not_found] end # Forwarder instances are what is actually used as middleware. class Forwarder # Make a subclass of +mid+ to use as the current middleware, # and store +app+ as the next middleware to call. def initialize(mid, app, *args, &block) @mid = Class.new(mid) RodaPlugins.set_temp_name(@mid){"#{mid}::middleware_subclass"} if @mid.opts[:middleware_next_if_not_found] @mid.plugin(:not_found, &NEXT_PROC) end if configure = @mid.opts[:middleware_configure] configure.call(@mid, *args, &block) elsif block || !args.empty? raise RodaError, "cannot provide middleware args or block unless loading middleware plugin with a block" end @app = app end # When calling the middleware, first call the current middleware. # If this returns a result, return that result directly. Otherwise, # pass handling of the request to the next middleware. def call(env) res = nil call_next = catch(:next) do env[@mid.opts[:middleware_env_var]] = true res = @mid.call(env) false end if call_next res = @app.call(env) if modified_headers = env.delete('roda.response_headers') res[1] = modified_headers.merge(res[1]) end end if handle_result = @mid.opts[:middleware_handle_result] handle_result.call(env, res) end res end end module ClassMethods # Create a Forwarder instead of a new instance if a non-Hash is given. def new(app, *args, &block) if app.is_a?(Hash) super else Forwarder.new(self, app, *args, &block) end end end module InstanceMethods # Override the route block so that if no route matches, we throw so # that the next middleware is called. Old Dispatch API. def call(&block) super do |r| res = instance_exec(r, &block) # call Fallback if r.forward_next r.env['roda.response_headers'] = response.headers if opts[:middleware_forward_response_headers] throw :next, true end res end end # Override the route block so that if no route matches, we throw so # that the next middleware is called. def _roda_run_main_route(r) res = super if r.forward_next r.env['roda.response_headers'] = response.headers if opts[:middleware_forward_response_headers] throw :next, true end res end end module RequestMethods # Whether to forward the request to the next application. Set only if # this request is being performed for middleware. def forward_next env[roda_class.opts[:middleware_env_var]] end end end register_plugin(:middleware, Middleware) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/middleware_stack.rb000066400000000000000000000066341516720775400244240ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The middleware_stack plugin adds methods to remove middleware # from the middleware stack, and insert new middleware at specific # positions in the middleware stack. # # plugin :middleware_stack # # # Remove csrf middleware # middleware_stack.remove{|m, *args| m == Rack::Csrf} # # # Insert csrf middleware # middleware_stack.before{|m, *args| m == Rack::CommonLogger}.use(Rack::Csrf, raise: true) # middleware_stack.after{|m, *args| m == Rack::CommonLogger}.use(Rack::Csrf, raise: true) module MiddlewareStack # Represents a specific position in the application's middleware stack where new # middleware can be inserted. class StackPosition def initialize(app, middleware, position) @app = app @middleware = middleware @position = position end # Insert a new middleware into the current position in the middleware stack. # Increments the position so that calling this multiple times adds later # middleware after earlier middleware, similar to how +Roda.use+ works. def use(*args, &block) @middleware.insert(@position, [args, block]) @app.send(:build_rack_app) @position += 1 nil end end # Represents the applications middleware as a stack, allowing for easily # removing middleware or finding places to insert new middleware. class Stack def initialize(app, middleware) @app = app @middleware = middleware end # Return a StackPosition representing the position after the middleware where # the block returns true. Yields the middleware and any middleware arguments # given, but not the middleware block. # It the block never returns true, returns a StackPosition that will insert # new middleware at the end of the stack. def after(&block) handle(1, &block) end # Return a StackPosition representing the position before the middleware where # the block returns true. Yields the middleware and any middleware arguments # given, but not the middleware block. # It the block never returns true, returns a StackPosition that will insert # new middleware at the end of the stack. def before(&block) handle(0, &block) end # Removes any middleware where the block returns true. Yields the middleware # and any middleware arguments given, but not the middleware block def remove @middleware.delete_if do |m, _| yield(*m) end @app.send(:build_rack_app) nil end private # Internals of before and after. def handle(offset) @middleware.each_with_index do |(m, _), i| if yield(*m) return StackPosition.new(@app, @middleware, i+offset) end end StackPosition.new(@app, @middleware, @middleware.length) end end module ClassMethods # Return a new Stack that allows removing middleware and inserting # middleware at specific places in the stack. def middleware_stack Stack.new(self, @middleware) end end end register_plugin(:middleware_stack, MiddlewareStack) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/module_include.rb000066400000000000000000000054701516720775400241070ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The module_include plugin adds request_module and response_module class methods # for adding modules/methods to request/response classes. It's designed to make # it easier to add request/response methods for a given roda class. To add a module # to the request or response class: # # Roda.request_module SomeRequestModule # Roda.response_module SomeResponseModule # # Alternatively, you can pass a block to the methods and it will create a module # automatically: # # Roda.request_module do # def description # "#{request_method} #{path_info}" # end # end module ModuleInclude module ClassMethods # Include the given module in the request class. If a block # is provided instead of a module, create a module using the # the block. Example: # # Roda.request_module SomeModule # # Roda.request_module do # def description # "#{request_method} #{path_info}" # end # end # # Roda.route do |r| # r.description # end def request_module(mod = nil, &block) module_include(:request, mod, &block) end # Include the given module in the response class. If a block # is provided instead of a module, create a module using the # the block. Example: # # Roda.response_module SomeModule # # Roda.response_module do # def error! # self.status = 500 # end # end # # Roda.route do |r| # response.error! # end def response_module(mod = nil, &block) module_include(:response, mod, &block) end private # Backbone of the request_module and response_module methods. def module_include(type, mod, &block) if type == :response klass = self::RodaResponse iv = :@response_module else klass = self::RodaRequest iv = :@request_module end if mod raise RodaError, "can't provide both argument and block to response_module" if defined?(yield) klass.send(:include, mod) else if instance_variable_defined?(iv) mod = instance_variable_get(iv) else mod = instance_variable_set(iv, Module.new) RodaPlugins.set_temp_name(mod){"#{klass}::module_include"} klass.send(:include, mod) end mod.module_eval(&block) if block end mod end end end register_plugin(:module_include, ModuleInclude) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/multi_public.rb000066400000000000000000000056431516720775400236110ustar00rootroot00000000000000# frozen-string-literal: true begin require 'rack/files' rescue LoadError require 'rack/file' end # class Roda module RodaPlugins # The multi_public plugin adds an +r.multi_public+ method that accepts an argument specifying # a directory from which to serve static files. It is similar to the public plugin, but # allows for multiple separate directories. # # Here's an example of using the multi_public plugin to serve 3 different types of files # from 3 different directories: # # plugin :multi_public, # img: 'static/images', # font: 'assets/fonts', # form: 'static/forms/pdfs' # # route do # r.on "images" do # r.multi_public(:img) # end # # r.on "fonts" do # r.multi_public(:font) # end # # r.on "forms" do # r.multi_public(:form) # end # end # # It is possible to simplify the routing tree for this using string keys and an array # matcher: # # plugin :multi_public, # 'images' => 'static/images', # 'fonts' => 'assets/fonts', # 'forms' => 'static/forms/pdfs' # # route do # r.on %w"images fonts forms" do |dir| # r.multi_public(dir) # end # end # # You can provide custom headers and default mime type for each directory using an array # of three elements as the value, with the first element being the path, the second # being the custom headers, and the third being the default mime type: # # plugin :multi_public, # 'images' => ['static/images', {'Cache-Control'=>'max-age=86400'}, nil], # 'fonts' => ['assets/fonts', {'Cache-Control'=>'max-age=31536000'}, 'font/ttf'], # 'forms' => ['static/forms/pdfs', nil, 'application/pdf'] # # route do # r.on %w"images fonts forms" do |dir| # r.multi_public(dir) # end # end module MultiPublic RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File def self.load_dependencies(app, _, opts=OPTS) app.plugin(:public, opts) end # Use the given directories to setup servers. Any opts are passed to the public plugin. def self.configure(app, directories, _=OPTS) roots = app.opts[:multi_public_servers] = (app.opts[:multi_public_servers] || {}).dup directories.each do |key, path| path, headers, mime = path roots[key] = RACK_FILES.new(app.expand_path(path), headers||{}, mime||'text/plain') end roots.freeze end module RequestMethods # Serve files from the directory corresponding to the given key if the file exists and # this is a GET request. def multi_public(key) public_serve_with(roda_class.opts[:multi_public_servers].fetch(key)) end end end register_plugin(:multi_public, MultiPublic) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/multi_route.rb000066400000000000000000000113211516720775400234570ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The multi_route plugin builds on the named_routes plugin and allows for # dispatching to multiple named routes # by calling the +r.multi_route+ method, # which will check # if the first segment in the path matches a named route, # and dispatch to that named route. # # The hash_branches plugin offers a +r.hash_branches+ method that is similar to # and performs better than the +r.multi_route+ method, and it is recommended # to consider using that instead of this plugin. # # Example: # # plugin :multi_route # # route('foo') do |r| # r.is 'bar' do # '/foo/bar' # end # end # # route('bar') do |r| # r.is 'foo' do # '/bar/foo' # end # end # # route do |r| # r.multi_route # end # # Note that only named routes with string names will be dispatched to by the # +r.multi_route+ method. Named routes with other names can be dispatched to # using the named_routes plugin API, but will not be automatically dispatched # to by +r.multi_route+. # # You can provide a block to +r.multi_route+ that is # called if the route matches but the named route did not handle the # request: # # r.multi_route do # "default body" # end # # If a block is not provided to multi_route, the return value of the named # route block will be used. # # == Namespace Support # # The multi_route plugin also has support for namespaces, allowing you to # use +r.multi_route+ at multiple levels in your routing tree. Example: # # route('foo') do |r| # r.multi_route('foo') # end # # route('bar') do |r| # r.multi_route('bar') # end # # route('baz', 'foo') do |r| # # handles /foo/baz prefix # end # # route('quux', 'foo') do |r| # # handles /foo/quux prefix # end # # route('baz', 'bar') do |r| # # handles /bar/baz prefix # end # # route('quux', 'bar') do |r| # # handles /bar/quux prefix # end # # route do |r| # r.multi_route # end module MultiRoute def self.load_dependencies(app) app.plugin :named_routes end # Initialize storage for the named routes. def self.configure(app) app::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {}) end module ClassMethods # Freeze the multi_route regexp matchers so that there can be no thread safety issues at runtime. def freeze super opts[:namespaced_routes].each_key do |k| self::RodaRequest.named_route_regexp(k) end self::RodaRequest.instance_variable_get(:@namespaced_route_regexps).freeze self end # Copy the named routes into the subclass when inheriting. def inherited(subclass) super subclass::RodaRequest.instance_variable_set(:@namespaced_route_regexps, {}) end # Clear the multi_route regexp matcher for the namespace. def route(name=nil, namespace=nil, &block) super if name self::RodaRequest.clear_named_route_regexp!(namespace) end end end module RequestClassMethods # Clear cached regexp for named routes, it will be regenerated # the next time it is needed. # # This shouldn't be an issue in production applications, but # during development it's useful to support new named routes # being added while the application is running. def clear_named_route_regexp!(namespace=nil) @namespaced_route_regexps.delete(namespace) end # A regexp matching any of the current named routes. def named_route_regexp(namespace=nil) @namespaced_route_regexps[namespace] ||= /(#{Regexp.union(roda_class.named_routes(namespace).select{|s| s.is_a?(String)}.sort.reverse)})/ end end module RequestMethods # Check if the first segment in the path matches any of the current # named routes. If so, call that named route. If not, do nothing. # If the named route does not handle the request, and a block # is given, yield to the block. def multi_route(namespace=nil) on self.class.named_route_regexp(namespace) do |section| res = route(section, namespace) if defined?(yield) yield else res end end end end end register_plugin(:multi_route, MultiRoute) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/multi_run.rb000066400000000000000000000131051516720775400231270ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The multi_run plugin provides the ability to easily dispatch to other # rack applications based on the request path prefix. # First, load the plugin: # # class App < Roda # plugin :multi_run # end # # Then, other rack applications can register with the multi_run plugin: # # App.run "ra", PlainRackApp # App.run "ro", OtherRodaApp # App.run "si", SinatraApp # # Inside your route block, you can call +r.multi_run+ to dispatch to all # three rack applications based on the prefix: # # App.route do |r| # r.multi_run # end # # This will dispatch routes starting with +/ra+ to +PlainRackApp+, routes # starting with +/ro+ to +OtherRodaApp+, and routes starting with +/si+ to # SinatraApp. # # You can pass a block to +r.multi_run+ that will be called with the prefix, # before dispatching to the rack app: # # App.route do |r| # r.multi_run do |prefix| # # do something based on prefix before the request is passed further # end # end # # This is useful for modifying the environment before passing it to the rack app. # # You can also call +Roda.run+ with a block: # # App.run("ra"){PlainRackApp} # App.run("ro"){OtherRodaApp} # App.run("si"){SinatraApp} # # When called with a block, Roda will call the block to get the app to dispatch to # every time the block is called. The expected usage is with autoloaded classes, # so that the related classes are not loaded until there is a request for the # related route. This can sigficantly speedup startup or testing a subset of the # application. When freezing an application, the blocks are called once to get the # app to dispatch to, and that is cached, to ensure the any autoloads are completed # before the application is frozen. # # The multi_run plugin is similar to the hash_branches and multi_route plugins, with # the difference being the hash_branches and multi_route plugins keep all routing # subtrees in the same Roda app/class, while multi_run dispatches to other rack apps. # If you want to isolate your routing subtrees, multi_run is a better approach, but # it does not let you set instance variables in the main Roda app and have those # instance variables usable in the routing subtrees. # # To handle development environments that reload code, you can call the # +run+ class method without an app to remove dispatching for the prefix. module MultiRun # Initialize the storage for the dispatched applications def self.configure(app) app.opts[:multi_run_apps] ||= {} app.opts[:multi_run_app_blocks] ||= {} end module ClassMethods # Convert app blocks into apps by calling them, in order to force autoloads # and to speed up subsequent calls. # Freeze the multi_run apps so that there can be no thread safety issues at runtime. def freeze app_blocks = opts[:multi_run_app_blocks] apps = opts[:multi_run_apps] app_blocks.each do |prefix, block| apps[prefix] = block.call end app_blocks.clear.freeze apps.freeze self::RodaRequest.refresh_multi_run_regexp! super end # Hash storing rack applications to dispatch to, keyed by the prefix # for the application. def multi_run_apps opts[:multi_run_apps] end # Add a rack application to dispatch to for the given prefix when # r.multi_run is called. If a block is given, it is called every time # there is a request for the route to get the app to call. If neither # a block or an app is provided, any stored route for the prefix is # removed. It is an error to provide both an app and block in the same call. def run(prefix, app=nil, &block) prefix = prefix.to_s if app raise Roda::RodaError, "cannot provide both app and block to Roda.run" if block opts[:multi_run_apps][prefix] = app opts[:multi_run_app_blocks].delete(prefix) elsif block opts[:multi_run_apps].delete(prefix) opts[:multi_run_app_blocks][prefix] = block else opts[:multi_run_apps].delete(prefix) opts[:multi_run_app_blocks].delete(prefix) end self::RodaRequest.refresh_multi_run_regexp! end end module RequestClassMethods # Refresh the multi_run_regexp, using the stored route prefixes, # preferring longer routes before shorter routes. def refresh_multi_run_regexp! @multi_run_regexp = /(#{Regexp.union((roda_class.opts[:multi_run_apps].keys + roda_class.opts[:multi_run_app_blocks].keys).sort.reverse)})/ end # Refresh the multi_run_regexp if it hasn't been loaded yet. def multi_run_regexp @multi_run_regexp || refresh_multi_run_regexp! end end module RequestMethods # If one of the stored route prefixes match the current request, # dispatch the request to the appropriate rack application. def multi_run on self.class.multi_run_regexp do |prefix| yield prefix if defined?(yield) opts = scope.opts run(opts[:multi_run_apps][prefix] || opts[:multi_run_app_blocks][prefix].call) end end end end register_plugin(:multi_run, MultiRun) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/multi_view.rb000066400000000000000000000045351516720775400233040ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The multi_view plugin makes it easy to render multiple views, where the # view template is the same as the matched element. It adds an +r.multi_view+ # method, which takes an argument that is passed to +r.get+, and should # capture a single argument, which is treated as the template name to pass # to +view+. This makes it possible to pass in an array of strings, or a # regexp with a single capture. # # The advantage of using a regexp over an array of strings is that the regexp # is generally faster and uses less memory. However, constructing the regexps # is more cumbersome. To make it easier, the multi_view plugin also offers a # +multi_view_compile+ class method that will take an array of view template # names and construct a regexp that can be passed to +r.multi_view+. # # Example: # # plugin :multi_view # # route do |r| # r.multi_view(['foo', 'bar', 'baz']) # end # # # or: # # route do |r| # r.multi_view(/(foo|bar|baz)/) # end # # # or: # # regexp = multi_view_compile(['foo', 'bar', 'baz']) # route do |r| # r.multi_view(regexp) # end # # # all are equivalent to: # # route do |r| # r.get 'foo' do # view('foo') # end # # r.get 'bar' do # view('bar') # end # # r.get 'baz' do # view('baz') # end # end module MultiView # Depend on the render plugin, since this plugin only makes # sense when the render plugin is used. def self.load_dependencies(app) app.plugin :render end module ClassMethods # Given the array of view template names, return a regexp that will # match on any of the names and capture the matched name. def multi_view_compile(array) /(#{Regexp.union(array)})/ end end module RequestMethods # Pass the argument to +get+, and assume if the matchers match that # there is one capture, and call view with that capture. def multi_view(arg) get(arg) do |page| scope.view(page) end end end end register_plugin(:multi_view, MultiView) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/multibyte_string_matcher.rb000066400000000000000000000031241516720775400262200ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The multibyte_string_matcher plugin allows multibyte # strings to be used in matchers. Roda's default string # matcher does not handle multibyte strings for performance # reasons. # # As browsers send multibyte characters in request paths URL # escaped, so this also loads the unescape_path plugin to # unescape the paths. # # plugin :multibyte_string_matcher # # path = "\xD0\xB8".force_encoding('UTF-8') # route do |r| # r.get path do # # GET /\xD0\xB8 (request.path in UTF-8 format) # end # # r.get /y-(#{path})/u do |x| # # GET /y-\xD0\xB8 (request.path in UTF-8 format) # x => "\xD0\xB8".force_encoding('BINARY') # end # end module MultibyteStringMatcher # Must load unescape_path plugin to decode multibyte # paths, which are submitted escaped. def self.load_dependencies(app) app.plugin :unescape_path end module RequestMethods private # Use multibyte safe string matcher, using the same # approach as in Roda 3.0. def _match_string(str) rp = @remaining_path if rp.start_with?("/#{str}") last = str.length + 1 case rp[last] when "/" @remaining_path = rp[last, rp.length] when nil @remaining_path = "" end end end end end register_plugin(:multibyte_string_matcher, MultibyteStringMatcher) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/named_routes.rb000066400000000000000000000114521516720775400236010ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The named_routes plugin allows for multiple named routes, which the # main route block can dispatch to by name at any point by calling +r.route+. # If the named route doesn't handle the request, execution will continue, # and if the named route does handle the request, the response returned by # the named route will be returned. # # Example: # # plugin :named_routes # # route('foo') do |r| # r.is 'bar' do # '/foo/bar' # end # end # # route('bar') do |r| # r.is 'foo' do # '/bar/foo' # end # end # # route do |r| # r.on "foo" do # r.route 'foo' # end # # r.on "bar" do # r.route 'bar' # end # end # # Note that in multi-threaded code, you should not attempt to add a # named route after accepting requests. # # To handle development environments that reload code, you can call the # +route+ class method without a block to remove an existing named route. # # == Routing Files # # The convention when using the named_routes plugin is to have a single # named route per file, and these routing files should be stored in # a routes subdirectory in your application. So for the above example, you # would use the following files: # # routes/bar.rb # routes/foo.rb # # == Namespace Support # # The named_routes plugin also has support for namespaces, allowing you to # use +r.route+ at multiple levels in your routing tree. Example: # # route('foo') do |r| # r.on("baz"){r.route("baz", "foo")} # r.on("quux"){r.route("quux", "foo")} # end # # route('bar') do |r| # r.on("baz"){r.route("baz", "bar")} # r.on("quux"){r.route("quux", "bar")} # end # # route('baz', 'foo') do |r| # # handles /foo/baz prefix # end # # route('quux', 'foo') do |r| # # handles /foo/quux prefix # end # # route('baz', 'bar') do |r| # # handles /bar/baz prefix # end # # route('quux', 'bar') do |r| # # handles /bar/quux prefix # end # # route do |r| # r.on "foo" do # r.route("foo") # end # # r.on "bar" do # r.route("bar") # end # end # # === Routing Files # # The convention when using namespaces with the multi_route plugin is to # store the routing files in subdirectories per namespace. So for the # above example, you would have the following routing files: # # routes/bar.rb # routes/bar/baz.rb # routes/bar/quux.rb # routes/foo.rb # routes/foo/baz.rb # routes/foo/quux.rb module NamedRoutes # Initialize storage for the named routes. def self.configure(app) app.opts[:namespaced_routes] ||= {} end module ClassMethods # Freeze the namespaced routes so that there can be no thread safety issues at runtime. def freeze opts[:namespaced_routes].freeze.each_value(&:freeze) super end # Copy the named routes into the subclass when inheriting. def inherited(subclass) super nsr = subclass.opts[:namespaced_routes] opts[:namespaced_routes].each{|k, v| nsr[k] = v.dup} end # The names for the currently stored named routes def named_routes(namespace=nil) unless routes = opts[:namespaced_routes][namespace] raise RodaError, "unsupported named_routes namespace used: #{namespace.inspect}" end routes.keys end # Return the named route with the given name. def named_route(name, namespace=nil) opts[:namespaced_routes][namespace][name] end # If the given route has a name, treat it as a named route and # store the route block. Otherwise, this is the main route, so # call super. def route(name=nil, namespace=nil, &block) if name routes = opts[:namespaced_routes][namespace] ||= {} if block routes[name] = define_roda_method(routes[name] || "named_routes_#{namespace}_#{name}", 1, &convert_route_block(block)) elsif meth = routes.delete(name) remove_method(meth) end else super(&block) end end end module RequestMethods # Dispatch to the named route with the given name. def route(name, namespace=nil) scope.send(roda_class.named_route(name, namespace), self) end end end register_plugin(:named_routes, NamedRoutes) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/named_templates.rb000066400000000000000000000055211516720775400242560ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The named_templates plugin allows you to specify templates by name, # providing the template code to use for a given name: # # plugin :named_templates # # template :layout do # "<%= yield %>" # end # template :index do # "

Hello <%= @user %>!

" # end # # route do |r| # @user = 'You' # render(:index) # end # # => "

Hello You!

" # # You can provide options for the template, for example to change the # engine that the template uses: # # template :index, engine: :str do # "

Hello #{@user}!

" # end # # The block you use is reevaluted on every call, allowing you to easily # include additional setup logic: # # template :index do # greeting = ['hello', 'hi', 'howdy'].sample # @user.downcase! # "

#{greating} <%= @user %>!

" # end # # This plugin also works with the view_options plugin, as long as you # prefix the template name with the view subdirectory: # # template "main/index" do # "<%= yield %>" # end # # route do |r| # set_view_subdir("main") # @user = 'You' # render(:index) # end module NamedTemplates # Depend on the render plugin def self.load_dependencies(app) app.plugin :render end # Initialize the storage for named templates. def self.configure(app) app.opts[:named_templates] ||= {} end module ClassMethods # Freeze the named templates so that there can be no thread safety issues at runtime. def freeze opts[:named_templates].freeze super end # Store a new template block and options for the given template name. def template(name, options=nil, &block) opts[:named_templates][name.to_s] = [options, define_roda_method("named_templates_#{name}", 0, &block)].freeze nil end end module InstanceMethods private # If a template name is given and it matches a named template, call # the named template block to get the inline template to use. def find_template(options) if options[:template] && (template_opts, meth = opts[:named_templates][template_name(options)]; meth) if template_opts options = template_opts.merge(options) else options = Hash[options] end options[:inline] = send(meth) super(options) else super end end end end register_plugin(:named_templates, NamedTemplates) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/not_allowed.rb000066400000000000000000000105011516720775400234150ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The not_allowed plugin makes Roda attempt to automatically # support the 405 Method Not Allowed response status. The plugin # changes the +r.get+ and +r.post+ verb methods to automatically # return a 405 status if they are called with any arguments, and # the arguments match but the request method does not match. So # this code: # # r.get '' do # "a" # end # # will return a 200 response for GET / and a 405 # response for POST /. # # This plugin changes the +r.root+ method to return a 405 status # for non-GET requests to +/+. # # This plugin also changes the +r.is+ method so that if you use # a verb method inside +r.is+, it returns a 405 status if none # of the verb methods match. So this code: # # r.is '' do # r.get do # "a" # end # # r.post do # "b" # end # end # # will return a 200 response for GET / and POST /, # but a 405 response for PUT /. # # Note that this plugin will probably not do what you want for # code such as: # # r.get '' do # "a" # end # # r.post '' do # "b" # end # # Since for a POST / request, when +r.get+ method matches # the path but not the request method, it will return an immediate # 405 response. You must DRY up this code for it work correctly, # like this: # # r.is '' do # r.get do # "a" # end # # r.post do # "b" # end # end # # In all cases where it uses a 405 response, it also sets the +Allow+ # header in the response to contain the request methods supported. # # This plugin depends on the all_verbs plugin. module NotAllowed # Depend on the all_verbs plugin, as this plugin overrides methods # defined by it and calls super. def self.load_dependencies(app) app.plugin :all_verbs end module RequestMethods # Keep track of verb calls inside the block. If there are any # verb calls inside the block, but the block returned, assume # that the verb calls inside the block did not match, and # since there was already a successful terminal match, the # request method must not be allowed, so return a 405 # response in that case. def is(*args) super(*args) do begin is_verbs = @_is_verbs = [] ret = if args.empty? yield else yield(*captures) end unless is_verbs.empty? method_not_allowed(is_verbs.join(', ')) end ret ensure @_is_verbs = nil end end end # Treat +r.root+ similar to r.get '', using a 405 # response for non-GET requests. def root super if @remaining_path == "/" && !is_get? always{method_not_allowed("GET")} end end # Setup methods for all verbs. If inside an is block and not given # arguments, record the verb used. If given an argument, add an is # check with the arguments. %w'get post delete head options link patch put trace unlink'.each do |verb| if ::Rack::Request.method_defined?("#{verb}?") class_eval(<<-END, __FILE__, __LINE__+1) def #{verb}(*args, &block) if (empty = args.empty?) && @_is_verbs @_is_verbs << "#{verb.to_s.upcase}" end super unless empty is(*args){method_not_allowed("#{verb.to_s.upcase}")} end end END end end private # Set the response status to 405 (Method Not Allowed), and set the Allow header # to the given string of allowed request methods. def method_not_allowed(verbs) res = response res.status = 405 res[RodaResponseHeaders::ALLOW] = verbs nil end end end register_plugin(:not_allowed, NotAllowed) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/not_found.rb000066400000000000000000000030311516720775400231010ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The not_found plugin adds a +not_found+ class method which sets # a block that is called whenever a 404 response with an empty body # would be returned. The usual use case for this is the desire for # nice error pages if the page is not found. # # You can provide the block with the plugin call: # # plugin :not_found do # "Where did it go?" # end # # Or later via a separate call to +not_found+: # # plugin :not_found # # not_found do # "Where did it go?" # end # # Before not_found is called, any existing headers on the response # will be cleared. So if you want to be sure the headers are set # even in a not_found block, you need to reset them in the # not_found block. # # This plugin is now a wrapper around the +status_handler+ plugin and # still exists mainly for backward compatibility. module NotFound # Require the status_handler plugin def self.load_dependencies(app, &_) app.plugin :status_handler end # If a block is given, install the block as the not_found handler. def self.configure(app, &block) if block app.not_found(&block) end end module ClassMethods # Install the given block as the not_found handler. def not_found(&block) status_handler(404, &block) end end end register_plugin(:not_found, NotFound) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/optimized_segment_matchers.rb000066400000000000000000000034201516720775400265240ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The optimized_segment_matchers plugin adds two optimized matcher methods, # +r.on_segment+ and +r.is_segment+. +r.on_segment+ is an optimized version of # +r.on String+ that accepts no arguments and yields the next segment if there # is a segment. +r.is_segment+ is an optimized version of +r.is String+ that accepts # no arguments and yields the next segment only if it is the last segment. # # plugin :optimized_segment_matchers # # route do |r| # r.on_segment do |x| # # matches any segment (e.g. /a, /b, but not /) # r.is_segment do |y| # # matches only if final segment (e.g. /a/b, /b/c, but not /c, /c/d/, /c/d/e) # end # end # end module OptimizedSegmentMatchers module RequestMethods # Optimized version of +r.on String+ that yields the next segment if there # is a segment. def on_segment rp = @remaining_path if rp.getbyte(0) == 47 if last = rp.index('/', 1) @remaining_path = rp[last, rp.length] always{yield rp[1, last-1]} elsif (len = rp.length) > 1 @remaining_path = "" always{yield rp[1, len]} end end end # Optimized version of +r.is String+ that yields the next segment only if it # is the final segment. def is_segment rp = @remaining_path if rp.getbyte(0) == 47 && !rp.index('/', 1) && (len = rp.length) > 1 @remaining_path = "" always{yield rp[1, len]} end end end end register_plugin(:optimized_segment_matchers, OptimizedSegmentMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/optimized_string_matchers.rb000066400000000000000000000030511516720775400263700ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The optimized_string_matchers plugin adds two optimized matcher methods, # +r.on_branch+ and +r.is_exactly+. +r.on_branch+ is an optimized version of # +r.on+ that only accepts a single string, and +r.is_exactly+ is an # optimized version of +r.is+ that only accepts a single string. # # plugin :optimized_string_matchers # # route do |r| # r.on_branch "x" do # # matches /x and paths starting with /x/ # r.is_exactly "y" do # # matches /x/y # end # end # end # # If you are using the placeholder_string_matchers plugin, note # that both of these methods only work with plain strings, not # with strings with embedded colons for capturing. Matching will work # correctly in such cases, but the captures will not be yielded to the # match blocks. module OptimizedStringMatchers module RequestMethods # Optimized version of +on+ that only supports a single string. def on_branch(s) always{yield} if _match_string(s) end # Optimized version of +is+ that only supports a single string. def is_exactly(s) rp = @remaining_path if _match_string(s) if @remaining_path.empty? always{yield} else @remaining_path = rp end end end end end register_plugin(:optimized_string_matchers, OptimizedStringMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/padrino_render.rb000066400000000000000000000024571516720775400241140ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The padrino_render plugin adds rendering support that is # similar to Padrino's. While not everything Padrino's # rendering supports is supported by this plugin, it # currently handles enough to be a drop in replacement for # some applications. # # plugin :padrino_render, views: 'path/2/views' # # Most notably, this makes the +render+ method default to # using the layout, similar to how the +view+ method works # in the render plugin. If you want to call render and not # use a layout, you can use the layout: false # option: # # render('test') # layout # render('test', layout: false) # no layout # # Note that this plugin loads the :partials plugin. module PadrinoRender # Depend on the render plugin, since this overrides # some of its methods. def self.load_dependencies(app, opts=OPTS) app.plugin :partials, opts end module InstanceMethods # Call view with the given arguments, so that render # uses a layout by default. def render(template, opts=OPTS) view(template, opts) end end end register_plugin(:padrino_render, PadrinoRender) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/param_matchers.rb000066400000000000000000000047331516720775400241060ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The param_matchers plugin adds hash matchers that operate # on the request's params. # # It adds a :param matcher for matching on any param with the # same name, yielding the value of the param: # # r.on param: 'foo' do |foo| # # Matches '?foo=bar', '?foo=' # # Doesn't match '?bar=foo' # end # # It adds a :param! matcher for matching on any non-empty param # with the same name, yielding the value of the param: # # r.on(param!: 'foo') do |foo| # # Matches '?foo=bar' # # Doesn't match '?foo=', '?bar=foo' # end # # It also adds :params and :params! matchers, for matching multiple # params at the same time: # # r.on params: ['foo', 'baz'] do |foo, baz| # # Matches '?foo=bar&baz=quuz', '?foo=&baz=' # # Doesn't match '?foo=bar', '?baz=' # end # # r.on params!: ['foo', 'baz'] do |foo, baz| # # Matches '?foo=bar&baz=quuz' # # Doesn't match '?foo=bar', '?baz=', '?foo=&baz=', '?foo=bar&baz=' # end # # Because users have some control over the types of submitted parameters, # it is recommended that you explicitly force the correct type for values # yielded by the block: # # r.get(:param=>'foo') do |foo| # foo = foo.to_s # end module ParamMatchers module RequestMethods # Match the given parameter if present, even if the parameter is empty. # Adds match to the captures. def match_param(key) if v = params[key.to_s] @captures << v end end # Match the given parameter if present and not empty. # Adds match to the captures. def match_param!(key) if (v = params[key.to_s]) && !v.empty? @captures << v end end # Match all given parameters if present, even if any/all parameters is empty. # Adds all matches to the captures. def match_params(keys) keys.each do |key| return false unless match_param(key) end end # Match all given parameters if present and not empty. # Adds all matches to the captures. def match_params!(keys) keys.each do |key| return false unless match_param!(key) end end end end register_plugin(:param_matchers, ParamMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/params_capturing.rb000066400000000000000000000067471516720775400244660ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The params_capturing plugin makes symbol matchers # update the request params with the value of the captured segments, # using the matcher as the key: # # plugin :params_capturing # # route do |r| # # GET /foo/123/abc/67 # r.on("foo", :bar, :baz, :quux) do # r.params['bar'] #=> '123' # r.params['baz'] #=> 'abc' # r.params['quux'] #=> '67' # end # end # # Note that this updating of the request params using the matcher as # the key is only done if all arguments to the matcher are symbols # or strings. # # All matchers will update the request params by adding all # captured segments to the +captures+ key: # # r.on(:x, /(\d+)\/(\w+)/, :y) do # r.params['x'] #=> nil # r.params['y'] #=> nil # r.params['captures'] #=> ["foo", "123", "abc", "67"] # end # # Note that the request params +captures+ entry will be appended to with # each nested match: # # r.on(:w) do # r.on(:x) do # r.on(:y) do # r.on(:z) do # r.params['captures'] # => ["foo", "123", "abc", "67"] # end # end # end # end # # Note that any existing params captures entry will be overwritten # by this plugin. You can use +r.GET+ or +r.POST+ to get the underlying # entry, depending on how it was submitted. # # This plugin will also handle string matchers with placeholders if # the placeholder_string_matchers plugin is loaded before this plugin. # # Also note that this plugin will not work correctly if you are using # the symbol_matchers plugin with custom symbol matching and are using # symbols that capture multiple values or no values. module ParamsCapturing module RequestMethods # Lazily initialize captures entry when params is called. def params ret = super ret['captures'] ||= [] ret end private # Add the capture names from this string to list of param # capture names if param capturing. def _match_string(str) cap_len = @captures.length if (ret = super) && (pc = @_params_captures) && (cap_len != @captures.length) # Handle use with placeholder_string_matchers plugin pc.concat(str.scan(/(?<=:)\w+/)) end ret end # Add the symbol to the list of param capture names if param capturing. def _match_symbol(sym) if pc = @_params_captures pc << sym.to_s end super end # If all arguments are strings or symbols, turn on param capturing during # the matching, but turn it back off before yielding to the block. Add # any captures to the params based on the param capture names added by # the matchers. def if_match(args) params = self.params if args.all?{|x| x.is_a?(String) || x.is_a?(Symbol)} pc = @_params_captures = [] end super do |*a| if pc @_params_captures = nil pc.zip(a).each do |k,v| params[k] = v end end params['captures'].concat(a) yield(*a) end end end end register_plugin(:params_capturing, ParamsCapturing) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/part.rb000066400000000000000000000041761516720775400220670ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The part plugin adds a part method, which is a # render-like method that treats all keywords as locals. # # # Can replace this: # render(:template, locals: {foo: 'bar'}) # # # With this: # part(:template, foo: 'bar') # # On Ruby 2.7+, the part method takes a keyword splat, so you # must pass keywords and not a positional hash for the locals. # # If you are using the :assume_fixed_locals render plugin option, # template caching is enabled, you are using Ruby 3+, and you # are freezing your Roda application, in addition to providing a # simpler API, this also provides a significant performance # improvement (more significant on Ruby 3.4+). module Part def self.load_dependencies(app) app.plugin :render end module ClassMethods # When freezing, optimize the part method if assuming fixed locals # and caching templates. def freeze if render_opts[:assume_fixed_locals] && !render_opts[:check_template_mtime] include AssumeFixedLocalsInstanceMethods end super end end module InstanceMethods if RUBY_VERSION >= '2.7' class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def part(template, **locals, &block) render(template, :locals=>locals, &block) end RUBY # :nocov: else def part(template, locals=OPTS, &block) render(template, :locals=>locals, &block) end end # :nocov: end module AssumeFixedLocalsInstanceMethods # :nocov: if RUBY_VERSION >= '3.0' # :nocov: class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def part(template, ...) if optimized_method = _optimized_render_method_for_locals(template, OPTS) send(optimized_method[0], ...) else super end end RUBY end end end register_plugin(:part, Part) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/partials.rb000066400000000000000000000043331516720775400227330ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The partials plugin adds a +partial+ method, which renders # templates without the layout. # # plugin :partials, views: 'path/2/views' # # Template files are prefixed with an underscore: # # partial('test') # uses _test.erb # partial('dir/test') # uses dir/_test.erb # # This is basically equivalent to: # # render('_test') # render('dir/_test') # # To render the same template once for each object in an enumerable, # you can use the +render_partials+ method: # # each_partial([1,2,3], :foo) # uses _foo.erb # # This is basically equivalent to: # # render_each([1,2,3], "_foo", local: :foo) # # This plugin depends on the render and render_each plugins. module Partials # Depend on the render plugin, passing received options to it. # Also depend on the render_each plugin. def self.load_dependencies(app, opts=OPTS) app.plugin :render, opts app.plugin :render_each end module InstanceMethods # For each object in the given enumerable, render the given # template (prefixing the template filename with an underscore). def each_partial(enum, template, opts=OPTS) unless opts.has_key?(:local) opts = Hash[opts] opts[:local] = render_each_default_local(template) end render_each(enum, partial_template_name(template.to_s), opts) end # Renders the given template without a layout, but # prefixes the template filename to use with an # underscore. def partial(template, opts=OPTS) opts = parse_template_opts(template, opts) if opts[:template] opts[:template] = partial_template_name(opts[:template]) end render_template(opts) end private # Prefix the template base filename with an underscore. def partial_template_name(template) segments = template.split('/') segments[-1] = "_#{segments[-1]}" segments.join('/') end end end register_plugin(:partials, Partials) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/pass.rb000066400000000000000000000017261516720775400220650ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The pass plugin adds a request +pass+ method to skip the current match # block as if it did not match. # # plugin :pass # # route do |r| # r.on "foo", :bar do |bar| # r.pass if bar == 'baz' # "/foo/#{bar} (not baz)" # end # # r.on "foo/baz" do # "/foo/baz" # end # end module Pass module RequestMethods # Skip the current match block as if it did not match. def pass throw :pass end private # Handle passing inside the match block. def always catch(:pass){super} end # Handle passing inside the match block. def if_match(_) rp = @remaining_path ret = catch(:pass){super} @remaining_path = rp ret end end end register_plugin(:pass, Pass) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/path.rb000066400000000000000000000230051516720775400220450ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The path plugin adds support for named paths. Using the +path+ class method, you can # easily create *_path instance methods for each named path. Those instance # methods can then be called if you need to get the path for a form action, link, # redirect, or anything else. # # Additionally, you can call the +path+ class method with a class and a block, and it will register # the class. You can then call the +path+ instance method with an instance of that class, and it will # execute the block in the context of the route block scope with the arguments provided to path. You # can call the +url+ instance method with the same arguments as the +path+ method to get the full URL. # # Example: # # plugin :path # path :foo, '/foo' # path :bar do |bar| # "/bar/#{bar.id}" # end # path Baz do |baz, *paths| # "/baz/#{baz.id}/#{paths.join('/')}" # end # path Quux do |quux, path| # "/quux/#{quux.id}/#{path}" # end # path 'FooBar', class_name: true do |foobar| # "/foobar/#{foobar.id}" # end # # route do |r| # r.post 'foo' do # r.redirect foo_path # /foo # end # # r.post 'bar' do # bar_params = r.params['bar'] # if bar_params.is_a?(Hash) # bar = Bar.create(bar_params) # r.redirect bar_path(bar) # /bar/1 # end # end # # r.post 'baz' do # baz = Baz[1] # r.redirect path(baz, 'c', 'd') # /baz/1/c/d # end # # r.post 'quux' do # quux = Quux[1] # r.redirect url(quux, '/bar') # http://example.com/quux/1/bar # end # end # # The path class method accepts the following options when not called with a class: # # :add_script_name :: Prefix the path generated with SCRIPT_NAME. This defaults to the app's # :add_script_name option. # :name :: Provide a different name for the method, instead of using *_path. # :relative :: Generate paths relative to the current request instead of absolute paths by prepending # an appropriate prefix. This implies :add_script_name. # :url :: Create a url method in addition to the path method, which will prefix the string generated # with the appropriate scheme, host, and port. If true, creates a *_url # method. If a Symbol or String, uses the value as the url method name. # :url_only :: Do not create a path method, just a url method. # # Note that if :add_script_name, :relative, :url, or :url_only is used, the path method will also create a # _*_path private method. # # If the path class method is passed a string or symbol as the first argument, and the second argument # is a hash with the :class_name option passed, the symbol/string is treated as a class name. # This enables the use of class-based paths without forcing autoloads for the related # classes. If the plugin is not registering classes by name, this will use the symbol or # string to find the related class. module Path DEFAULT_PORTS = {'http' => 80, 'https' => 443}.freeze # Regexp for valid constant names, to prevent code execution. VALID_CONSTANT_NAME_REGEXP = /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/.freeze # Initialize the path classes when loading the plugin. Options: # :by_name :: Register classes by name, which is friendlier when reloading code (defaults to # true in development mode) def self.configure(app, opts=OPTS) app.instance_eval do self.opts[:path_class_by_name] = opts.fetch(:by_name, ENV['RACK_ENV'] == 'development') self.opts[:path_classes] ||= {} self.opts[:path_class_methods] ||= {} unless path_block(String) path(String){|str| str} end end end module ClassMethods # Hash of recognizes classes for path instance method. Keys are classes, values are procs. def path_classes opts[:path_classes] end # Freeze the path classes when freezing the app. def freeze path_classes.freeze opts[:path_classes_methods].freeze super end # Create a new instance method for the named path. See plugin module documentation for options. def path(name, path=nil, opts=OPTS, &block) if name.is_a?(Class) || (path.is_a?(Hash) && (class_name = path[:class_name])) raise RodaError, "can't provide path when calling path with a class" if path && !class_name raise RodaError, "can't provide options when calling path with a class" unless opts.empty? raise RodaError, "must provide a block when calling path with a class" unless block if self.opts[:path_class_by_name] if class_name name = name.to_s else name = name.name end elsif class_name name = name.to_s raise RodaError, "invalid class name passed when using class_name option" unless VALID_CONSTANT_NAME_REGEXP =~ name name = Object.class_eval(name, __FILE__, __LINE__) end path_classes[name] = block self.opts[:path_class_methods][name] = define_roda_method("path_#{name}", :any, &block) return end if path.is_a?(Hash) raise RodaError, "cannot provide two option hashses to Roda.path" unless opts.empty? opts = path path = nil end raise RodaError, "cannot provide both path and block to Roda.path" if path && block raise RodaError, "must provide either path or block to Roda.path" unless path || block if path path = path.dup.freeze block = lambda{path} end meth = opts[:name] || "#{name}_path" url = opts[:url] url_only = opts[:url_only] relative = opts[:relative] add_script_name = opts.fetch(:add_script_name, self.opts[:add_script_name]) if relative if (url || url_only) raise RodaError, "cannot provide :url or :url_only option if using :relative option" end add_script_name = true plugin :relative_path end if add_script_name || url || url_only || relative _meth = "_#{meth}" define_method(_meth, &block) private _meth end unless url_only if relative define_method(meth) do |*a, &blk| # Allow calling private _method to get path relative_path(request.script_name.to_s + send(_meth, *a, &blk)) end # :nocov: ruby2_keywords(meth) if respond_to?(:ruby2_keywords, true) # :nocov: elsif add_script_name define_method(meth) do |*a, &blk| # Allow calling private _method to get path request.script_name.to_s + send(_meth, *a, &blk) end # :nocov: ruby2_keywords(meth) if respond_to?(:ruby2_keywords, true) # :nocov: else define_method(meth, &block) end end if url || url_only url_meth = if url.is_a?(String) || url.is_a?(Symbol) url else "#{name}_url" end url_block = lambda do |*a, &blk| # Allow calling private _method to get path "#{_base_url}#{request.script_name if add_script_name}#{send(_meth, *a, &blk)}" end define_method(url_meth, &url_block) # :nocov: ruby2_keywords(url_meth) if respond_to?(:ruby2_keywords, true) # :nocov: end nil end # Return the block related to the given class, or nil if there is no block. def path_block(klass) # RODA4: Remove if opts[:path_class_by_name] klass = klass.name end path_classes[klass] end end module InstanceMethods # Return a path based on the class of the object. The object passed must have # had its class previously registered with the application. If the app's # :add_script_name option is true, this prepends the SCRIPT_NAME to the path. def path(obj, *args, &block) app = self.class opts = app.opts klass = opts[:path_class_by_name] ? obj.class.name : obj.class unless meth = opts[:path_class_methods][klass] raise RodaError, "unrecognized object given to Roda#path: #{obj.inspect}" end path = send(meth, obj, *args, &block) path = request.script_name.to_s + path if opts[:add_script_name] path end # Similar to #path, but returns a complete URL. def url(*args, &block) "#{_base_url}#{path(*args, &block)}" end private # The string to prepend to the path to make the path a URL. def _base_url r = @_request scheme = r.scheme port = r.port "#{scheme}://#{r.host}#{":#{port}" unless DEFAULT_PORTS[scheme] == port}" end end end register_plugin(:path, Path) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/path_matchers.rb000066400000000000000000000032701516720775400237350ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The path_matchers plugin adds hash matchers that operate # on the request's path. # # It adds a :prefix matcher for matching on the path's prefix, # yielding the rest of the matched segment: # # r.on prefix: 'foo' do |suffix| # # Matches '/foo-bar', yielding '-bar' # # Does not match bar-foo # end # # It adds a :suffix matcher for matching on the path's suffix, # yielding the part of the segment before the suffix: # # r.on suffix: 'bar' do |prefix| # # Matches '/foo-bar', yielding 'foo-' # # Does not match bar-foo # end # # It adds an :extension matcher for matching on the given file extension, # yielding the part of the segment before the extension: # # r.on extension: 'bar' do |reset| # # Matches '/foo.bar', yielding 'foo' # # Does not match bar.foo # end module PathMatchers module RequestMethods # Match when the current segment ends with the given extension. # request path end with the extension. def match_extension(ext) match_suffix(".#{ext}") end # Match when the current path segment starts with the given prefix. def match_prefix(prefix) consume(self.class.cached_matcher([:prefix, prefix]){/#{prefix}([^\\\/]+)/}) end # Match when the current path segment ends with the given suffix. def match_suffix(suffix) consume(self.class.cached_matcher([:suffix, suffix]){/([^\\\/]+)#{suffix}/}) end end end register_plugin(:path_matchers, PathMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/path_rewriter.rb000066400000000000000000000100521516720775400237660ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The path_rewriter plugin allows you to rewrite the remaining path # or the path info for requests. This is useful if you want to # transparently treat some paths the same as other paths. # # By default, +rewrite_path+ will rewrite just the remaining path. So # only routing in the current Roda app will be affected. This is useful # if you have other code in your app that uses PATH_INFO and needs to # see the original PATH_INFO (for example, when using relative links). # # rewrite_path '/a', '/b' # # PATH_INFO '/a' => remaining_path '/b' # # PATH_INFO '/a/c' => remaining_path '/b/c' # # In some cases, you may want to override PATH_INFO for the rewritten # paths, such as when you are passing the request to another Rack app. # For those cases, you can use the path_info: true option to # +rewrite_path+. # # rewrite_path '/a', '/b', path_info: true # # PATH_INFO '/a' => PATH_INFO '/b' # # PATH_INFO '/a/c' => PATH_INFO '/b/c' # # If you pass a string to +rewrite_path+, it will rewrite all paths starting # with that string. You can provide a regexp if you want more complete control, # such as only matching exact paths. # # rewrite_path /\A\/a\z/, '/b' # # PATH_INFO '/a' => remaining_path '/b' # # PATH_INFO '/a/c' => remaining_path '/a/c', no change # # Patterns can be rewritten dynamically by providing a block accepting a MatchData # object and evaluating to the replacement. # # rewrite_path(/\A\/a\/(\w+)/){|match| "/a/#{match[1].capitalize}"} # # PATH_INFO '/a/moo' => remaining_path '/a/Moo' # rewrite_path(/\A\/a\/(\w+)/, path_info: true){|match| "/a/#{match[1].capitalize}"} # # PATH_INFO '/a/moo' => PATH_INFO '/a/Moo' # # All path rewrites are applied in order, so if a path is rewritten by one rewrite, # it can be rewritten again by a later rewrite. Note that PATH_INFO rewrites are # processed before remaining_path rewrites. module PathRewriter def self.configure(app) app.instance_exec do app.opts[:remaining_path_rewrites] ||= [] app.opts[:path_info_rewrites] ||= [] end end module ClassMethods # Freeze the path rewrite metadata. def freeze opts[:remaining_path_rewrites].freeze opts[:path_info_rewrites].freeze super end # Record a path rewrite from path +was+ to path +is+. Options: # :path_info :: Modify PATH_INFO, not just remaining path. def rewrite_path(was, is = nil, opts=OPTS, &block) if is.is_a? Hash raise RodaError, "cannot provide two hashes to rewrite_path" unless opts.empty? opts = is is = nil end if block raise RodaError, "cannot provide both block and string replacement to rewrite_path" if is is = block end was = /\A#{Regexp.escape(was)}/ unless was.is_a?(Regexp) array = @opts[opts[:path_info] ? :path_info_rewrites : :remaining_path_rewrites] array << [was, is.dup.freeze].freeze end end module RequestMethods # Rewrite remaining_path and/or PATH_INFO based on the path rewrites. def initialize(scope, env) path_info = env['PATH_INFO'] rewrite_path(scope.class.opts[:path_info_rewrites], path_info) super remaining_path = @remaining_path = @remaining_path.dup rewrite_path(scope.class.opts[:remaining_path_rewrites], remaining_path) end private # Rewrite the given path using the given replacements. def rewrite_path(replacements, path) replacements.each do |was, is| if is.is_a?(Proc) path.sub!(was){is.call($~)} else path.sub!(was, is) end end end end end register_plugin(:path_rewriter, PathRewriter) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/permissions_policy.rb000066400000000000000000000237621516720775400250550ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # A permissions_policy plugin has been added that allows you to easily set a # Permissions-Policy header for the application, which browsers can use to # determine whether to allow specific functionality on the returned page # (mainly related to which JavaScript APIs the page is allowed to use). # # You would generally call the plugin with a block to set the default policy: # # plugin :permissions_policy do |pp| # pp.camera :none # pp.fullscreen :self # pp.clipboard_read :self, 'https://example.com' # end # # Then, anywhere in the routing tree, you can customize the policy for just that # branch or action using the same block syntax: # # r.get 'foo' do # permissions_policy do |pp| # pp.camera :self # end # # ... # end # # In addition to using a block, you can also call methods on the object returned # by the method: # # r.get 'foo' do # permissions_policy.camera :self # # ... # end # # You can use the :default plugin option to set the default for all settings. # For example, to disallow all access for each setting by default: # # plugin :permissions_policy, default: :none # # The following methods are available for configuring the permissions policy, # which specify the setting (substituting _ with -): # # * accelerometer # * ambient_light_sensor # * autoplay # * bluetooth # * camera # * clipboard_read # * clipboard_write # * display_capture # * encrypted_media # * fullscreen # * geolocation # * gyroscope # * hid # * idle_detection # * keyboard_map # * magnetometer # * microphone # * midi # * payment # * picture_in_picture # * publickey_credentials_get # * screen_wake_lock # * serial # * sync_xhr # * usb # * web_share # * window_management # # All of these methods support any number of arguments, and each argument should # be one of the following values: # # :all :: Grants permission to all domains (must be only argument) # :none :: Does not allow permission at all (must be only argument) # :self :: Allows feature in current document and any nested browsing contexts # that use the same domain as the current document. # :src :: Allows feature in current document and any nested browsing contexts # that use the same domain as the src of the iframe. # String :: Specifies origin domain where access is allowed # # When calling a method with no arguments, the setting is removed from the policy instead # of being left empty, since all of these setting require at least one value. Likewise, # if the policy does not have any settings, the header will not be added. # # Calling the method overrides any previous setting. Each of the methods has +add_*+ and # +get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method # returns the current value for the setting (this will be +:all+ if all domains are allowed, or # any array of strings/:self/:src). # # permissions_policy.fullscreen :self, 'https://example.com' # # fullscreen (self "https://example.com") # # permissions_policy.add_fullscreen 'https://*.example.com' # # fullscreen (self "https://example.com" "https://*.example.com") # # permissions_policy.get_fullscreen # # => [:self, "https://example.com", "https://*.example.com"] # # The clear method can be used to remove all settings from the policy. Empty policies # do not set any headers. You can use +response.skip_permissions_policy!+ to skip # setting a policy. This is faster than calling +permissions_policy.clear+, since # it does not duplicate the default policy. module PermissionsPolicy SUPPORTED_SETTINGS = %w' accelerometer ambient-light-sensor autoplay bluetooth camera clipboard-read clipboard-write display-capture encrypted-media fullscreen geolocation gyroscope hid idle-detection keyboard-map magnetometer microphone midi payment picture-in-picture publickey-credentials-get screen-wake-lock serial sync-xhr usb web-share window-management '.each(&:freeze).freeze private_constant :SUPPORTED_SETTINGS # Represents a permissions policy. class Policy SUPPORTED_SETTINGS.each do |setting| meth = setting.tr('-', '_').freeze # Setting method name sets the setting value, or removes it if no args are given. define_method(meth) do |*args| if args.empty? @opts.delete(setting) else @opts[setting] = option_value(args) end nil end # add_* method name adds to the setting value, or clears setting if no values # are given. define_method(:"add_#{meth}") do |*args| unless args.empty? case v = @opts[setting] when :all # If all domains are already allowed, there is no reason to add more. return when Array @opts[setting] = option_value(v + args) else @opts[setting] = option_value(args) end end nil end # get_* method always returns current setting value. define_method(:"get_#{meth}") do @opts[setting] end end def initialize clear end # Clear all settings, useful to remove any inherited settings. def clear @opts = {} end # Do not allow future modifications to any settings. def freeze @opts.freeze header_value.freeze super end # The header name to use, depends on whether report only mode has been enabled. def header_key @report_only ? RodaResponseHeaders::PERMISSIONS_POLICY_REPORT_ONLY : RodaResponseHeaders::PERMISSIONS_POLICY end # The header value to use. def header_value return @header_value if @header_value s = String.new @opts.each do |k, vs| s << k << "=" if vs == :all s << '*, ' else s << '(' vs.each{|v| append_formatted_value(s, v)} s.chop! unless vs.empty? s << '), ' end end s.chop! s.chop! @header_value = s end # Set whether the Permissions-Policy-Report-Only header instead of the # default Permissions-Policy header. def report_only(report=true) @report_only = report end # Whether this policy uses report only mode. def report_only? !!@report_only end # Set the current policy in the headers hash. If no settings have been made # in the policy, does not set a header. def set_header(headers) return if @opts.empty? headers[header_key] ||= header_value end private # Formats nested values, quoting strings and using :self and :src verbatim. def append_formatted_value(s, v) case v when String s << v.inspect << ' ' when :self s << 'self ' when :src s << 'src ' else raise RodaError, "unsupported Permissions-Policy item value used: #{v.inspect}" end end # Make object copy use copy of settings, and remove cached header value. def initialize_copy(_) super @opts = @opts.dup @header_value = nil end # The option value to store for the given args. def option_value(args) if args.length == 1 case args[0] when :all :all when :none EMPTY_ARRAY else args.freeze end else args.freeze end end end # Yield the current Permissions Policy to the block. def self.configure(app, opts=OPTS) policy = app.opts[:permissions_policy] = if policy = app.opts[:permissions_policy] policy.dup else Policy.new end if default = opts[:default] SUPPORTED_SETTINGS.each do |setting| policy.send(setting.tr('-', '_'), *default) end end yield policy if defined?(yield) policy.freeze end module InstanceMethods # If a block is given, yield the current permission policy. Returns the # current permissions policy. def permissions_policy policy = @_response.permissions_policy yield policy if defined?(yield) policy end end module ResponseMethods # Unset any permissions policy when reinitializing def initialize super @permissions_policy &&= nil end # The current permissions policy to be used for this response. def permissions_policy @permissions_policy ||= roda_class.opts[:permissions_policy].dup end # Do not set a permissions policy header for this response. def skip_permissions_policy! @skip_permissions_policy = true end private # Set the appropriate permissions policy header. def set_default_headers super unless @skip_permissions_policy (@permissions_policy || roda_class.opts[:permissions_policy]).set_header(headers) end end end end register_plugin(:permissions_policy, PermissionsPolicy) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/placeholder_string_matchers.rb000066400000000000000000000026761516720775400266620ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The placeholder_string_matcher plugin exists for backwards compatibility # with previous versions of Roda that allowed placeholders inside strings # if they were prefixed by colons: # # plugin :placeholder_string_matchers # # route do |r| # r.is("foo/:bar") |v| # # matches foo/baz, yielding "baz" # # does not match foo, foo/, or foo/baz/ # end # end # # It is not recommended to use this in new applications, and it is encouraged # to use separate string class or symbol matchers instead: # # r.is "foo", String # r.is "foo", :bar # # If used with the symbol_matchers plugin, this plugin respects the regexps # for the registered symbols, but it does not perform the conversions, the # captures for the regexp are used directly as the captures for the match method. module PlaceholderStringMatchers def self.load_dependencies(app) app.plugin :_symbol_regexp_matchers end module RequestMethods private def _match_string(str) if str.index(":") consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:(\w+)/){|m| _match_symbol_regexp($1)}}) else super end end end end register_plugin(:placeholder_string_matchers, PlaceholderStringMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/plain_hash_response_headers.rb000066400000000000000000000017651516720775400266410ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The plain_hash_response_headers plugin will change Roda to # use a plain hash for response headers. This is Roda's # default behavior on Rack 2, but on Rack 3+, Roda defaults # to using Rack::Headers for response headers for backwards # compatibility (Rack::Headers automatically lower cases header # keys). # # On Rack 3+, you should use this plugin for better performance # if you are sure all headers in your application and middleware # are already lower case (lower case response header keys are # required by the Rack 3 spec). module PlainHashResponseHeaders if defined?(Rack::Headers) && Rack::Headers.is_a?(Class) module ResponseMethods private # Use plain hash for headers def _initialize_headers {} end end end end register_plugin(:plain_hash_response_headers, PlainHashResponseHeaders) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/precompile_templates.rb000066400000000000000000000140441516720775400253310ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The precompile_templates plugin adds support for precompiling template code. # This can result in a large memory savings for applications that have large # templates or a large number of small templates if the application uses a # forking webserver. By default, template compilation is lazy, so all the # child processes in a forking webserver will have their own copy of the # compiled template. By using the precompile_templates plugin, you can # precompile the templates in the parent process before forking, and then # all of the child processes can use the same precompiled templates, which # saves memory. # # Another advantage of the precompile_templates plugin is that after # template precompilation, access to the template file in the file system is # no longer needed, so this can be used with security features that do not # allow access to the template files at runtime. # # After loading the plugin, you should call precompile_views with an array # of views to compile, using the same argument you are passing to view or # render: # # plugin :precompile_templates # precompile_views %w'view1 view2' # # If the view requires local variables, you should call precompile_views with a second # argument for the local variables: # # plugin :precompile_templates # precompile_views :view3, [:local_var1, :local_var2] # # After all templates are precompiled, you can optionally use freeze_template_caches!, # which will freeze the template caches so that any template compilation at runtime # will result in an error. This also speeds up template cache access, since the # template caches no longer need a mutex. # # freeze_template_caches! # # Note that you should use Tilt 2.0.1+ if you are using this plugin, so # that locals are handled in the same order. module PrecompileTemplates # Load the render plugin as precompile_templates depends on it. def self.load_dependencies(app, opts=OPTS) app.plugin :render end module ClassMethods # Freeze the template caches. Should be called after precompiling all templates during # application startup, if you don't want to allow templates to be cached at runtime. # In addition to ensuring that no templates are compiled at runtime, this also speeds # up rendering by freezing the template caches, so that a mutex is not needed to access # them. def freeze_template_caches! _freeze_layout_method opts[:render] = render_opts.merge( :cache=>render_opts[:cache].freeze, :template_method_cache=>render_opts[:template_method_cache].freeze, ).freeze self::RodaCompiledTemplates.freeze nil end # Precompile the templates using the given options. Note that this doesn't # handle optimized template methods supported in newer versions of Roda, but # there are still cases where makes sense to use it. # # You can call +precompile_templates+ with the pattern of templates you would # like to precompile: # # precompile_templates "views/**/*.erb" # # That will precompile all erb template files in the views directory or # any subdirectory. # # If the templates use local variables, you need to specify which local # variables to precompile, which should be an array of symbols: # # precompile_templates 'views/users/_*.erb', locals: [:user] # # You can specify other render options when calling +precompile_templates+, # including +:cache_key+, +:template_class+, and +:template_opts+. If you # are passing any of those options to render/view for the template, you # should pass the same options when precompiling the template. # # To compile inline templates, just pass a single hash containing an :inline # to +precompile_templates+: # # precompile_templates inline: some_template_string def precompile_templates(pattern, opts=OPTS) if pattern.is_a?(Hash) opts = pattern.merge(opts) end if locals = opts[:locals] locals.sort! else locals = EMPTY_ARRAY end compile_opts = if pattern.is_a?(Hash) [opts] else Dir[pattern].map{|file| opts.merge(:path=>File.expand_path(file, nil))} end instance = allocate compile_opts.each do |compile_opt| template = instance.send(:retrieve_template, compile_opt) begin Render.tilt_template_compiled_method(template, locals, self) rescue NotImplementedError # When freezing template caches, you may want to precompile a template for a # template type that doesn't support template precompilation, just to populate # the cache. Tilt rescues NotImplementedError in this case, which we can ignore. nil end end nil end # Precompile the given views with the given locals, handling optimized template methods. def precompile_views(views, locals=EMPTY_ARRAY) instance = allocate views = Array(views) if locals.empty? opts = OPTS else locals_hash = {} locals.each{|k| locals_hash[k] = nil} opts = {:locals=>locals_hash} end views.each do |view| instance.send(:retrieve_template, instance.send(:render_template_opts, view, opts)) end if locals_hash views.each do |view| instance.send(:_optimized_render_method_for_locals, view, locals_hash) end end nil end end end register_plugin(:precompile_templates, PrecompileTemplates) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/public.rb000066400000000000000000000152441516720775400223750ustar00rootroot00000000000000# frozen-string-literal: true require 'uri' begin require 'rack/files' rescue LoadError require 'rack/file' end # class Roda module RodaPlugins # The public plugin adds a +r.public+ routing method to serve static files # from a directory. # # The public plugin recognizes the application's :root option, and defaults to # using the +public+ subfolder of the application's +:root+ option. If the application's # :root option is not set, it defaults to the +public+ folder in the working # directory. Additionally, if a relative path is provided as the +:root+ # option to the plugin, it will be considered relative to the application's # +:root+ option. # # Examples: # # # Use public folder as location of files # plugin :public # # # Use /path/to/app/static as location of files # opts[:root] = '/path/to/app' # plugin :public, root: 'static' # # # Assuming public is the location of files # route do # # Make GET /images/foo.png look for public/images/foo.png # r.public # # # Make GET /static/images/foo.png look for public/images/foo.png # r.on(:static) do # r.public # end # end module Public SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact) RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File ENCODING_MAP = {:zstd=>'zstd', :brotli=>'br', :gzip=>'gzip'}.freeze ENCODING_EXTENSIONS = {'br'=>'.br', 'gzip'=>'.gz', 'zstd'=>'.zst'}.freeze # :nocov: PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER MATCH_METHOD = RUBY_VERSION >= '2.4' ? :match? : :match # :nocov: # Use options given to setup a Rack::File instance for serving files. Options: # :brotli :: Whether to serve already brotli-compressed files with a .br extension # for clients supporting "br" transfer encoding. # :default_mime :: The default mime type to use if the mime type is not recognized. # :encodings :: An enumerable of pairs to handle accepted encodings. The first # element of the pair is the accepted encoding name (e.g. 'gzip'), # and the second element of the pair is the file extension (e.g. # '.gz'). This allows configuration of the order in which encodings # are tried, to prefer brotli to zstd for example, or to support # encodings other than zstd, brotli, and gzip. This takes # precedence over the :brotli, :gzip, and :zstd options if given. # :gzip :: Whether to serve already gzipped files with a .gz extension for clients # supporting "gzip" transfer encoding. # :headers :: A hash of headers to use for statically served files # :root :: Use this option for the root of the public directory (default: "public") # :zstd :: Whether to serve already zstd-compressed files with a .zst extension # for clients supporting "zstd" transfer encoding. def self.configure(app, opts={}) if opts[:root] app.opts[:public_root] = app.expand_path(opts[:root]) elsif !app.opts[:public_root] app.opts[:public_root] = app.expand_path("public") end app.opts[:public_server] = RACK_FILES.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain') unless encodings = opts[:encodings] if ENCODING_MAP.any?{|k,| opts.has_key?(k)} encodings = ENCODING_MAP.map{|k, v| [v, ENCODING_EXTENSIONS[v]] if opts[k]}.compact end end encodings = (encodings || app.opts[:public_encodings] || EMPTY_ARRAY).map(&:dup).freeze encodings.each do |a| a << /\b#{a[0]}\b/ end encodings.each(&:freeze) app.opts[:public_encodings] = encodings end module RequestMethods # Serve files from the public directory if the file exists and this is a GET request. def public public_serve_with(roda_class.opts[:public_server]) end private # Return an array of segments for the given path, handling .. # and . components def public_path_segments(path) segments = [] path.split(SPLIT).each do |seg| next if seg.empty? || seg == '.' seg == '..' ? segments.pop : segments << seg end segments end # Return whether the given path is a readable regular file. def public_file_readable?(path) ::File.file?(path) && ::File.readable?(path) rescue SystemCallError # :nocov: false # :nocov: end def public_serve_with(server) return unless is_get? path = PARSER.unescape(real_remaining_path) return if path.include?("\0") roda_opts = roda_class.opts path = ::File.join(server.root, *public_path_segments(path)) if accept_encoding = env['HTTP_ACCEPT_ENCODING'] roda_opts[:public_encodings].each do |enc, ext, regexp| if regexp.send(MATCH_METHOD, accept_encoding) public_serve_compressed(server, path, ext, enc) end end end if public_file_readable?(path) s, h, b = public_serve(server, path) headers = response.headers headers.replace(h) halt [s, headers, b] end end # Serve the compressed file if it exists. This should only # be called if the client will accept the related encoding. def public_serve_compressed(server, path, suffix, encoding) compressed_path = path + suffix if public_file_readable?(compressed_path) s, h, b = public_serve(server, compressed_path) headers = response.headers headers.replace(h) unless s == 304 headers[RodaResponseHeaders::CONTENT_TYPE] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain') headers[RodaResponseHeaders::CONTENT_ENCODING] = encoding end halt [s, headers, b] end end if ::Rack.release > '2' # Serve the given path using the given Rack::Files server. def public_serve(server, path) server.serving(self, path) end else def public_serve(server, path) server = server.dup server.path = path server.serving(env) end end end end register_plugin(:public, Public) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/r.rb000066400000000000000000000012721516720775400213540ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The r plugin adds an +r+ instance method that will return the request. # This allows you to use common Roda idioms such as +r.halt+ and # +r.redirect+ even when +r+ isn't a local variable in scope. Example: # # plugin :r # # def foo # r.redirect "/bar" # end # # route do |r| # r.get "foo" do # foo # end # r.get "bar" do # "bar" # end # end module R module InstanceMethods # The request object. def r @_request end end end register_plugin(:r, R) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/recheck_precompiled_assets.rb000066400000000000000000000075001516720775400264640ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The recheck_precompiled_assets plugin enables checking for the precompiled asset metadata file. # You need to have already loaded the assets plugin with the +:precompiled+ option and the file # specified by the +:precompiled+ option must already exist in order to use the # recheck_precompiled_assets plugin. # # Any time you want to check whether the precompiled asset metadata file has changed and should be # reloaded, you can call the +recheck_precompiled_assets+ class method. This method will check # whether the file has changed, and reload it if it has. If you want to check for modifications on # every request, you can use +self.class.recheck_precompiled_assets+ inside your route block. module RecheckPrecompiledAssets # Thread safe wrapper for the compiled asset metadata hash. Does not wrap all # hash methods, only a few that are used. class CompiledAssetsHash include Enumerable def initialize @hash = {} @mutex = Mutex.new end def [](key) @mutex.synchronize{@hash[key]} end def []=(key, value) @mutex.synchronize{@hash[key] = value} end def replace(hash) hash = hash.instance_variable_get(:@hash) if (CompiledAssetsHash === hash) @mutex.synchronize{@hash.replace(hash)} self end def each(&block) @mutex.synchronize{@hash.dup}.each(&block) self end def to_json(*args) @mutex.synchronize{@hash.dup}.to_json(*args) end end def self.load_dependencies(app) unless app.respond_to?(:assets_opts) && app.assets_opts[:precompiled] raise RodaError, "must load assets plugin with precompiled option before loading recheck_precompiled_assets plugin" end end def self.configure(app) precompiled_file = app.assets_opts[:precompiled] prev_mtime = ::File.mtime(precompiled_file) app.instance_exec do opts[:assets] = opts[:assets].merge(:compiled=>_compiled_assets_initial_hash.replace(assets_opts[:compiled])).freeze define_singleton_method(:recheck_precompiled_assets) do new_mtime = ::File.mtime(precompiled_file) if new_mtime != prev_mtime prev_mtime = new_mtime assets_opts[:compiled].replace(_precompiled_asset_metadata(precompiled_file)) # Unset the cached asset matchers, so new ones will be generated. # This is needed in case the new precompiled metadata uses # different files. app::RodaRequest.instance_variable_set(:@assets_matchers, nil) end end singleton_class.send(:alias_method, :recheck_precompiled_assets, :recheck_precompiled_assets) end end module ClassMethods private # Wrap the precompiled asset metadata in a thread-safe hash. def _precompiled_asset_metadata(file) CompiledAssetsHash.new.replace(super) end # Use a thread-safe wrapper of a hash for the :compiled assets option, since # the recheck_precompiled_asset_metadata can modify it at runtime. def _compiled_assets_initial_hash CompiledAssetsHash.new end end module RequestClassMethods private # Use a regexp that matches any digest. When the precompiled asset metadata # file is updated, this allows requests for a previous precompiled asset to # still work. def _asset_regexp(type, key, _) /#{Regexp.escape(key.sub(/\A#{type}/, ''))}\.[0-9a-fA-F]+\.#{type}/ end end end register_plugin(:recheck_precompiled_assets, RecheckPrecompiledAssets) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/redirect_http_to_https.rb000066400000000000000000000077301516720775400257040ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The redirect_http_to_https plugin exposes a +redirect_http_to_https+ # request method that redirects HTTP requests to HTTPS, helping to ensure # that future requests by the same browser will be submitted securely. # # You should use this plugin if you have an application that can receive # requests using both HTTP and HTTPS, and you want to make sure that all # or a subset of routes are only handled for HTTPS requests. # # The reason this exposes a request method is so that you can choose where # in your routing tree to do the redirection: # # route do |r| # # routes available via both HTTP and HTTPS # r.redirect_http_to_https # # routes available only via HTTPS # end # # If you want to redirect to HTTPS for all routes in the routing tree, you # can have this as the very first method call in the routing tree. Note that # in Roda it is possible to handle routing before the normal routing tree # using before hooks. The static_routing and heartbeat plugins use this # feature. If you would like to handle routes before the normal routing tree, # you can setup a before hook: # # plugin :hooks # # before do # request.redirect_http_to_https # end module RedirectHttpToHttps status_map = Hash.new(307) status_map['GET'] = status_map['HEAD'] = 301 status_map.freeze DEFAULTS = {:status_map => status_map}.freeze private_constant :DEFAULTS # Configures redirection from HTTP to HTTPS. Available options: # # :body :: The body used in the redirect. If not set, uses an empty body. # :headers :: Any additional headers used in the redirect response. By default, # no additional headers are set, the only header used is the Location header. # :host :: The host to redirect to. If not set, redirects to the same host as the HTTP # requested to. It is highly recommended that you set this if requests with # arbitrary Host headers can be submitted to the application. # :port :: The port to use in the redirect. By default, will not set an explicit port, # so that it will implicitly use the HTTPS default port of 443. # :status_map :: A hash mapping request methods to response status codes. By default, # uses a hash that redirects GET and HEAD requests with a 301 status, # and other request methods with a 307 status. def self.configure(app, opts=OPTS) previous = app.opts[:redirect_http_to_https] || DEFAULTS opts = app.opts[:redirect_http_to_https] = previous.merge(opts) opts[:port_string] = opts[:port] ? ":#{opts[:port]}".freeze : "".freeze opts[:prefix] = opts[:host] ? "https://#{opts[:host]}#{opts[:port_string]}".freeze : nil opts.freeze end module RequestMethods # Redirect HTTP requests to HTTPS. While this doesn't secure the # current request, it makes it more likely that the browser will submit # future requests securely via HTTPS. def redirect_http_to_https return if ssl? opts = roda_class.opts[:redirect_http_to_https] res = response if body = opts[:body] res.write(body) end if headers = opts[:headers] res.headers.merge!(headers) end path = if prefix = opts[:prefix] prefix + fullpath else "https://#{host}#{opts[:port_string]}#{fullpath}" end unless status = opts[:status_map][@env['REQUEST_METHOD']] raise RodaError, "redirect_http_to_https :status_map provided does not support #{@env['REQUEST_METHOD']}" end redirect(path, status) end end end register_plugin(:redirect_http_to_https, RedirectHttpToHttps) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/redirect_path.rb000066400000000000000000000026741516720775400237370ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The redirect_path plugin builds on top of the path plugin, # and allows the +r.redirect+ method to be passed a non-string # object that will be passed to +path+, and redirect to the # result of +path+. # # In the second argument, you can provide a suffix to the # generated path. However, in this case you cannot provide a # non-default redirect status in the same call). # # Example: # # Foo = Struct.new(:id) # foo = Foo.new(1) # # plugin :redirect_path # path Foo do |foo| # "/foo/#{foo.id}" # end # # route do |r| # r.get "example" do # # redirects to /foo/1 # r.redirect(foo) # end # # r.get "suffix-example" do # # redirects to /foo/1/status # r.redirect(foo, "/status") # end # end module RedirectPath def self.load_dependencies(app) app.plugin :path end module RequestMethods def redirect(path=default_redirect_path, status=default_redirect_status) if String === path super else path = scope.path(path) if status.is_a?(String) super(path + status) else super end end end end end register_plugin(:redirect_path, RedirectPath) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/relative_path.rb000066400000000000000000000044021516720775400237400ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The relative_path plugin adds a relative_path method that accepts # an absolute path and returns a path relative to the current request # by adding an appropriate prefix: # # plugin :relative_path # route do |r| # relative_path("/foo") # end # # # GET / # "./foo" # # # GET /bar # "./foo" # # # GET /bar/ # "../foo" # # # GET /bar/baz/quux # "../../foo" # # It also offers a relative_prefix method that returns a string that can # be prepended to an absolute path. This can be more efficient if you # need to convert multiple paths. # # This plugin is mostly designed for applications using Roda as a static # site generator, where the generated site can be hosted at any subpath. module RelativePath module InstanceMethods # Return a relative path for the absolute path based on the current path # of the request by adding the appropriate prefix. def relative_path(absolute_path) relative_prefix + absolute_path end # Return a relative prefix to append to an absolute path to a relative path # based on the current path of the request. def relative_prefix return @_relative_prefix if @_relative_prefix env = @_request.env script_name = env["SCRIPT_NAME"] path_info = env["PATH_INFO"] # Check path begins with slash. All valid paths should, but in case this # request is bad, just skip using a relative prefix. case script_name.getbyte(0) when nil # SCRIPT_NAME empty unless path_info.getbyte(0) == 47 # PATH_INFO starts with / return(@_relative_prefix = '') end when 47 # SCRIPT_NAME starts with / # nothing else return(@_relative_prefix = '') end slash_count = script_name.count('/') + path_info.count('/') @_relative_prefix = if slash_count > 1 ("../" * (slash_count - 2)) << ".." else "." end end end end register_plugin(:relative_path, RelativePath) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/render.rb000066400000000000000000001225001516720775400223700ustar00rootroot00000000000000# frozen-string-literal: true require "tilt" class Roda module RodaPlugins # The render plugin adds support for template rendering using the tilt # library. Two methods are provided for template rendering, +view+ # (which uses the layout) and +render+ (which does not). # # plugin :render # # route do |r| # r.is 'foo' do # view('foo') # renders views/foo.erb inside views/layout.erb # end # # r.is 'bar' do # render('bar') # renders views/bar.erb # end # end # # The +render+ and +view+ methods just return strings, they do not have # side effects (unless the templates themselves have side effects). # As Roda uses the routing block return value as the body of the response, # in most cases you will call these methods as the last expression in a # routing block to have the response body be the result of the template # rendering. # # Because +render+ and +view+ just return strings, you can call them inside # templates (i.e. for subtemplates/partials), or multiple times in the # same route and combine the results together: # # route do |r| # r.is 'foo-bars' do # @bars = Bar.where(:foo).map{|b| render(:bar, locals: {bar: b})}.join # view('foo') # end # end # # You can provide options to the plugin method: # # plugin :render, engine: 'haml', views: 'admin_views' # # = Plugin Options # # The following plugin options are supported: # # :allowed_paths :: Set the template paths to allow. Attempts to render paths outside # of these paths will raise an error. Defaults to the +:views+ directory. # :assume_fixed_locals :: Set if you are sure all templates in your application use fixed locals # to allow for additional optimization. This is ignored unless both # compiled methods and fixed locals are not supported. # :cache :: nil/false to explicitly disable permanent template caching. By default, permanent # template caching is disabled by default if RACK_ENV is development. When permanent # template caching is disabled, for templates with paths in the file system, the # modification time of the file will be checked on every render, and if it has changed, # a new template will be created for the current content of the file. # :cache_class :: A class to use as the template cache instead of the default. # :check_paths :: Can be set to false to turn off template path checking. # :engine :: The tilt engine to use for rendering, also the default file extension for # templates, defaults to 'erb'. # :escape :: Use Erubi as the ERB template engine, and enable escaping by default, # which makes <%= %> escape output and <%== %> not escape output. # If given, sets the escape: true option for all template engines, which # can break some non-ERB template engines. You can use a string or array of strings # as the value for this option to only set the escape: true option for those # specific template engines. # :layout :: The base name of the layout file, defaults to 'layout'. This can be provided as a hash # with the :template or :inline options. # :layout_opts :: The options to use when rendering the layout, if different from the default options. # :template_opts :: The tilt options used when rendering all templates. defaults to: # {outvar: '@_out_buf', default_encoding: Encoding.default_external}. # :engine_opts :: The tilt options to use per template engine. Keys are # engine strings, values are hashes of template options. # :views :: The directory holding the view files, defaults to the 'views' subdirectory of the # application's :root option (the process's working directory by default). # # = Render/View Method Options # # Most of these options can be overridden at runtime by passing options # to the +view+ or +render+ methods: # # view('foo', engine: 'html.erb') # render('foo', views: 'admin_views') # # There are additional options to +view+ and +render+ that are # available at runtime: # # :cache :: Set to false to not cache this template, even when # caching is on by default. Set to true to force caching for # this template, even when the default is to not permantently cache (e.g. # when using the :template_block option). # :cache_key :: Explicitly set the hash key to use when caching. # :content :: Only respected by +view+, provides the content to render # inside the layout, instead of rendering a template to get # the content. # :inline :: Use the value given as the template code, instead of looking # for template code in a file. # :locals :: Hash of local variables to make available inside the template. # :path :: Use the value given as the full pathname for the file, instead # of using the :views and :engine option in combination with the # template name. # :scope :: The object in which context to evaluate the template. By # default, this is the Roda instance. # :template :: Provides the name of the template to use. This allows you # pass a single options hash to the render/view method, while # still allowing you to specify the template name. # :template_block :: Pass this block when creating the underlying template, # ignored when using :inline. Disables caching of the # template by default. # :template_class :: Provides the template class to use, instead of using # Tilt or Tilt[:engine]. # # Here's an example of using these options: # # view(inline: '<%= @foo %>') # render(path: '/path/to/template.erb') # # If you pass a hash as the first argument to +view+ or +render+, it should # have either +:template+, +:inline+, +:path+, or +:content+ (for +view+) as # one of the keys. # # = Fixed Locals in Templates # # By default, you can pass any local variables to any templates. A separate # template method is compiled for each combination of locals. This causes # multiple issues: # # * It is inefficient, especially for large templates that are called with # many combinations of locals. # * It hides issues if unused local variable names are passed to the template # * It does not support default values for local variables # * It does not support required local variables # * It does not support cases where you want to pass values via a keyword splat # * It does not support named blocks # # If you are using Tilt 2.6+, you can used fixed locals in templates, by # passing the appropriate options in :template_opts. For example, if you # are using ERB templates, the recommended way to use the render plugin is to # use the +:extract_fixed_locals+ and +:default_fixed_locals+ template options: # # plugin :render, template_opts: {extract_fixed_locals: true, default_fixed_locals: '()'} # # This will default templates to not allowing any local variables to be passed. # If the template requires local variables, you can specify them using a magic # comment in the template, such as: # # <%# locals(required_local:, optional_local: nil) %> # # The magic comment is used as method parameters when defining the compiled template # method. # # For better debugging of issues with invalid keywords being passed to templates that # have not been updated to support fixed locals, it can be helpful to set # +:default_fixed_locals+ to use a single optional keyword argument # '(_no_kw: nil)'. This makes the error message show which keywords # were passed, instead of showing that the takes no arguments (if you use '()'), # or that no keywords are accepted (if you pass (**nil)). # # If you are sure your application works with all templates using fixed locals, # set the :assume_fixed_locals render plugin option, which will allow the plugin # to optimize cache lookup for renders with locals, and avoid duplicate compiled # methods for templates rendered both with and without locals. # # See Tilt's documentation for more information regarding fixed locals. # # = Speeding Up Template Rendering # # The render/view method calls are optimized for usage with a single symbol/string # argument specifying the template name. So for fastest rendering, pass only a # symbol/string to render/view. Next best optimized are template calls with a # single :locals option. Use of other options disables the compiled template # method optimizations and can be significantly slower. # # If you must pass a hash to render/view, either as a second argument or as the # only argument, you can speed things up by specifying a +:cache_key+ option in # the hash, making sure the +:cache_key+ is unique to the template you are # rendering. # # = Recommended +template_opts+ # # Here are the recommended values of :template_opts for new applications (a couple # are Erubi-specific and can be ignored if you are using other templates engines): # # plugin :render, # assume_fixed_locals: true, # Optimize plugin by assuming all templates use fixed locals # template_opts: { # scope_class: self, # Always uses current class as scope class for compiled templates # freeze: true, # Freeze string literals in templates # extract_fixed_locals: true, # Support fixed locals in templates # default_fixed_locals: '()', # Default to templates not supporting local variables # escape: true, # For Erubi templates, escapes <%= by default (use <%== for unescaped # chain_appends: true, # For Erubi templates, improves performance # skip_compiled_encoding_detection: true, # Unless you need encodings explicitly specified # } # # = Accepting Template Blocks in Methods # # If you are used to Rails, you may be surprised that this type of template code # doesn't work in Roda: # # <%= some_method do %> # Some HTML # <% end %> # # The reason this doesn't work is that this is not valid ERB syntax, it is Rails syntax, # and requires attempting to parse the some_method do Ruby code with a regular # expression. Since Roda uses ERB syntax, it does not support this. # # In general, these methods are used to wrap the content of the block and # inject the content into the output. To get similar behavior with Roda, you have # a few different options you can use. # # == Use Erubi::CaptureBlockEngine # # Roda defaults to using Erubi for erb template rendering. Erubi 1.13.0+ includes # support for an erb variant that supports blocks in <%= and <%== # tags. To use it: # # require 'erubi/capture_block' # plugin :render, template_opts: {engine_class: Erubi::CaptureBlockEngine} # # See the Erubi documentation for how to capture data inside the block. Make sure # the method call (+some_method+ in the example) returns the output you want added # to the rendered body. # # == Directly Inject Template Output # # You can switch from a <%= tag to using a <% tag: # # <% some_method do %> # Some HTML # <% end %> # # While this would output Some HTML into the template, it would not be able # to inject content before or after the block. However, you can use the inject_erb_plugin # to handle the injection: # # def some_method # inject_erb "content before block" # yield # inject_erb "content after block" # end # # If you need to modify the captured block before injecting it, you can use the # capture_erb plugin to capture content from the template block, and modify that content, # then use inject_erb to inject it into the template output: # # def some_method(&block) # inject_erb "content before block" # inject_erb capture_erb(&block).upcase # inject_erb "content after block" # end # # This is the recommended approach for handling this type of method, if you want to keep # the template block in the same template. # # == Separate Block Output Into Separate Template # # By moving the Some HTML into a separate template, you can render that # template inside the block: # # <%= some_method{render('template_name')} %> # # It's also possible to use an inline template: # # <%= some_method do render(:inline=><<-END) # Some HTML # END # end %> # # This approach is useful if it makes sense to separate the template block into its # own template. You lose the ability to use local variable from outside the # template block inside the template block with this approach. # # == Separate Header and Footer # # You can define two separate methods, one that outputs the content before the block, # and one that outputs the content after the block, and use those instead of a single # call: # # <%= some_method_before %> # Some HTML # <%= some_method_after %> # # This is the simplest option to setup, but it is fairly tedious to use. module Render # Support for using compiled methods directly requires Ruby 2.3 for the # method binding to work, and Tilt 1.2 for Tilt::Template#compiled_method. tilt_compiled_method_support = defined?(Tilt::VERSION) && Tilt::VERSION >= '1.2' && ([1, -2].include?(((compiled_method_arity = Tilt::Template.instance_method(:compiled_method).arity) rescue false))) NO_CACHE = {:cache=>false}.freeze COMPILED_METHOD_SUPPORT = RUBY_VERSION >= '2.3' && tilt_compiled_method_support && ENV['RODA_RENDER_COMPILED_METHOD_SUPPORT'] != 'no' FIXED_LOCALS_COMPILED_METHOD_SUPPORT = COMPILED_METHOD_SUPPORT && Tilt::Template.method_defined?(:fixed_locals?) if FIXED_LOCALS_COMPILED_METHOD_SUPPORT def self.tilt_template_fixed_locals?(template) template.fixed_locals? end # :nocov: else def self.tilt_template_fixed_locals?(template) false end end # :nocov: if compiled_method_arity == -2 def self.tilt_template_compiled_method(template, locals_keys, scope_class) template.send(:compiled_method, locals_keys, scope_class) end # :nocov: else def self.tilt_template_compiled_method(template, locals_keys, scope_class) template.send(:compiled_method, locals_keys) end # :nocov: end # Setup default rendering options. See Render for details. def self.configure(app, opts=OPTS) if app.opts[:render] orig_cache = app.opts[:render][:cache] orig_method_cache = app.opts[:render][:template_method_cache] opts = app.opts[:render][:orig_opts].merge(opts) end app.opts[:render] = opts.dup app.opts[:render][:orig_opts] = opts opts = app.opts[:render] opts[:engine] = (opts[:engine] || "erb").dup.freeze opts[:views] = app.expand_path(opts[:views]||"views").freeze opts[:allowed_paths] ||= [opts[:views]].freeze opts[:allowed_paths] = opts[:allowed_paths].map{|f| app.expand_path(f, nil)}.uniq.freeze opts[:check_paths] = true unless opts.has_key?(:check_paths) opts[:assume_fixed_locals] &&= FIXED_LOCALS_COMPILED_METHOD_SUPPORT unless opts.has_key?(:check_template_mtime) opts[:check_template_mtime] = if opts[:cache] == false || opts[:explicit_cache] true else ENV['RACK_ENV'] == 'development' end end begin app.const_get(:RodaCompiledTemplates, false) rescue NameError compiled_templates_module = Module.new app.send(:include, compiled_templates_module) app.const_set(:RodaCompiledTemplates, compiled_templates_module) end opts[:template_method_cache] = orig_method_cache || (opts[:cache_class] || RodaCache).new opts[:template_method_cache][:_roda_layout] = nil if opts[:template_method_cache][:_roda_layout] opts[:cache] = orig_cache || (opts[:cache_class] || RodaCache).new opts[:layout_opts] = (opts[:layout_opts] || {}).dup opts[:layout_opts][:_is_layout] = true if opts[:layout_opts][:views] opts[:layout_opts][:views] = app.expand_path(opts[:layout_opts][:views]).freeze end if layout = opts.fetch(:layout, true) opts[:layout] = true case layout when Hash opts[:layout_opts].merge!(layout) when true opts[:layout_opts][:template] ||= 'layout' else opts[:layout_opts][:template] = layout end opts[:optimize_layout] = (opts[:layout_opts][:template] if opts[:layout_opts].keys.sort == [:_is_layout, :template]) end opts[:layout_opts].freeze template_opts = opts[:template_opts] = (opts[:template_opts] || {}).dup template_opts[:outvar] ||= '@_out_buf' unless template_opts.has_key?(:default_encoding) template_opts[:default_encoding] = Encoding.default_external end engine_opts = opts[:engine_opts] = (opts[:engine_opts] || {}).dup engine_opts.to_a.each do |k,v| engine_opts[k] = v.dup.freeze end if escape = opts[:escape] require 'tilt/erubi' case escape when String, Array Array(escape).each do |engine| engine_opts[engine] = (engine_opts[engine] || {}).merge(:escape => true).freeze end else template_opts[:escape] = true end end template_opts.freeze engine_opts.freeze opts.freeze end # Wrapper object for the Tilt template, that checks the modified # time of the template file, and rebuilds the template if the # template file has been modified. This is an internal class and # the API is subject to change at any time. class TemplateMtimeWrapper def initialize(roda_class, opts, template_opts) @roda_class = roda_class @opts = opts @template_opts = template_opts reset_template @path = opts[:path] deps = opts[:dependencies] @dependencies = ([@path] + Array(deps)) if deps @mtime = template_last_modified end # If the template file exists and the modification time has # changed, rebuild the template file, then call render on it. def render(*args, &block) res = nil modified = false if_modified do res = @template.render(*args, &block) modified = true end modified ? res : @template.render(*args, &block) end # Return when the template was last modified. If the template depends on any # other files, check the modification times of all dependencies and # return the maximum. def template_last_modified if deps = @dependencies deps.map{|f| File.mtime(f)}.max else File.mtime(@path) end end # If the template file has been updated, return true and update # the template object and the modification time. Other return false. def if_modified begin mtime = template_last_modified rescue # ignore errors else if mtime != @mtime reset_template yield @mtime = mtime end end end if COMPILED_METHOD_SUPPORT # Whether the underlying template uses fixed locals. def fixed_locals? Render.tilt_template_fixed_locals?(@template) end # Compile a method in the given module with the given name that will # call the compiled template method, updating the compiled template method def define_compiled_method(roda_class, method_name, locals_keys=EMPTY_ARRAY) mod = roda_class::RodaCompiledTemplates internal_method_name = :"_#{method_name}" begin mod.send(:define_method, internal_method_name, compiled_method(locals_keys, roda_class)) rescue ::NotImplementedError return false end mod.send(:private, internal_method_name) mod.send(:define_method, method_name, &compiled_method_lambda(roda_class, internal_method_name, locals_keys)) mod.send(:private, method_name) method_name end # Returns an appropriate value for the template method cache. def define_compiled_method_cache_value(roda_class, method_name, locals_keys=EMPTY_ARRAY) if compiled_method = define_compiled_method(roda_class, method_name, locals_keys) [compiled_method, false].freeze else compiled_method end end private # Return the compiled method for the current template object. def compiled_method(locals_keys=EMPTY_ARRAY, roda_class=nil) Render.tilt_template_compiled_method(@template, locals_keys, roda_class) end # Return the lambda used to define the compiled template method. This # is separated into its own method so the lambda does not capture any # unnecessary local variables def compiled_method_lambda(roda_class, method_name, locals_keys=EMPTY_ARRAY) mod = roda_class::RodaCompiledTemplates template = self lambda do |locals, &block| template.if_modified do mod.send(:define_method, method_name, Render.tilt_template_compiled_method(template, locals_keys, roda_class)) mod.send(:private, method_name) end _call_optimized_template_method([method_name, Render.tilt_template_fixed_locals?(template)], locals, &block) end end end private # Reset the template, done every time the template or one of its # dependencies is modified. def reset_template @template = @roda_class.create_template(@opts, @template_opts) end end module ClassMethods if COMPILED_METHOD_SUPPORT # If using compiled methods and there is an optimized layout, speed up # access to the layout method to improve the performance of view. def freeze begin _freeze_layout_method rescue # This is only for optimization, if any errors occur, they can be ignored. # One possibility for error is the app doesn't use a layout, but doesn't # specifically set the :layout=>false plugin option. nil end # Optimize _call_optimized_template_method if you know all templates # are going to be using fixed locals. if render_opts[:assume_fixed_locals] && !render_opts[:check_template_mtime] include AssumeFixedLocalsInstanceMethods end super end end # Return an Tilt::Template object based on the given opts and template_opts. def create_template(opts, template_opts) opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block]) end # A proc that returns content, used for inline templates, so that the template # doesn't hold a reference to the instance of the class def inline_template_block(content) Proc.new{content} end # Copy the rendering options into the subclass, duping # them as necessary to prevent changes in the subclass # affecting the parent class. def inherited(subclass) super opts = subclass.opts[:render] = subclass.opts[:render].dup if COMPILED_METHOD_SUPPORT opts[:template_method_cache] = (opts[:cache_class] || RodaCache).new end opts[:cache] = opts[:cache].dup opts.freeze end # Return the render options for this class. def render_opts opts[:render] end private # Precompile the layout method, to reduce method calls to look it up at runtime. def _freeze_layout_method if render_opts[:layout] instance = allocate # This needs to be called even if COMPILED_METHOD_SUPPORT is not set, # in order for the precompile_templates plugin to work correctly. instance.send(:retrieve_template, instance.send(:view_layout_opts, OPTS)) if COMPILED_METHOD_SUPPORT && (layout_template = render_opts[:optimize_layout]) && !opts[:render][:optimized_layout_method_created] instance.send(:retrieve_template, :template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout) layout_method = opts[:render][:template_method_cache][:_roda_layout] define_method(:_layout_method){layout_method} private :_layout_method alias_method(:_layout_method, :_layout_method) opts[:render] = opts[:render].merge(:optimized_layout_method_created=>true) end end end end module InstanceMethods # Render the given template. See Render for details. def render(template, opts = (no_opts = true; optimized_template = _cached_template_method(template); OPTS), &block) if optimized_template _call_optimized_template_method(optimized_template, OPTS, &block) elsif !no_opts && opts.length == 1 && (locals = opts[:locals]) && (optimized_template = _optimized_render_method_for_locals(template, locals)) _call_optimized_template_method(optimized_template, locals, &block) else opts = render_template_opts(template, opts) retrieve_template(opts).render((opts[:scope]||self), (opts[:locals]||OPTS), &block) end end # Return the render options for the instance's class. def render_opts self.class.render_opts end # Render the given template. If there is a default layout # for the class, take the result of the template rendering # and render it inside the layout. Blocks passed to view # are passed to render when rendering the template. # See Render for details. def view(template, opts = (content = _optimized_view_content(template) unless defined?(yield); OPTS), &block) if content # First, check if the optimized layout method has already been created, # and use it if so. This way avoids the extra conditional and local variable # assignments in the next section. if layout_method = _layout_method return _call_optimized_template_method(layout_method, OPTS){content} end # If we have an optimized template method but no optimized layout method, create the # optimized layout method if possible and use it. If you can't create the optimized # layout method, fall through to the slower approach. if layout_template = self.class.opts[:render][:optimize_layout] retrieve_template(:template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout) if layout_method = _layout_method return _call_optimized_template_method(layout_method, OPTS){content} end end else opts = parse_template_opts(template, opts) content = opts[:content] || render_template(opts, &block) end if layout_opts = view_layout_opts(opts) content = render_template(layout_opts){content} end content end private if COMPILED_METHOD_SUPPORT # If there is an instance method for the template, return the instance # method symbol. This optimization is only used for render/view calls # with a single string or symbol argument. def _cached_template_method(template) case template when String, Symbol if (method_cache = render_opts[:template_method_cache]) _cached_template_method_lookup(method_cache, template) end end end # The key to use in the template method cache for the given template. def _cached_template_method_key(template) template end # Return the instance method symbol for the template in the method cache. def _cached_template_method_lookup(method_cache, template) method_cache[template] end # Return a symbol containing the optimized layout method def _layout_method self.class.opts[:render][:template_method_cache][:_roda_layout] end # Use an optimized render path for templates with a hash of locals. Returns the result # of the template render if the optimized path is used, or nil if the optimized # path is not used and the long method needs to be used. def _optimized_render_method_for_locals(template, locals) render_opts = self.render_opts return unless method_cache = render_opts[:template_method_cache] case template when String, Symbol if fixed_locals = render_opts[:assume_fixed_locals] key = template if optimized_template = _cached_template_method_lookup(method_cache, key) return optimized_template end else key = [:_render_locals, template] if optimized_template = _cached_template_method_lookup(method_cache, key) # Fixed locals case return optimized_template end locals_keys = locals.keys.sort key << locals_keys if optimized_template = _cached_template_method_lookup(method_cache, key) # Regular locals case return optimized_template end end else return end if method_cache_key = _cached_template_method_key(key) template_obj = retrieve_template(render_template_opts(template, NO_CACHE)) unless fixed_locals key.pop if fixed_locals = Render.tilt_template_fixed_locals?(template_obj) key.freeze end method_name = :"_roda_template_locals_#{self.class.object_id}_#{method_cache_key}" method_cache[method_cache_key] = case template_obj when Render::TemplateMtimeWrapper template_obj.define_compiled_method_cache_value(self.class, method_name, locals_keys) else begin unbound_method = Render.tilt_template_compiled_method(template_obj, locals_keys, self.class) rescue ::NotImplementedError false else self.class::RodaCompiledTemplates.send(:define_method, method_name, unbound_method) self.class::RodaCompiledTemplates.send(:private, method_name) [method_name, fixed_locals].freeze end end end end # Get the content for #view, or return nil to use the unoptimized approach. Only called if # a single argument is passed to view. def _optimized_view_content(template) if optimized_template = _cached_template_method(template) _call_optimized_template_method(optimized_template, OPTS) elsif template.is_a?(Hash) && template.length == 1 template[:content] end end if RUBY_VERSION >= '3' class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def _call_optimized_template_method((meth, fixed_locals), locals, &block) if fixed_locals send(meth, **locals, &block) else send(meth, locals, &block) end end RUBY # :nocov: elsif RUBY_VERSION >= '2' class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def _call_optimized_template_method((meth, fixed_locals), locals, &block) if fixed_locals if locals.empty? send(meth, &block) else send(meth, **locals, &block) end else send(meth, locals, &block) end end RUBY else # Call the optimized template method. This is designed to be used with the # method cache, which caches the method name and whether the method uses # fixed locals. Methods with fixed locals need to be called with a keyword # splat. def _call_optimized_template_method((meth, fixed_locals), locals, &block) send(meth, locals, &block) end end # :nocov: else def _cached_template_method(_) nil end def _cached_template_method_key(_) nil end def _optimized_render_method_for_locals(_, _) nil end def _optimized_view_content(template) nil end end # Convert template options to single hash when rendering templates using render. def render_template_opts(template, opts) parse_template_opts(template, opts) end # Private alias for render. Should be used by other plugins when they want to render a template # without a layout, as plugins can override render to use a layout. alias render_template render # If caching templates, attempt to retrieve the template from the cache. Otherwise, just yield # to get the template. def cached_template(opts, &block) if key = opts[:cache_key] cache = render_opts[:cache] unless template = cache[key] template = cache[key] = yield end template else yield end end # Given the template name and options, set the template class, template path/content, # template block, and locals to use for the render in the passed options. def find_template(opts) render_opts = self.class.opts[:render] engine_override = opts[:engine] engine = opts[:engine] ||= render_opts[:engine] if content = opts[:inline] path = opts[:path] = content template_class = opts[:template_class] ||= ::Tilt[engine] opts[:template_block] = self.class.inline_template_block(content) else opts[:views] ||= render_opts[:views] path = opts[:path] ||= template_path(opts) template_class = opts[:template_class] opts[:template_class] ||= ::Tilt end if (cache = opts[:cache]).nil? cache = content || !opts[:template_block] end if cache unless opts.has_key?(:cache_key) template_block = opts[:template_block] unless content template_opts = opts[:template_opts] opts[:cache_key] = if template_class || engine_override || template_opts || template_block [path, template_class, engine_override, template_opts, template_block] else path end end else opts.delete(:cache_key) end opts end # Return a single hash combining the template and opts arguments. def parse_template_opts(template, opts) opts = Hash[opts] if template.is_a?(Hash) opts.merge!(template) else if opts.empty? && (key = _cached_template_method_key(template)) opts[:template_method_cache_key] = key end opts[:template] = template opts end end # The default render options to use. These set defaults that can be overridden by # providing a :layout_opts option to the view/render method. def render_layout_opts Hash[render_opts[:layout_opts]] end # Retrieve the Tilt::Template object for the given template and opts. def retrieve_template(opts) cache = opts[:cache] if !opts[:cache_key] || cache == false found_template_opts = opts = find_template(opts) end cached_template(opts) do opts = found_template_opts || find_template(opts) render_opts = self.class.opts[:render] template_opts = render_opts[:template_opts] if engine_opts = render_opts[:engine_opts][opts[:engine]] template_opts = template_opts.merge(engine_opts) end if current_template_opts = opts[:template_opts] template_opts = template_opts.merge(current_template_opts) end define_compiled_method = COMPILED_METHOD_SUPPORT && (method_cache_key = opts[:template_method_cache_key]) && (method_cache = render_opts[:template_method_cache]) && (method_cache[method_cache_key] != false) && !opts[:inline] if render_opts[:check_template_mtime] && !opts[:template_block] && !cache template = TemplateMtimeWrapper.new(self.class, opts, template_opts) if define_compiled_method method_name = :"_roda_template_#{self.class.object_id}_#{method_cache_key}" method_cache[method_cache_key] = template.define_compiled_method_cache_value(self.class, method_name) end else template = self.class.create_template(opts, template_opts) if define_compiled_method && cache != false begin unbound_method = Render.tilt_template_compiled_method(template, EMPTY_ARRAY, self.class) rescue ::NotImplementedError method_cache[method_cache_key] = false else method_name = :"_roda_template_#{self.class.object_id}_#{method_cache_key}" self.class::RodaCompiledTemplates.send(:define_method, method_name, unbound_method) self.class::RodaCompiledTemplates.send(:private, method_name) method_cache[method_cache_key] = [method_name, Render.tilt_template_fixed_locals?(template)].freeze end end end template end end # The name to use for the template. By default, just converts the :template option to a string. def template_name(opts) opts[:template].to_s end # The template path for the given options. def template_path(opts) path = "#{opts[:views]}/#{template_name(opts)}.#{opts[:engine]}" if opts.fetch(:check_paths){render_opts[:check_paths]} full_path = self.class.expand_path(path) unless render_opts[:allowed_paths].any?{|f| full_path.start_with?(f)} raise RodaError, "attempt to render path not in allowed_paths: #{full_path} (allowed: #{render_opts[:allowed_paths].join(', ')})" end end path end # If a layout should be used, return a hash of options for # rendering the layout template. If a layout should not be # used, return nil. def view_layout_opts(opts) if layout = opts.fetch(:layout, render_opts[:layout]) layout_opts = render_layout_opts method_layout_opts = opts[:layout_opts] layout_opts.merge!(method_layout_opts) if method_layout_opts case layout when Hash layout_opts.merge!(layout) when true # use default layout else layout_opts[:template] = layout end layout_opts end end end module AssumeFixedLocalsInstanceMethods # :nocov: if RUBY_VERSION >= '3.0' # :nocov: class_eval(<<-RUBY, __FILE__, __LINE__ + 1) def _call_optimized_template_method((meth,_), locals, &block) send(meth, **locals, &block) end RUBY end end end register_plugin(:render, Render) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/render_coverage.rb000066400000000000000000000102321516720775400242410ustar00rootroot00000000000000# frozen-string-literal: true require 'tilt' # :nocov: raise 'Tilt version does not support coverable templates' unless Tilt::Template.method_defined?(:compiled_path=) # :nocov: # class Roda module RodaPlugins # The render_coverage plugin builds on top of the render plugin # and sets compiled_path on created templates. This allows # Ruby's coverage library before Ruby 3.2 to consider code created # by templates. You may not need this plugin on Ruby 3.2+, since # on Ruby 3.2+, coverage can consider code loaded with +eval+. # This plugin is only supported when using tilt 2.1+, since it # requires the compiled_path supported added in tilt 2.1. # # By default, the render_coverage plugin will use +coverage/views+ # as the directory containing the compiled template files. You can # change this by passing the :dir option when loading the plugin. # By default, the plugin will set the compiled_path by taking the # template file path, stripping off any of the allowed_paths used # by the render plugin, and converting slashes to dashes. You can # override the allowed_paths to strip by passing the :strip_paths # option when loading the plugin. Paths outside :strip_paths (or # the render plugin allowed_paths if :strip_paths is not set) will # not have a compiled_path set. # # Due to how Ruby's coverage library works in regards to loading # a compiled template file with identical code more than once, # it may be beneficial to run coverage testing with the # +RODA_RENDER_COMPILED_METHOD_SUPPORT+ environment variable set # to +no+ if using this plugin. module RenderCoverage def self.load_dependencies(app, opts=OPTS) app.plugin :render end # Use the :dir option to set the directory to store the compiled # template files, and the :strip_paths directory for paths to # strip. def self.configure(app, opts=OPTS) app.opts[:render_coverage_strip_paths] = opts[:strip_paths].map{|f| File.expand_path(f)} if opts.has_key?(:strip_paths) coverage_dir = app.opts[:render_coverage_dir] = opts[:dir] || app.opts[:render_coverage_dir] || 'coverage/views' Dir.mkdir(coverage_dir) unless File.directory?(coverage_dir) end module ClassMethods # Set a compiled path on the created template, if the path for # the template is in one of the allowed_views. def create_template(opts, template_opts) return super if opts[:template_block] path = File.expand_path(opts[:path]) compiled_path = nil (self.opts[:render_coverage_strip_paths] || render_opts[:allowed_paths]).each do |dir| if path.start_with?(dir + '/') compiled_path = File.join(self.opts[:render_coverage_dir], path[dir.length+1, 10000000].tr('/', '-')) break end end # For Tilt 2.6+, when using :scope_class and fixed locals, must provide # compiled path as option, since compilation happens during initalization # in that case. This option should be ignored if the template does not # support it, but some template class may break if the option is not # handled, so for compatibility, only set the method if Tilt::Template # will handle it. if compiled_path && Tilt::Template.method_defined?(:fixed_locals?) template_opts = template_opts.dup template_opts[:compiled_path] = compiled_path compiled_path = nil end template = super # Set compiled path for template when using older tilt versions. # :nocov: template.compiled_path = compiled_path if compiled_path # :nocov: template end end module InstanceMethods private # Convert template paths to real paths to try to ensure the same template is cached. def template_path(opts) path = super if File.file?(path) File.realpath(path) else path end end end end register_plugin(:render_coverage, RenderCoverage) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/render_each.rb000066400000000000000000000176301516720775400233570ustar00rootroot00000000000000# frozen-string-literal: true require_relative 'render' # class Roda module RodaPlugins # The render_each plugin allows you to render a template for each # value in an enumerable, returning the concatention of all of the # template renderings. For example: # # render_each([1,2,3], :foo) # # will render the +foo+ template 3 times. Each time the template # is rendered, the local variable +foo+ will contain the given # value (e.g. on the first rendering +foo+ is 1). # # If you provide a block when calling the method, it will yield # each rendering instead of returning a concatentation of the # renderings. This is useful if you want to wrap each rendering in # something else. For example, instead of calling +render+ multiple # times in a loop: # # <% [1,2,3].each do |v| %> #

<%= render(:foo, locals: {foo: v}) %>

# <% end %> # # You can use +render_each+, allowing for simpler and more optimized # code: # # <% render_each([1,2,3], :foo) do |text| %> #

<%= text %>

# <% end %> # # You can also provide a block to avoid excess memory usage. For # example, if you are calling the method inside an erb template, # instead of doing: # # <%= render_each([1,2,3], :foo) %> # # You can do: # # <% render_each([1,2,3], :foo) %><%= body %><% end %> # # This results in the same behavior, but avoids building a large # intermediate string just to concatenate to the template result. # # When passing a block, +render_each+ returns +nil+. # # You can pass additional render options via an options hash: # # render_each([1,2,3], :foo, views: 'partials') # # One additional option supported by is +:local+, which sets the # local variable containing the current value to use. So: # # render_each([1,2,3], :foo, local: :bar) # # Will render the +foo+ template, but the local variable used inside # the template will be +bar+. You can use local: nil to # not set a local variable inside the template. By default, the # local variable name is based on the template name, with any # directories and file extensions removed. module RenderEach # Load the render plugin before this plugin, since this plugin # calls the render method. def self.load_dependencies(app) app.plugin :render end ALLOWED_KEYS = [:locals, :local].freeze if Render::COMPILED_METHOD_SUPPORT module ClassMethods # If using compiled methods and assuming fixed locals, optimize # _cached_render_each_template_method. def freeze if render_opts[:assume_fixed_locals] && !render_opts[:check_template_mtime] include AssumeFixedLocalsInstanceMethods end super end end module AssumeFixedLocalsInstanceMethods private # Optimize method since this module is only loaded when using fixed locals # and when caching templates. def _cached_render_each_template_method(template) case template when String, Symbol _cached_template_method_lookup(render_opts[:template_method_cache], template) else false end end end end module InstanceMethods # For each value in enum, render the given template using the # given opts. The template and options hash are passed to +render+. # Additional options supported: # :local :: The local variable to use for the current enum value # inside the template. An explicit +nil+ value does not # set a local variable. If not set, uses the template name. def render_each(enum, template, opts=(no_opts = true; optimized_template = _cached_render_each_template_method(template); OPTS), &block) if optimized_template return _optimized_render_each(enum, optimized_template, render_each_default_local(template), {}, &block) elsif opts.has_key?(:local) as = opts[:local] else as = render_each_default_local(template) if no_opts && optimized_template.nil? && (optimized_template = _optimized_render_method_for_locals(template, (locals = {as=>nil}))) return _optimized_render_each(enum, optimized_template, as, locals, &block) end end if as opts = opts.dup if locals opts[:locals] = locals else locals = opts[:locals] = if locals = opts[:locals] Hash[locals] else {} end locals[as] = nil end if (opts.keys - ALLOWED_KEYS).empty? && (optimized_template = _optimized_render_method_for_locals(template, locals)) return _optimized_render_each(enum, optimized_template, as, locals, &block) end end if defined?(yield) enum.each do |v| locals[as] = v if as yield render_template(template, opts) end nil else enum.map do |v| locals[as] = v if as render_template(template, opts) end.join end end private if File.basename("a/b") == "b" && File.basename("a\\b") == "a\\b" && RUBY_VERSION >= '3' # The default local variable name to use for the template, if the :local option # is not used when calling render_each. def render_each_default_local(template) # Optimize to avoid allocations when possible template = case template when Symbol s = template.name return template unless s.include?("/") || s.include?(".") s when String return template.to_sym unless template.include?("/") || template.include?(".") template else template.to_s end File.basename(template).sub(/\..+\z/, '').to_sym end # :nocov: else def render_each_default_local(template) File.basename(template.to_s).sub(/\..+\z/, '').to_sym end # :nocov: end if Render::COMPILED_METHOD_SUPPORT # If compiled method support is enabled in the render plugin, return the # method name to call to render the template. Return false if not given # a string or symbol, or if compiled method support for this template has # been explicitly disabled. Otherwise return nil. def _cached_render_each_template_method(template) case template when String, Symbol if (method_cache = render_opts[:template_method_cache]) key = render_opts[:assume_fixed_locals] ? template : [:_render_locals, template, [template.to_sym]] _cached_template_method_lookup(method_cache, key) end else false end end # Use an optimized render for each value in the enum. def _optimized_render_each(enum, optimized_template, as, locals) if defined?(yield) enum.each do |v| locals[as] = v yield _call_optimized_template_method(optimized_template, locals) end nil else enum.map do |v| locals[as] = v _call_optimized_template_method(optimized_template, locals) end.join end end else def _cached_render_each_template_method(template) nil end end end end register_plugin(:render_each, RenderEach) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/render_locals.rb000066400000000000000000000061171516720775400237320ustar00rootroot00000000000000# frozen-string-literal: true require_relative 'render' # class Roda module RodaPlugins # The render_locals plugin allows setting default locals for rendering templates. # # plugin :render_locals, render: {heading: 'Hello'} # # route do |r| # r.get "foo" do # view 'foo', locals: {name: 'Foo'} # locals: {:heading=>'Hello', :name=>'Foo'} # end # # r.get "bar" do # view 'foo', locals: {heading: 'Bar'} # locals: {:heading=>'Bar'} # end # # view "default" # locals: {:heading=>'Hello'} # end # # The render_locals plugin accepts the following options: # # render :: The default locals to use for template rendering # layout :: The default locals to use for layout rendering # merge :: Whether to merge template locals into layout locals module RenderLocals def self.load_dependencies(app, opts=OPTS) app.plugin :render end def self.configure(app, opts=OPTS) app.opts[:render_locals] = (app.opts[:render_locals] || {}).merge(opts[:render]||{}).freeze app.opts[:layout_locals] = (app.opts[:layout_locals] || {}).merge(opts[:layout]||{}).freeze if opts.has_key?(:merge) app.opts[:merge_locals] = opts[:merge] app.opts[:layout_locals] = app.opts[:render_locals].merge(app.opts[:layout_locals]).freeze end end module InstanceMethods private if Render::COMPILED_METHOD_SUPPORT # Disable use of cached templates, since it assumes a render/view call with no # options will have no locals. def _cached_template_method(template) nil end def _optimized_view_content(template) nil end end def render_locals opts[:render_locals] end def layout_locals opts[:layout_locals] end # If this isn't the layout template, then use the plugin's render locals as the default locals. def render_template_opts(template, opts) opts = super return opts if opts[:_is_layout] plugin_locals = render_locals if locals = opts[:locals] plugin_locals = Hash[plugin_locals].merge!(locals) end opts[:locals] = plugin_locals opts end # If using a layout, then use the plugin's layout locals as the default locals. def view_layout_opts(opts) if layout_opts = super merge_locals = layout_opts.has_key?(:merge_locals) ? layout_opts[:merge_locals] : self.opts[:merge_locals] locals = {} locals.merge!(layout_locals) if merge_locals && (method_locals = opts[:locals]) locals.merge!(method_locals) end if method_layout_locals = layout_opts[:locals] locals.merge!(method_layout_locals) end layout_opts[:locals] = locals layout_opts end end end end register_plugin(:render_locals, RenderLocals) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/request_aref.rb000066400000000000000000000046071516720775400236050ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The request_aref plugin allows for custom handling of the r[] and r[]= # methods (where r is the Request instance). In the current version of # rack, these methods are deprecated, but the deprecation message is only # printed in verbose mode. This plugin can allow for handling calls to # these methods in one of three ways: # # :allow :: Allow the method calls without a deprecation, which is the # historical behavior # :warn :: Always issue a deprecation message by calling +warn+, not just # in verbose mode. # :raise :: Raise an error if either method is called module RequestAref # Make #[] and #[]= methods work as configured by aliasing the appropriate # request_a(ref|set)_* methods to them. def self.configure(app, setting) case setting when :allow, :raise, :warn app::RodaRequest.class_eval do alias_method(:[], :"request_aref_#{setting}") alias_method(:[]=, :"request_aset_#{setting}") public :[], :[]= end else raise RodaError, "Unsupport request_aref plugin setting: #{setting.inspect}" end end # Exception class raised when #[] or #[]= are called when the # :raise setting is used. class Error < RodaError end module RequestMethods private # Allow #[] calls def request_aref_allow(k) params[k.to_s] end # Always warn on #[] calls def request_aref_warn(k) warn("#{self.class}#[] is deprecated, use #params.[] instead") params[k.to_s] end # Raise error on #[] calls def request_aref_raise(k) raise Error, "#{self.class}#[] has been removed, use #params.[] instead" end # Allow #[]= calls def request_aset_allow(k, v) params[k.to_s] = v end # Always warn on #[]= calls def request_aset_warn(k, v) warn("#{self.class}#[]= is deprecated, use #params.[]= instead") params[k.to_s] = v end # Raise error on #[]= calls def request_aset_raise(k, v) raise Error, "#{self.class}#[]= has been removed, use #params.[]= instead" end end end register_plugin(:request_aref, RequestAref) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/request_headers.rb000066400000000000000000000040131516720775400242720ustar00rootroot00000000000000# frozen-string-literal: true require 'set' class Roda module RodaPlugins # The request_headers plugin provides access to headers sent in the # request in a more natural way than directly accessing the env hash. # # In practise this means you don't need to uppercase, convert dashes # to underscores, or add a HTTP_ prefix. # # For example, to access a header called X-My-Header you # would previously need to do: # # r.env['HTTP_X_MY_HEADER'] # # But with this plugin you can now say: # # r.headers['X-My-Header'] # # The name is actually case-insensitive so x-my-header will work as well. # # Example: # # plugin :request_headers module RequestHeaders module RequestMethods # Provide access to the request headers while normalizing indexes. def headers @request_headers ||= Headers.new(@env) end end class Headers # Set of environment variable names that don't need HTTP_ prepended to them. CGI_VARIABLES = Set.new(%w' AUTH_TYPE CONTENT_LENGTH CONTENT_TYPE GATEWAY_INTERFACE HTTPS PATH_INFO PATH_TRANSLATED QUERY_STRING REMOTE_ADDR REMOTE_HOST REMOTE_IDENT REMOTE_USER REQUEST_METHOD SCRIPT_NAME SERVER_NAME SERVER_PORT SERVER_PROTOCOL SERVER_SOFTWARE ').freeze def initialize(env) @env = env end # Returns the value for the given key mapped to @env def [](key) @env[env_name(key)] end private # Convert a HTTP header name into an environment variable name def env_name(key) key = key.to_s.upcase key.tr!('-', '_') key = 'HTTP_' + key unless CGI_VARIABLES.include?(key) key end end end register_plugin(:request_headers, RequestHeaders) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/response_attachment.rb000066400000000000000000000077021516720775400251650ustar00rootroot00000000000000# frozen-string-literal: true require 'rack/mime' # class Roda module RodaPlugins # The attachment plugin adds a response.attachment method. # When called with no filename, +attachment+ sets the Content-Disposition # to attachment. When called with a filename,+attachment+ sets the Content-Disposition # to attachment with the appropriate filename parameter, and if the filename # extension is recognized, this also sets the Content-Type to the appropriate # MIME type if not already set. # # # set Content-Disposition to 'attachment' # response.attachment # # # set Content-Disposition to 'attachment; filename="a.csv"', # # also set Content-Type to 'text/csv' # response.attachment 'a.csv' # # == License # # The implementation was originally taken from Sinatra, # which is also released under the MIT License: # # Copyright (c) 2007, 2008, 2009 Blake Mizerany # Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. module ResponseAttachment UTF8_ENCODING = Encoding.find('UTF-8') ISO88591_ENCODING = Encoding.find('ISO-8859-1') BINARY_ENCODING = Encoding.find('BINARY') module ResponseMethods # Set the Content-Disposition to "attachment" with the specified filename, # instructing the user agents to prompt to save. def attachment(filename = nil, disposition='attachment') if filename param_filename = File.basename(filename) encoding = param_filename.encoding needs_encoding = param_filename.gsub!(/[^ 0-9a-zA-Z!\#$&\+\.\^_`\|~]+/, '-') params = "; filename=#{param_filename.inspect}" if needs_encoding && (encoding == UTF8_ENCODING || encoding == ISO88591_ENCODING) # File name contains non attr-char characters from RFC 5987 Section 3.2.1 encoded_filename = File.basename(filename).force_encoding(BINARY_ENCODING) # Similar regexp as above, but treat each byte separately, and encode # space characters, since those aren't allowed in attr-char encoded_filename.gsub!(/[^0-9a-zA-Z!\#$&\+\.\^_`\|~]/) do |c| "%%%X" % c.ord end encoded_params = "; filename*=#{encoding.to_s}''#{encoded_filename}" end unless @headers[RodaResponseHeaders::CONTENT_TYPE] ext = File.extname(filename) if !ext.empty? && (content_type = Rack::Mime.mime_type(ext, nil)) @headers[RodaResponseHeaders::CONTENT_TYPE] = content_type end end end @headers[RodaResponseHeaders::CONTENT_DISPOSITION] = "#{disposition}#{params}#{encoded_params}" end end end register_plugin(:response_attachment, ResponseAttachment) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/response_content_type.rb000066400000000000000000000051671516720775400255530ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The response_content_type extension adds response.content_type # and response.content_type= methods for getting and setting the # response content-type. # # When setting the content-type, you can pass either a string, which # is used directly: # # response.content_type = "text/html" # # Or, if you have registered mime types when loading the plugin: # # plugin :response_content_type, mime_types: { # plain: "text/plain", # html: "text/html", # pdf: "application/pdf" # } # # You can use a symbol: # # response.content_type = :html # # If you would like to load all mime types supported by rack/mime, # you can use the mime_types: :from_rack_mime option: # # plugin :response_content_type, mime_types: :from_rack_mime # # Note that you are unlikely to be using all of these mime types, # so doing this will likely result in unnecessary memory usage. It # is recommended to use a hash with only the mime types your # application actually uses. # # To prevent silent failures, if you attempt to set the response # type with a symbol, and the symbol is not recognized, a KeyError # is raised. module ResponseContentType def self.configure(app, opts=OPTS) if mime_types = opts[:mime_types] mime_types = if mime_types == :from_rack_mime require "rack/mime" h = {} Rack::Mime::MIME_TYPES.each do |k, v| h[k.slice(1,100).to_sym] = v end h else mime_types.dup end app.opts[:repsonse_content_types] = mime_types.freeze else app.opts[:repsonse_content_types] ||= {} end end module ResponseMethods # Return the content-type of the response. Will be nil if it has # not yet been explicitly set. def content_type @headers[RodaResponseHeaders::CONTENT_TYPE] end # Set the content-type of the response. If given a string, # it is used directly. If given a symbol, looks up the mime # type with the given file extension. If the symbol is not # a recognized mime type, raises KeyError. def content_type=(mime_type) mime_type = roda_class.opts[:repsonse_content_types].fetch(mime_type) if mime_type.is_a?(Symbol) @headers[RodaResponseHeaders::CONTENT_TYPE] = mime_type end end end register_plugin(:response_content_type, ResponseContentType) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/response_request.rb000066400000000000000000000012321516720775400245150ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The response_request plugin gives the response access to the # related request instance via the #request method. # # Example: # # plugin :response_request module ResponseRequest module InstanceMethods # Set the response's request to the current request. def initialize(env) super @_response.request = @_request end end module ResponseMethods # The request related to this response. attr_accessor :request end end register_plugin(:response_request, ResponseRequest) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/route_block_args.rb000066400000000000000000000027451516720775400244450ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The route_block_args plugin lets you customize what arguments are passed to # the +route+ block. So if you have an application that always needs access # to the +response+, the +params+, the +env+, or the +session+, you can use # this plugin so that any of those can be arguments to the route block. # Example: # # class App < Roda # plugin :route_block_args do # [request, request.params, response] # end # # route do |r, params, res| # r.post do # artist = Artist.create(name: params['name'].to_s) # res.status = 201 # artist.id.to_s # end # end # end module RouteBlockArgs def self.configure(app, &block) app.instance_exec do define_roda_method(:_roda_route_block_args, 0, &block) route(&@raw_route_block) if @raw_route_block end end # Override the route block input so that the block # given is passed the arguments specified by the # block given to the route_block_args plugin. module ClassMethods private def convert_route_block(block) meth = define_roda_method("convert_route_block_args", :any, &super(block)) lambda do |_| send(meth, *_roda_route_block_args) end end end end register_plugin :route_block_args, RouteBlockArgs end end jeremyevans-roda-4f30bb3/lib/roda/plugins/route_csrf.rb000066400000000000000000000460721516720775400232750ustar00rootroot00000000000000# frozen-string-literal: true require 'openssl' require 'securerandom' require 'uri' require 'rack/utils' class Roda module RodaPlugins # The route_csrf plugin is the recommended plugin to use to support # CSRF protection in Roda applications. This plugin allows you set # where in the routing tree to enforce CSRF protection. Additionally, # the route_csrf plugin uses modern security practices. # # By default, the plugin requires tokens be specific to the request # method and request path, so a CSRF token generated for one form will # not be usable to submit a different form. # # This plugin also takes care to not expose the underlying CSRF key # (except in the session), so that it is not possible for an attacker # to generate valid CSRF tokens specific to an arbitrary request method # and request path even if they have access to a token that is not # specific to request method and request path. To get this security # benefit, you must ensure an attacker does not have access to the # session. Rack::Session::Cookie versions shipped with Rack before # Rack 3 use signed sessions, not encrypted # sessions, so if the attacker has the ability to read cookie data # and you are using one of those Rack::Session::Cookie versions, # it will still be possible # for an attacker to generate valid CSRF tokens specific to arbitrary # request method and request path. Roda's session plugin uses # encrypted sessions and therefore is safe even if the attacker can # read cookie data. # # == Usage # # It is recommended to use the plugin defaults, loading the # plugin with no options: # # plugin :route_csrf # # This plugin supports the following options: # # :field :: Form input parameter name for CSRF token (default: '_csrf') # :formaction_field :: Form input parameter name for path-specific CSRF tokens (used by the # +csrf_formaction_tag+ method). If present, this parameter should be # submitted as a hash, keyed by path, with CSRF token values. # :header :: HTTP header name for CSRF token (default: 'X-CSRF-Token') # :key :: Session key for CSRF secret (default: '_roda_csrf_secret') # :require_request_specific_tokens :: Whether request-specific tokens are required (default: true). # A false value will allow tokens that are not request-specific # to also work. You should only set this to false if it is # impossible to use request-specific tokens. If you must # use non-request-specific tokens in certain cases, it is best # to leave this option true by default, and override it on a # per call basis in those specific cases. # :csrf_failure :: The action to taken if a request fails the CSRF check (default: :raise). Options: # :raise :: raise a Roda::RodaPlugins::RouteCsrf::InvalidToken exception # :empty_403 :: return a blank 403 page (rack_csrf's default behavior) # :clear_session :: Clear the current session # Proc :: Treated as a routing block, called with request object # :check_header :: Whether the HTTP header should be checked for the token value (default: false). # If true, checks the HTTP header after checking for the form input parameter. # If :only, only checks the HTTP header and doesn't check the form input parameter. # :check_request_methods :: Which request methods require CSRF protection # (default: ['POST', 'DELETE', 'PATCH', 'PUT']) # :upgrade_from_rack_csrf_key :: If provided, the session key that should be checked for the # rack_csrf raw token. If the session key is present, the value # will be checked against the submitted token, and if it matches, # the CSRF check will be passed. Should only be set temporarily # if upgrading from using rack_csrf to the route_csrf plugin, and # should be removed as soon as you are OK with CSRF forms generated # before the upgrade not longer being usable. The default rack_csrf # key is 'csrf.token'. # # The plugin also supports a block, in which case the block will be used # as the value of the :csrf_failure option. # # == Methods # # This adds the following instance methods: # # check_csrf!(opts={}) :: Used for checking if the submitted CSRF token is valid. # If a block is provided, it is treated as a routing block if the # CSRF token is not valid. Otherwise, by default, raises a # Roda::RodaPlugins::RouteCsrf::InvalidToken exception if a CSRF # token is necessary for the request and there is no token provided # or the provided token is not valid. Options can be provided to # override any of the plugin options for this specific call. # The :token option can be used to specify the provided CSRF token # (instead of looking for the token in the submitted parameters). # csrf_formaction_tag(path, method='POST') :: An HTML hidden input tag string containing the CSRF token, suitable # for placing in an HTML form that has inputs that use formaction # attributes to change the endpoint to which the form is submitted. # Takes the same arguments as csrf_token. # csrf_field :: The field name to use for the hidden tag containing the CSRF token. # csrf_path(action) :: This takes an argument that would be the value of the HTML form's # action attribute, and returns a path you can pass to csrf_token # that should be valid for the form submission. The argument should # either be nil or a string representing a relative path, absolute # path, or full URL (using appropriate URL encoding). # csrf_tag(path=nil, method='POST') :: An HTML hidden input tag string containing the CSRF token, suitable # for placing in an HTML form. Takes the same arguments as csrf_token. # csrf_token(path=nil, method='POST') :: The value of the csrf token, in case it needs to be accessed # directly. It is recommended to call this method with a # path, which will create a request-specific token. Calling # this method without an argument will create a token that is # not specific to the request, but such a token will only # work if you set the :require_request_specific_tokens option # to false, which is a bad idea from a security standpoint. # use_request_specific_csrf_tokens? :: Whether the plugin is configured to only support # request-specific tokens, true by default. # valid_csrf?(opts={}) :: Returns whether the submitted CSRF token is valid (also true if # the request does not require a CSRF token). Takes same option hash # as check_csrf!. # # This plugin also adds the following instance methods for compatibility with the # older csrf plugin, but it is not recommended to use these methods in new code: # # csrf_header :: The header name to use for submitting the CSRF token via an HTTP header # (useful for javascript). Note that this plugin will not look in # the HTTP header by default, it will only do so if the :check_header # option is used. # csrf_metatag :: An HTML meta tag string containing the CSRF token, suitable # for placing in the page header. It is not recommended to use # this method, as the token generated is not request-specific and # will not work unless you set the :require_request_specific_tokens option to # false, which is a bad idea from a security standpoint. # # == Token Cryptography # # route_csrf uses HMAC-SHA-256 to generate all CSRF tokens. It generates a random 32-byte secret, # which is stored base64 encoded in the session. For each CSRF token, it generates 31 bytes # of random data. # # For request-specific CSRF tokens, this pseudocode generates the HMAC: # # hmac = HMAC(secret, method + path + random_data) # # For CSRF tokens not specific to a request, this pseudocode generates the HMAC: # # hmac = HMAC(secret, random_data) # # This pseudocode generates the final CSRF token in both cases: # # token = Base64Encode(random_data + hmac) # # Using this construction for generating CSRF tokens means that generating any # valid CSRF token without knowledge of the secret is equivalent to a successful generic attack # on HMAC-SHA-256. # # By using an HMAC for tokens not specific to a request, it is not possible to use a # valid CSRF token that is not specific to a request to generate a valid request-specific # CSRF token. # # By including random data in the HMAC for all tokens, different tokens are generated # each time, mitigating compression ratio attacks such as BREACH. module RouteCsrf # Default CSRF option values DEFAULTS = { :field => '_csrf'.freeze, :formaction_field => '_csrfs'.freeze, :header => 'X-CSRF-Token'.freeze, :key => '_roda_csrf_secret'.freeze, :require_request_specific_tokens => true, :csrf_failure => :raise, :check_header => false, :check_request_methods => %w'POST DELETE PATCH PUT'.freeze.each(&:freeze) }.freeze # Exception class raised when :csrf_failure option is :raise and # a valid CSRF token was not provided. class InvalidToken < RodaError; end def self.load_dependencies(app, opts=OPTS, &_) app.plugin :_base64 end def self.configure(app, opts=OPTS, &block) options = app.opts[:route_csrf] = (app.opts[:route_csrf] || DEFAULTS).merge(opts) if block || opts[:csrf_failure].is_a?(Proc) if block && opts[:csrf_failure] raise RodaError, "Cannot specify both route_csrf plugin block and :csrf_failure option" end block ||= opts[:csrf_failure] options[:csrf_failure] = :csrf_failure_method app.define_roda_method(:_roda_route_csrf_failure, 1, &app.send(:convert_route_block, block)) end options[:env_header] = "HTTP_#{options[:header].to_s.tr('-', '_').upcase}".freeze options.freeze end module InstanceMethods # Check that the submitted CSRF token is valid, if the request requires a CSRF token. # If the CSRF token is valid or the request does not require a CSRF token, return nil. # Otherwise, if a block is given, treat it as a routing block and yield to it, and # if a block is not given, use the :csrf_failure option to determine how to handle it. def check_csrf!(opts=OPTS, &block) if msg = csrf_invalid_message(opts) if block @_request.on(&block) end case failure_action = opts.fetch(:csrf_failure, csrf_options[:csrf_failure]) when :raise raise InvalidToken, msg when :empty_403 @_response.status = 403 headers = @_response.headers headers.clear headers[RodaResponseHeaders::CONTENT_TYPE] = 'text/html' headers[RodaResponseHeaders::CONTENT_LENGTH] ='0' throw :halt, @_response.finish_with_body([]) when :clear_session session.clear when :csrf_failure_method @_request.on{_roda_route_csrf_failure(@_request)} when Proc RodaPlugins.warn "Passing a Proc as the :csrf_failure option value to check_csrf! is deprecated" @_request.on{instance_exec(@_request, &failure_action)} # Deprecated else raise RodaError, "Unsupported :csrf_failure option: #{failure_action.inspect}" end end end # The name of the hidden input tag containing the CSRF token. Also used as the name # for the meta tag. def csrf_field csrf_options[:field] end # The HTTP header name to use when submitting CSRF tokens in an HTTP header, if # such support is enabled (it is not by default). def csrf_header csrf_options[:header] end # An HTML meta tag string containing a CSRF token that is not request-specific. # It is not recommended to use this, as it doesn't support request-specific tokens. def csrf_metatag "" end # Given a form action, return the appropriate path to use for the CSRF token. # This makes it easier to generate request-specific tokens without having to # worry about the different types of form actions (relative paths, absolute # paths, URLs, empty paths). def csrf_path(action) case action when nil, '', /\A[#?]/ # use current path request.path when /\A(?:https?:\/)?\// # Either full URI or absolute path, extract just the path URI.parse(action).path else # relative path, join to current path URI.join(request.url, action).path end end # An HTML hidden input tag string containing the CSRF token, used for inputs # with formaction, so the same form can be used to submit to multiple endpoints # depending on which button was clicked. See csrf_token for arguments, but the # path argument is required. def csrf_formaction_tag(path, *args) "" end # An HTML hidden input tag string containing the CSRF token. See csrf_token for # arguments. def csrf_tag(*args) "" end # The value of the csrf token. For a path specific token, provide a path # argument. By default, it a path is provided, the POST request method will # be assumed. To generate a token for a non-POST request method, pass the # method as the second argument. def csrf_token(path=nil, method=('POST' if path)) token = SecureRandom.random_bytes(31) token << csrf_hmac(token, method, path) [token].pack("m0") end # Whether request-specific CSRF tokens should be used by default. def use_request_specific_csrf_tokens? csrf_options[:require_request_specific_tokens] end # Whether the submitted CSRF token is valid for the request. True if the # request does not require a CSRF token. def valid_csrf?(opts=OPTS) csrf_invalid_message(opts).nil? end private # Returns error message string if the CSRF token is not valid. # Returns nil if the CSRF token is valid. def csrf_invalid_message(opts) opts = opts.empty? ? csrf_options : csrf_options.merge(opts) method = request.request_method unless opts[:check_request_methods].include?(method) return end path = @_request.path unless encoded_token = opts[:token] encoded_token = case opts[:check_header] when :only env[opts[:env_header]] when true return (csrf_invalid_message(opts.merge(:check_header=>false)) && csrf_invalid_message(opts.merge(:check_header=>:only))) else params = @_request.params ((formactions = params[opts[:formaction_field]]).is_a?(Hash) && (formactions[path])) || params[opts[:field]] end end unless encoded_token.is_a?(String) return "encoded token is not a string" end if (rack_csrf_key = opts[:upgrade_from_rack_csrf_key]) && (rack_csrf_value = session[rack_csrf_key]) && csrf_compare(rack_csrf_value, encoded_token) return end # 31 byte random initialization vector # 32 byte HMAC # 63 bytes total # 84 bytes when base64 encoded unless encoded_token.bytesize == 84 return "encoded token length is not 84" end begin submitted_hmac = Base64_.decode64(encoded_token) rescue ArgumentError return "encoded token is not valid base64" end random_data = submitted_hmac.slice!(0...31) if csrf_compare(csrf_hmac(random_data, method, path), submitted_hmac) return end if opts[:require_request_specific_tokens] "decoded token is not valid for request method and path" else unless csrf_compare(csrf_hmac(random_data, '', ''), submitted_hmac) "decoded token is not valid for either request method and path or for blank method and path" end end end # Helper for getting the plugin options. def csrf_options opts[:route_csrf] end # Perform a constant-time comparison of the two strings, returning true if they match and false otherwise. def csrf_compare(s1, s2) Rack::Utils.secure_compare(s1, s2) end # Return the HMAC-SHA-256 for the secret and the given arguments. def csrf_hmac(random_data, method, path) OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, csrf_secret, "#{method.to_s.upcase}#{path}#{random_data}") end # If a secret has not already been specified, generate a random 32-byte # secret, stored base64 encoded in the session (to handle cases where # JSON is used for session serialization). def csrf_secret key = session[csrf_options[:key]] ||= SecureRandom.base64(32) Base64_.decode64(key) end end end register_plugin(:route_csrf, RouteCsrf) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/run_append_slash.rb000066400000000000000000000030111516720775400244310ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The run_append_slash plugin makes +r.run+ use +/+ as the +PATH_INFO+ # when calling the rack application if +PATH_INFO+ would be empty. # Example: # # route do |r| # r.on "a" do # r.run App # end # end # # # without run_append_slash: # # GET /a => App gets "" as PATH_INFO # # GET /a/ => App gets "/" as PATH_INFO # # # with run_append_slash: # # GET /a => App gets "/" as PATH_INFO # # GET /a/ => App gets "/" as PATH_INFO module RunAppendSlash # Set plugin specific options. Options: # :use_redirects :: Whether to issue 302 redirects when appending the # trailing slash. def self.configure(app, opts=OPTS) app.opts[:run_append_slash_redirect] = !!opts[:use_redirects] end module RequestMethods # Calls the given rack app. If the path matches the root of the app but # does not contain a trailing slash, a trailing slash is appended to the # path internally, or a redirect is issued when configured with # use_redirects: true. def run(*) if @remaining_path.empty? if scope.opts[:run_append_slash_redirect] redirect("#{path}/") else @remaining_path += '/' end end super end end end register_plugin(:run_append_slash, RunAppendSlash) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/run_handler.rb000066400000000000000000000031331516720775400234120ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The run_handler plugin allows r.run to take a block, which is yielded # the rack response array, before it returns it as a response. # # Additionally, r.run also takes a options hash, and you can provide a # not_found: :pass option to keep routing normally if the rack # app returns a 404 response. # # # plugin :run_handler # # route do |r| # r.on 'a' do # # Keep running code if RackAppFoo doesn't return a result # r.run RackAppFoo, not_found: :pass # # # Change response status codes before returning. # r.run(RackAppBar) do |response| # response[0] = 200 if response[0] == 201 # end # end # end module RunHandler module RequestMethods # If a block is given, yield the rack response array to it. The response can # be modified before it is returned by the current app. # # If the not_found: :pass option is given, and the rack response # returned by the app is a 404 response, do not return the response, continue # routing normally. def run(app, opts=OPTS) res = catch(:halt){super(app)} yield res if defined?(yield) if opts[:not_found] == :pass && res[0] == 404 body = res[2] body.close if body.respond_to?(:close) nil else throw(:halt, res) end end end end register_plugin(:run_handler, RunHandler) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/run_require_slash.rb000066400000000000000000000026571516720775400246550ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The run_require_slash plugin makes +r.run+ a no-op if the remaining # path is not empty and does not start with +/+. The Rack SPEC requires that # +PATH_INFO+ start with a slash if not empty, so this plugin prevents # dispatching to the application with an environment that would violate the # Rack SPEC. # # You are unlikely to want to use this plugin unless you are consuming partial # segments of the request path, or using the match_affix plugin to change # how routing is done: # # plugin :match_affix, "", /(\/|\z)/ # route do |r| # r.on "/a" do # r.on "b" do # r.run App # end # end # end # # # with run_require_slash: # # GET /a/b/e => Not dispatched to application # # GET /a/b => App gets "" as PATH_INFO # # # without run_require_slash: # # GET /a/b/e => App gets "e" as PATH_INFO, violating rack SPEC # # GET /a/b => App gets "" as PATH_INFO module RunRequireSlash module RequestMethods # Calls the given rack app only if the remaining patch is empty or # starts with a slash. def run(*) if @remaining_path.empty? || @remaining_path.start_with?('/') super end end end end register_plugin(:run_require_slash, RunRequireSlash) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/sec_fetch_site_csrf.rb000066400000000000000000000132061516720775400250770ustar00rootroot00000000000000# frozen-string-literal: true class Roda module RodaPlugins # The sec_fetch_site plugin allows for CSRF protection using the # Sec-Fetch-Site header added in modern browsers. It allows for CSRF # protection without the use of CSRF tokens, which can simplify # form creation. # # The protection offered by the sec_fetch_site plugin is weaker than # the protection offered by the route_csrf plugin with default settings, # since it doesn't support per-request tokens. Be aware you are trading # security for simplicity when using the sec_fetch_site plugin instead # of the route_csrf plugin. Other caveats in using the sec_fetch_site # plugin: # # * Not all browsers set the Sec-Fetch-Site header. Some browsers # didn't start setting the header until 2023. In these cases, you # need to decide how to handle the request. The default is to deny # the request, though you can use the :allow_missing option to allow # it. # # * Sec-Fetch-Site headers are not set for http requests, only https # requests, so this doesn't offer protection for http requests. # # * It isn't possible to share a CSRF secret between applications in # different origins to allow cross-site requests between the # applications. # # This plugin adds the +check_sec_fetch_site!+ method to the routing # block scope. You should call this method at the appropriate place # in the routing tree to enforce the CSRF protection. The method can # accept a block to override the :csrf_failure plugin option behavior # on a per-call basis. # # When loading the plugin with no options: # # plugin :sec_fetch_site_csrf # # Only same-origin requests are allowed by default. # # This plugin supports the following options: # # :allow_missing :: Whether to allow requests lacking the Sec-Fetch-Site # header (false by default). # :allow_none :: Whether to allow requests where Sec-Fetch-Value is none # (false by default). # :allow_same_site :: Whether to allow requests where Sec-Fetch-Value is # same-site (false by default) # :check_request_methods :: Which request methods require CSRF protection # (default: ['POST', 'DELETE', 'PATCH', 'PUT']) # :csrf_failure :: The action to taken if a request does not have a valid header # (default: :raise). Options: # :raise :: raise a Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure # exception # :empty_403 :: return a blank 403 page # :clear_session :: clear the current session # # The plugin also supports a block, in which case failures will call the block # as a routing block (the block should accept the request object). module SecFetchSiteCsrf DEFAULTS = { :csrf_failure => :raise, :check_request_methods => %w'POST DELETE PATCH PUT'.freeze.each(&:freeze) }.freeze # Exception class raised when :csrf_failure option is :raise and # the Sec-Fetch-Site header is not considered valid. class CsrfFailure < RodaError; end def self.configure(app, opts=OPTS, &block) options = app.opts[:sec_fetch_site_csrf] = (app.opts[:sec_fetch_site_csrf] || DEFAULTS).merge(opts) allowed_values = options[:allowed_values] = ["same-origin"] allowed_values << "same-site" if opts[:allow_same_site] allowed_values << "none" if opts[:allow_none] allowed_values << nil if opts[:allow_missing] allowed_values.freeze if block options[:csrf_failure] = :method app.define_roda_method(:_roda_sec_fetch_site_csrf_failure, 1, &app.send(:convert_route_block, block)) end case options[:csrf_failure] when :raise, :empty_403, :clear_session, :method # nothing else raise RodaError, "Unsupported :csrf_failure plugin option: #{options[:csrf_failure].inspect}" end options.freeze end module InstanceMethods # Check that the Sec-Fetch-Site header is valid, if the request requires it. # If the header is valid or the request does not require the header, return nil. # Otherwise, if a block is given, treat it as a routing block and yield to it, and # if a block is not given, use the plugin :csrf_failure option to determine how to # handle it. def check_sec_fetch_site!(&block) plugin_opts = self.class.opts[:sec_fetch_site_csrf] return unless plugin_opts[:check_request_methods].include?(request.request_method) sec_fetch_site = env["HTTP_SEC_FETCH_SITE"] return if plugin_opts[:allowed_values].include?(sec_fetch_site) @_request.on(&block) if block case failure_action = plugin_opts[:csrf_failure] when :raise raise CsrfFailure, "potential cross-site request, Sec-Fetch-Site value: #{sec_fetch_site.inspect}" when :empty_403 @_response.status = 403 headers = @_response.headers headers.clear headers[RodaResponseHeaders::CONTENT_TYPE] = 'text/html' headers[RodaResponseHeaders::CONTENT_LENGTH] ='0' throw :halt, @_response.finish_with_body([]) when :clear_session session.clear else # when :method @_request.on{_roda_sec_fetch_site_csrf_failure(@_request)} end end end end register_plugin(:sec_fetch_site_csrf, SecFetchSiteCsrf) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/send_file.rb000066400000000000000000000107241516720775400230450ustar00rootroot00000000000000# frozen-string-literal: true begin require 'rack/files' rescue LoadError require 'rack/file' end # class Roda module RodaPlugins # The send_file plugin adds a send_file method, used for # returning the contents of a file as the body of a request. # It also loads the response_attachment plugin to set the # Content-Disposition and Content-Type based on the file's # extension. # # senf_file will serve the file with the given path from the file system: # # send_file 'path/to/file.txt' # # Options: # # :disposition :: Set the Content-Disposition to the given disposition. # :filename :: Set the Content-Disposition to attachment (unless :disposition is set), # and set the filename parameter to the value. # :last_modified :: Explicitly set the Last-Modified header to the given value, and # return a not modified response if there has not been modified since # the previous request. This option requires the caching plugin. # :status :: Override the status for the response. # :type :: Set the Content-Type to use for this response. # # == License # # The implementation was originally taken from Sinatra, # which is also released under the MIT License: # # Copyright (c) 2007, 2008, 2009 Blake Mizerany # Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. module SendFile RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File # Depend on the status_303 plugin. def self.load_dependencies(app) app.plugin :response_attachment end module InstanceMethods # Use the contents of the file at +path+ as the response body. See plugin documentation for options. def send_file(path, opts = OPTS) r = @_request res = @_response headers = res.headers if (type = opts[:type]) || !headers[RodaResponseHeaders::CONTENT_TYPE] type_str = type.to_s if type_str.include?('/') type = type_str else if type type = ".#{type}" unless type_str.start_with?(".") else type = ::File.extname(path) end type &&= Rack::Mime.mime_type(type, nil) type ||= 'application/octet-stream' end headers[RodaResponseHeaders::CONTENT_TYPE] = type end disposition = opts[:disposition] filename = opts[:filename] if disposition || filename disposition ||= 'attachment' filename = path if filename.nil? res.attachment(filename, disposition) end if lm = opts[:last_modified] r.last_modified(lm) end file = RACK_FILES.new nil s, h, b = if Rack.release > '2' file.serving(r, path) else file.path = path file.serving(env) end res.status = opts[:status] || s headers.delete(RodaResponseHeaders::CONTENT_LENGTH) headers.replace(h.merge!(headers)) r.halt res.finish_with_body(b) rescue Errno::ENOENT response.status = 404 r.halt end end end register_plugin(:send_file, SendFile) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/sessions.rb000066400000000000000000000625041516720775400227660ustar00rootroot00000000000000# frozen-string-literal: true require 'openssl' begin OpenSSL::Cipher.new("aes-256-ctr") rescue OpenSSL::Cipher::CipherError # :nocov: raise LoadError, "Roda sessions plugin requires the aes-256-ctr cipher" # :nocov: end require 'json' require 'securerandom' require 'zlib' require 'rack/utils' class Roda module RodaPlugins # The sessions plugin adds support for sessions using cookies. It is the recommended # way to support sessions in Roda applications. # # The session cookies are encrypted with AES-256-CTR using a separate encryption key per cookie, # and then signed with HMAC-SHA-256. By default, session data is padded to reduce information # leaked based on the session size. # # Sessions are serialized via JSON, so session information should only store data that # allows roundtrips via JSON (String, Integer, Float, Array, Hash, true, false, and nil). # In particular, note that Symbol does not round trip via JSON, so symbols should not be # used in sessions when this plugin is used. This plugin sets the # +:sessions_convert_symbols+ application option to +true+ if it hasn't been set yet, # for better integration with plugins that can use either symbol or string session or # flash keys. Unlike Rack::Session::Cookie, the session is stored as a plain ruby hash, # and does not convert all keys to strings. # # All sessions are timestamped and session expiration is enabled by default, with sessions # being valid for 30 days maximum and 7 days since last use by default. Session creation time is # reset whenever the session is empty when serialized and also whenever +clear_session+ # is called while processing the request. # # Session secrets can be rotated. See options below. # # The sessions plugin can transparently upgrade sessions from versions of Rack::Session::Cookie # shipped with Rack before Rack 3, # if the default Rack::Session::Cookie coder and HMAC are used, see options below. # It is recommended to only enable transparent upgrades for a brief transition period, # and remove support for them once old sessions have converted or timed out. # # If the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge # exception will be raised. # # = Required Options # # The session cookies this plugin uses are both encrypted and signed, so two separate # secrets are used internally. However, for ease of use, these secrets are combined into # a single +:secret+ option. The +:secret+ option must be a string of at least 64 bytes # and should be randomly generated. The first 32 bytes are used as the secret for the # cipher, any remaining bytes are used for the secret for the HMAC. # # = Other Options # # :cookie_options :: Any cookie options to set on the session cookie. By default, uses # httponly: true, path: '/', same_site: :lax so that the cookie is not accessible # to javascript, allowed for all paths, and will not be used for cross-site non-GET requests # that. If the +:secure+ option is not present in the hash, then # secure: true is also set if the request is made over HTTPS. If this option is # given, it will be merged into the default cookie options. # :env_key :: The key in `env` where the session should be located. Defaults to "rack.session", the # default key for sessions in Rack. # :gzip_over :: For session data over this many bytes, compress it with the deflate algorithm (default: nil, # so never compress). Note that compression should not be enabled if you are storing data in # the session derived from user input and also storing sensitive data in the session. # :key :: The cookie name to use (default: 'roda.session') # :max_seconds :: The maximum number of seconds to allow for total session lifetime, starting with when # the session was originally created. Default is 86400*30 (30 days). Can be set to # +nil+ to disable session lifetime checks. # :max_idle_seconds :: The maximum number of seconds to allow since the session was last updated. # Default is 86400*7 (7 days). Can be set to nil to disable session idleness # checks. # :old_secret :: The previous secret to use, allowing for secret rotation. Must be a string of at least 64 # bytes if given. # :pad_size :: Pad session data (after possible compression, before encryption), to a multiple of this # many bytes (default: 32). This can be between 2-4096 bytes, or +nil+ to disable padding. # :per_cookie_cipher_secret :: Uses a separate cipher key for every cookie, with the key used generated using # HMAC-SHA-256 of 32 bytes of random data with the default cipher secret. This # offers additional protection in case the random initialization vector used when # encrypting the session data has been reused. Odds of that are 1 in 2**64 if # initialization vector is truly random, but weaknesses in the random number # generator could make the odds much higher. Default is +true+. # :parser :: The parser for the serialized session data (default: JSON.method(:parse)). # :serializer :: The serializer for the session data (default +:to_json.to_proc+). # :skip_within :: If the last update time for the session cookie is less than this number of seconds from the # current time, and the session has not been modified, do not set a new session cookie # (default: 3600). # :upgrade_from_rack_session_cookie_key :: The cookie name to use for transparently upgrading from # Rack::Session:Cookie (defaults to 'rack.session'). # :upgrade_from_rack_session_cookie_secret :: The secret for the HMAC-SHA1 signature when allowing # transparent upgrades from Rack::Session::Cookie. Using this # option is only recommended during a short transition period, # and is not enabled by default as it lowers security. # :upgrade_from_rack_session_cookie_options :: Options to pass when deleting the cookie used by # Rack::Session::Cookie after converting it to use the session # cookies used by this plugin. # # = Not a Rack Middleware # # Unlike some other approaches to sessions, the sessions plugin does not use # a rack middleware, so session information is not available to other rack middleware, # only to the application itself, with the session not being loaded from the cookie # until the +session+ method is called. # # If you need rack middleware to access the session information, then # require 'roda/session_middleware' and use RodaSessionMiddleware. # RodaSessionMiddleware passes the options given to this plugin. # # = Session Cookie Cryptography/Format # # Session cookies created by this plugin by default use the following format: # # urlsafe_base64("\1" + random_data + IV + encrypted session data + HMAC) # # If +:per_cookie_cipher_secret+ option is set to +false+, an older format is used: # # urlsafe_base64("\0" + IV + encrypted session data + HMAC) # # where: # # version :: 1 byte, currently must be 1 or 0, other values reserved for future expansion. # random_data :: 32 bytes, used for generating the per-cookie secret # IV :: 16 bytes, initialization vector for AES-256-CTR cipher. # encrypted session data :: >=12 bytes of data encrypted with AES-256-CTR cipher, see below. # HMAC :: 32 bytes, HMAC-SHA-256 of all preceding data plus cookie key (so that a cookie value # for a different key cannot be used even if the secret is the same). # # The encrypted session data uses the following format: # # bitmap + creation time + update time + padding + serialized data # # where: # # bitmap :: 2 bytes in little endian format, lower 12 bits storing number of padding # bytes, 13th bit storing whether serialized data is compressed with deflate. # Bits 14-16 reserved for future expansion. # creation time :: 4 byte integer in unsigned little endian format, storing unix timestamp # since session initially created. # update time :: 4 byte integer in unsigned little endian format, storing unix timestamp # since session last updated. # padding :: >=0 padding bytes specified in bitmap, filled with random data, can be ignored. # serialized data :: >=2 bytes of serialized data in JSON format. If the bitmap indicates # deflate compression, this contains the deflate compressed data. module Sessions DEFAULT_COOKIE_OPTIONS = {:httponly=>true, :path=>'/'.freeze, :same_site=>:lax}.freeze DEFAULT_OPTIONS = {:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>nil, :skip_within=>3600, :env_key=>'rack.session'}.freeze DEFLATE_BIT = 0x1000 PADDING_MASK = 0x0fff SESSION_CREATED_AT = 'roda.session.created_at'.freeze SESSION_UPDATED_AT = 'roda.session.updated_at'.freeze SESSION_SERIALIZED = 'roda.session.serialized'.freeze SESSION_VERSION_NUM = 'roda.session.version'.freeze SESSION_DELETE_RACK_COOKIE = 'roda.session.delete_rack_session_cookie'.freeze # Exception class used when creating a session cookie that would exceed the # allowable cookie size limit. class CookieTooLarge < RodaError end # Split given secret into a cipher secret and an hmac secret. def self.split_secret(name, secret) raise RodaError, "sessions plugin :#{name} option must be a String" unless secret.is_a?(String) raise RodaError, "invalid sessions plugin :#{name} option length: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64 hmac_secret = secret = secret.dup.force_encoding('BINARY') cipher_secret = secret.slice!(0, 32) [cipher_secret.freeze, hmac_secret.freeze] end def self.load_dependencies(app, opts=OPTS) app.plugin :_base64 end # Configure the plugin, see Sessions for details on options. def self.configure(app, opts=OPTS) opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts) co = opts[:cookie_options] = DEFAULT_COOKIE_OPTIONS.merge(opts[:cookie_options] || OPTS).freeze opts[:remove_cookie_options] = co.merge(:max_age=>'0', :expires=>Time.at(0)) opts[:parser] ||= app.opts[:json_parser] || JSON.method(:parse) opts[:serializer] ||= app.opts[:json_serializer] || :to_json.to_proc opts[:per_cookie_cipher_secret] = true unless opts.has_key?(:per_cookie_cipher_secret) opts[:session_version_num] = opts[:per_cookie_cipher_secret] ? 1 : 0 if opts[:upgrade_from_rack_session_cookie_secret] opts[:upgrade_from_rack_session_cookie_key] ||= 'rack.session' rsco = opts[:upgrade_from_rack_session_cookie_options] = Hash[opts[:upgrade_from_rack_session_cookie_options] || OPTS] rsco[:path] ||= co[:path] rsco[:domain] ||= co[:domain] end opts[:cipher_secret], opts[:hmac_secret] = split_secret(:secret, opts[:secret]) opts[:old_cipher_secret], opts[:old_hmac_secret] = (split_secret(:old_secret, opts[:old_secret]) if opts[:old_secret]) case opts[:pad_size] when nil # no changes when Integer raise RodaError, "invalid :pad_size: #{opts[:pad_size]}, must be >=2, < 4096" unless opts[:pad_size] >= 2 && opts[:pad_size] < 4096 else raise RodaError, "invalid :pad_size option: #{opts[:pad_size].inspect}, must be Integer or nil" end app.opts[:sessions] = opts.freeze app.opts[:sessions_convert_symbols] = true unless app.opts.has_key?(:sessions_convert_symbols) end module InstanceMethods # Clear data from the session, and update the request environment # so that the session cookie will use a new creation timestamp # instead of the previous creation timestamp. def clear_session session.clear env.delete(SESSION_CREATED_AT) env.delete(SESSION_UPDATED_AT) nil end private # If session information has been set in the request environment, # update the rack response headers to set the session cookie in # the response. def _roda_after_50__sessions(res) if res && (session = env[self.class.opts[:sessions][:env_key]]) @_request.persist_session(res[1], session) end end end module RequestMethods # Load the session information from the cookie. With the sessions # plugin, you must call this method to get the session, instead of # trying to access the session directly through the request environment. # For maximum compatibility with other software that uses rack sessions, # this method stores the session in 'rack.session' in the request environment, # but that does not happen until this method is called. def session @env[roda_class.opts[:sessions][:env_key]] ||= _load_session end # The time the session was originally created. nil if there is no active session. def session_created_at session Time.at(@env[SESSION_CREATED_AT]) if @env[SESSION_SERIALIZED] end # The time the session was last updated. nil if there is no active session. def session_updated_at session Time.at(@env[SESSION_UPDATED_AT]) if @env[SESSION_SERIALIZED] end # Persist the session data as a cookie. If transparently upgrading from # Rack::Session::Cookie, mark the related cookie for expiration so it isn't # sent in the future. def persist_session(headers, session) opts = roda_class.opts[:sessions] if session.empty? if env[SESSION_SERIALIZED] # If session was submitted and is now empty, remove the cookie Rack::Utils.delete_cookie_header!(headers, opts[:key], opts[:remove_cookie_options]) # else # If no session was submitted, and the session is empty # then there is no need to do anything end elsif cookie_value = _serialize_session(session) cookie = Hash[opts[:cookie_options]] cookie[:value] = cookie_value cookie[:secure] = true if !cookie.has_key?(:secure) && ssl? before_size = if (set_cookie_before = headers[RodaResponseHeaders::SET_COOKIE]).is_a?(String) set_cookie_before.bytesize else 0 end Rack::Utils.set_cookie_header!(headers, opts[:key], cookie) cookie_size = case set_cookie_after = headers[RodaResponseHeaders::SET_COOKIE] when String # Rack < 3 always takes this branch, combines cookies into string, subtract previous size # Rack 3+ takes this branch if this is the first cookie set, in which case before size is 0 set_cookie_after.bytesize - before_size else # when Array # Rack 3+ takes branch if this is not the first cookie set, and last element of the array # is most recently added cookie set_cookie_after.last.bytesize end if cookie_size >= 4096 raise CookieTooLarge, "attempted to create cookie larger than 4096 bytes (bytes: #{cookie_size})" end end if env[SESSION_DELETE_RACK_COOKIE] Rack::Utils.delete_cookie_header!(headers, opts[:upgrade_from_rack_session_cookie_key], opts[:upgrade_from_rack_session_cookie_options]) end nil end private # Load the session by looking for the appropriate cookie, or falling # back to the rack session cookie if configured. def _load_session opts = roda_class.opts[:sessions] cs = cookies if data = cs[opts[:key]] _deserialize_session(data) elsif (key = opts[:upgrade_from_rack_session_cookie_key]) && (data = cs[key]) _deserialize_rack_session(data) end || {} end # If 'rack.errors' is set, write the error message to it. # This is used for errors that shouldn't be raised as exceptions, # such as improper session cookies. def _session_serialization_error(msg) return unless error_stream = @env['rack.errors'] error_stream.puts(msg) nil end # Interpret given cookie data as a Rack::Session::Cookie # serialized session using the default Rack::Session::Cookie # hmac and coder. def _deserialize_rack_session(data) opts = roda_class.opts[:sessions] data, digest = data.split("--", 2) unless digest return _session_serialization_error("Not decoding Rack::Session::Cookie session: invalid format") end unless Rack::Utils.secure_compare(digest, OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, opts[:upgrade_from_rack_session_cookie_secret], data)) return _session_serialization_error("Not decoding Rack::Session::Cookie session: HMAC invalid") end begin session = Marshal.load(data.unpack('m').first) rescue return _session_serialization_error("Error decoding Rack::Session::Cookie session: not base64 encoded marshal dump") end # Mark rack session cookie for deletion on success env[SESSION_DELETE_RACK_COOKIE] = true # Delete the session id before serializing it. Starting in rack 2.0.8, # this is an object and not just a string, and calling to_s on it raises # a RuntimeError. session.delete("session_id") # Convert the rack session by roundtripping it through # the parser and serializer, so that you would get the # same result as you would if the session was handled # by this plugin. env[SESSION_SERIALIZED] = data = opts[:serializer].call(session) env[SESSION_CREATED_AT] = Time.now.to_i opts[:parser].call(data) end # Interpret given cookie data as a Rack::Session::Cookie def _deserialize_session(data) opts = roda_class.opts[:sessions] begin data = Base64_.urlsafe_decode64(data) rescue ArgumentError return _session_serialization_error("Unable to decode session: invalid base64") end case version = data.getbyte(0) when 1 per_cookie_secret = true # minimum length (1+32+16+12+32) (version+random_data+cipher_iv+minimum session+hmac) # 1 : version # 32 : random_data (if per_cookie_cipher_secret) # 16 : cipher_iv # 12 : minimum_session # 2 : bitmap for gzip + padding info # 4 : creation time # 4 : update time # 2 : data # 32 : HMAC-SHA-256 min_data_length = 93 when 0 per_cookie_secret = false # minimum length (1+16+12+32) (version+cipher_iv+minimum session+hmac) min_data_length = 61 when nil return _session_serialization_error("Unable to decode session: no data") else return _session_serialization_error("Unable to decode session: version marker unsupported") end length = data.bytesize if data.length < min_data_length return _session_serialization_error("Unable to decode session: data too short") end encrypted_data = data.slice!(0, length-32) unless Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], encrypted_data+opts[:key])) if opts[:old_hmac_secret] && Rack::Utils.secure_compare(data, OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:old_hmac_secret], encrypted_data+opts[:key])) use_old_cipher_secret = true else return _session_serialization_error("Not decoding session: HMAC invalid") end end # Remove version encrypted_data.slice!(0) cipher_secret = opts[use_old_cipher_secret ? :old_cipher_secret : :cipher_secret] if per_cookie_secret cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, encrypted_data.slice!(0, 32)) end cipher_iv = encrypted_data.slice!(0, 16) cipher = OpenSSL::Cipher.new("aes-256-ctr") # Not rescuing cipher errors. If there is an error in the decryption, that's # either a bug in the plugin that needs to be fixed, or an attacker is already # able to forge a valid HMAC, in which case the error should be raised to # alert the application owner about the problem. cipher.decrypt cipher.key = cipher_secret cipher.iv = cipher_iv data = cipher.update(encrypted_data) << cipher.final bitmap, created_at, updated_at = data.unpack('vVV') padding_bytes = bitmap & PADDING_MASK now = Time.now.to_i if (max = opts[:max_seconds]) && now > created_at + max return _session_serialization_error("Not returning session: maximum session time expired") end if (max = opts[:max_idle_seconds]) && now > updated_at + max return _session_serialization_error("Not returning session: maximum session idle time expired") end data = data.slice(10+padding_bytes, data.bytesize) if bitmap & DEFLATE_BIT > 0 data = Zlib::Inflate.inflate(data) end env = @env env[SESSION_CREATED_AT] = created_at env[SESSION_UPDATED_AT] = updated_at env[SESSION_SERIALIZED] = data env[SESSION_VERSION_NUM] = version opts[:parser].call(data) end def _serialize_session(session) opts = roda_class.opts[:sessions] env = @env now = Time.now.to_i json_data = opts[:serializer].call(session).force_encoding('BINARY') if (serialized_session = env[SESSION_SERIALIZED]) && (opts[:session_version_num] == env[SESSION_VERSION_NUM]) && (updated_at = env[SESSION_UPDATED_AT]) && (now - updated_at < opts[:skip_within]) && (serialized_session == json_data) return end bitmap = 0 json_length = json_data.bytesize gzip_over = opts[:gzip_over] if gzip_over && json_length > gzip_over json_data = Zlib.deflate(json_data) json_length = json_data.bytesize bitmap |= DEFLATE_BIT end # When calculating padding bytes to use, include 10 bytes for bitmap and # session create/update times, so total size of encrypted data is a # multiple of pad_size. if (pad_size = opts[:pad_size]) && (padding_bytes = (json_length+10) % pad_size) != 0 padding_bytes = pad_size - padding_bytes bitmap |= padding_bytes padding_data = SecureRandom.random_bytes(padding_bytes) end session_create_time = env[SESSION_CREATED_AT] serialized_data = [bitmap, session_create_time||now, now].pack('vVV') serialized_data << padding_data if padding_data serialized_data << json_data cipher_secret = opts[:cipher_secret] if opts[:per_cookie_cipher_secret] version = "\1" per_cookie_secret_base = SecureRandom.random_bytes(32) cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, per_cookie_secret_base) else version = "\0" end cipher = OpenSSL::Cipher.new("aes-256-ctr") cipher.encrypt cipher.key = cipher_secret cipher_iv = cipher.random_iv encrypted_data = cipher.update(serialized_data) << cipher.final data = String.new data << version data << per_cookie_secret_base if per_cookie_secret_base data << cipher_iv data << encrypted_data data << OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, opts[:hmac_secret], data+opts[:key]) data = Base64_.urlsafe_encode64(data) if data.bytesize >= 4096 raise CookieTooLarge, "attempted to create cookie larger than 4096 bytes (bytes: #{data.bytesize})" end data end end end register_plugin(:sessions, Sessions) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/shared_vars.rb000066400000000000000000000045111516720775400234130ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The shared_vars plugin adds a +shared+ method for storing # shared variables across nested Roda apps. # # class API < Roda # plugin :shared_vars # route do |r| # user = shared[:user] # # ... # end # end # # class App < Roda # plugin :shared_vars # # route do |r| # r.on Integer do |user_id| # shared[:user] = User[user_id] # r.run API # end # end # end # # If you pass a hash to shared, it will update the shared # vars with the content of the hash: # # route do |r| # r.on Integer do |user_id| # shared(user: User[user_id]) # r.run API # end # end # # You can also pass a block to shared, which will set the # shared variables only for the given block, restoring the # previous shared variables afterward: # # route do |r| # r.on Integer do |user_id| # shared(user: User[user_id]) do # r.run API # end # end # end module SharedVars module InstanceMethods # Returns the current shared vars for the request. These are # stored in the request's environment, so they will be implicitly # shared with other apps using this plugin. # # If the +vars+ argument is given, it should be a hash that will be # merged into the current shared vars. # # If a block is given, a +vars+ argument must be provided, and it will # only make the changes to the shared vars for the duration of the # block, restoring the previous shared vars before the block returns. def shared(vars=nil) h = env['roda.shared'] ||= {} if defined?(yield) if vars begin env['roda.shared'] = h.merge(vars) yield ensure env['roda.shared'] = h end else raise RodaError, "must pass a vars hash when calling shared with a block" end elsif vars h.merge!(vars) end h end end end register_plugin(:shared_vars, SharedVars) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/sinatra_helpers.rb000066400000000000000000000355531516720775400243070ustar00rootroot00000000000000# frozen-string-literal: true require 'rack/mime' # class Roda module RodaPlugins # The sinatra_helpers plugin ports most of the helper methods # defined in Sinatra::Helpers to Roda, other than those # helpers that were already covered by other plugins such # as caching and streaming. # # Unlike Sinatra, the helper methods are added to either # the request or response classes instead of directly to # the scope of the route block. However, for consistency # with Sinatra, delegate methods are added to the scope # of the route block that call the methods on the request # or response. If you do not want to pollute the namespace # of the route block, you should load the plugin with the # delegate: false option: # # plugin :sinatra_helpers, delegate: false # # == Class Methods Added # # The only class method added by this plugin is +mime_type+, # which is a shortcut for retrieving or setting MIME types # in Rack's MIME database: # # Roda.mime_type 'csv' # => 'text/csv' # Roda.mime_type 'foobar', 'application/foobar' # set # # == Request Methods Added # # In addition to adding the following methods, this changes # +redirect+ to use a 303 response status code by default for # HTTP 1.1 non-GET requests, and to automatically use # absolute URIs if the +:absolute_redirects+ Roda class option # is true, and to automatically prefix redirect paths with the # script name if the +:prefixed_redirects+ Roda class option is # true. # # When adding delegate methods, a logger method is added to # the route block scope that calls the logger method on the request. # # === back # # +back+ is an alias to referrer, so you can do: # # redirect back # # === error # # +error+ sets the response status code to 500 (or a status code you provide), # and halts the request. It takes an optional body: # # error # 500 response, empty boby # error 501 # 501 response, empty body # error 'b' # 500 response, 'b' body # error 501, 'b' # 501 response, 'b' body # # === not_found # # +not_found+ sets the response status code to 404 and halts the request. # It takes an optional body: # # not_found # 404 response, empty body # not_found 'b' # 404 response, 'b' body # # === uri # # +uri+ by default returns absolute URIs that are prefixed # by the script name: # # request.script_name # => '/foo' # uri '/bar' # => 'http://example.org/foo/bar' # # You can turn of the absolute or script name prefixing if you want: # # uri '/bar', false # => '/foo/bar' # uri '/bar', true, false # => 'http://example.org/bar' # uri '/bar', false, false # => '/bar' # # This method is aliased as +url+ and +to+. # # === send_file # # See send_file plugin documentation for details. # # == Response Methods Added # # === body # # When called with an argument or block, +body+ sets the body, otherwise # it returns the body: # # body # => [] # body('b') # set body to 'b' # body{'b'} # set body to 'b', but don't call until body is needed # # === body= # # +body+ sets the body to the given value: # # response.body = 'v' # # This method is not delegated to the scope of the route block, # call +body+ with an argument to set the value. # # === status # # When called with an argument, +status+ sets the status, otherwise # it returns the status: # # status # => 200 # status(301) # sets status to 301 # # === headers # # When called with an argument, +headers+ merges the given headers # into the current headers, otherwise it returns the headers: # # headers['Foo'] = 'Bar' # headers 'Foo' => 'Bar' # # === mime_type # # +mime_type+ just calls the Roda class method to get the mime_type. # # === content_type # # When called with an argument, +content_type+ sets the Content-Type # based on the argument, otherwise it returns the Content-Type. # # mime_type # => 'text/html' # mime_type 'csv' # set Content-Type to 'text/csv' # mime_type :csv # set Content-Type to 'text/csv' # mime_type '.csv' # set Content-Type to 'text/csv' # mime_type 'text/csv' # set Content-Type to 'text/csv' # # Options: # # :charset :: Set the charset for the mime type to the given charset, if the charset is # not already set in the mime type. # :default :: Uses the given type if the mime type is not known. If this option is not # used and the mime type is not known, an exception will be raised. # # === attachment # # See response_attachment plugin for details. # # === status predicates # # This adds the following predicate methods for checking the status: # # informational? # 100-199 # success? # 200-299 # redirect? # 300-399 # client_error? # 400-499 # not_found? # 404 # server_error? # 500-599 # # If the status has not yet been set for the response, these will # return +nil+. # # == License # # The implementation was originally taken from Sinatra, # which is also released under the MIT License: # # Copyright (c) 2007, 2008, 2009 Blake Mizerany # Copyright (c) 2010, 2011, 2012, 2013, 2014 Konstantin Haase # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation # files (the "Software"), to deal in the Software without # restriction, including without limitation the rights to use, # copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the # Software is furnished to do so, subject to the following # conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. module SinatraHelpers # Depend on the status_303 plugin. def self.load_dependencies(app, _opts = nil) app.plugin :status_303 app.plugin :send_file end # Add delegate methods to the route block scope # calling request or response methods, unless the # :delegate option is false. def self.configure(app, opts=OPTS) app.send(:include, DelegateMethods) unless opts[:delegate] == false end # Class used when the response body is set explicitly, instead # of using Roda's default body array and response.write to # write to it. class DelayedBody # Save the block that will return the body, it won't be # called until the body is needed. def initialize(&block) @block = block end # If the body is a String, yield it, otherwise yield each string # returned by calling each on the body. def each v = value if v.is_a?(String) yield v else v.each{|s| yield s} end end # Assume that if the body has been set directly that it is # never empty. def empty? false end # Return the body as a single string, mostly useful during testing. def join a = [] each{|s| a << s} a.join end # Calculate the length for the body. def length length = 0 each{|s| length += s.bytesize} length end private # Cache the body returned by the block. This way the block won't # be called multiple times. def value @value ||= @block.call end end module RequestMethods # Alias for referrer def back referrer end # Halt processing and return the error status provided with the given code and # optional body. # If a single argument is given and it is not an integer, consider it the body # and use a 500 status code. def error(code=500, body = nil) unless code.is_a?(Integer) body = code code = 500 end response.status = code response.body = body if body halt end # Halt processing and return a 404 response with an optional body. def not_found(body = nil) error(404, body) end # If the absolute_redirects or :prefixed_redirects roda class options has been set, respect those # and update the path. def redirect(path=(no_add_script_name = true; default_redirect_path), status=default_redirect_status) opts = roda_class.opts absolute_redirects = opts[:absolute_redirects] prefixed_redirects = no_add_script_name ? false : opts[:prefixed_redirects] path = uri(path, absolute_redirects, prefixed_redirects) if absolute_redirects || prefixed_redirects super(path, status) end # Backwards compatibility for callers of r.send_file. def send_file(path, opts = OPTS) scope.send_file(path, opts) end # Generates the absolute URI for a given path in the app. # Takes Rack routers and reverse proxies into account. def uri(addr = nil, absolute = true, add_script_name = true) addr = addr.to_s if addr return addr if addr =~ /\A[A-z][A-z0-9\+\.\-]*:/ uri = if absolute h = if @env.has_key?("HTTP_X_FORWARDED_HOST") || port != (ssl? ? 443 : 80) host_with_port else host end ["http#{'s' if ssl?}://#{h}"] else [''] end uri << script_name.to_s if add_script_name uri << (addr || path_info) File.join(uri) end alias url uri alias to uri end module ResponseMethods # Set or retrieve the response status code. def status(value = nil || (return @status)) @status = value end # Set or retrieve the response body. When a block is given, # evaluation is deferred until the body is needed. def body(value = (return @body unless defined?(yield); nil), &block) if block @body = DelayedBody.new(&block) else self.body = value end end # Set the body to the given value. def body=(body) @body = DelayedBody.new{body} end # If the body is a DelayedBody, set the appropriate length for it. def finish @length = @body.length if @body.is_a?(DelayedBody) && !@headers[RodaResponseHeaders::CONTENT_LENGTH] super end # Set multiple response headers with Hash, or return the headers if no # argument is given. def headers(hash = nil || (return @headers)) @headers.merge!(hash) end # Look up a media type by file extension in Rack's mime registry. def mime_type(type) roda_class.mime_type(type) end # Set the Content-Type of the response body given a media type or file # extension. See plugin documentation for options. def content_type(type = nil || (return @headers[RodaResponseHeaders::CONTENT_TYPE]), opts = OPTS) unless (mime_type = mime_type(type) || opts[:default]) raise RodaError, "Unknown media type: #{type}" end unless opts.empty? opts.each do |key, val| next if key == :default || (key == :charset && mime_type.include?('charset')) val = val.inspect if val =~ /[";,]/ mime_type += "#{mime_type.include?(';') ? ', ' : ';'}#{key}=#{val}" end end @headers[RodaResponseHeaders::CONTENT_TYPE] = mime_type end # Whether or not the status is set to 1xx. Returns nil if status not yet set. def informational? @status.between?(100, 199) if @status end # Whether or not the status is set to 2xx. Returns nil if status not yet set. def success? @status.between?(200, 299) if @status end # Whether or not the status is set to 3xx. Returns nil if status not yet set. def redirect? @status.between?(300, 399) if @status end # Whether or not the status is set to 4xx. Returns nil if status not yet set. def client_error? @status.between?(400, 499) if @status end # Whether or not the status is set to 5xx. Returns nil if status not yet set. def server_error? @status.between?(500, 599) if @status end # Whether or not the status is set to 404. Returns nil if status not yet set. def not_found? @status == 404 if @status end end module ClassMethods # If a type and value are given, set the value in Rack's MIME registry. # If only a type is given, lookup the type in Rack's MIME registry and # return it. def mime_type(type=nil || (return), value = nil) return type.to_s if type.to_s.include?('/') type = ".#{type}" unless type.to_s[0] == ?. if value Rack::Mime::MIME_TYPES[type] = value else Rack::Mime.mime_type(type, nil) end end end module DelegateMethods [:logger, :back].each do |meth| define_method(meth){@_request.public_send(meth)} end [:redirect, :uri, :url, :to, :error, :not_found].each do |meth| define_method(meth){|*v, &block| @_request.public_send(meth, *v, &block)} end [:informational?, :success?, :redirect?, :client_error?, :server_error?, :not_found?].each do |meth| define_method(meth){@_response.public_send(meth)} end [:status, :body, :headers, :mime_type, :content_type, :attachment].each do |meth| define_method(meth){|*v, &block| @_response.public_send(meth, *v, &block)} end end end register_plugin(:sinatra_helpers, SinatraHelpers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/slash_path_empty.rb000066400000000000000000000014451516720775400244610ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The slash_path_empty plugin considers "/" as an empty path, # in addition to the default of "" being considered an empty # path. This makes it so +r.is+ without an argument will match # a path of "/", and +r.is+ and verb methods such as +r.get+ and # +r.post+ will match if the path is "/" after the arguments # are processed. This can make it easier to handle applications # where a trailing "/" in the path should be ignored. module SlashPathEmpty module RequestMethods private # Consider the path empty if it is "/". def empty_path? super || remaining_path == '/' end end end register_plugin(:slash_path_empty, SlashPathEmpty) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/static.rb000066400000000000000000000031241516720775400224000ustar00rootroot00000000000000# frozen-string-literal: true require 'rack/static' # class Roda module RodaPlugins # The static plugin loads the Rack::Static middleware into the application. # It mainly exists to make serving static files simpler, by supplying # defaults to Rack::Static that are appropriate for Roda. # # The static plugin recognizes the application's :root option, and by default # sets the Rack::Static +:root+ option to the +public+ subfolder of the application's # +:root+ option. Additionally, if a relative path is provided as the :root # option to the plugin, it will be considered relative to the application's # +:root+ option. # # Since the :urls option for Rack::Static is always required, the static plugin # uses a separate option for it. # # Users of this plugin may want to consider using the public plugin instead. # # Examples: # # opts[:root] = '/path/to/app' # plugin :static, ['/js', '/css'] # path: /path/to/app/public # plugin :static, ['/images'], root: 'pub' # path: /path/to/app/pub # plugin :static, ['/media'], root: '/path/to/public' # path: /path/to/public module Static # Load the Rack::Static middleware. Use the paths given as the :urls option, # and set the :root option to be relative to the application's :root option. def self.configure(app, paths, opts={}) opts = opts.dup opts[:urls] = paths opts[:root] = app.expand_path(opts[:root]||"public") app.use ::Rack::Static, opts end end register_plugin(:static, Static) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/static_routing.rb000066400000000000000000000064361516720775400241600ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The static_routing plugin adds static_* routing class methods for handling # static routes (i.e. routes with static paths, no nesting or placeholders). These # routes are processed before the normal routing tree and designed for # maximum performance. This can be substantially faster than Roda's normal # tree based routing if you have large numbers of static routes, about 3-4x # for 100-10000 static routes. Example: # # plugin :static_routing # # static_route '/foo' do |r| # @var = :foo # # r.get do # 'Not actually reached' # end # # r.post{'static POST /#{@var}'} # end # # static_get '/foo' do |r| # 'static GET /foo' # end # # route do |r| # 'Not a static route' # end # # A few things to note in the above example. First, unlike most other # routing methods in Roda, these take the full path of the request, and only # match if r.path_info matches exactly. This is why you need to include the # leading slash in the path argument. # # Second, the static_* routing methods only take a single string argument for # the path, they do not accept other options, and do not handle placeholders # in strings. For any routes needing placeholders, you should use Roda's # routing tree. # # There are separate static_* methods for each type of request method, and these # request method specific routes are tried first. There is also a static_route # method that will match regardless of the request method, if there is no # matching request methods specific route. This is why the static_get # method call takes precedence over the static_route method call for /foo. # As shown above, you can use Roda's routing tree methods inside the # static_route block to have shared behavior for different request methods, # while still handling the request methods differently. module StaticRouting def self.load_dependencies(app) app.plugin :hash_paths end module ClassMethods # Add a static route for any request method. These are # tried after the request method specific static routes (e.g. # static_get), but allow you to use Roda's routing tree # methods inside the route for handling shared behavior while # still allowing request method specific handling. def static_route(path, &block) hash_path(:static_routing, path, &block) end [:get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth| request_method = meth.to_s.upcase define_method("static_#{meth}") do |path, &block| hash_path(request_method, path, &block) end end end module InstanceMethods private # If there is a static routing method for the given path, call it # instead having the routing tree handle the request. def _roda_before_30__static_routing r = @_request r.hash_paths(r.request_method) r.hash_paths(:static_routing) end end end register_plugin(:static_routing, StaticRouting) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/status_303.rb000066400000000000000000000013501516720775400230200ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The status_303 plugin sets the default redirect status to be 303 # rather than 302 when the request is not a GET and the # redirection occurs on an HTTP 1.1 connection as per RFC 7231. # There are some frontend frameworks that require this behavior. # # Example: # # plugin :status_303 module Status303 module RequestMethods private def default_redirect_status return super if is_get? case http_version when 'HTTP/1.0', 'HTTP/0.9', nil super else 303 end end end end register_plugin(:status_303, Status303) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/status_handler.rb000066400000000000000000000056301516720775400241350ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The status_handler plugin adds a +status_handler+ method which sets a # block that is called whenever a response with the relevant response code # with an empty body would be returned. # # This plugin does not support providing the blocks with the plugin call; # you must provide them to status_handler calls afterwards: # # plugin :status_handler # # status_handler(403) do # "You are forbidden from seeing that!" # end # # status_handler(404) do # "Where did it go?" # end # # status_handler(405, keep_headers: ['Accept']) do # "Use a different method!" # end # # Before a block is called, any existing headers on the response will be # cleared, unless the +:keep_headers+ option is used. If the +:keep_headers+ # option is used, the value should be an array, and only the headers listed # in the array will be kept. module StatusHandler CLEAR_HEADERS = :clear.to_proc private_constant :CLEAR_HEADERS def self.configure(app) app.opts[:status_handler] ||= {} end module ClassMethods # Install the given block as a status handler for the given HTTP response code. def status_handler(code, opts=OPTS, &block) # For backwards compatibility, pass request argument if block accepts argument arity = block.arity == 0 ? 0 : 1 handle_headers = case keep_headers = opts[:keep_headers] when nil, false CLEAR_HEADERS when Array if Rack.release >= '3' keep_headers = keep_headers.map(&:downcase) end lambda{|headers| headers.delete_if{|k,_| !keep_headers.include?(k)}} else raise RodaError, "Invalid :keep_headers option" end meth = define_roda_method(:"_roda_status_handler__#{code}", arity, &block) self.opts[:status_handler][code] = define_roda_method(:"_roda_status_handler_#{code}", 1) do |result| res = @_response res.status = result[0] handle_headers.call(res.headers) result.replace(_roda_handle_route{arity == 1 ? send(meth, @_request) : send(meth)}) end end # Freeze the hash of status handlers so that there can be no thread safety issues at runtime. def freeze opts[:status_handler].freeze super end end module InstanceMethods private # If routing returns a response we have a handler for, call that handler. def _roda_after_20__status_handler(result) if result && (meth = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty? send(meth, result) end end end end register_plugin(:status_handler, StatusHandler) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/streaming.rb000066400000000000000000000116721516720775400231110ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The streaming plugin adds support for streaming responses # from roda using the +stream+ method: # # plugin :streaming # # route do |r| # stream do |out| # ['a', 'b', 'c'].each{|v| out << v; sleep 1} # end # end # # In order for streaming to work, any webservers used in # front of the roda app must not buffer responses. # # The stream method takes the following options: # # :callback :: A callback proc to call when the connection is closed. # :loop :: Whether to call the stream block continuously until the connection is closed. # :async :: Whether to call the stream block in a separate thread (default: false). Only supported on Ruby 2.3+. # :queue :: A queue object to use for asynchronous streaming (default: `SizedQueue.new(10)`). # # If the :loop option is used, you can override the # handle_stream_error method to change how exceptions # are handled during streaming. This method is passed the # exception and the stream. By default, this method # just reraises the exception, but you can choose to output # the an error message to the stream, before raising: # # def handle_stream_error(e, out) # out << 'ERROR!' # raise e # end # # Ignore errors completely while streaming: # # def handle_stream_error(e, out) # end # # or handle the errors in some other way. module Streaming # Class of the response body in case you use #stream. class Stream include Enumerable # Handle streaming options, see Streaming for details. def initialize(opts=OPTS, &block) @block = block @out = nil @callback = opts[:callback] @closed = false end # Add output to the streaming response body. Returns number of bytes written. def write(data) data = data.to_s @out.call(data) data.bytesize end # Add output to the streaming response body. Returns self. def <<(data) write(data) self end # If not already closed, close the connection, and call # any callbacks. def close return if closed? @closed = true @callback.call if @callback end # Whether the connection has already been closed. def closed? @closed end # Yield values to the block as they are passed in via #<<. def each(&out) @out = out @block.call(self) ensure close end end # Class of the response body if you use #stream with :async set to true. # Uses a separate thread that pushes streaming results to a queue, so that # data can be streamed to clients while it is being prepared by the application. class AsyncStream include Enumerable # Handle streaming options, see Streaming for details. def initialize(opts=OPTS, &block) @stream = Stream.new(opts, &block) @queue = opts[:queue] || SizedQueue.new(10) # have some default backpressure @thread = Thread.new { enqueue_chunks } end # Continue streaming data until the stream is finished. def each(&out) dequeue_chunks(&out) @thread.join end # Stop streaming. def close @queue.close # terminate the producer thread @stream.close end private # Push each streaming chunk onto the queue. def enqueue_chunks @stream.each do |chunk| @queue.push(chunk) end rescue ClosedQueueError # connection was closed ensure @queue.close end # Pop each streaming chunk from the queue and yield it. def dequeue_chunks while chunk = @queue.pop yield chunk end end end module InstanceMethods # Immediately return a streaming response using the current response # status and headers, calling the block to get the streaming response. # See Streaming for details. def stream(opts=OPTS, &block) if opts[:loop] block = proc do |out| until out.closed? begin yield(out) rescue => e handle_stream_error(e, out) end end end end stream_class = (opts[:async] && RUBY_VERSION >= '2.3') ? AsyncStream : Stream throw :halt, @_response.finish_with_body(stream_class.new(opts, &block)) end # Handle exceptions raised while streaming when using :loop def handle_stream_error(e, out) raise e end end end register_plugin(:streaming, Streaming) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/strip_path_prefix.rb000066400000000000000000000022701516720775400246440ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The strip_path_prefix plugin makes Roda strip a given prefix off internal absolute paths, # turning them to relative paths. Roda by default stores internal paths as absolute paths. # The main reason to use this plugin is when the internal absolute path could change at # runtime, either due to a symlink change or chroot call, or you really want to use # relative paths instead of absolute paths. # # Examples: # # plugin :strip_path_prefix # Defaults to Dir.pwd # plugin :strip_path_prefix, File.dirname(Dir.pwd) module StripPathPrefix # Set the regexp to use when stripping prefixes from internal paths. def self.configure(app, prefix=Dir.pwd) prefix += '/' unless prefix.end_with?("/") app.opts[:strip_path_prefix] = /\A#{Regexp.escape(prefix)}/ end module ClassMethods # Strip the path prefix from the gien path if it starts with the prefix. def expand_path(path, root=opts[:root]) super.sub(opts[:strip_path_prefix], '') end end end register_plugin(:strip_path_prefix, StripPathPrefix) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/symbol_matchers.rb000066400000000000000000000144361516720775400243140ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The symbol_matchers plugin allows you do define custom regexps to use # for specific symbols. For example, if you have a route such as: # # r.on :username do |username| # # ... # end # # By default this will match all nonempty segments. However, if your usernames # must be 6-20 characters, and can only contain +a-z+ and +0-9+, you can do: # # plugin :symbol_matchers # symbol_matcher :username, /([a-z0-9]{6,20})/ # # Then the route will only if the path is +/foobar123+, but not if it is # +/foo+, +/FooBar123+, or +/foobar_123+. # # By default, this plugin sets up the following symbol matchers: # # :d :: /(\d+)/, a decimal segment # :rest :: /(.*)/, all remaining characters, if any # :w :: /(\w+)/, an alphanumeric segment # # If the placeholder_string_matchers plugin is loaded, this feature also applies to # placeholders in strings, so the following: # # r.on "users/:username" do |username| # # ... # end # # Would match +/users/foobar123+, but not +/users/foo+, +/users/FooBar123+, # or +/users/foobar_123+. # # If using this plugin with the params_capturing plugin, this plugin should # be loaded first. # # You can provide a block when calling +symbol_matcher+, and it will be called # for all matches to allow for type conversion: # # symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| # Date.new(y.to_i, m.to_i, d.to_i) # end # # route do |r| # r.on :date do |date| # # date is an instance of Date # end # end # # If you have a segment match the passed regexp, but decide during block # processing that you do not want to treat it as a match, you can have the # block return nil or false. This is useful if you want to make sure you # are using valid data: # # symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d| # y = y.to_i # m = m.to_i # d = d.to_i # Date.new(y, m, d) if Date.valid_date?(y, m, d) # end # # You can have the block return an array to yield multiple captures. # # The second argument to symbol_matcher can be a symbol already registered # as a symbol matcher. This can DRY up code that wants a conversion # performed by an existing class matcher or to use the same regexp: # # symbol_matcher :employee_id, :d do |id| # id.to_i # end # symbol_matcher :employee, :employee_id do |id| # Employee[id] # end # # With the above example, the :d matcher matches only decimal strings, but # yields them as string. The registered :employee_id matcher converts the # decimal string to an integer. The registered :employee matcher builds # on that and uses the integer to lookup the related employee. If there is # no employee with that id, then the :employee matcher will not match. # # If using the class_matchers plugin, you can provide a recognized class # matcher as the second argument to symbol_matcher, and it will work in # a similar manner: # # symbol_matcher :employee, Integer do |id| # Employee[id] # end # # Blocks passed to the symbol matchers plugin are evaluated in route # block context. # # If providing a block to the symbol_matchers plugin, the symbol may # not work with the params_capturing plugin. Note that the use of # symbol matchers inside strings when using the placeholder_string_matchers # plugin only uses the regexp, it does not respect the conversion blocks # registered with the symbols. module SymbolMatchers def self.load_dependencies(app) app.plugin :_symbol_regexp_matchers app.plugin :_symbol_class_matchers end def self.configure(app) app.opts[:symbol_matchers] ||= {} app.symbol_matcher(:d, /(\d+)/) app.symbol_matcher(:w, /(\w+)/) app.symbol_matcher(:rest, /(.*)/) end module ClassMethods # Set the matcher and block to use for the given class. # The matcher can be a regexp, registered symbol matcher, or registered class # matcher (if using the class_matchers plugin). # # If providing a regexp, the block given will be called with all regexp captures. # If providing a registered symbol or class, the block will be called with the # captures returned by the block for the registered symbol or class, or the regexp # captures if no block was registered with the symbol or class. In either case, # if a block is given, it should return an array with the captures to yield to # the match block. def symbol_matcher(s, matcher, &block) _symbol_class_matcher(Symbol, s, matcher, block) do |meth, array| define_method(meth){array} end nil end # Freeze the class_matchers hash when freezing the app. def freeze opts[:symbol_matchers].freeze super end end module RequestMethods private # Use regular expressions to the symbol-specific regular expression # if the symbol is registered. Otherwise, call super for the default # behavior. def _match_symbol(s) meth = :"match_symbol_#{s}" if respond_to?(meth, true) # Allow calling private match methods _, re, convert_meth = send(meth) if re consume(re, convert_meth) else # defined in class_matchers plugin _consume_segment(convert_meth) end else super end end # Return the symbol-specific regular expression if one is registered. # Otherwise, call super for the default behavior. def _match_symbol_regexp(s) meth = :"match_symbol_#{s}" if respond_to?(meth, true) # Allow calling private match methods re, = send(meth) re else super end end end end register_plugin(:symbol_matchers, SymbolMatchers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/symbol_status.rb000066400000000000000000000015331516720775400240230ustar00rootroot00000000000000# frozen-string-literal: true require 'rack/utils' class Roda module RodaPlugins # The symbol_status plugin patches the +status=+ response method to # accept the status name as a symbol. If given an integer value, # the default behaviour is used. # # Examples: # r.is "needs_authorization" do # response.status = :unauthorized # end # r.is "nothing" do # response.status = :no_content # end # # The conversion is done through Rack::Utils.status_code. module SymbolStatus module ResponseMethods # Sets the response status code by fixnum or symbol name def status=(code) code = Rack::Utils.status_code(code) if code.is_a?(Symbol) super(code) end end end register_plugin(:symbol_status, SymbolStatus) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/symbol_views.rb000066400000000000000000000013651516720775400236400ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The symbol_views plugin allows match blocks to return # symbols, and consider those symbols as views to use for the # response body. So you can take code like: # # r.root do # view :index # end # r.is "foo" do # view :foo # end # # and DRY it up: # # r.root do # :index # end # r.is "foo" do # :foo # end module SymbolViews def self.load_dependencies(app) app.plugin :custom_block_results end def self.configure(app) app.opts[:custom_block_results][Symbol] = :view end end register_plugin(:symbol_views, SymbolViews) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/timestamp_public.rb000066400000000000000000000060151516720775400244540ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The timestamp_public plugin adds a +timestamp_path+ method for constructing # timestamp paths, and a +r.timestamp_public+ routing method to serve static files # from a directory (using the public plugin). This plugin is useful when you want # to modify the path to static files when the modify timestamp on the file changes, # ensuring that requests for the static file will not be cached. # # Note that while this plugin will not serve files outside of the public directory, # for performance reasons it does not check the path of the file is inside the public # directory when getting the modify timestamp. If the +timestamp_path+ method is # called with untrusted input, it is possible for an attacker to get the modify # timestamp for any file on the file system. # # Examples: # # # Use public folder as location of files, and static as the path prefix # plugin :timestamp_public # # # Use /path/to/app/static as location of files, and public as the path prefix # opts[:root] = '/path/to/app' # plugin :public, root: 'static', prefix: 'public' # # # Assuming public is the location of files, and static is the path prefix # route do # # Make GET /static/1238099123/images/foo.png look for public/images/foo.png # r.timestamp_public # # r.get "example" do # # "/static/1238099123/images/foo.png" # timestamp_path("images/foo.png") # end # end module TimestampPublic # Use options given to setup timestamped file serving. The following option is # recognized by the plugin: # # :prefix :: The prefix for paths, before the timestamp segment # # The options given are also passed to the public plugin. def self.configure(app, opts={}) app.plugin :public, opts app.opts[:timestamp_public_prefix] = (opts[:prefix] || app.opts[:timestamp_public_prefix] || "static").dup.freeze end module InstanceMethods # Return a path to the static file that could be served by r.timestamp_public. # This does not check the file is inside the directory for performance reasons, # so this should not be called with untrusted input. def timestamp_path(file) mtime = File.mtime(File.join(opts[:public_root], file)) "/#{opts[:timestamp_public_prefix]}/#{sprintf("%i%06i", mtime.to_i, mtime.usec)}/#{file}" end end module RequestMethods # Serve files from the public directory if the file exists, # it includes the timestamp_public prefix segment followed by # a integer segment for the timestamp, and this is a GET request. def timestamp_public if is_get? on roda_class.opts[:timestamp_public_prefix], Integer do |_| public end end end end end register_plugin(:timestamp_public, TimestampPublic) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/type_routing.rb000066400000000000000000000163111516720775400236430ustar00rootroot00000000000000# frozen_string_literal: true # class Roda module RodaPlugins # This plugin makes it easier to to respond to specific request data types. User agents can request # specific data types by either supplying an appropriate +Accept+ request header # or by appending it as file extension to the path. # # Example: # # plugin :type_routing # # route do |r| # r.get 'a' do # r.html{ "

This is the HTML response

" } # r.json{ '{"json": "ok"}' } # r.xml{ "This is the XML response" } # "Unsupported data type" # end # end # # This application will handle the following paths: # /a.html :: HTML response # /a.json :: JSON response # /a.xml :: XML response # /a :: HTML, JSON, or XML response, depending on the Accept header # # The response +Content-Type+ header will be set to a suitable value when # the +r.html+, +r.json+, or +r.xml+ block is matched. # # Note that if no match is found, code will continue to execute, which can # result in unexpected behaviour. This should only happen if you do not # handle all supported/configured types. If you want to simplify handling, # you can just place the html handling after the other types, without using # a separate block: # # route do |r| # r.get 'a' do # r.json{ '{"json": "ok"}' } # r.xml{ "This is the XML response" } # # "

This is the HTML response

" # end # end # # This works correctly because Roda's default Content-Type is text/html. Note that # if you use this approach, the type_routing plugin's :html content type will not be # used for html responses, since you aren't using an +r.html+ block. Instead, the # Content-Type header will be set to Roda's default (which you can override via # the default_headers plugin). # # If the type routing is based on the +Accept+ request header and not the file extension, # then an appropriate +Vary+ header will be set or appended to, so that HTTP caches do # not serve the same result for requests with different +Accept+ headers. # # To match custom extensions, use the :types option: # # plugin :type_routing, types: { # yaml: 'application/x-yaml', # js: 'application/javascript; charset=utf-8' # } # # route do |r| # r.get 'a' do # r.yaml{ YAML.dump "YAML data" } # r.js{ "JavaScript code" } # # or: # r.on_type(:js){ "JavaScript code" } # "Unsupported data type" # end # end # # = Plugin options # # The following plugin options are supported: # # :default_type :: The default data type to assume if the client did not # provide one. Defaults to +:html+. # :exclude :: Exclude one or more types from the default set (default set # is :html, :xml, :json). # :types :: Mapping from a data type to its MIME-Type. Used both to match # incoming requests and to provide +Content-Type+ values. If the # value is +nil+, no +Content-Type+ will be set. The type may # contain media type parameters, which will be sent to the client # but ignored for request matching. # :use_extension :: Whether to take the path extension into account. # Default is +true+. # :use_header :: Whether to take the +Accept+ header into account. # Default is +true+. module TypeRouting CONFIGURATION = { :mimes => { 'text/json' => :json, 'application/json' => :json, 'text/xml' => :xml, 'application/xml' => :xml, 'text/html' => :html, }.freeze, :types => { :json => 'application/json'.freeze, :xml => 'application/xml'.freeze, :html => 'text/html'.freeze, }.freeze, :use_extension => true, :use_header => true, :default_type => :html }.freeze def self.configure(app, opts = {}) config = (app.opts[:type_routing] || CONFIGURATION).dup [:use_extension, :use_header, :default_type].each do |key| config[key] = opts[key] if opts.has_key?(key) end types = config[:types] = config[:types].dup mimes = config[:mimes] = config[:mimes].dup Array(opts[:exclude]).each do |type| types.delete(type) mimes.reject!{|_, v| v == type} end if mapping = opts[:types] types.merge!(mapping) mapping.each do |k, v| if v mimes[v.split(';', 2).first] = k end end end types.freeze mimes.freeze type_keys = config[:types].keys config[:extension_regexp] = /(.*?)\.(#{Regexp.union(type_keys.map(&:to_s))})\z/ type_keys.each do |type| app::RodaRequest.send(:define_method, type) do |&block| on_type(type, &block) end app::RodaRequest.send(:alias_method, type, type) end app.opts[:type_routing] = config.freeze end module RequestMethods # Yields if the given +type+ matches the requested data type and halts # the request afterwards, returning the result of the block. def on_type(type, &block) return unless type == requested_type response[RodaResponseHeaders::CONTENT_TYPE] ||= @scope.opts[:type_routing][:types][type] always(&block) end # Returns the data type the client requests. def requested_type return @requested_type if defined?(@requested_type) opts = @scope.opts[:type_routing] @requested_type = accept_response_type if opts[:use_header] @requested_type ||= opts[:default_type] end # Append the type routing extension back to the path if it was # removed before routing. def real_remaining_path if defined?(@type_routing_extension) "#{super}.#{@type_routing_extension}" else super end end private # Removes a trailing file extension from the path, and sets # the requested type if so. def _remaining_path(env) opts = scope.opts[:type_routing] path = super if opts[:use_extension] if m = opts[:extension_regexp].match(path) @type_routing_extension = @requested_type = m[2].to_sym path = m[1] end end path end # The response type indicated by the Accept request header. def accept_response_type mimes = @scope.opts[:type_routing][:mimes] @env['HTTP_ACCEPT'].to_s.split(/\s*,\s*/).map do |part| mime, _= part.split(/\s*;\s*/, 2) if sym = mimes[mime] response[RodaResponseHeaders::VARY] = (vary = response[RodaResponseHeaders::VARY]) ? "#{vary}, Accept" : 'Accept' return sym end end nil end end end register_plugin(:type_routing, TypeRouting) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/typecast_params.rb000066400000000000000000001330731516720775400243170ustar00rootroot00000000000000# frozen-string-literal: true require 'date' require 'time' class Roda module RodaPlugins # The typecast_params plugin allows for type conversion of submitted parameters. # Submitted parameters should be considered untrusted input, and in standard use # with browsers, parameters are submitted as strings (or a hash/array containing # strings). In most cases it makes sense to explicitly convert the parameter to the # desired type. While this can be done via manual conversion: # # val = request.params['key'].to_i # val = nil unless val > 0 # # the typecast_params plugin adds a friendlier interface: # # val = typecast_params.pos_int('key') # # As +typecast_params+ is a fairly long method name, and may be a method you call # frequently, you may want to consider aliasing it to something more terse in your # application, such as +tp+. # # typecast_params offers support for default values: # # val = typecast_params.pos_int('key', 1) # # The default value is only used if no value has been submitted for the parameter, # or if the conversion of the value results in +nil+. Handling defaults for parameter # conversion manually is more difficult, since the parameter may not be present at all, # or it may be present but an empty string because the user did not enter a value on # the related form. Use of typecast_params for the conversion handles both cases. # # In many cases, parameters should be required, and if they aren't submitted, that # should be considered an error. typecast_params handles this with ! methods: # # val = typecast_params.pos_int!('key') # # These ! methods raise an error instead of returning +nil+, and do not allow defaults. # # The errors raised by this plugin use a specific exception class, # +Roda::RodaPlugins::TypecastParams::Error+. This allows you to handle # this specific exception class globally and return an appropriate 4xx # response to the client. You can use the Error#param_name and Error#reason # methods to get more information about the error. # # To make it easy to handle cases where many parameters need the same conversion # done, you can pass an array of keys to a conversion method, and it will return an array # of converted values: # # val1, val2 = typecast_params.pos_int(['key1', 'key2']) # # This is equivalent to: # # val1 = typecast_params.pos_int('key1') # val2 = typecast_params.pos_int('key2') # # The ! methods also support arrays, ensuring that all parameters have a value: # # val1, val2 = typecast_params.pos_int!(['key1', 'key2']) # # For handling of array parameters, where all entries in the array use the # same conversion, there is an +array+ method which takes the type as the first argument # and the keys to convert as the second argument: # # vals = typecast_params.array(:pos_int, 'keys') # # If you want to ensure that all entries in the array are converted successfully and that # there is a value for the array itself, you can use +array!+: # # vals = typecast_params.array!(:pos_int, 'keys') # # This will raise an exception if any of the values in the array for parameter +keys+ cannot # be converted to integer. # # Both +array+ and +array!+ support default values which are used if no value is present # for the parameter: # # vals1 = typecast_params.array(:pos_int, 'keys1', []) # vals2 = typecast_params.array!(:pos_int, 'keys2', []) # # You can also pass an array of keys to +array+ or +array!+, if you would like to perform # the same conversion on multiple arrays: # # foo_ids, bar_ids = typecast_params.array!(:pos_int, ['foo_ids', 'bar_ids']) # # The previous examples have shown use of the +pos_int+ method, which uses +to_i+ to convert the # value to an integer, but returns +nil+ if the resulting integer is not positive. Unless you need # to handle negative numbers, it is recommended to use +pos_int+ instead of +int+ as +int+ will # convert invalid values to 0 (since that is how String#to_i works). # # There are many built in methods for type conversion: # # any :: Returns the value as is without conversion # str :: Raises if value is not already a string # nonempty_str :: Raises if value is not already a string, and converts # the empty string or string containing only whitespace to +nil+ # bool :: Converts entry to boolean if in one of the recognized formats: # nil :: nil, '' # true :: true, 1, '1', 't', 'true', 'yes', 'y', 'on' # case insensitive # false :: false, 0, '0', 'f', 'false', 'no', 'n', 'off' # case insensitive # If not in one of those formats, raises an error. # int :: Converts value to integer using +to_i+ (note that invalid input strings will be # returned as 0) # pos_int :: Converts value using +to_i+, but non-positive values are converted to +nil+ # Integer :: Converts value to integer using Kernel::Integer, with base 10 for # string inputs, and a check that the output value is equal to the input # value for numeric inputs. # float :: Converts value to float using +to_f+ (note that invalid input strings will be # returned as 0.0) # Float :: Converts value to float using Kernel::Float(value) # Hash :: Raises if value is not already a hash # date :: Converts value to Date using Date.parse(value) # time :: Converts value to Time using Time.parse(value) # datetime :: Converts value to DateTime using DateTime.parse(value) # file :: Raises if value is not already a hash with a :tempfile key whose value # responds to +read+ (this is the format rack uses for uploaded files). # # All of these methods also support ! methods (e.g. +pos_int!+), and all of them can be # used in the +array+ and +array!+ methods to support arrays of values. # # Since parameter hashes can be nested, the [] method can be used to access nested # hashes: # # # params: {'key'=>{'sub_key'=>'1'}} # typecast_params['key'].pos_int!('sub_key') # => 1 # # This works to an arbitrary depth: # # # params: {'key'=>{'sub_key'=>{'sub_sub_key'=>'1'}}} # typecast_params['key']['sub_key'].pos_int!('sub_sub_key') # => 1 # # And also works with arrays at any depth, if those arrays contain hashes: # # # params: {'key'=>[{'sub_key'=>{'sub_sub_key'=>'1'}}]} # typecast_params['key'][0]['sub_key'].pos_int!('sub_sub_key') # => 1 # # # params: {'key'=>[{'sub_key'=>['1']}]} # typecast_params['key'][0].array!(:pos_int, 'sub_key') # => [1] # # To allow easier access to nested data, there is a +dig+ method: # # typecast_params.dig(:pos_int, 'key', 'sub_key') # typecast_params.dig(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key') # # +dig+ will return +nil+ if any access while looking up the nested value returns +nil+. # There is also a +dig!+ method, which will raise an Error if +dig+ would return +nil+: # # typecast_params.dig!(:pos_int, 'key', 'sub_key') # typecast_params.dig!(:pos_int, 'key', 0, 'sub_key', 'sub_sub_key') # # Note that none of these conversion methods modify +request.params+. They purely do the # conversion and return the converted value. However, in some cases it is useful to do all # the conversion up front, and then pass a hash of converted parameters to an internal # method that expects to receive values in specific types. The +convert!+ method does # this, and there is also a +convert_each!+ method # designed for converting multiple values using the same block: # # converted_params = typecast_params.convert! do |tp| # tp.int('page') # tp.pos_int!('artist_id') # tp.array!(:pos_int, 'album_ids') # tp.convert!('sales') do |stp| # stp.pos_int!(['num_sold', 'num_shipped']) # end # tp.convert!('members') do |mtp| # mtp.convert_each! do |stp| # stp.str!(['first_name', 'last_name']) # end # end # end # # # converted_params: # # { # # 'page' => 1, # # 'artist_id' => 2, # # 'album_ids' => [3, 4], # # 'sales' => { # # 'num_sold' => 5, # # 'num_shipped' => 6 # # }, # # 'members' => [ # # {'first_name' => 'Foo', 'last_name' => 'Bar'}, # # {'first_name' => 'Baz', 'last_name' => 'Quux'} # # ] # # } # # +convert!+ and +convert_each!+ only return values you explicitly specify for conversion # inside the passed block. # # You can specify the +:symbolize+ option to +convert!+ or +convert_each!+, which will # symbolize the resulting hash keys: # # converted_params = typecast_params.convert!(symbolize: true) do |tp| # tp.int('page') # tp.pos_int!('artist_id') # tp.array!(:pos_int, 'album_ids') # tp.convert!('sales') do |stp| # stp.pos_int!(['num_sold', 'num_shipped']) # end # tp.convert!('members') do |mtp| # mtp.convert_each! do |stp| # stp.str!(['first_name', 'last_name']) # end # end # end # # # converted_params: # # { # # :page => 1, # # :artist_id => 2, # # :album_ids => [3, 4], # # :sales => { # # :num_sold => 5, # # :num_shipped => 6 # # }, # # :members => [ # # {:first_name => 'Foo', :last_name => 'Bar'}, # # {:first_name => 'Baz', :last_name => 'Quux'} # # ] # # } # # Using the +:symbolize+ option makes it simpler to transition from untrusted external # data (string keys), to semitrusted data that can be used internally (trusted in the sense that # the expected types are used, not that you trust the values). # # Note that if there are multiple conversion errors raised inside a +convert!+ or +convert_each!+ # block, they are recorded and a single TypecastParams::Error instance is raised after # processing the block. TypecastParams::Error#param_names can be called on the exception to # get an array of all parameter names with conversion issues, and TypecastParams::Error#all_errors # can be used to get an array of all Error instances. # # Because of how +convert!+ and +convert_each!+ work, you should avoid calling # TypecastParams::Params#[] inside the block you pass to these methods, because if the #[] # call fails, it will skip the reminder of the block. # # Be aware that when you use +convert!+ and +convert_each!+, the conversion methods called # inside the block may return nil if there is a error raised, and nested calls to # +convert!+ and +convert_each!+ may not return values. # # When loading the typecast_params plugin, a subclass of +TypecastParams::Params+ is created # specific to the Roda application. You can add support for custom types by passing a block # when loading the typecast_params plugin. This block is executed in the context of the # subclass, and calling +handle_type+ in the block can be used to add conversion methods. # +handle_type+ accepts a type name, an options hash, and the block used to convert the type. # Supported options are: # +:invalid_value_message+ :: The message to use for type conversions that result in a nil value # (a space and the parameter name is appended to this). # +:max_input_bytesize+ :: The maximum bytesize of string input. # # You can override the invalid value message of an existing type using the # +invalid_value_message+ method. You can also override the max input bytesize of an existing # type using the +max_input_bytesize+ method. # # plugin :typecast_params do # handle_type(:album, max_input_bytesize: 100, # invalid_value_message: "invalid album id in parameter") do |value| # if id = convert_pos_int(val) # Album[id] # end # end # max_input_bytesize(:date, 256) # invalid_value_message(:pos_int, "value must be greater than 0 for parameter") # end # # By default, the typecast_params conversion procs are passed the parameter value directly # from +request.params+ without modification. In some cases, it may be beneficial to # strip leading and trailing whitespace from parameter string values before processing, which # you can do by passing the strip: :all option when loading the plugin. # # By default, the typecasting methods for some types check whether the bytesize of input # strings is over the maximum expected values, and raise an error in such cases. The input # bytesize is checked prior to any type conversion. If you would like to skip this check # and allow any bytesize when doing type conversion for param string values, you can do so by # passing the # :skip_bytesize_checking option when loading the plugin. By default, # there is an 100 byte limit on integer input, an 1000 byte input on float input, and a 128 # byte limit on date/time input. # # By default, the typecasting methods check whether input strings have null bytes, and raise # an error in such cases. This check for null bytes occurs prior to any type conversion. # If you would like to skip this check and allow null bytes in param string values, # you can do so by passing the :allow_null_bytes option when loading the plugin. # # You can use the :date_parse_input_handler option to specify custom handling of date # parsing input. Modern versions of Ruby and the date gem internally raise if the input to # date parsing methods is too large to prevent denial of service. If you are using an # older version of Ruby, you can use this option to enforce the same check: # # plugin :typecast_params, date_parse_input_handler: proc {|string| # raise ArgumentError, "too big" if string.bytesize > 128 # string # } # # You can also use this option to modify the input, such as truncating it to the first # 128 bytes: # # plugin :typecast_params, date_parse_input_handler: proc {|string| # string.b[0, 128] # } # # The +date_parse_input_handler+ is only called if the value is under the max input # bytesize, so you may need to call +max_input_bytesize+ for the +:date+, +:time+, and # +:datetime+ methods to override the max input bytesize if you want to use this option # for input strings over 128 bytes. # # By design, typecast_params only deals with string keys, it is not possible to use # symbol keys as arguments to the conversion methods and have them converted. module TypecastParams # Sentinal value for whether to raise exception during #process CHECK_NIL = Object.new.freeze # Exception class for errors that are caused by misuse of the API by the programmer. # These are different from +Error+ which are raised because the submitted parameters # do not match what is expected. Should probably be treated as a 5xx error. class ProgrammerError < RodaError; end # Exception class for errors that are due to the submitted parameters not matching # what is expected. Should probably be treated as a 4xx error. class Error < RodaError # Set the keys in the given exception. If the exception is not already an # instance of the class, create a new instance to wrap it. def self.create(keys, reason, e) if e.is_a?(self) e.keys ||= keys e.reason ||= reason e else backtrace = e.backtrace e = new("#{e.class}: #{e.message}") e.keys = keys e.reason = reason e.set_backtrace(backtrace) if backtrace e end end # The keys used to access the parameter that caused the error. This is an array # that can be splatted to +dig+ to get the value of the parameter causing the error. attr_accessor :keys # An array of all other errors that were raised with this error. If the error # was not raised inside Params#convert! or Params#convert_each!, this will just be # an array containing the current receiver. # # This allows you to use Params#convert! to process a form input, and if any # conversion errors occur inside the block, it can provide an array of all parameter # names and reasons for parameters with problems. attr_writer :all_errors def all_errors @all_errors ||= [self] end # The reason behind this error. If this error was caused by a conversion method, # this will be the conversion method symbol. If this error was caused # because a value was missing, then it will be +:missing+. If this error was # caused because a value was not the correct type, then it will be +:invalid_type+. attr_accessor :reason # The likely parameter name where the contents were not expected. This is # designed for cases where the parameter was submitted with the typical # application/x-www-form-urlencoded or multipart/form-data content types, # and assumes the typical rack parsing of these content types into # parameters. # If the parameters were submitted via JSON, #keys should be # used directly. # # Example: # # # keys: ['page'] # param_name => 'page' # # # keys: ['artist', 'name'] # param_name => 'artist[name]' # # # keys: ['album', 'artist', 'name'] # param_name => 'album[artist][name]' def param_name if keys.length > 1 first, *rest = keys v = first.dup rest.each do |param| v << "[" v << param unless param.is_a?(Integer) v << "]" end v else keys.first end end # An array of all parameter names for parameters where the context were not # expected. If Params#convert! was not used, this will be an array containing # #param_name. If Params#convert! was used and multiple exceptions were # captured inside the convert! block, this will contain the parameter names # related to all captured exceptions. def param_names all_errors.map(&:param_name) end end module AllowNullByte private # Allow ASCII NUL bytes ("\0") in parameter string values. def check_null_byte(v) end end module StringStripper private # Strip any resulting input string. def param_value(key) v = super if v.is_a?(String) v = v.strip end v end end module DateParseInputHandler # Pass input string to date parsing through handle_date_parse_input. def _string_parse!(klass, v) v = handle_date_parse_input(v) super end end module SkipBytesizeChecking private # Do not check max input bytesize def check_allowed_bytesize(v, max) end end # Class handling conversion of submitted parameters to desired types. class Params # Handle conversions for the given type using the given block. # For a type named +foo+, this will create the following methods: # # * foo(key, default=nil) # * foo!(key) # * convert_foo(value) # private # * _convert_array_foo(value) # private # * _invalid_value_message_for_foo # private # * _max_input_bytesize_for_foo # private # # This method is used to define all type conversions, even the built # in ones. It can be called in subclasses to setup subclass-specific # types. def self.handle_type(type, opts=OPTS, &block) convert_meth = :"convert_#{type}" define_method(convert_meth, &block) convert_array_meth = :"_convert_array_#{type}" define_method(convert_array_meth) do |v| raise Error, "expected array but received #{v.inspect}" unless v.is_a?(Array) v.map! do |val| check_allowed_bytesize(val, _max_input_bytesize_for(type)) check_null_byte(val) send(convert_meth, val) end end private convert_meth, convert_array_meth invalid_value_message(type, opts[:invalid_value_message]) max_input_bytesize(type, opts[:max_input_bytesize]) define_method(type) do |key, default=nil| process_arg(convert_meth, key, default, type) if require_hash! end define_method(:"#{type}!") do |key| send(type, key, CHECK_NIL) end end # Set the invalid message for the given type. def self.invalid_value_message(type, message) invalid_value_message_meth = :"_invalid_value_message_for_#{type}" define_method(invalid_value_message_meth){message} private invalid_value_message_meth alias_method invalid_value_message_meth, invalid_value_message_meth end # Set the maximum input bytesize for the given type. def self.max_input_bytesize(type, bytesize) max_input_bytesize_meth = :"_max_input_bytesize_for_#{type}" define_method(max_input_bytesize_meth){bytesize} private max_input_bytesize_meth alias_method max_input_bytesize_meth, max_input_bytesize_meth end # Create a new instance with the given object and nesting level. # +obj+ should be an array or hash, and +nesting+ should be an # array. Designed for internal use, should not be called by # external code. def self.nest(obj, nesting) v = allocate v.instance_variable_set(:@nesting, nesting) v.send(:initialize, obj) v end handle_type(:any) do |v| v end handle_type(:str) do |v| raise Error, "expected string but received #{v.inspect}" unless v.is_a?(::String) v end handle_type(:nonempty_str, :invalid_value_message=>"empty string provided for parameter") do |v| if (v = convert_str(v)) && !v.strip.empty? v end end handle_type(:bool, :invalid_value_message=>"empty string provided for parameter") do |v| case v when '' nil when false, 0, /\A(?:0|f(?:alse)?|no?|off)\z/i false when true, 1, /\A(?:1|t(?:rue)?|y(?:es)?|on)\z/i true else raise Error, "expected bool but received #{v.inspect}" end end handle_type(:int, :max_input_bytesize=>100, :invalid_value_message=>"empty string provided for parameter") do |v| string_or_numeric!(v) && v.to_i end alias base_convert_int convert_int handle_type(:pos_int, :max_input_bytesize=>100, :invalid_value_message=>"empty string, non-integer, or non-positive integer provided for parameter") do |v| if (v = base_convert_int(v)) && v > 0 v end end handle_type(:Integer, :max_input_bytesize=>100, :invalid_value_message=>"empty string provided for parameter") do |v| if string_or_numeric!(v) case v when String ::Kernel::Integer(v, 10) when Integer v else i = ::Kernel::Integer(v) raise Error, "numeric value passed to Integer contains non-Integer part: #{v.inspect}" unless i == v i end end end alias base_convert_Integer convert_Integer handle_type(:float, :max_input_bytesize=>1000, :invalid_value_message=>"empty string provided for parameter") do |v| string_or_numeric!(v) && v.to_f end handle_type(:Float, :max_input_bytesize=>1000, :invalid_value_message=>"empty string provided for parameter") do |v| string_or_numeric!(v) && ::Kernel::Float(v) end handle_type(:Hash) do |v| raise Error, "expected hash but received #{v.inspect}" unless v.is_a?(::Hash) v end handle_type(:date, :max_input_bytesize=>128) do |v| parse!(::Date, v) end handle_type(:time, :max_input_bytesize=>128) do |v| parse!(::Time, v) end handle_type(:datetime, :max_input_bytesize=>128) do |v| parse!(::DateTime, v) end handle_type(:file) do |v| raise Error, "expected hash with :tempfile entry" unless v.is_a?(::Hash) && v.has_key?(:tempfile) && v[:tempfile].respond_to?(:read) v end # Set the object used for converting. Conversion methods will convert members of # the passed object. def initialize(obj) case @obj = obj when Hash, Array # nothing else if @nesting handle_error(nil, (@obj.nil? ? :missing : :invalid_type), "value of #{param_name(nil)} parameter not an array or hash: #{obj.inspect}", true) else handle_error(nil, :invalid_type, "parameters given not an array or hash: #{obj.inspect}", true) end end end # If key is a String Return whether the key is present in the object, def present?(key) case key when String !any(key).nil? when Array key.all? do |k| raise ProgrammerError, "non-String element in array argument passed to present?: #{k.inspect}" unless k.is_a?(String) !any(k).nil? end else raise ProgrammerError, "unexpected argument passed to present?: #{key.inspect}" end end # Return a new Params instance for the given +key+. The value of +key+ should be an array # if +key+ is an integer, or hash otherwise. def [](key) @subs ||= {} if sub = @subs[key] return sub end if @obj.is_a?(Array) unless key.is_a?(Integer) handle_error(key, :invalid_type, "invalid use of non-integer key for accessing array: #{key.inspect}", true) end else if key.is_a?(Integer) handle_error(key, :invalid_type, "invalid use of integer key for accessing hash: #{key}", true) end end v = @obj[key] v = yield if v.nil? && defined?(yield) begin sub = self.class.nest(v, Array(@nesting) + [key]) rescue => e handle_error(key, :invalid_type, e, true) end @subs[key] = sub sub.sub_capture(@capture, @symbolize, @skip_missing) sub end # Return the nested value for key. If there is no nested_value for +key+, # calls the block to return the value, or returns nil if there is no block given. def fetch(key) send(:[], key){return(yield if defined?(yield))} end # Captures conversions inside the given block, and returns a hash of all conversions, # including conversions of subkeys. +keys+ should be an array of subkeys to access, # or nil to convert the current object. If +keys+ is given as a hash, it is used as # the options hash. Options: # # :raise :: If set to false, do not raise errors for missing keys # :skip_missing :: If set to true, does not store values if the key is not # present in the params. # :symbolize :: Convert any string keys in the resulting hash and for any # conversions below def convert!(keys=nil, opts=OPTS) if keys.is_a?(Hash) opts = keys keys = nil end _capture!(:nested_params, opts) do if sub = subkey(Array(keys).dup, opts.fetch(:raise, true)) yield sub end end end # Runs conversions similar to convert! for each key specified by the :keys option. If :keys option is not given # and the object is an array, runs conversions for all entries in the array. If the :keys # option is not given and the object is a Hash with string keys '0', '1', ..., 'N' (with # no skipped keys), runs conversions for all entries in the hash. If :keys option is a Proc # or a Method, calls the proc/method with the current object, which should return an # array of keys to use. # Supports options given to #convert!, and this additional option: # # :keys :: The keys to extract from the object. If a proc or method, # calls the value with the current object, which should return the array of keys # to use. def convert_each!(opts=OPTS, &block) np = !@capture _capture!(nil, opts) do case keys = opts[:keys] when nil keys = (0...@obj.length) valid = if @obj.is_a?(Array) true else keys = keys.map(&:to_s) keys.all?{|k| @obj.has_key?(k)} end unless valid handle_error(nil, :invalid_type, "convert_each! called on object not an array or hash with keys '0'..'N'") next end when Array # nothing to do when Proc, Method keys = keys.call(@obj) else raise ProgrammerError, "unsupported convert_each! :keys option: #{keys.inspect}" end keys.map do |i| begin if v = subkey([i], opts.fetch(:raise, true)) yield v v.nested_params if np end rescue => e handle_error(i, :invalid_type, e) end end end end # Convert values nested under the current obj. Traverses the current object using +nest+, then converts # +key+ on that object using +type+: # # tp.dig(:pos_int, 'foo') # tp.pos_int('foo') # tp.dig(:pos_int, 'foo', 'bar') # tp['foo'].pos_int('bar') # tp.dig(:pos_int, 'foo', 'bar', 'baz') # tp['foo']['bar'].pos_int('baz') # # Returns nil if any of the values are not present or not the expected type. If the nest path results # in an object that is not an array or hash, then raises an Error. # # You can use +dig+ to get access to nested arrays by using :array or :array! as # the first argument and providing the type in the second argument: # # tp.dig(:array, :pos_int, 'foo', 'bar', 'baz') # tp['foo']['bar'].array(:pos_int, 'baz') def dig(type, *nest, key) _dig(false, type, nest, key) end # Similar to +dig+, but raises an Error instead of returning +nil+ if no value is found. def dig!(type, *nest, key) _dig(true, type, nest, key) end # Convert the value of +key+ to an array of values of the given +type+. If +default+ is # given, any +nil+ values in the array are replaced with +default+. If +key+ is an array # then this returns an array of arrays, one for each respective value of +key+. If there is # no value for +key+, nil is returned instead of an array. def array(type, key, default=nil) meth = :"_convert_array_#{type}" raise ProgrammerError, "no typecast_params type registered for #{type.inspect}" unless respond_to?(meth, true) process_arg(meth, key, default, type) if require_hash! end # Call +array+ with the +type+, +key+, and +default+, but if the return value is nil or any value in # the returned array is +nil+, raise an Error. def array!(type, key, default=nil) v = array(type, key, default) if key.is_a?(Array) key.zip(v).each do |k, arr| check_array!(k, arr) end else check_array!(key, v) end v end protected # Recursively descendent into all known subkeys and get the converted params from each. def nested_params return @nested_params if @nested_params params = @params if @subs @subs.each do |key, v| if key.is_a?(String) && symbolize? key = key.to_sym end params[key] = v.nested_params end end params end # Recursive method to get subkeys. def subkey(keys, do_raise) unless key = keys.shift return self end reason = :invalid_type case key when String unless @obj.is_a?(Hash) raise Error, "parameter #{param_name(nil)} is not a hash" if do_raise return end present = !@obj[key].nil? when Integer unless @obj.is_a?(Array) raise Error, "parameter #{param_name(nil)} is not an array" if do_raise return end present = key < @obj.length else raise ProgrammerError, "invalid argument used to traverse parameters: #{key.inspect}" end unless present reason = :missing raise Error, "parameter #{param_name(key)} is not present" if do_raise return end self[key].subkey(keys, do_raise) rescue => e handle_error(key, reason, e) end # Inherit given capturing and symbolize setting from parent object. def sub_capture(capture, symbolize, skip_missing) if @capture = capture @symbolize = symbolize @skip_missing = skip_missing @params = @obj.class.new end end private # Whether to symbolize keys when capturing. Note that the method # is renamed to +symbolize?+. attr_reader :symbolize alias symbolize? symbolize undef symbolize # Internals of convert! and convert_each!. def _capture!(ret, opts) previous_symbolize = @symbolize previous_skip_missing = @skip_missing unless cap = @capture @params = @obj.class.new @subs.clear if @subs capturing_started = true cap = @capture = [] end if opts.has_key?(:symbolize) @symbolize = !!opts[:symbolize] end if opts.has_key?(:skip_missing) @skip_missing = !!opts[:skip_missing] end begin v = yield rescue Error => e cap << e unless cap.last == e end if capturing_started unless cap.empty? e = cap[0] e.all_errors = cap raise e end if ret == :nested_params nested_params else v end end ensure @nested_params = nil if capturing_started # Unset capturing if capturing was already started. @capture = nil else # If capturing was not already started, update cached nested params # before resetting symbolize setting. @nested_params = nested_params end @symbolize = previous_symbolize @skip_missing = previous_skip_missing end # Raise an error if the array given does contains nil values. def check_array!(key, arr) if arr if arr.any?{|val| val.nil?} handle_error(key, :invalid_type, "invalid value in array parameter #{param_name(key)}") end else handle_error(key, :missing, "missing parameter for #{param_name(key)}") end end # Internals of dig/dig! def _dig(force, type, nest, key) if type == :array || type == :array! conv_type = nest.shift unless conv_type.is_a?(Symbol) raise ProgrammerError, "incorrect subtype given when using #{type} as argument for dig/dig!: #{conv_type.inspect}" end meth = type type = conv_type args = [meth, type] else meth = type args = [type] end unless respond_to?("_convert_array_#{type}", true) raise ProgrammerError, "no typecast_params type registered for #{meth.inspect}" end if v = subkey(nest, force) v.send(*args, key, (CHECK_NIL if force)) end end # Format a reasonable parameter name value, for use in exception messages. def param_name(key) first, *rest = keys(key) if first v = first.dup rest.each do |param| v << "[#{param}]" end v end end # If +key+ is not +nil+, add it to the given nesting. Otherwise, just return the given nesting. # Designed for use in setting the +keys+ values in raised exceptions. def keys(key) Array(@nesting) + Array(key) end # Handle any conversion errors. By default, reraises Error instances with the keys set, # converts ::ArgumentError instances to Error instances, and reraises other exceptions. def handle_error(key, reason, e, do_raise=false) case e when String handle_error(key, reason, Error.new(e), do_raise) when Error, ArgumentError if @capture && (le = @capture.last) && le == e raise e if do_raise return end e = Error.create(keys(key), reason, e) if @capture @capture << e raise e if do_raise nil else raise e end else raise e end end # Issue an error unless the current object is a hash. Used to ensure we don't try to access # entries if the current object is an array. def require_hash! @obj.is_a?(Hash) || handle_error(nil, :invalid_type, "expected hash object in #{param_name(nil)} but received array object") end # If +key+ is not an array, convert the value at the given +key+ using the +meth+ method and +default+ # value. If +key+ is an array, return an array with the conversion done for each respective member of +key+. def process_arg(meth, key, default, type) case key when String v = process(meth, key, default, type) if @capture key = key.to_sym if symbolize? if !@skip_missing || @obj.has_key?(key) @params[key] = v end end v when Array key.map do |k| raise ProgrammerError, "non-String element in array argument passed to typecast_params: #{k.inspect}" unless k.is_a?(String) process_arg(meth, k, default, type) end else raise ProgrammerError, "Unsupported argument for typecast_params conversion method: #{key.inspect}" end end # The invalid message to use if the given type conversion fails, which may be nil to use the default. def _invalid_value_message_for(type) send(:"_invalid_value_message_for_#{type}") end # The maximum input bytesize for the given type, which may be nil. def _max_input_bytesize_for(type) send(:"_max_input_bytesize_for_#{type}") end # Raise an Error if the value is a string with bytesize over max (if max is given) def check_allowed_bytesize(v, max) if max && v.is_a?(String) && v.bytesize > max handle_error(nil, :too_long, "string parameter is too long for type", true) end end # Raise an Error if the value is a string containing a null byte. def check_null_byte(v) if v.is_a?(String) && v.index("\0") handle_error(nil, :null_byte, "string parameter contains null byte", true) end end # Get the value of +key+ for the object, and convert it to the expected type using +meth+. # If the value either before or after conversion is nil, return the +default+ value. def process(meth, key, default, type) orig_v = v = param_value(key) if v.nil? if default == CHECK_NIL handle_error(key, :missing, "missing parameter for #{param_name(key)}") end else check_allowed_bytesize(v, _max_input_bytesize_for(type)) check_null_byte(v) v = send(meth, v) end if v.nil? if !orig_v.nil? && default == CHECK_NIL invalid_value_message = _invalid_value_message_for(type) invalid_value_message ||= "invalid parameter value for" handle_error(key, :invalid_value, "#{invalid_value_message} #{param_name(key)}") end default else v end rescue => e handle_error(key, meth.to_s.sub(/\A_?convert_/, '').to_sym, e) end # Get the value for the given key in the object. def param_value(key) @obj[key] end # Helper for conversion methods where '' should be considered nil, # and only String or Numeric values should be converted. def string_or_numeric!(v) case v when '' nil when String, Numeric true else raise Error, "unexpected value received: #{v.inspect}" end end # Helper for conversion methods where '' should be considered nil, # and only String values should be converted by calling +parse+ on # the given +klass+. def parse!(klass, v) case v when '' nil when String _string_parse!(klass, v) else raise Error, "unexpected value received: #{v.inspect}" end end # Handle parsing for string values passed to parse!. def _string_parse!(klass, v) klass.parse(v) end end # Set application-specific Params subclass unless one has been set, # and if a block is passed, eval it in the context of the subclass. # Respect the strip: :all to strip all parameter strings # before processing them. def self.configure(app, opts=OPTS, &block) app.const_set(:TypecastParams, Class.new(RodaPlugins::TypecastParams::Params)) unless app.const_defined?(:TypecastParams) app::TypecastParams.class_eval(&block) if block if opts[:strip] == :all app::TypecastParams.send(:include, StringStripper) end if opts[:allow_null_bytes] app::TypecastParams.send(:include, AllowNullByte) end if opts[:skip_bytesize_checking] app::TypecastParams.send(:include, SkipBytesizeChecking) end if opts[:date_parse_input_handler] app::TypecastParams.class_eval do include DateParseInputHandler define_method(:handle_date_parse_input, &opts[:date_parse_input_handler]) private :handle_date_parse_input alias handle_date_parse_input handle_date_parse_input end end end module ClassMethods # Freeze the Params subclass when freezing the class. def freeze self::TypecastParams.freeze super end # Assign the application subclass a subclass of the current Params subclass. def inherited(subclass) super subclass.const_set(:TypecastParams, Class.new(self::TypecastParams)) end end module InstanceMethods # Return and cache the instance of the TypecastParams class wrapping access # to the request's params (merging query string params and body params). # Type conversion methods will be called on the result of this method. def typecast_params @_typecast_params ||= self.class::TypecastParams.new(@_request.params) end # Return and cache the instance of the TypecastParams class wrapping # access to parameters in the request's query string. # Type conversion methods will be called on the result of this method. def typecast_query_params @_typecast_query_params ||= self.class::TypecastParams.new(@_request.GET) end # Return and cache the instance of the TypecastParams class wrapping # access to parameters in the request's body. # Type conversion methods will be called on the result of this method. def typecast_body_params @_typecast_body_params ||= self.class::TypecastParams.new(@_request.POST) end end end register_plugin(:typecast_params, TypecastParams) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/typecast_params_sized_integers.rb000066400000000000000000000113411516720775400274060ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The typecast_params_sized_integers plugin adds sized integer conversion # methods to typecast_params: # # * int8, uint8, pos_int8, pos_uint8, Integer8, Integeru8 # * int16, uint16, pos_int16, pos_uint16, Integer16, Integeru16 # * int32, uint32, pos_int32, pos_uint32, Integer32, Integeru32 # * int64, uint64, pos_int64, pos_uint64, Integer64, Integeru64 # # The int*, pos_int*, and Integer* methods operate the same as the # standard int, pos_int, and Integer methods in typecast_params, # except that they will only handle parameter values in the given # range for the signed integer type. The uint*, pos_int*, and # Integeru* methods are similar to the int*, pos_int*, and # Integer* methods, except they use the range of the unsigned # integer type instead of the range of the signed integer type. # # Here are the signed and unsigned integer type ranges: # 8 :: [-128, 127], [0, 255] # 16 :: [-32768, 32767], [0, 65535] # 32 :: [-2147483648, 2147483647], [0, 4294967295] # 64 :: [-9223372036854775808, 9223372036854775807], [0, 18446744073709551615] # # To only create methods for certain integer sizes, you can pass a # :sizes option when loading the plugin, and it will only create # methods for the sizes you specify. # # You can provide a :default_size option when loading the plugin, # in which case the int, uint, pos_int, pos_uint, Integer, and Integeru, # typecast_params conversion methods will be aliases to the conversion # methods for the given sized type: # # plugin :typecast_params_sized_integers, default_size: 64 # # route do |r| # # Returns nil unless param.to_i > 0 && param.to_i <= 9223372036854775807 # typecast_params.pos_int('param_name') # end module TypecastParamsSizedIntegers def self.load_dependencies(app, opts=OPTS) app.plugin :typecast_params do (opts[:sizes] || [8, 16, 32, 64]).each do |i| # Avoid defining the same methods more than once next if method_defined?(:"pos_int#{i}") min_signed = -(2**(i-1)) max_signed = 2**(i-1)-1 max_unsigned = 2**i-1 handle_type(:"int#{i}", :max_input_bytesize=>100, :invalid_value_message=>"empty string, non-integer, or too-large integer provided for parameter") do |v| if (v = base_convert_int(v)) && v >= min_signed && v <= max_signed v end end handle_type(:"uint#{i}", :max_input_bytesize=>100, :invalid_value_message=>"empty string, non-integer, negative integer, or too-large integer provided for parameter") do |v| if (v = base_convert_int(v)) && v >= 0 && v <= max_unsigned v end end handle_type(:"pos_int#{i}", :max_input_bytesize=>100, :invalid_value_message=>"empty string, non-integer, non-positive integer, or too-large integer provided for parameter") do |v| if (v = base_convert_int(v)) && v > 0 && v <= max_signed v end end handle_type(:"pos_uint#{i}", :max_input_bytesize=>100, :invalid_value_message=>"empty string, non-integer, non-positive integer, or too-large integer provided for parameter") do |v| if (v = base_convert_int(v)) && v > 0 && v <= max_unsigned v end end handle_type(:"Integer#{i}", :max_input_bytesize=>100, :invalid_value_message=>"empty string, non-integer, or too-large integer provided for parameter") do |v| if (v = base_convert_Integer(v)) && v >= min_signed && v <= max_signed v end end handle_type(:"Integeru#{i}", :max_input_bytesize=>100, :invalid_value_message=>"empty string, non-integer, negative integer, or too-large integer provided for parameter") do |v| if (v = base_convert_Integer(v)) && v >= 0 && v <= max_unsigned v end end end end if default = opts[:default_size] app::TypecastParams.class_eval do meths = ['', 'convert_', '_convert_array_', '_max_input_bytesize_for_', '_invalid_value_message_for_'] %w[int uint pos_int pos_uint Integer Integeru].each do |type| meths.each do |prefix| alias_method :"#{prefix}#{type}", :"#{prefix}#{type}#{default}" end alias_method :"#{type}!", :"#{type}#{default}!" end end end end end register_plugin(:typecast_params_sized_integers, TypecastParamsSizedIntegers) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/unescape_path.rb000066400000000000000000000017751516720775400237420ustar00rootroot00000000000000# frozen-string-literal: true require 'rack/utils' # class Roda module RodaPlugins # The unescape_path plugin decodes a URL-encoded path # before routing. This fixes routing when the slashes # are URL-encoded as %2f and returns decoded parameters # when matched by symbols or regexps. # # plugin :unescape_path # # route do |r| # # Assume /b/a URL encoded at %2f%62%2f%61 # r.on :x, /(.)/ do |*x| # # x => ['b', 'a'] # end # end module UnescapePath module RequestMethods # Make sure the matched path calculation handles the unescaping # of the remaining path. def matched_path e = @env Rack::Utils.unescape(e["SCRIPT_NAME"] + e["PATH_INFO"]).chomp(@remaining_path) end private # Unescape the path. def _remaining_path(env) Rack::Utils.unescape(super) end end end register_plugin(:unescape_path, UnescapePath) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/view_options.rb000066400000000000000000000142451516720775400236440ustar00rootroot00000000000000# frozen-string-literal: true require_relative 'render' # class Roda module RodaPlugins # The view_options plugin allows you to override view and layout # options for specific branches and routes. # # plugin :render # plugin :view_options # # route do |r| # r.on "users" do # set_layout_options template: 'users_layout' # set_view_options engine: 'haml' # # # ... # end # end # # The options you specify via the set_view_options and # set_layout_options methods have higher precedence than # the render plugin options, but lower precedence than options # you directly pass to the view/render methods. # # = View Subdirectories # # The view_options plugin also has special support for sites # that have outgrown a flat view directory and use subdirectories # for views. It allows you to set the view directory to # use, and template names that do not contain a slash will # automatically use that view subdirectory. Example: # # plugin :render, layout: './layout' # plugin :view_options # # route do |r| # r.on "users" do # set_view_subdir 'users' # # r.get Integer do |id| # append_view_subdir 'profile' # view 'index' # uses ./views/users/profile/index.erb # end # # r.get 'list' do # view 'lists/users' # uses ./views/lists/users.erb # end # end # end # # Note that when a view subdirectory is set, the layout will # also be looked up in the subdirectory unless it contains # a slash. So if you want to use a view subdirectory for # templates but have a shared layout, you should make sure your # layout contains a slash, similar to the example above. # # = Per-branch HTML escaping # # If you have an existing Roda application that doesn't use # automatic HTML escaping for <%= %> tags via the # render plugin's +:escape+ option, but you want to switch to # using the +:escape+ option, you can now do so without making # all changes at once. With set_view_options, you can now # specify escaping or not on a per branch basis in the routing # tree: # # plugin :render, escape: true # plugin :view_options # # route do |r| # # Don't escape <%= %> by default # set_view_options template_opts: {escape: false} # # r.on "users" do # # Escape <%= %> in this branch # set_view_options template_opts: {escape: true} # end # end module ViewOptions # Load the render plugin before this plugin, since this plugin # works by overriding methods in the render plugin. def self.load_dependencies(app) app.plugin :render end module InstanceMethods # Append a view subdirectory to use. If there hasn't already # been a view subdirectory set, this just sets it to the argument. # If there has already been a view subdirectory set, this sets # the view subdirectory to a subdirectory of the existing # view subdirectory. def append_view_subdir(v) if subdir = @_view_subdir set_view_subdir("#{subdir}/#{v}") else set_view_subdir(v) end end # Set the view subdirectory to use. This can be set to nil # to not use a view subdirectory. def set_view_subdir(v) @_view_subdir = v end # Set branch/route options to use when rendering the layout def set_layout_options(opts) if options = @_layout_options @_layout_options = options.merge!(opts) else @_layout_options = opts end end # Set branch/route options to use when rendering the view def set_view_options(opts) if options = @_view_options @_view_options = options.merge!(opts) else @_view_options = opts end end private if Render::COMPILED_METHOD_SUPPORT # Return nil if using custom view or layout options. # If using a view subdir, prefix the template key with the subdir. def _cached_template_method_key(template) return if @_view_options || @_layout_options if subdir = @_view_subdir template = [subdir, template].freeze end super end # Return nil if using custom view or layout options. # If using a view subdir, prefix the template key with the subdir. def _cached_template_method_lookup(method_cache, template) return if @_view_options || @_layout_options if subdir = @_view_subdir template = [subdir, template] end super end end # If view options or locals have been set and this # template isn't a layout template, merge the options # and locals into the returned hash. def parse_template_opts(template, opts) t_opts = super if !t_opts[:_is_layout] && (v_opts = @_view_options) t_opts.merge!(v_opts) end t_opts end # If layout options or locals have been set, # merge the options and locals into the returned hash. def render_layout_opts opts = super if l_opts = @_layout_options opts.merge!(l_opts) end opts end # Override the template name to use the view subdirectory if the # there is a view subdirectory and the template name does not # contain a slash. def template_name(opts) name = super if (v = @_view_subdir) && use_view_subdir_for_template_name?(name) "#{v}/#{name}" else name end end # Whether to use a view subdir for the template name. def use_view_subdir_for_template_name?(name) !name.include?('/') end end end register_plugin(:view_options, ViewOptions) end end jeremyevans-roda-4f30bb3/lib/roda/plugins/view_subdir_leading_slash.rb000066400000000000000000000025771516720775400263230ustar00rootroot00000000000000# frozen-string-literal: true # class Roda module RodaPlugins # The view_subdir_leading_slash plugin builds on the view_options # plugin, and changes the behavior so that if a view subdir is set, # it is used for all templates, unless the template starts with a # leading slash: # # plugin :view_subdir_leading_slash # # route do |r| # r.on "users" do # set_view_subdir 'users' # # r.get 'list' do # view 'lists/users' # uses ./views/users/lists/users.erb # end # # r.get 'list' do # view '/lists/users' # uses ./views//lists/users.erb # end # end # end # # The default for the view_options plugin is to not use a # view subdir if the template name contains a slash at all. module ViewSubdirLeadingSlash # Load the view_options plugin before this plugin, since this plugin # works by overriding a method in the view_options plugin. def self.load_dependencies(app) app.plugin :view_options end module InstanceMethods private # Use a view subdir unless the template starts with a slash. def use_view_subdir_for_template_name?(name) !name.start_with?('/') end end end register_plugin(:view_subdir_leading_slash, ViewSubdirLeadingSlash) end end jeremyevans-roda-4f30bb3/lib/roda/request.rb000066400000000000000000000542271516720775400211320ustar00rootroot00000000000000# frozen-string-literal: true begin require "rack/version" rescue LoadError require "rack" else if Rack.release >= '3' require "rack/request" else require "rack" end end require 'set' require_relative "cache" class Roda # Base class used for Roda requests. The instance methods for this # class are added by Roda::RodaPlugins::Base::RequestMethods, the # class methods are added by Roda::RodaPlugins::Base::RequestClassMethods. class RodaRequest < ::Rack::Request @roda_class = ::Roda @match_pattern_cache = ::Roda::RodaCache.new end module RodaPlugins module Base # Class methods for RodaRequest module RequestClassMethods # Reference to the Roda class related to this request class. attr_accessor :roda_class # The cache to use for match patterns for this request class. attr_accessor :match_pattern_cache # Return the cached pattern for the given object. If the object is # not already cached, yield to get the basic pattern, and convert the # basic pattern to a pattern that does not match partial segments. def cached_matcher(obj) cache = @match_pattern_cache unless pattern = cache[obj] pattern = cache[obj] = consume_pattern(yield) end pattern end # Since RodaRequest is anonymously subclassed when Roda is subclassed, # and then assigned to a constant of the Roda subclass, make inspect # reflect the likely name for the class. def inspect "#{roda_class.inspect}::RodaRequest" end private # The pattern to use for consuming, based on the given argument. The returned # pattern requires the path starts with a string and does not match partial # segments. def consume_pattern(pattern) /\A\/(?:#{pattern})(?=\/|\z)/ end end # Instance methods for RodaRequest, mostly related to handling routing # for the request. module RequestMethods TERM = Object.new def TERM.inspect "TERM" end TERM.freeze # The current captures for the request. This gets modified as routing # occurs. attr_reader :captures # The Roda instance related to this request object. Useful if routing # methods need access to the scope of the Roda route block. attr_reader :scope # Store the roda instance and environment. def initialize(scope, env) @scope = scope @captures = [] @remaining_path = _remaining_path(env) @env = env end # Handle match block return values. By default, if a string is given # and the response is empty, use the string as the response body. def block_result(result) res = response if res.empty? && (body = block_result_body(result)) res.write(body) end end # Match GET requests. If no arguments are provided, matches all GET # requests, otherwise, matches only GET requests where the arguments # given fully consume the path. def get(*args, &block) _verb(args, &block) if is_get? end # Immediately stop execution of the route block and return the given # rack response array of status, headers, and body. If no argument # is given, uses the current response. # # r.halt [200, {'Content-Type'=>'text/html'}, ['Hello World!']] # # response.status = 200 # response['Content-Type'] = 'text/html' # response.write 'Hello World!' # r.halt def halt(res=response.finish) throw :halt, res end # Show information about current request, including request class, # request method and full path. # # r.inspect # # => '#' def inspect "#<#{self.class.inspect} #{@env["REQUEST_METHOD"]} #{path}>" end if Rack.release >= '3' def http_version # Prefer SERVER_PROTOCOL as it is required in Rack 3. # Still fall back to HTTP_VERSION if SERVER_PROTOCOL # is not set, in case the server in use is not Rack 3 # compliant. @env['SERVER_PROTOCOL'] || @env['HTTP_VERSION'] end else # What HTTP version the request was submitted with. def http_version # Prefer HTTP_VERSION as it is backwards compatible # with previous Roda versions. Fallback to # SERVER_PROTOCOL for servers that do not set # HTTP_VERSION. @env['HTTP_VERSION'] || @env['SERVER_PROTOCOL'] end end # Does a terminal match on the current path, matching only if the arguments # have fully matched the path. If it matches, the match block is # executed, and when the match block returns, the rack response is # returned. # # r.remaining_path # # => "/foo/bar" # # r.is 'foo' do # # does not match, as path isn't fully matched (/bar remaining) # end # # r.is 'foo/bar' do # # matches as path is empty after matching # end # # If no arguments are given, matches if the path is already fully matched. # # r.on 'foo/bar' do # r.is do # # matches as path is already empty # end # end # # Note that this matches only if the path after matching the arguments # is empty, not if it still contains a trailing slash: # # r.remaining_path # # => "/foo/bar/" # # r.is 'foo/bar' do # # does not match, as path isn't fully matched (/ remaining) # end # # r.is 'foo/bar/' do # # matches as path is empty after matching # end # # r.on 'foo/bar' do # r.is "" do # # matches as path is empty after matching # end # end def is(*args, &block) if args.empty? if empty_path? always(&block) end else args << TERM if_match(args, &block) end end # Optimized method for whether this request is a +GET+ request. # Similar to the default Rack::Request get? method, but can be # overridden without changing rack's behavior. def is_get? @env["REQUEST_METHOD"] == 'GET' end # Does a match on the path, matching only if the arguments # have matched the path. Because this doesn't fully match the # path, this is usually used to setup branches of the routing tree, # not for final handling of the request. # # r.remaining_path # # => "/foo/bar" # # r.on 'foo' do # # matches, path is /bar after matching # end # # r.on 'bar' do # # does not match # end # # Like other routing methods, If it matches, the match block is # executed, and when the match block returns, the rack response is # returned. However, in general you will call another routing method # inside the match block that fully matches the path and does the # final handling for the request: # # r.on 'foo' do # r.is 'bar' do # # handle /foo/bar request # end # end def on(*args, &block) if args.empty? always(&block) else if_match(args, &block) end end # The already matched part of the path, including the original SCRIPT_NAME. def matched_path e = @env e["SCRIPT_NAME"] + e["PATH_INFO"].chomp(@remaining_path) end # This an an optimized version of Rack::Request#path. # # r.env['SCRIPT_NAME'] = '/foo' # r.env['PATH_INFO'] = '/bar' # r.path # # => '/foo/bar' def path e = @env "#{e["SCRIPT_NAME"]}#{e["PATH_INFO"]}" end # The current path to match requests against. attr_reader :remaining_path # An alias of remaining_path. If a plugin changes remaining_path then # it should override this method to return the untouched original. alias real_remaining_path remaining_path # Match POST requests. If no arguments are provided, matches all POST # requests, otherwise, matches only POST requests where the arguments # given fully consume the path. def post(*args, &block) _verb(args, &block) if post? end # Immediately redirect to the path using the status code. This ends # the processing of the request: # # r.redirect '/page1', 301 if r['param'] == 'value1' # r.redirect '/page2' # uses 302 status code # response.status = 404 # not reached # # If you do not provide a path, by default it will redirect to the same # path if the request is not a +GET+ request. This is designed to make # it easy to use where a +POST+ request to a URL changes state, +GET+ # returns the current state, and you want to show the current state # after changing: # # r.is "foo" do # r.get do # # show state # end # # r.post do # # change state # r.redirect # end # end def redirect(path=default_redirect_path, status=default_redirect_status) response.redirect(path, status) throw :halt, response.finish end # The response related to the current request. See ResponseMethods for # instance methods for the response, but in general the most common usage # is to override the response status and headers: # # response.status = 200 # response['Header-Name'] = 'Header value' def response @scope.response end # Return the Roda class related to this request. def roda_class self.class.roda_class end # Match method that only matches +GET+ requests where the current # path is +/+. If it matches, the match block is executed, and when # the match block returns, the rack response is returned. # # [r.request_method, r.remaining_path] # # => ['GET', '/'] # # r.root do # # matches # end # # This is usuable inside other match blocks: # # [r.request_method, r.remaining_path] # # => ['GET', '/foo/'] # # r.on 'foo' do # r.root do # # matches # end # end # # Note that this does not match non-+GET+ requests: # # [r.request_method, r.remaining_path] # # => ['POST', '/'] # # r.root do # # does not match # end # # Use r.post "" for +POST+ requests where the current path # is +/+. # # Nor does it match empty paths: # # [r.request_method, r.remaining_path] # # => ['GET', '/foo'] # # r.on 'foo' do # r.root do # # does not match # end # end # # Use r.get true to handle +GET+ requests where the current # path is empty. def root(&block) if @remaining_path == "/" && is_get? always(&block) end end # Call the given rack app with the environment and return the response # from the rack app as the response for this request. This ends # the processing of the request: # # r.run(proc{[403, {}, []]}) unless r['letmein'] == '1' # r.run(proc{[404, {}, []]}) # response.status = 404 # not reached # # This updates SCRIPT_NAME/PATH_INFO based on the current remaining_path # before dispatching to another rack app, so the app still works as # a URL mapper. def run(app) e = @env path = real_remaining_path sn = "SCRIPT_NAME" pi = "PATH_INFO" script_name = e[sn] path_info = e[pi] begin e[sn] += path_info.chomp(path) e[pi] = path throw :halt, app.call(e) ensure e[sn] = script_name e[pi] = path_info end end # The session for the current request. Raises a RodaError if # a session handler has not been loaded. def session @env['rack.session'] || raise(RodaError, "You're missing a session handler, try using the sessions plugin.") end private # Match any of the elements in the given array. Return at the # first match without evaluating future matches. Returns false # if no elements in the array match. def _match_array(matcher) matcher.any? do |m| if matched = match(m) if m.is_a?(String) @captures.push(m) end end matched end end # Match the given class. Currently, the following classes # are supported by default: # Integer :: Match an integer segment, yielding result to block as an integer # String :: Match any non-empty segment, yielding result to block as a string def _match_class(klass) meth = :"_match_class_#{klass}" if respond_to?(meth, true) # Allow calling private methods, as match methods are generally private send(meth) else unsupported_matcher(klass) end end # Match the given hash if all hash matchers match. def _match_hash(hash) # Allow calling private methods, as match methods are generally private hash.all?{|k,v| send("match_#{k}", v)} end # Match integer segment of up to 100 decimal characters, and yield resulting value as an # integer. def _match_class_Integer consume(/\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer) end # Match only if all of the arguments in the given array match. # Match the given regexp exactly if it matches a full segment. def _match_regexp(re) consume(self.class.cached_matcher(re){re}) end # Match the given string to the request path. Matches only if the # request path ends with the string or if the next character in the # request path is a slash (indicating a new segment). def _match_string(str) rp = @remaining_path length = str.length match = case rp.rindex(str, length) when nil # segment does not match, most common case return when 1 # segment matches, check first character is / rp.getbyte(0) == 47 else # must be 0 # segment matches at first character, only a match if # empty string given and first character is / length == 0 && rp.getbyte(0) == 47 end if match length += 1 case rp.getbyte(length) when 47 # next character is /, update remaining path to rest of string @remaining_path = rp[length, 100000000] when nil # end of string, so remaining path is empty @remaining_path = "" # else # Any other value means this was partial segment match, # so we return nil in that case without updating the # remaining_path. No need for explicit else clause. end end end # Match only if the next segment in the path is one of the # set's elements, and yield the next segment. def _match_set(set) rp = @remaining_path if key = _match_class_String if set.include?(@captures[-1]) true else @remaining_path = rp @captures.pop false end end end # Match the given symbol if any segment matches. def _match_symbol(sym=nil) rp = @remaining_path if rp.getbyte(0) == 47 if last = rp.index('/', 1) @captures << rp[1, last-1] @remaining_path = rp[last, rp.length] elsif (len = rp.length) > 1 @captures << rp[1, len] @remaining_path = "" end end end # Match any nonempty segment. This should be called without an argument. alias _match_class_String _match_symbol # The base remaining path to use. def _remaining_path(env) env["PATH_INFO"] end # Backbone of the verb method support, using a terminal match if # args is not empty, or a regular match if it is empty. def _verb(args, &block) if args.empty? always(&block) else args << TERM if_match(args, &block) end end # Yield to the match block and return rack response after the block returns. def always block_result(yield) throw :halt, response.finish end # The body to use for the response if the response does not already have # a body. By default, a String is returned directly, and nil is # returned otherwise. def block_result_body(result) case result when String result when nil, false # nothing else unsupported_block_result(result) end end # Attempts to match the pattern to the current path. If there is no # match, returns false without changes. Otherwise, modifies # SCRIPT_NAME to include the matched path, removes the matched # path from PATH_INFO, and updates captures with any regex captures. def consume(pattern, meth=nil) if matchdata = pattern.match(@remaining_path) captures = matchdata.captures if meth return unless captures = scope.send(meth, *captures) # :nocov: elsif defined?(yield) # RODA4: Remove return unless captures = yield(*captures) # :nocov: end @remaining_path = matchdata.post_match if captures.is_a?(Array) @captures.concat(captures) else @captures << captures end end end # The default path to use for redirects when a path is not given. # For non-GET requests, redirects to the current path, which will # trigger a GET request. This is to make the common case where # a POST request will redirect to a GET request at the same location # will work fine. # # If the current request is a GET request, raise an error, as otherwise # it is easy to create an infinite redirect. def default_redirect_path raise RodaError, "must provide path argument to redirect for get requests" if is_get? path end # The default status to use for redirects if a status is not provided, # 302 by default. def default_redirect_status 302 end # Whether the current path is considered empty. def empty_path? @remaining_path.empty? end # If all of the arguments match, yields to the match block and # returns the rack response when the block returns. If any of # the match arguments doesn't match, does nothing. def if_match(args) path = @remaining_path # For every block, we make sure to reset captures so that # nesting matchers won't mess with each other's captures. captures = @captures.clear if match_all(args) block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end end # Attempt to match the argument to the given request, handling # common ruby types. def match(matcher) case matcher when String _match_string(matcher) when Class _match_class(matcher) when TERM empty_path? when Regexp _match_regexp(matcher) when true matcher when Array _match_array(matcher) when Hash _match_hash(matcher) when Set _match_set(matcher) when Symbol _match_symbol(matcher) when false, nil matcher when Proc matcher.call else unsupported_matcher(matcher) end end # Match only if all of the arguments in the given array match. def match_all(args) args.all?{|arg| match(arg)} end # Match by request method. This can be an array if you want # to match on multiple methods. def match_method(type) if type.is_a?(Array) type.any?{|t| match_method(t)} else type.to_s.upcase == @env["REQUEST_METHOD"] end end # How to handle block results that are not nil, false, or a String. # By default raises an exception. def unsupported_block_result(result) raise RodaError, "unsupported block result: #{result.inspect}" end # Handle an unsupported matcher. def unsupported_matcher(matcher) raise RodaError, "unsupported matcher: #{matcher.inspect}" end end end end end jeremyevans-roda-4f30bb3/lib/roda/response.rb000066400000000000000000000161621516720775400212740ustar00rootroot00000000000000# frozen-string-literal: true begin require 'rack/headers' rescue LoadError end class Roda # Contains constants for response headers. This approach is used so that all # headers used internally by Roda can be lower case on Rack 3, so that it is # possible to use a plain hash of response headers instead of using Rack::Headers. module RodaResponseHeaders downcase = defined?(Rack::Headers) && Rack::Headers.is_a?(Class) %w'Allow Cache-Control Content-Disposition Content-Encoding Content-Length Content-Security-Policy Content-Security-Policy-Report-Only Content-Type ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary Permissions-Policy Permissions-Policy-Report-Only Strict-Transport-Security'. each do |value| value = value.downcase if downcase const_set(value.tr('-', '_').upcase!.to_sym, value.freeze) end end # Base class used for Roda responses. The instance methods for this # class are added by Roda::RodaPlugins::Base::ResponseMethods, the class # methods are added by Roda::RodaPlugins::Base::ResponseClassMethods. class RodaResponse @roda_class = ::Roda end module RodaPlugins module Base # Class methods for RodaResponse module ResponseClassMethods # Reference to the Roda class related to this response class. attr_accessor :roda_class # Since RodaResponse is anonymously subclassed when Roda is subclassed, # and then assigned to a constant of the Roda subclass, make inspect # reflect the likely name for the class. def inspect "#{roda_class.inspect}::RodaResponse" end end # Instance methods for RodaResponse module ResponseMethods # The body for the current response. attr_reader :body # The hash of response headers for the current response. attr_reader :headers # The status code to use for the response. If none is given, will use 200 # code for non-empty responses and a 404 code for empty responses. attr_accessor :status # Set the default headers when creating a response. def initialize @headers = _initialize_headers @body = [] @length = 0 end # Return the response header with the given key. Example: # # response['Content-Type'] # => 'text/html' def [](key) @headers[key] end # Set the response header with the given key to the given value. # # response['Content-Type'] = 'application/json' def []=(key, value) @headers[key] = value end # The default headers to use for responses. def default_headers DEFAULT_HEADERS end # Whether the response body has been written to yet. Note # that writing an empty string to the response body marks # the response as not empty. Example: # # response.empty? # => true # response.write('a') # response.empty? # => false def empty? @body.empty? end # Return the rack response array of status, headers, and body # for the current response. If the status has not been set, # uses the return value of default_status if the body has # been written to, otherwise uses a 404 status. # Adds the Content-Length header to the size of the response body. # # Example: # # response.finish # # => [200, # # {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, # # []] def finish b = @body set_default_headers h = @headers if b.empty? s = @status || 404 if (s == 304 || s == 204 || (s >= 100 && s <= 199)) h.delete(RodaResponseHeaders::CONTENT_TYPE) elsif s == 205 empty_205_headers(h) else h[RodaResponseHeaders::CONTENT_LENGTH] ||= '0' end else s = @status || default_status h[RodaResponseHeaders::CONTENT_LENGTH] ||= @length.to_s end [s, h, b] end # Return the rack response array using a given body. Assumes a # 200 response status unless status has been explicitly set, # and doesn't add the Content-Length header or use the existing # body. def finish_with_body(body) set_default_headers [@status || default_status, @headers, body] end # Return the default response status to be used when the body # has been written to. This is split out to make overriding # easier in plugins. def default_status 200 end # Show response class, status code, response headers, and response body def inspect "#<#{self.class.inspect} #{@status.inspect} #{@headers.inspect} #{@body.inspect}>" end # Set the Location header to the given path, and the status # to the given status. Example: # # response.redirect('foo', 301) # response.redirect('bar') def redirect(path, status = 302) @headers[RodaResponseHeaders::LOCATION] = path @status = status nil end # Return the Roda class related to this response. def roda_class self.class.roda_class end # Write to the response body. Returns nil. # # response.write('foo') def write(str) s = str.to_s @length += s.bytesize @body << s nil end private if defined?(Rack::Headers) && Rack::Headers.is_a?(Class) DEFAULT_HEADERS = Rack::Headers[{RodaResponseHeaders::CONTENT_TYPE => "text/html".freeze}].freeze # Use Rack::Headers for headers by default on Rack 3 def _initialize_headers Rack::Headers.new end else DEFAULT_HEADERS = {RodaResponseHeaders::CONTENT_TYPE => "text/html".freeze}.freeze # Use plain hash for headers by default on Rack 1-2 def _initialize_headers {} end end if Rack.release < '2.0.2' # Don't use a content length for empty 205 responses on # rack 1, as it violates Rack::Lint in that version. def empty_205_headers(headers) headers.delete(RodaResponseHeaders::CONTENT_TYPE) headers.delete(RodaResponseHeaders::CONTENT_LENGTH) end else # Set the content length for empty 205 responses to 0 def empty_205_headers(headers) headers.delete(RodaResponseHeaders::CONTENT_TYPE) headers[RodaResponseHeaders::CONTENT_LENGTH] = '0' end end # For each default header, if a header has not already been set for the # response, set the header in the response. def set_default_headers h = @headers default_headers.each do |k,v| h[k] ||= v end end end end end end jeremyevans-roda-4f30bb3/lib/roda/session_middleware.rb000066400000000000000000000105521516720775400233130ustar00rootroot00000000000000# frozen-string-literal: true require_relative '../roda' require_relative 'plugins/sessions' # Session middleware that can be used in any Rack application # that uses Roda's sessions plugin for encrypted and signed cookies. # See Roda::RodaPlugins::Sessions for details on options. class RodaSessionMiddleware # Class to hold session data. This is designed to mimic the API # of Rack::Session::Abstract::SessionHash, but is simpler and faster. # Undocumented methods operate the same as hash methods, but load the # session from the cookie if it hasn't been loaded yet, and convert # keys to strings. # # One difference between SessionHash and Rack::Session::Abstract::SessionHash # is that SessionHash does not attempt to setup a session id, since # one is not needed for cookie-based sessions, only for sessions # that are loaded out of a database. If you need to have a session id # for other reasons, manually create a session id using a randomly generated # string. class SessionHash # The Roda::RodaRequest subclass instance related to the session. attr_reader :req # The underlying data hash, or nil if the session has not yet been # loaded. attr_reader :data def initialize(req) @req = req end # The Roda sessions plugin options used by the middleware for this # session hash. def options @req.roda_class.opts[:sessions] end def each(&block) load! @data.each(&block) end def [](key) load! @data[key.to_s] end def fetch(key, default = (no_default = true), &block) load! if no_default @data.fetch(key.to_s, &block) else @data.fetch(key.to_s, default, &block) end end def has_key?(key) load! @data.has_key?(key.to_s) end alias :key? :has_key? alias :include? :has_key? def []=(key, value) load! @data[key.to_s] = value end alias :store :[]= # Clear the session, also removing a couple of roda session # keys from the environment so that the related cookie will # either be set or cleared in the rack response. def clear load! env = @req.env env.delete('roda.session.created_at') env.delete('roda.session.updated_at') @data.clear end alias :destroy :clear def to_hash load! @data.dup end def update(hash) load! hash.each do |key, value| @data[key.to_s] = value end @data end alias :merge! :update def replace(hash) load! @data.clear update(hash) end def delete(key) load! @data.delete(key.to_s) end # If the session hasn't been loaded, display that. def inspect if loaded? @data.inspect else "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>" end end # Return whether the session cookie already exists. # If this is false, then the session was set to an empty hash. def exists? load! req.env.has_key?('roda.session.serialized') end # Whether the session has already been loaded from the cookie yet. def loaded? !!defined?(@data) end def empty? load! @data.empty? end def keys load! @data.keys end def values load! @data.values end private # Load the session from the cookie. def load! @data ||= @req.send(:_load_session) end end module RequestMethods # Work around for if type_routing plugin is loaded into Roda class itself. def _remaining_path(_) end end # Setup the middleware, passing +opts+ as the Roda sessions plugin options. def initialize(app, opts) mid = Class.new(Roda) Roda::RodaPlugins.set_temp_name(mid){"RodaSessionMiddleware::_RodaSubclass"} mid.plugin :sessions, opts @req_class = mid::RodaRequest @req_class.send(:include, RequestMethods) @app = app end # Initialize the session hash in the environment before calling the next # application, and if the session has been loaded after the result has been # returned, then persist the session in the cookie. def call(env) session = env['rack.session'] = SessionHash.new(@req_class.new(nil, env)) res = @app.call(env) if session.loaded? session.req.persist_session(res[1], session.data) end res end end jeremyevans-roda-4f30bb3/lib/roda/version.rb000066400000000000000000000012071516720775400211150ustar00rootroot00000000000000class Roda # The major version of Roda, updated only for major changes that are # likely to require modification to Roda apps. RodaMajorVersion = 3 # The minor version of Roda, updated for new feature releases of Roda. RodaMinorVersion = 103 # The patch version of Roda, updated only for bug fixes from the last # feature release. RodaPatchVersion = 0 # The full version of Roda as a string. RodaVersion = "#{RodaMajorVersion}.#{RodaMinorVersion}.#{RodaPatchVersion}".freeze # The full version of Roda as a number (3.7.0 => 30070) RodaVersionNumber = RodaMajorVersion*10000 + RodaMinorVersion*10 + RodaPatchVersion end jeremyevans-roda-4f30bb3/roda.gemspec000066400000000000000000000025201516720775400177010ustar00rootroot00000000000000require File.expand_path("../lib/roda/version", __FILE__) Gem::Specification.new do |s| s.name = "roda" s.version = Roda::RodaVersion.dup s.summary = "Routing tree web toolkit" s.authors = ["Jeremy Evans"] s.email = ["code@jeremyevans.net"] s.homepage = "https://roda.jeremyevans.net" s.license = "MIT" s.required_ruby_version = ">= 1.9.2" s.metadata = { 'bug_tracker_uri' => 'https://github.com/jeremyevans/roda/issues', 'changelog_uri' => 'https://roda.jeremyevans.net/rdoc/files/CHANGELOG.html', 'documentation_uri' => 'https://roda.jeremyevans.net/documentation.html', 'mailing_list_uri' => 'https://github.com/jeremyevans/roda/discussions', "source_code_uri" => "https://github.com/jeremyevans/roda" } s.files = %w'MIT-LICENSE' + Dir['lib/**/*.rb'] s.extra_rdoc_files = %w'MIT-LICENSE' s.add_dependency "rack" s.add_development_dependency "rake" s.add_development_dependency "minitest", ">= 5.7.0" s.add_development_dependency "minitest-hooks" s.add_development_dependency "minitest-global_expectations" s.add_development_dependency "tilt" s.add_development_dependency "erubi" s.add_development_dependency "rack_csrf" s.add_development_dependency "json" s.add_development_dependency "mail" end jeremyevans-roda-4f30bb3/spec/000077500000000000000000000000001516720775400163425ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/all.rb000066400000000000000000000001171516720775400174360ustar00rootroot00000000000000(Dir['./spec/*_spec.rb'] + Dir['./spec/plugin/*_spec.rb']).each{|f| require f} jeremyevans-roda-4f30bb3/spec/assets/000077500000000000000000000000001516720775400176445ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/assets/css/000077500000000000000000000000001516720775400204345ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/assets/css/app.html000066400000000000000000000000251516720775400220770ustar00rootroot00000000000000body { color: red; } jeremyevans-roda-4f30bb3/spec/assets/css/app.str000066400000000000000000000000251516720775400217430ustar00rootroot00000000000000body { color: red; } jeremyevans-roda-4f30bb3/spec/assets/css/import.str000066400000000000000000000001041516720775400224730ustar00rootroot00000000000000#{File.binread(File.join(File.dirname(__FILE__), 'importdep.str'))} jeremyevans-roda-4f30bb3/spec/assets/css/no_access.css000066400000000000000000000000121516720775400230740ustar00rootroot00000000000000no access jeremyevans-roda-4f30bb3/spec/assets/css/raw.css000066400000000000000000000000261516720775400217350ustar00rootroot00000000000000body { color: blue; } jeremyevans-roda-4f30bb3/spec/assets/js/000077500000000000000000000000001516720775400202605ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/assets/js/head/000077500000000000000000000000001516720775400211615ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/assets/js/head/app.js000066400000000000000000000000241516720775400222730ustar00rootroot00000000000000console.log('test') jeremyevans-roda-4f30bb3/spec/assets/js/head/comment_1.js000066400000000000000000000000071516720775400233760ustar00rootroot00000000000000// testjeremyevans-roda-4f30bb3/spec/assets/js/head/comment_2.js000066400000000000000000000000151516720775400233760ustar00rootroot00000000000000/* a = 1; */ jeremyevans-roda-4f30bb3/spec/autoload_hash_branches/000077500000000000000000000000001516720775400230225ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/autoload_hash_branches/a.rb000066400000000000000000000001231516720775400235630ustar00rootroot00000000000000$roda_app.opts[:loaded] << :a $roda_app.hash_branch('a'){|r| r.hash_branches; 'a'} jeremyevans-roda-4f30bb3/spec/autoload_hash_branches/a/000077500000000000000000000000001516720775400232425ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/autoload_hash_branches/a/c.rb000066400000000000000000000001141516720775400240050ustar00rootroot00000000000000$roda_app.opts[:loaded] << :a_c $roda_app.hash_branch('/a', 'c'){|r| 'a-c'} jeremyevans-roda-4f30bb3/spec/autoload_hash_branches/a/d.rb000066400000000000000000000001141516720775400240060ustar00rootroot00000000000000$roda_app.opts[:loaded] << :a_d $roda_app.hash_branch('/a', 'd'){|r| 'a-d'} jeremyevans-roda-4f30bb3/spec/autoload_hash_branches/a/e.rb000066400000000000000000000000401516720775400240050ustar00rootroot00000000000000$roda_app.opts[:loaded] << :a_e jeremyevans-roda-4f30bb3/spec/autoload_hash_branches/b.rb000066400000000000000000000001021516720775400235610ustar00rootroot00000000000000$roda_app.opts[:loaded] << :b $roda_app.hash_branch('b'){|r| 'b'} jeremyevans-roda-4f30bb3/spec/autoload_named_routes/000077500000000000000000000000001516720775400227175ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/autoload_named_routes/a.rb000066400000000000000000000002321516720775400234610ustar00rootroot00000000000000$roda_app.opts[:loaded] << :a $roda_app.route(:a) do |r| r.on('c'){r.route(:c, :a)} r.on('d'){r.route(:d, :a)} r.on('e'){r.route(:e, :a)} 'a' end jeremyevans-roda-4f30bb3/spec/autoload_named_routes/a/000077500000000000000000000000001516720775400231375ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/autoload_named_routes/a/c.rb000066400000000000000000000001031516720775400237000ustar00rootroot00000000000000$roda_app.opts[:loaded] << :a_c $roda_app.route(:c, :a){|r| 'a-c'} jeremyevans-roda-4f30bb3/spec/autoload_named_routes/a/d.rb000066400000000000000000000001031516720775400237010ustar00rootroot00000000000000$roda_app.opts[:loaded] << :a_d $roda_app.route(:d, :a){|r| 'a-d'} jeremyevans-roda-4f30bb3/spec/autoload_named_routes/a/e.rb000066400000000000000000000000401516720775400237020ustar00rootroot00000000000000$roda_app.opts[:loaded] << :a_e jeremyevans-roda-4f30bb3/spec/autoload_named_routes/b.rb000066400000000000000000000000731516720775400234650ustar00rootroot00000000000000$roda_app.opts[:loaded] << :b $roda_app.route(:b){|r| 'b'} jeremyevans-roda-4f30bb3/spec/cache_spec.rb000066400000000000000000000012711516720775400207450ustar00rootroot00000000000000require_relative "spec_helper" describe "Roda::RodaCache" do before do @cache = Roda::RodaCache.new end it "should provide a hash like interface" do @cache[1].must_be_nil @cache[1] = 2 @cache[1].must_equal 2 end it "should have dup return a copy of the cache" do @cache[1].must_be_nil @cache[1] = 2 cache = @cache.dup @cache[2] = 3 cache[3] = 4 cache[1].must_equal 2 cache[2].must_be_nil cache[3].must_equal 4 @cache[1].must_equal 2 @cache[2].must_equal 3 @cache[3].must_be_nil end it "should have freeze return a frozen hash" do v = @cache.freeze v.must_equal({}) v.must_be_instance_of(Hash) end end jeremyevans-roda-4f30bb3/spec/composition_spec.rb000066400000000000000000000015461516720775400222520ustar00rootroot00000000000000require_relative "spec_helper" describe "r.run" do it "should allow composition of apps" do a = app do |r| r.on "services", :id do |id| "View #{id}" end end app(:new) do |r| r.on "provider" do r.run a end end body("/provider/services/101").must_equal 'View 101' end it "modifies SCRIPT_NAME/PATH_INFO when calling run" do a = app{|r| "#{r.script_name}|#{r.path_info}"} app{|r| r.on("a"){r.run a}} body("/a/b").must_equal "/a|/b" end it "restores SCRIPT_NAME/PATH_INFO before returning from run" do a = app{|r| "#{r.script_name}|#{r.path_info}"} x = nil app do |r| s = catch(:halt){r.on("a"){r.run a}} x = s[2] x.close if x.respond_to?(:close) "#{r.script_name}|#{r.path_info}" end body("/a/b").must_equal "|/a/b" x = '/a|/b' end end jeremyevans-roda-4f30bb3/spec/define_roda_method_spec.rb000066400000000000000000000272071516720775400235100ustar00rootroot00000000000000require_relative "spec_helper" describe "Roda.define_roda_method" do before do @scope = app.new({'PATH_INFO'=>'/'}) end it "should define methods using block" do m0 = app.define_roda_method("x", 0){1} m0.must_be_kind_of Symbol m0.must_match(/\A_roda_x_\d+\z/) @scope.send(m0).must_equal 1 m1 = app.define_roda_method("x", 1){|x| [x, 2]} m1.must_be_kind_of Symbol m1.must_match(/\A_roda_x_\d+\z/) @scope.send(m1, 3).must_equal [3, 2] end it "should define private methods" do proc{@scope.public_send(app.define_roda_method("x", 0){1})}.must_raise NoMethodError end it "should accept symbols as method name and return the same symbol" do m0 = app.define_roda_method(:_roda_foo, 0){1} m0.must_equal :_roda_foo @scope.send(m0).must_equal 1 end it "should handle optional arguments and splats for expected_arity 0" do m2 = app.define_roda_method("x", 0){|*x| [x, 3]} @scope.send(m2).must_equal [[], 3] m3 = app.define_roda_method("x", 0){|x=5| [x, 4]} @scope.send(m3).must_equal [5, 4] m4 = app.define_roda_method("x", 0){|x=6, *y| [x, y, 5]} @scope.send(m4).must_equal [6, [], 5] end it "should should optional arguments and splats for expected_arity 1" do m2 = app.define_roda_method("x", 1){|y, *x| [y, x, 3]} @scope.send(m2, :a).must_equal [:a, [], 3] m3 = app.define_roda_method("x", 1){|y, x=5| [y, x, 4]} @scope.send(m3, :b).must_equal [:b, 5, 4] m4 = app.define_roda_method("x", 1){|y, x=6, *z| [y, x, z, 5]} @scope.send(m4, :c).must_equal [:c, 6, [], 5] end it "should handle differences in arity" do m0 = app.define_roda_method("x", 0){|x| [x, 1]} @scope.send(m0).must_equal [nil, 1] m1 = app.define_roda_method("x", 1){2} @scope.send(m1, 3).must_equal 2 m1 = app.define_roda_method("x", 1){|x, y| [x, y]} @scope.send(m1, 4).must_equal [4, nil] end it "should raise for unexpected expected_arity" do proc{app.define_roda_method("x", 2){|x|}}.must_raise Roda::RodaError end it "should fail if :check_arity false app option is used and a block with invalid arity is passed" do app.opts[:check_arity] = false m0 = app.define_roda_method("x", 0){|x| [x, 1]} proc{@scope.send(m0)}.must_raise ArgumentError m1 = app.define_roda_method("x", 1){2} proc{@scope.send(m1, 1)}.must_raise ArgumentError m1 = app.define_roda_method("x", 1){|x, y| [x, y]} proc{@scope.send(m1, 4)}.must_raise ArgumentError end deprecated "should warn if :check_arity :warn app option is used and a block with invalid arity is passed" do app.opts[:check_arity] = :warn m0 = app.define_roda_method("x", 0){|x| [x, 1]} @scope.send(m0).must_equal [nil, 1] m1 = app.define_roda_method("x", 1){2} @scope.send(m1, 3).must_equal 2 m1 = app.define_roda_method("x", 1){|x, y| [x, y]} @scope.send(m1, 4).must_equal [4, nil] end [false, true].each do |warn_dynamic_arity| meth = warn_dynamic_arity ? :deprecated : :it send meth, "should handle expected_arity :any and do dynamic arity check/fix" do if warn_dynamic_arity app.opts[:check_dynamic_arity] = :warn end m0 = app.define_roda_method("x", :any){1} @scope.send(m0).must_equal 1 @scope.send(m0, 2).must_equal 1 m1 = app.define_roda_method("x", :any){|x| [x, 1]} @scope.send(m1).must_equal [nil, 1] @scope.send(m1, 2).must_equal [2, 1] @scope.send(m1, 2, 3).must_equal [2, 1] m2 = app.define_roda_method("x", :any){|x=5| [x, 2]} @scope.send(m2).must_equal [5, 2] @scope.send(m2, 2).must_equal [2, 2] @scope.send(m2, 2, 3).must_equal [2, 2] m3 = app.define_roda_method("x", :any){|y, x=5| [x, y, 3]} @scope.send(m3).must_equal [5, nil, 3] @scope.send(m3, 2).must_equal [5, 2, 3] @scope.send(m3, 2, 3).must_equal [3, 2, 3] @scope.send(m3, 2, 3, 4).must_equal [3, 2, 3] m4 = app.define_roda_method("x", :any){|*z| [z, 1]} @scope.send(m4).must_equal [[], 1] @scope.send(m4, 2).must_equal [[2], 1] m5 = app.define_roda_method("x", :any){|x, *z| [x, z, 1]} @scope.send(m5).must_equal [nil, [], 1] @scope.send(m5, 2).must_equal [2, [], 1] @scope.send(m5, 2, 3).must_equal [2, [3], 1] m6 = app.define_roda_method("x", :any){|x=5, *z| [x, z, 2]} @scope.send(m6).must_equal [5, [], 2] @scope.send(m6, 2).must_equal [2, [], 2] @scope.send(m6, 2, 3).must_equal [2, [3], 2] m7 = app.define_roda_method("x", :any){|y, x=5, *z| [x, y, z, 3]} @scope.send(m7).must_equal [5, nil, [], 3] @scope.send(m7, 2).must_equal [5, 2, [], 3] @scope.send(m7, 2, 3).must_equal [3, 2, [], 3] @scope.send(m7, 2, 3, 4).must_equal [3, 2, [4], 3] end end it "should not fix dynamic arity issues if :check_dynamic_arity false app option is used" do app.opts[:check_dynamic_arity] = false m0 = app.define_roda_method("x", :any){1} @scope.send(m0).must_equal 1 proc{@scope.send(m0, 2)}.must_raise ArgumentError m1 = app.define_roda_method("x", :any){|x| [x, 1]} proc{@scope.send(m1)}.must_raise ArgumentError @scope.send(m1, 2).must_equal [2, 1] proc{@scope.send(m1, 2, 3)}.must_raise ArgumentError m2 = app.define_roda_method("x", :any){|x=5| [x, 2]} @scope.send(m2).must_equal [5, 2] @scope.send(m2, 2).must_equal [2, 2] proc{@scope.send(m2, 2, 3)}.must_raise ArgumentError m3 = app.define_roda_method("x", :any){|y, x=5| [x, y, 3]} proc{@scope.send(m3)}.must_raise ArgumentError @scope.send(m3, 2).must_equal [5, 2, 3] @scope.send(m3, 2, 3).must_equal [3, 2, 3] proc{@scope.send(m3, 2, 3, 4)}.must_raise ArgumentError m4 = app.define_roda_method("x", :any){|*z| [z, 1]} @scope.send(m4).must_equal [[], 1] @scope.send(m4, 2).must_equal [[2], 1] m5 = app.define_roda_method("x", :any){|x, *z| [x, z, 1]} proc{@scope.send(m5)}.must_raise ArgumentError @scope.send(m5, 2).must_equal [2, [], 1] @scope.send(m5, 2, 3).must_equal [2, [3], 1] m6 = app.define_roda_method("x", :any){|x=5, *z| [x, z, 2]} @scope.send(m6).must_equal [5, [], 2] @scope.send(m6, 2).must_equal [2, [], 2] @scope.send(m6, 2, 3).must_equal [2, [3], 2] m7 = app.define_roda_method("x", :any){|y, x=5, *z| [x, y, z, 3]} proc{@scope.send(m7)}.must_raise ArgumentError @scope.send(m7, 2).must_equal [5, 2, [], 3] @scope.send(m7, 2, 3).must_equal [3, 2, [], 3] @scope.send(m7, 2, 3, 4).must_equal [3, 2, [4], 3] end if RUBY_VERSION > '2.1' it "should raise for required keyword arguments for expected_arity 0 or 1" do proc{eval("app.define_roda_method('x', 0){|b:| [b, 1]}", binding)}.must_raise Roda::RodaError proc{eval("app.define_roda_method('x', 0){|c=1, b:| [c, b, 1]}", binding)}.must_raise Roda::RodaError proc{eval("app.define_roda_method('x', 1){|x, b:| [b, 1]}", binding)}.must_raise Roda::RodaError proc{eval("app.define_roda_method('x', 1){|x, c=1, b:| [c, b, 1]}", binding)}.must_raise Roda::RodaError end it "should ignore keyword arguments for expected_arity 0" do @scope.send(eval("app.define_roda_method('x', 0){|b:2| [b, 1]}", binding)).must_equal [2, 1] @scope.send(eval("app.define_roda_method('x', 0){|**b| [b, 1]}", binding)).must_equal [{}, 1] @scope.send(eval("app.define_roda_method('x', 0){|c=1, b:2| [c, b, 1]}", binding)).must_equal [1, 2, 1] @scope.send(eval("app.define_roda_method('x', 0){|c=1, **b| [c, b, 1]}", binding)).must_equal [1, {}, 1] @scope.send(eval("app.define_roda_method('x', 0){|x, b:2| [x, b, 1]}", binding)).must_equal [nil, 2, 1] @scope.send(eval("app.define_roda_method('x', 0){|x, **b| [x, b, 1]}", binding)).must_equal [nil, {}, 1] @scope.send(eval("app.define_roda_method('x', 0){|x, c=1, b:2| [x, c, b, 1]}", binding)).must_equal [nil, 1, 2, 1] @scope.send(eval("app.define_roda_method('x', 0){|x, c=1, **b| [x, c, b, 1]}", binding)).must_equal [nil, 1, {}, 1] end it "should ignore keyword arguments for expected_arity 1" do @scope.send(eval("app.define_roda_method('x', 1){|b:2| [b, 1]}", binding), 3).must_equal [2, 1] @scope.send(eval("app.define_roda_method('x', 1){|**b| [b, 1]}", binding), 3).must_equal [{}, 1] @scope.send(eval("app.define_roda_method('x', 1){|c=1, b:2| [c, b, 1]}", binding), 3).must_equal [3, 2, 1] @scope.send(eval("app.define_roda_method('x', 1){|c=1, **b| [c, b, 1]}", binding), 3).must_equal [3, {}, 1] @scope.send(eval("app.define_roda_method('x', 1){|x, b:2| [x, b, 1]}", binding), 3).must_equal [3, 2, 1] @scope.send(eval("app.define_roda_method('x', 1){|x, **b| [x, b, 1]}", binding), 3).must_equal [3, {}, 1] @scope.send(eval("app.define_roda_method('x', 1){|x, c=1, b:2| [x, c, b, 1]}", binding), 3).must_equal [3, 1, 2, 1] @scope.send(eval("app.define_roda_method('x', 1){|x, c=1, **b| [x, c, b, 1]}", binding), 3).must_equal [3, 1, {}, 1] end it "should handle expected_arity :any with keyword arguments" do if RUBY_VERSION >= '2.7' && RUBY_VERSION < '3' suppress = proc do |&b| begin stderr = $stderr $stderr = rack_input b.call ensure $stderr = stderr end end else suppress = proc{|&b| b.call} end m = eval('app.define_roda_method("x", :any){|b:2| b}', binding) @scope.send(m).must_equal 2 @scope.send(m, 4).must_equal 2 @scope.send(m, b: 3).must_equal 3 @scope.send(m, 4, b: 3).must_equal 3 m = eval('app.define_roda_method("x", :any){|b:| b}', binding) proc{@scope.send(m)}.must_raise ArgumentError proc{@scope.send(m, 4)}.must_raise ArgumentError @scope.send(m, b: 3).must_equal 3 @scope.send(m, 4, b: 3).must_equal 3 m = eval('app.define_roda_method("x", :any){|**b| b}', binding) @scope.send(m).must_equal({}) @scope.send(m, 4).must_equal({}) @scope.send(m, b: 3).must_equal(b: 3) @scope.send(m, 4, b: 3).must_equal(b: 3) m = eval('app.define_roda_method("x", :any){|x, b:9| [x, b, 1]}', binding) suppress.call{@scope.send(m)[1..-1]}.must_equal [9, 1] @scope.send(m, 2).must_equal [2, 9, 1] @scope.send(m, 2, 3).must_equal [2, 9, 1] eval("@scope.send(m, {b: 4}#{', **{}' if RUBY_VERSION > '2'})").must_equal [{b: 4}, 9, 1] @scope.send(m, 2, b: 4).must_equal [2, 4, 1] @scope.send(m, 2, 3, b: 4).must_equal [2, 4, 1] m = eval('app.define_roda_method("x", :any){|x, b:| [x, b, 1]}', binding) proc{suppress.call{@scope.send(m)}}.must_raise ArgumentError proc{@scope.send(m, 2)}.must_raise ArgumentError proc{@scope.send(m, 2, 3)}.must_raise ArgumentError proc{eval("@scope.send(m, {b: 4}#{', **{}' if RUBY_VERSION > '2'})")}.must_raise ArgumentError @scope.send(m, 2, b: 4).must_equal [2, 4, 1] @scope.send(m, 2, 3, b: 4).must_equal [2, 4, 1] m = eval('app.define_roda_method("x", :any){|x, **b| [x, b, 1]}', binding) suppress.call{@scope.send(m)[1..-1]}.must_equal [{}, 1] @scope.send(m, 2).must_equal [2, {}, 1] @scope.send(m, 2, 3).must_equal [2, {}, 1] eval("@scope.send(m, {b: 4}#{', **{}' if RUBY_VERSION > '2'})").must_equal [{b: 4}, {}, 1] @scope.send(m, 2, b: 4).must_equal [2, {b: 4}, 1] @scope.send(m, 2, 3, b: 4).must_equal [2, {b: 4}, 1] m = eval('m = app.define_roda_method("x", :any){|x=5, b:9| [x, b, 2]}', binding) @scope.send(m).must_equal [5, 9, 2] @scope.send(m, 2).must_equal [2, 9, 2] @scope.send(m, 2, 3).must_equal [2, 9, 2] @scope.send(m, b: 4).must_equal [5, 4, 2] @scope.send(m, 2, b: 4).must_equal [2, 4, 2] @scope.send(m, 2, 3, b: 4).must_equal [2, 4, 2] end end end jeremyevans-roda-4f30bb3/spec/env_spec.rb000066400000000000000000000002741516720775400204740ustar00rootroot00000000000000require_relative "spec_helper" describe "Roda#env" do it "should return the environment" do app do |r| env['PATH_INFO'] end body("/foo").must_equal "/foo" end end jeremyevans-roda-4f30bb3/spec/freeze_spec.rb000066400000000000000000000020351516720775400211610ustar00rootroot00000000000000require_relative "spec_helper" describe "Roda.freeze" do before do app{'a'}.freeze end it "should result in a working application" do body.must_equal 'a' end it "should not break if called more than once" do app.freeze body.must_equal 'a' end it "should make opts not be modifiable after calling finalize!" do proc{app.opts[:foo] = 'bar'}.must_raise end it "should make use and route raise errors" do proc{app.use Class.new}.must_raise proc{app.route{}}.must_raise end it "should make plugin raise errors" do proc{app.plugin Module.new}.must_raise end it "should make subclassing raise errors" do proc{Class.new(app)}.must_raise end it "should freeze app" do app.frozen?.must_equal true end it "should work after adding middleware" do app(:bare) do use(Class.new do def initialize(app) @app = app end def call(env) @app.call(env) end end) route do |_| 'a' end end app.freeze body.must_equal 'a' end end jeremyevans-roda-4f30bb3/spec/integration_spec.rb000066400000000000000000000123261516720775400222300ustar00rootroot00000000000000require_relative "spec_helper" describe "integration" do before do @c = Class.new do def initialize(app, first, second, &block) @app, @first, @second, @block = app, first, second, block end def call(env) env["m.first"] = @first env["m.second"] = @second env["m.block"] = @block.call @app.call(env) end end end it "should setup middleware using use" do c = @c app(:bare) do use c, "First", "Second" do "Block" end route do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end end body('/hello').must_equal 'D First Second Block' end it "should clear middleware when clear_middleware! is called" do c = @c app(:bare) do use c, "First", "Second" do "Block" end route do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end clear_middleware! end body('/hello').must_equal 'D ' end it "should freeze middleware if opts[:freeze_middleware] is true" do c = Class.new do def initialize(app) @app = app end def call(env) @a = 1; @app.call(env) end end app do "D" end body.must_equal 'D' app.use c body.must_equal 'D' app.clear_middleware! app.opts[:freeze_middleware] = true app.use c proc{body}.must_raise RuntimeError, TypeError end it "should support adding middleware using use after route block setup" do c = @c app(:bare) do route do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end use c, "First", "Second" do "Block" end end body('/hello').must_equal 'D First Second Block' end it "should inherit middleware in subclass" do c = @c @app = Class.new(app(:bare){use(c, '1', '2'){"3"}}) @app.route do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end body('/hello').must_equal 'D 1 2 3' end it "should not inherit middleware in subclass if inhert_middleware = false" do c = @c app(:bare){use(c, '1', '2'){"3"}} @app.inherit_middleware = false @app = Class.new(@app) @app.route do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end body('/hello').must_equal 'D ' end it "should inherit route in subclass" do c = @c app(:bare) do use(c, '1', '2'){"3"} route do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end end @app = Class.new(app) body('/hello').must_equal 'D 1 2 3' end it "should use instance of subclass when inheriting routes" do c = @c obj = nil app(:bare) do use(c, '1', '2'){"3"} route do |r| r.get "hello" do obj = self "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end end @app = Class.new(app) body('/hello').must_equal 'D 1 2 3' obj.must_be_kind_of(@app) end it "should handle middleware added to subclass using superclass route" do c = @c app(:bare) do route do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end end @app = Class.new(app) @app.use(c, '1', '2'){"3"} body('/hello').must_equal 'D 1 2 3' end it "should not have future middleware additions to superclass affect subclass" do c = @c a = app @app = Class.new(a) @app.route do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end a.use(c, '1', '2'){"3"} body('/hello').must_equal 'D ' end it "should not have future middleware additions to subclass affect superclass" do c = @c a = app do |r| r.get "hello" do "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}" end end @app = Class.new(a) @app.use(c, '1', '2'){"3"} @app = a body('/hello').must_equal 'D ' end it "should have app return the rack application to call" do app(:bare){}.app.must_be_kind_of(Proc) app.route{|r|} app.app.must_be_kind_of(Proc) c = Class.new{def initialize(app) @app = app end; def call(env) @app.call(env) end} app.use c app.app.must_be_kind_of(c) end unless ENV['LINT'] it "should have route_block return the route block" do app{|r| 1}.route_block.call(nil).must_equal 1 end it "supports configuring middleware with keyword arguments" do m1 = Class.new do eval <<-END def initialize(app, key: 1) @app = app @key = key end END def call(env) status, headers, _body = @app.call(env) [status, headers.merge(RodaResponseHeaders::CONTENT_LENGTH=>@key.length.to_s), [@key]] end end app(:bare) do use m1, key: 'test' route do 'a' end end body.must_equal 'test' end if RUBY_VERSION >= '2' end jeremyevans-roda-4f30bb3/spec/matchers_spec.rb000066400000000000000000000401411516720775400215070ustar00rootroot00000000000000require_relative "spec_helper" describe "capturing" do it "doesn't yield the verb for verb matcher" do app do |r| r.get do |*args| args.size.to_s end end body.must_equal '0' end it "doesn't yield the path for string matcher" do app do |r| r.get "home" do |*args| args.size.to_s end end body('/home').must_equal '0' end it "yields the segment for symbol matcher" do app do |r| r.get "user", :id do |id| id end end body("/user/johndoe").must_equal 'johndoe' end it "yields an integer segment as a string when using symbol matcher" do app do |r| r.get "user", :id do |id| id end end body("/user/101").must_equal '101' end it "yields a segment per nested block for symbol matcher" do app do |r| r.on :one do |one| r.on :two do |two| r.on :three do |three| one + two + three end end end end body("/one/two/three").must_equal "onetwothree" end it "yields a segment per argument for symbol matcher" do app do |r| r.on :one, :two, :three do |one, two, three| one + two + three end end body("/one/two/three").must_equal "onetwothree" end it "yields regex captures as separate arguments" do app do |r| r.get %r{posts/(\d+)-(.*)} do |id, slug| id + slug end end body("/posts/123-postal-service").must_equal "123postal-service" end it "yields an integer segment as an integer segment over 100 bytes when using Integer matcher" do app do |r| r.get "user", Integer do |id| "#{id}-#{id.is_a?(Integer)}" end "b" end body("/user/101").must_equal '101-true' body("/user/a").must_equal 'b' body("/user/"+"1"*100).must_equal '1'*100+'-true' body("/user/"+"1"*101).must_equal 'b' end it "yields the segment for String class matcher" do app do |r| r.get "user", String do |id| id end end body("/user/johndoe").must_equal 'johndoe' end it "raises error for unsupported class matcher" do app do |r| r.get Hash do |id| id end end proc{status}.must_raise Roda::RodaError end end describe "r.is" do it "ensures the patch is matched fully" do app do |r| r.is "" do "+1" end end body.must_equal '+1' status('//').must_equal 404 end it "handles no arguments" do app do |r| r.on "" do r.is do "+1" end end end body.must_equal '+1' status('//').must_equal 404 end it "matches strings" do app do |r| r.is "123" do "+1" end end body("/123").must_equal '+1' status("/123/").must_equal 404 end it "matches regexps" do app do |r| r.is(/(\w+)/) do |id| id end end body("/123").must_equal '123' status("/123/").must_equal 404 end it "matches regexps" do app do |r| r.on(/foo/) do |id| 'a' end end body("/foo").must_equal 'a' body("/foo/").must_equal 'a' status("/food").must_equal 404 end it "matches segments" do app do |r| r.is :id do |id| id end end body("/123").must_equal '123' status("/123/").must_equal 404 end end describe "matchers" do it "should not handle string with embedded param if :verbatim_string_matcher option is set" do app do |r| r.on "posts/:id" do '1' end r.on "responses-:id" do '2' end end status('/post/123').must_equal 404 status('/posts/123').must_equal 404 body('/posts/:id').must_equal '1' status('/responses-123').must_equal 404 body('/responses-:id').must_equal '2' end it "should handle regexes and nesting" do app do |r| r.on(/u\/(\w+)/) do |uid| r.on(/posts\/(\d+)/) do |id| uid + id end end end body("/u/jdoe/posts/123").must_equal 'jdoe123' status("/u/jdoe/pots/123").must_equal 404 end it "should handle regex nesting colon param style" do app do |r| r.on(/u:(\w+)/) do |uid| r.on(/posts:(\d+)/) do |id| uid + id end end end body("/u:jdoe/posts:123").must_equal 'jdoe123' status("/u:jdoe/poss:123").must_equal 404 end unless ENV['LINT'] it "symbol matching" do app do |r| r.on "user", :id do |uid| r.on "posts", :pid do |id| uid + id end end end body("/user/jdoe/posts/123").must_equal 'jdoe123' status("/user/jdoe/pots/123").must_equal 404 end it "paths and numbers" do app do |r| r.on "about" do r.on :one, :two do |one, two| one + two end end end body("/about/1/2").must_equal '12' status("/about/1").must_equal 404 end it "paths and decimals" do app do |r| r.on "about" do r.on(/(\d+)/) do |one| one end end end body("/about/1").must_equal '1' status("/about/1.2").must_equal 404 end it "should allow arrays to match any value" do app do |r| r.on [/(\d+)/, /\d+(bar)?/] do |id| id end end body('/123').must_equal '123' body('/123bar').must_equal 'bar' status('/123bard').must_equal 404 end it "should allow arrays to match optional segments with splats" do app do |r| r.on :foo, [:bar, true] do |*m| m.inspect end end body('/123').must_equal '["123"]' body('/123/456').must_equal '["123", "456"]' end it "should allow arrays to match optional segments with separate arguments" do app do |r| r.on :foo, [:bar, true] do |f, b| [f, b].inspect end end body('/123').must_equal '["123", nil]' body('/123/456').must_equal '["123", "456"]' end it "should allow regexp to match optional segments with separate arguments" do app do |r| r.on(/(\d+)(?:\/(\d+))?/) do |f, b| [f, b].inspect end end body('/123').must_equal '["123", nil]' body('/123/456').must_equal '["123", "456"]' end it "should have array capture match string if match" do app do |r| r.on %w'p q' do |id| id end end body('/p').must_equal 'p' body('/q').must_equal 'q' status('/r').must_equal 404 end it "should have set return next segment if one of the set's elements" do app do |r| r.on Set['p', 'q'] do |id| id end end body('/p').must_equal 'p' body('/q').must_equal 'q' status('/r').must_equal 404 end end describe "r.on" do it "executes on no arguments" do app do |r| r.on do "+1" end end body.must_equal '+1' end it "executes on true" do app do |r| r.on true do "+1" end end body.must_equal '+1' end it "does not execute on false" do app do |r| r.on false do "+1" end end status.must_equal 404 end it "does not execute on nil" do app do |r| r.on nil do "+1" end end status.must_equal 404 end it "raises on arbitrary object" do app(:bare) do route do |r| r.on Object.new do "+1" end end end proc{body}.must_raise Roda::RodaError end it "executes on non-false" do app do |r| r.on "123" do "+1" end end body("/123").must_equal '+1' end it "does not modify SCRIPT_NAME/PATH_INFO during routing" do app(:pass) do |r| r.on "foo" do r.is "bar" do "bar|#{env['SCRIPT_NAME']}|#{env['PATH_INFO']}" end r.is "baz" do r.pass end "foo|#{env['SCRIPT_NAME']}|#{env['PATH_INFO']}" end "#{env['SCRIPT_NAME']}|#{env['PATH_INFO']}" end body.must_equal '|/' body('SCRIPT_NAME'=>'/a').must_equal '/a|/' body('/foo').must_equal 'foo||/foo' body('/foo', 'SCRIPT_NAME'=>'/a').must_equal 'foo|/a|/foo' body('/foo/bar').must_equal 'bar||/foo/bar' body('/foo/bar', 'SCRIPT_NAME'=>'/a').must_equal 'bar|/a|/foo/bar' body('/foo/baz').must_equal 'foo||/foo/baz' body('/foo/baz', 'SCRIPT_NAME'=>'/a').must_equal 'foo|/a|/foo/baz' end it "should have path/matched_path/remaining_path work correctly" do app do |r| r.on "foo" do "#{r.path}:#{r.matched_path}:#{r.remaining_path}" end end body("/foo/bar").must_equal "/foo/bar:/foo:/bar" end it "ensures remaining_path is reverted if modified in failing matcher" do app do |r| r.on lambda { @remaining_path = "/blah"; false } do "Unreachable" end r.on do r.matched_path + ':' + r.remaining_path end end body("/hello").must_equal ':/hello' end it "modifies matched_path/remaining_path during routing" do app do |r| r.on 'login', 'foo' do "Unreachable" end r.on 'hello' do r.matched_path + ':' + r.remaining_path end end body("/hello/you").must_equal '/hello:/you' end it "doesn't modify SCRIPT_NAME/PATH_INFO during routing" do app do |r| r.on 'login', 'foo' do "Unreachable" end r.on 'hello' do r.env["SCRIPT_NAME"] + ':' + r.env["PATH_INFO"] end end body("/hello/you").must_equal ':/hello/you' end it "doesn't mutate SCRIPT_NAME or PATH_INFO after request is returned" do app do |r| r.on 'login', 'foo' do "Unreachable" end r.on do r.env["SCRIPT_NAME"] + ':' + r.env["PATH_INFO"] end end pi, sn = '/login', '' env = {"REQUEST_METHOD" => "GET", "PATH_INFO" => pi, "SCRIPT_NAME" => sn} _req(app, env)[2].join.must_equal ":/login" env["PATH_INFO"].must_equal pi env["SCRIPT_NAME"].must_equal sn end it "skips consecutive matches" do app do |r| r.on do "foo" end r.on do "bar" end end body.must_equal "foo" end it "finds first match available" do app do |r| r.on false do "foo" end r.on do "bar" end end body.must_equal "bar" end it "reverts a half-met matcher" do app do |r| r.on "post", false do "Should be unmet" end r.on do r.env["SCRIPT_NAME"] + ':' + r.env["PATH_INFO"] end end body("/hello").must_equal ':/hello' end it "doesn't write to body if body already written to" do app do |r| r.on do response.write "a" "b" end end body.must_equal 'a' end end describe "path matchers" do it "one level path" do app do |r| r.on "about" do "About" end end body('/about').must_equal "About" status("/abot").must_equal 404 end it "two level nested paths" do app do |r| r.on "about" do r.on "1" do "+1" end r.on "2" do "+2" end end end body('/about/1').must_equal "+1" body('/about/2').must_equal "+2" status('/about/3').must_equal 404 end it "two level inlined paths" do app do |r| r.on "a/b" do "ab" end end body('/a/b').must_equal "ab" status('/a/d').must_equal 404 end it "a path with some regex captures" do app do |r| r.on(/user(\d+)/) do |uid| uid end end body('/user123').must_equal "123" status('/useradf').must_equal 404 end it "matching the root with a string" do app do |r| r.is "" do "Home" end end body.must_equal 'Home' status("//").must_equal 404 status("/foo").must_equal 404 end it "matching the root with the root method" do app do |r| r.root do "Home" end end body.must_equal 'Home' status('REQUEST_METHOD'=>'POST').must_equal 404 status("//").must_equal 404 status("/foo").must_equal 404 end end describe "root/empty segment matching" do it "matching an empty segment" do app do |r| r.on "" do r.path end end body.must_equal '/' status("/foo").must_equal 404 end it "nested empty segments" do app do |r| r.on "" do r.on "" do r.on "1" do r.path end end end end body("///1").must_equal '///1' status("/1").must_equal 404 status("//1").must_equal 404 end it "/events/? scenario" do a = app do |r| r.on "" do "Hooray" end r.is do "Foo" end end app(:new) do |r| r.on "events" do r.run a end end unless_lint do body("/events").must_equal 'Foo' end body("/events/").must_equal 'Hooray' status("/events/foo").must_equal 404 end end describe "segment handling" do before do app do |r| r.on "post" do r.on :id do |id| id end end end end it "matches numeric ids" do body('/post/1').must_equal '1' end it "matches decimal numbers" do body('/post/1.1').must_equal '1.1' end it "matches slugs" do body('/post/my-blog-post-about-cuba').must_equal 'my-blog-post-about-cuba' end it "matches only the first segment available" do body('/post/one/two/three').must_equal 'one' end end describe "request verb methods" do it "executes if verb matches" do app do |r| r.get do "g" end r.post do "p" end end body.must_equal 'g' body('REQUEST_METHOD'=>'POST').must_equal 'p' end it "requires exact match if given arguments" do app do |r| r.get "" do "g" end r.post "" do "p" end end body.must_equal 'g' body('REQUEST_METHOD'=>'POST').must_equal 'p' status("/a").must_equal 404 status("/a", 'REQUEST_METHOD'=>'POST').must_equal 404 end it "does not require exact match if given arguments" do app do |r| r.get do r.is "" do "g" end "get" end r.post do r.is "" do "p" end "post" end end body.must_equal 'g' body('REQUEST_METHOD'=>'POST').must_equal 'p' body("/a").must_equal 'get' body("/a", 'REQUEST_METHOD'=>'POST').must_equal 'post' end end describe "all matcher" do it "should match only all all arguments match" do app do |r| r.is :all=>['foo', :y] do |file| file end end body("/foo/bar").must_equal 'bar' status.must_equal 404 status("/foo").must_equal 404 status("/foo/").must_equal 404 status("/foo/bar/baz").must_equal 404 end end describe "method matcher" do it "should match given request types" do app do |r| r.is "", :method=>:get do "foo" end r.is "", :method=>[:patch, :post] do "bar" end end body("REQUEST_METHOD"=>"GET").must_equal 'foo' body("REQUEST_METHOD"=>"PATCH").must_equal 'bar' body("REQUEST_METHOD"=>"POST").must_equal 'bar' status("REQUEST_METHOD"=>"DELETE").must_equal 404 end end describe "route block that returns string" do it "should be treated as if an on block returned string" do app do |r| "+1" end body.must_equal '+1' end end describe "app with :unsupported_block_result => :raise option" do def app_value(v) app(:bare) do opts[:unsupported_block_result] = :raise route do |r| r.is 'a' do v end v end end end it "should handle String as body" do app_value '1' status.must_equal 200 body.must_equal '1' status('/a').must_equal 200 body('/a').must_equal '1' end it "should handle nil and false as not found" do app_value nil status.must_equal 404 body.must_equal '' status('/a').must_equal 404 body('/a').must_equal '' end it "should handle false as not found" do app_value false status.must_equal 404 body.must_equal '' status('/a').must_equal 404 body('/a').must_equal '' end it "should raise RodaError for other types" do app_value Object.new proc{body}.must_raise Roda::RodaError proc{body('/a')}.must_raise Roda::RodaError end end jeremyevans-roda-4f30bb3/spec/optimization_spec.rb000066400000000000000000000441061516720775400224340ustar00rootroot00000000000000require_relative "spec_helper" [true, false].each do |frozen| describe(frozen ? "optimized matchers for frozen applications" : "matchers") do if frozen def app(*) super.freeze end end it "r.is without arguments should only match if already matched" do app do |r| r.is do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '0' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.get and r.post without arguments should only match if already matched" do app do |r| r.get do |*args| "get-#{args.length}" end r.post do |*args| "post-#{args.length}" end '' end unless_lint do body('').must_equal 'get-0' body('', 'REQUEST_METHOD'=>'POST').must_equal 'post-0' body('', 'REQUEST_METHOD'=>'PUT').must_equal '' end end [:is, :get].each do |meth| it "r.#{meth} true should only match if already matched" do app do |r| r.send(meth, true) do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '0' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.#{meth} 'string' should match only final segment containing the string" do app do |r| r.send(meth, 'foo') do |*args| "foo-#{args.length}" end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal 'foo-0' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.#{meth} String should match only final segment" do app do |r| r.send(meth, String) do |*args| args.inspect end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '["fo"]' body('/foo').must_equal '["foo"]' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.#{meth} Integer should match only final segment of 1-100 decimal characters" do app do |r| r.send(meth, Integer) do |*args| args.inspect end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' body('1').must_equal '' body('2').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' body('/1').must_equal '[1]' body('/2').must_equal '[2]' body('/1/').must_equal '' body('/2/1').must_equal '' body('/'+'2'*100).must_equal '['+'2'*100+']' body('/'+'2'*101).must_equal '' end it "r.#{meth} with other supported class argument should match if one if the elements matches" do app(:bare) do plugin :class_matchers class_matcher(Float, /(\d+\.\d+)/) do |str| [str.to_f] end route do |r| r.send meth, Float do |arg| "#{arg.class}-#{arg}-#{r.remaining_path}" end '' end end unless_lint do body('').must_equal '' body('fo').must_equal '' body('123.3').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/123.3').must_equal 'Float-123.3-' body('/123.3/').must_equal '' body('/123.3/1').must_equal '' end it "r.#{meth} with unsupported class should raise" do app do |r| r.send(meth, Array) do |*args| args.inspect end '' end proc{body}.must_raise Roda::RodaError end it "r.#{meth} /regexp/ should match only final segment" do app do |r| r.send(meth, /(foo?)/) do |*args| args.inspect end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/f').must_equal '' body('/fo').must_equal '["fo"]' body('/foo').must_equal '["foo"]' body('/food').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.#{meth} String, Integer should match only string segment followed by final integer segment" do app do |r| r.send(meth, String, Integer) do |*args| args.inspect end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '["foo", 1]' body('/foo/1/').must_equal '' body('/foo/'+'2'*100).must_equal '["foo", '+'2'*100+']' body('/foo/'+'2'*101).must_equal '' end it "r.#{meth} with false argument should never match" do app do |r| r.send(meth, false) do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.#{meth} with nil argument should never match" do app do |r| r.send(meth, nil) do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.#{meth} with array argument should match if one if the elements matches" do app do |r| r.send(meth, ['fo', 'foo']) do |arg| "#{arg}-#{r.remaining_path}" end '' end unless_lint do body('').must_equal '' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal 'fo-' body('/foo').must_equal 'foo-' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.#{meth} with hash argument should match if one if hash matches" do app do |r| r.send(meth, :all=>['fo', 'foo']) do |*args| "#{args.length}-#{r.remaining_path}" end '' end unless_lint do body('').must_equal '' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/fo/foo').must_equal '0-' body('/fo/foo/').must_equal '' body('/fo/foo/1').must_equal '' end it "r.#{meth} with symbol argument should match next segment if non-empty" do app do |r| r.send(meth, :foo) do |*args| "#{args.inspect}-#{r.remaining_path}" end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '["fo"]-' body('/foo').must_equal '["foo"]-' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.#{meth} with proc argument should match unless it returns nil/false" do x = nil app do |r| r.send(meth, proc{request.remaining_path.clear if request.remaining_path == '/'; x}) do |*args| args.length.to_s end '' end body.must_equal '' x = false body.must_equal '' x = true body.must_equal '0' body("/a").must_equal '' end it "r.#{meth} with custom argument should match if one if the elements matches" do x = Object.new app(:bare) do plugin :custom_matchers custom_matcher(Object) do |obj| @captures << obj.object_id.to_s @remaining_path = '' if @remaining_path == '/' end route do |r| r.send(meth, x) do |arg| "#{arg}-#{r.remaining_path}" end '' end end body.must_equal "#{x.object_id}-" body('/a').must_equal "" end end it "r.on without arguments should always match" do app do |r| r.on do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '0' body('fo').must_equal '0' body('foo').must_equal '0' end body.must_equal '0' body('/fo').must_equal '0' body('/foo').must_equal '0' body('/foo/').must_equal '0' body('/foo/1').must_equal '0' end it "r.on true should always match" do app do |r| r.on true do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '0' body('fo').must_equal '0' body('foo').must_equal '0' end body.must_equal '0' body('/fo').must_equal '0' body('/foo').must_equal '0' body('/foo/').must_equal '0' body('/foo/1').must_equal '0' end it "r.on 'string' should match string against next segment" do app do |r| r.on('foo') do |*args| "foo-#{args.length}-#{r.remaining_path}" end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal 'foo-0-' body('/foo/').must_equal 'foo-0-/' body('/foo/1').must_equal 'foo-0-/1' end it "r.on String should match next segment if non-empty" do app do |r| r.on(String) do |*args| "#{args.inspect}-#{r.remaining_path}" end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '["fo"]-' body('/foo').must_equal '["foo"]-' body('/foo/').must_equal '["foo"]-/' body('/foo/1').must_equal '["foo"]-/1' end it "r.on Integer should match next segment if integer" do app do |r| r.on(Integer) do |*args| "#{args.inspect}-#{r.remaining_path}" end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' body('1').must_equal '' body('2').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' body('/1').must_equal '[1]-' body('/2').must_equal '[2]-' body('/1/').must_equal '[1]-/' body('/2/1').must_equal '[2]-/1' body('/'+'2'*100).must_equal '['+'2'*100+']-' body('/'+'2'*101).must_equal '' end it "r.on with unsupported class should raise" do app do |r| r.on(Array) do |*args| args.inspect end '' end proc{body}.must_raise Roda::RodaError end it "r.on /regexp/ should match regexp against next segment" do app do |r| r.on(/(foo?)/) do |*args| "#{args.inspect}-#{r.remaining_path}" end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/f').must_equal '' body('/fo').must_equal '["fo"]-' body('/foo').must_equal '["foo"]-' body('/food').must_equal '' body('/foo/').must_equal '["foo"]-/' body('/foo/1').must_equal '["foo"]-/1' end it "r.on String, Integer should match string segment followed by integer segment" do app do |r| r.on(String, Integer) do |*args| "#{args.inspect}-#{r.remaining_path}" end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '["foo", 1]-' body('/foo/1/').must_equal '["foo", 1]-/' body('/fo/2/a').must_equal '["fo", 2]-/a' end it "r.is with slash_path_empty should match remaining path with only slash" do app(:slash_path_empty) do |r| r.is do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '0' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '0' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.on with true argument should always match" do app do |r| r.on true do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '0' body('fo').must_equal '0' body('foo').must_equal '0' end body.must_equal '0' body('/fo').must_equal '0' body('/foo').must_equal '0' body('/foo/').must_equal '0' body('/foo/1').must_equal '0' end it "r.on with false argument should never match" do app do |r| r.on false do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.on with nil argument should never match" do app do |r| r.on nil do |*args| args.length.to_s end '' end unless_lint do body('').must_equal '' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/foo/').must_equal '' body('/foo/1').must_equal '' end it "r.on with array argument should match if one if the elements matches" do app do |r| r.on ['fo', 'foo'] do |arg| "#{arg}-#{r.remaining_path}" end '' end unless_lint do body('').must_equal '' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal 'fo-' body('/foo').must_equal 'foo-' body('/foo/').must_equal 'foo-/' body('/foo/1').must_equal 'foo-/1' end it "r.on with hash argument should match if one if hash matches" do app do |r| r.on :all=>['fo', 'foo'] do |*args| "#{args.length}-#{r.remaining_path}" end '' end unless_lint do body('').must_equal '' body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/foo').must_equal '' body('/fo/foo').must_equal '0-' body('/fo/foo/').must_equal '0-/' body('/fo/foo/1').must_equal '0-/1' end it "r.on with symbol argument should match next segment if non-empty" do app do |r| r.on(:foo) do |*args| "#{args.inspect}-#{r.remaining_path}" end '' end unless_lint do body('fo').must_equal '' body('foo').must_equal '' end body.must_equal '' body('/fo').must_equal '["fo"]-' body('/foo').must_equal '["foo"]-' body('/foo/').must_equal '["foo"]-/' body('/foo/1').must_equal '["foo"]-/1' end it "r.on with proc argument should match unless it returns nil/false" do x = nil app do |r| r.on(proc{x}) do |*args| args.length.to_s end '' end body.must_equal '' x = false body.must_equal '' x = true body.must_equal '0' end it "r.on with class argument should match if one if the elements matches" do app(:bare) do plugin :class_matchers class_matcher(Float, /(\d+\.\d+)/) do |str| [str.to_f] end route do |r| r.on Float do |arg| "#{arg.class}-#{arg}-#{r.remaining_path}" end '' end end unless_lint do body('').must_equal '' body('fo').must_equal '' body('123.3').must_equal '' end body.must_equal '' body('/fo').must_equal '' body('/123.3').must_equal 'Float-123.3-' body('/123.3/').must_equal 'Float-123.3-/' body('/123.3/1').must_equal 'Float-123.3-/1' end it "r.on with custom argument should match if one if the elements matches" do x = Object.new app(:bare) do plugin :custom_matchers custom_matcher(Object) do |obj| @captures << obj.object_id.to_s end route do |r| r.on x do |arg| "#{arg}-#{r.remaining_path}" end '' end end body.must_equal "#{x.object_id}-/" end end end jeremyevans-roda-4f30bb3/spec/opts_spec.rb000066400000000000000000000015211516720775400206650ustar00rootroot00000000000000require_relative "spec_helper" describe "opts" do it "is inheritable and allows overriding" do c = Class.new(Roda) c.opts[:foo] = "bar" c.opts[:foo].must_equal "bar" sc = Class.new(c) sc.opts[:foo].must_equal "bar" sc.opts[:foo] = "baz" sc.opts[:foo].must_equal "baz" c.opts[:foo].must_equal "bar" end it "should be available as an instance methods" do app(:bare) do opts[:hello] = "Hello World" route do |r| r.on do opts[:hello] end end end body.must_equal "Hello World" end it "should only shallow clone by default" do c = Class.new(Roda) c.opts[:foo] = "bar".dup c.opts[:foo].must_equal "bar" sc = Class.new(c) sc.opts[:foo].replace("baz") sc.opts[:foo].must_equal "baz" c.opts[:foo].must_equal "baz" end end jeremyevans-roda-4f30bb3/spec/plugin/000077500000000000000000000000001516720775400176405ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/plugin/Integer_matcher_max_spec.rb000066400000000000000000000013161516720775400251450ustar00rootroot00000000000000require_relative "../spec_helper" describe "Integer_matcher_max plugin" do it "matches values up to 2**63-1 by default" do app(:Integer_matcher_max) do |r| r.is Integer do |i| i.to_s end end max = (2**63-1).to_s body("/0").must_equal '0' body("/#{max}").must_equal max body("/#{max.next}").must_equal '' end it "matches configured values if an argument is provided" do app(:bare) do plugin :Integer_matcher_max, 2**64-1 route do |r| r.is Integer do |i| i.to_s end end end max = (2**64-1).to_s body("/0").must_equal '0' body("/#{max}").must_equal max body("/#{max.next}").must_equal '' end end jeremyevans-roda-4f30bb3/spec/plugin/_after_hook_spec.rb000066400000000000000000000005741516720775400234650ustar00rootroot00000000000000require_relative "../spec_helper" describe "deprecated _after_hook plugin" do it "shouldn't break things" do x = [] app(:_after_hook) do |r| x << 0 'a' end @app.send(:include, Module.new do define_method(:_roda_after_00_test){|_| x << 1} private :_roda_after_00_test end) body.must_equal 'a' x.must_equal [0, 1] end end jeremyevans-roda-4f30bb3/spec/plugin/additional_render_engines_spec.rb000066400000000000000000000013461516720775400263620ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt' require 'tilt/erb' require 'tilt/string' require_relative '../../lib/roda/plugins/render' rescue LoadError warn "tilt not installed, skipping render plugin test" else describe "additional_render_engines plugin" do it "supports additional render engines" do app(:bare) do plugin :render, :views=>'spec/views' plugin :additional_render_engines, ['str', 'html'] route do |r| render(r.remaining_path[1, 1000]).strip end end body('/a').must_equal 'a' body('/a1').must_equal 'a1-str' body('/a2').must_equal 'a2-html' body('/a3').must_equal 'a3-erb' proc{body('/nonexistent')}.must_raise Errno::ENOENT end end end jeremyevans-roda-4f30bb3/spec/plugin/additional_view_directories_spec.rb000066400000000000000000000034661516720775400267460ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt' require 'tilt/erb' require 'tilt/string' require_relative '../../lib/roda/plugins/render' rescue LoadError warn "tilt not installed, skipping render plugin test" else describe "additional_view_directories plugin" do it "supports additional view directories" do app(:bare) do plugin :render, :views=>'spec/views' plugin :additional_view_directories, ['spec/views/about', 'spec/views/additional'] route do |r| find_template(:template=>r.remaining_path[1, 1000])[:path] end end # Only in :views, returns :views body('/a').must_equal File.expand_path('spec/views/a.erb') # In both :views and additional, returns :views body('/_test').must_equal File.expand_path('spec/views/_test.erb') # In both additional, returns first additional body('/only').must_equal File.expand_path('spec/views/about/only.erb') # Only in first additional, returns first additional body('/only_about').must_equal File.expand_path('spec/views/about/only_about.erb') # Only in second additional, returns second additional body('/only_add').must_equal File.expand_path('spec/views/additional/only_add.erb') # Not in any, returns :views body('/bogus').must_equal File.expand_path('spec/views/bogus.erb') end it "keeps additional view directories in allowed_paths after reloading render plugin" do app(:bare) do plugin :render, :views=>'spec/views' plugin :additional_view_directories, ['spec/views/about', 'spec/views/additional'] end expected = ['spec/views', 'spec/views/about', 'spec/views/additional'].map{|x| File.expand_path(x)} app.render_opts[:allowed_paths].must_equal expected app.plugin :render app.render_opts[:allowed_paths].must_equal expected end end end jeremyevans-roda-4f30bb3/spec/plugin/all_verbs_spec.rb000066400000000000000000000015021516720775400231460ustar00rootroot00000000000000require_relative "../spec_helper" describe "all_verbs plugin" do it "adds method for each http verb" do app(:all_verbs) do |r| r.delete{'d'} r.head{''} r.options{'o'} r.patch{'pa'} r.put{'pu'} r.trace{'t'} if Rack::Request.method_defined?(:link?) r.link{'l'} r.unlink{'u'} end end body('REQUEST_METHOD'=>'DELETE').must_equal 'd' body('REQUEST_METHOD'=>'HEAD').must_equal '' body('REQUEST_METHOD'=>'OPTIONS').must_equal 'o' body('REQUEST_METHOD'=>'PATCH').must_equal 'pa' body('REQUEST_METHOD'=>'PUT').must_equal 'pu' body('REQUEST_METHOD'=>'TRACE').must_equal 't' if Rack::Request.method_defined?(:link?) body('REQUEST_METHOD'=>'LINK').must_equal 'l' body('REQUEST_METHOD'=>'UNLINK').must_equal 'u' end end end jeremyevans-roda-4f30bb3/spec/plugin/assets_preloading_spec.rb000066400000000000000000000044341516720775400247120ustar00rootroot00000000000000require_relative "../spec_helper" run_tests = true begin begin require 'tilt' rescue LoadError warn "tilt not installed, skipping assets_preloading plugin test" run_tests = false end end describe "assets_preloading plugin" do before do app(:bare) do plugin :assets, { :css => ['app.str'], :js => { :head => ['app.js'] }, :path => 'spec/assets', :public => 'spec', } plugin :assets_preloading route do |r| r.is 'header-css' do preload_assets_link_header :css end r.is 'header-js' do preload_assets_link_header [:js, :head] end r.is 'header-multiple' do preload_assets_link_header :css, [:js, :head] end r.is 'tags-css' do preload_assets_link_tags :css end r.is 'tags-js' do preload_assets_link_tags [:js, :head] end r.is 'tags-multiple' do preload_assets_link_tags :css, [:js, :head] end end end end it "preload_assets_link_header returns a well-formed header" do html = body('/header-multiple') assets = html.split(',') assets.count.must_equal 2 assets.each do |asset| parts = asset.split(';') parts.count.must_equal 3 parts[0].scan(/^<.*>$/).count.must_equal 1 parts.select{|c| c == 'rel=preload' }.count.must_equal 1 parts.select{|c| c.match(/^as=\w+$/) }.count.must_equal 1 end end it "preload_assets_link_header returns the correct 'as' for an asset type" do html = body('/header-css') html.scan('as=style').count.must_equal 1 html = body('/header-js') html.scan('as=script').count.must_equal 1 end it "preload_assets_link_tags returns well-formed tags" do html = body('/tags-multiple') tags = html.scan(//) tags.count.must_equal 2 tags.each do |tag| tag.scan(' rel="preload"').count.must_equal 1 tag.scan(/ href=".+"/).count.must_equal 1 tag.scan(/ as=".+"/).count.must_equal 1 end end it "preload_assets_link_tags returns the correct 'as' for an asset type" do html = body('/tags-css') html.scan('as="style"').count.must_equal 1 html = body('/tags-js') html.scan('as="script"').count.must_equal 1 end end if run_tests jeremyevans-roda-4f30bb3/spec/plugin/assets_spec.rb000066400000000000000000001143351516720775400225100ustar00rootroot00000000000000require_relative "../spec_helper" require 'fileutils' run_tests = true begin begin require 'tilt' rescue LoadError warn "tilt not installed, skipping assets plugin test" run_tests = false end end if run_tests pid_dir = "spec/pid-#{$$}" assets_dir = File.join(pid_dir, "assets") metadata_file = File.expand_path(File.join(assets_dir, 'precompiled.json')) importdep_file = File.expand_path(File.join(assets_dir, 'css/importdep.str')) js_file = File.expand_path(File.join(assets_dir, 'js/head/app.js')) css_file = File.expand_path(File.join(assets_dir, 'css/no_access.css')) describe 'assets plugin' do before(:all) do Dir.mkdir(pid_dir) FileUtils.cp_r('spec/assets', assets_dir) @js_mtime = File.mtime(js_file) @js_atime = File.atime(js_file) @css_mtime = File.mtime(css_file) @css_atime = File.atime(css_file) end before do app(:bare) do plugin :assets, :css => ['app.str', 'raw.css'], :js => { :head => ['app.js'] }, :path => assets_dir, :public => pid_dir, :css_opts => {:cache=>false}, :css_compressor => :none, :js_compressor => :none route do |r| r.assets r.is 'test' do "#{assets(:css)}\n#{assets([:js, :head])}" end r.on 'paths_test' do css_paths = assets_paths(:css) js_paths = assets_paths([:js, :head]) empty_paths = assets_paths(:empty) { 'css' => css_paths, 'js' => js_paths, 'empty' => empty_paths }.map do |k, a| "#{k}:#{a.class}:#{a.length}:#{a.join(',')}" end.join("\n") end end end end after do File.utime(@js_atime, @js_mtime, js_file) File.utime(@css_atime, @css_mtime, css_file) File.delete(metadata_file) if File.file?(metadata_file) File.delete(importdep_file) if File.file?(importdep_file) FileUtils.rm_r(File.join(assets_dir, 'tmp')) if File.directory?(File.join(assets_dir, 'tmp')) FileUtils.rm_r(File.join(pid_dir, 'public')) if File.directory?(File.join(pid_dir, 'public')) FileUtils.rm(Dir["#{pid_dir}/app.*.{js,css}*"]) end after(:all) do FileUtils.rm_r(pid_dir) if File.directory?(pid_dir) end def gunzip(body) Zlib::GzipReader.wrap(rack_input(body), &:read) end it 'assets_opts should use correct paths given options' do fpaths = [:js_path, :css_path, :compiled_js_path, :compiled_css_path] rpaths = [:js_prefix, :css_prefix, :compiled_js_prefix, :compiled_css_prefix] app.assets_opts.values_at(*fpaths).must_equal %w"js/ css/ app app".map{|s| File.join(Dir.pwd, assets_dir, s)} app.assets_opts.values_at(*rpaths).must_equal %w"assets/js/ assets/css/ assets/app assets/app" app.plugin :assets, :path=>'bar/', :public=>'foo/', :prefix=>'as/', :js_dir=>'j/', :css_dir=>'c/', :compiled_name=>'a' app.assets_opts.values_at(*fpaths).must_equal %w"bar/j/ bar/c/ foo/as/a foo/as/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/j/ as/c/ as/a as/a" app.plugin :assets, :path=>'bar', :public=>'foo', :prefix=>'as', :js_dir=>'j', :css_dir=>'c', :compiled_name=>'a' app.assets_opts.values_at(*fpaths).must_equal %w"bar/j/ bar/c/ foo/as/a foo/as/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/j/ as/c/ as/a as/a" app.plugin :assets, :compiled_js_dir=>'cj', :compiled_css_dir=>'cs', :compiled_path=>'cp' app.assets_opts.values_at(*fpaths).must_equal %w"bar/j/ bar/c/ foo/cp/cj/a foo/cp/cs/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/j/ as/c/ as/cj/a as/cs/a" app.plugin :assets, :compiled_js_route=>'cjr', :compiled_css_route=>'ccr', :js_route=>'jr', :css_route=>'cr' app.assets_opts.values_at(*fpaths).must_equal %w"bar/j/ bar/c/ foo/cp/cj/a foo/cp/cs/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/jr/ as/cr/ as/cjr/a as/ccr/a" app.plugin :assets, :compiled_js_route=>'cj', :compiled_css_route=>'cs', :js_route=>'j', :css_route=>'c' app.assets_opts.values_at(*fpaths).must_equal %w"bar/j/ bar/c/ foo/cp/cj/a foo/cp/cs/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/j/ as/c/ as/cj/a as/cs/a" app.plugin :assets app.assets_opts.values_at(*fpaths).must_equal %w"bar/j/ bar/c/ foo/cp/cj/a foo/cp/cs/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/j/ as/c/ as/cj/a as/cs/a" app.plugin :assets, :compiled_js_dir=>'', :compiled_css_dir=>nil, :compiled_js_route=>nil, :compiled_css_route=>nil app.assets_opts.values_at(*fpaths).must_equal %w"bar/j/ bar/c/ foo/cp/a foo/cp/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/j/ as/c/ as/a as/a" app.plugin :assets, :js_dir=>'', :css_dir=>nil, :js_route=>nil, :css_route=>nil app.assets_opts.values_at(*fpaths).must_equal %w"bar/ bar/ foo/cp/a foo/cp/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/ as/ as/a as/a" app.plugin :assets, :public=>'' app.assets_opts.values_at(*fpaths).must_equal %w"bar/ bar/ cp/a cp/a".map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal %w"as/ as/ as/a as/a" app.plugin :assets, :path=>'', :compiled_path=>nil app.assets_opts.values_at(*fpaths).must_equal ['', '', 'a', 'a'].map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal ['as/', 'as/', 'as/a', 'as/a'] app.plugin :assets, :prefix=>'' app.assets_opts.values_at(*fpaths).must_equal ['', '', 'a', 'a'].map{|s| File.join(Dir.pwd, s)} app.assets_opts.values_at(*rpaths).must_equal ['', '', 'a', 'a'] app.plugin :assets, :compiled_name=>nil app.assets_opts.values_at(*fpaths).must_equal ['', ''].map{|s| File.join(Dir.pwd, s)} + ['', ''].map{|s| File.join(Dir.pwd, s).chop} app.assets_opts.values_at(*rpaths).must_equal ['', '', '', ''] end it 'assets_opts should use headers and dependencies given options' do keys = [:css_headers, :js_headers, :dependencies] asset_opts_must_equal = lambda do |array| app.assets_opts.values_at(*keys).must_be(:==, array) end asset_opts_must_equal.call [{RodaResponseHeaders::CONTENT_TYPE=>"text/css; charset=UTF-8"}, {RodaResponseHeaders::CONTENT_TYPE=>"application/javascript; charset=UTF-8"}, {}] app.plugin :assets, :headers=>{'a'=>'b'}, :dependencies=>{'a'=>'b'} asset_opts_must_equal.call [{RodaResponseHeaders::CONTENT_TYPE=>"text/css; charset=UTF-8", 'a'=>'b'}, {RodaResponseHeaders::CONTENT_TYPE=>"application/javascript; charset=UTF-8", 'a'=>'b'}, {'a'=>'b'}] app.plugin :assets, :css_headers=>{'c'=>'d'}, :js_headers=>{'e'=>'f'}, :dependencies=>{'c'=>'d'} asset_opts_must_equal.call [{RodaResponseHeaders::CONTENT_TYPE=>"text/css; charset=UTF-8", 'a'=>'b', 'c'=>'d'}, {RodaResponseHeaders::CONTENT_TYPE=>"application/javascript; charset=UTF-8", 'a'=>'b', 'e'=>'f'}, {'a'=>'b', 'c'=>'d'}] app.plugin :assets asset_opts_must_equal.call [{RodaResponseHeaders::CONTENT_TYPE=>"text/css; charset=UTF-8", 'a'=>'b', 'c'=>'d'}, {RodaResponseHeaders::CONTENT_TYPE=>"application/javascript; charset=UTF-8", 'a'=>'b', 'e'=>'f'}, {'a'=>'b', 'c'=>'d'}] app.plugin :assets asset_opts_must_equal.call [{RodaResponseHeaders::CONTENT_TYPE=>"text/css; charset=UTF-8", 'a'=>'b', 'c'=>'d'}, {RodaResponseHeaders::CONTENT_TYPE=>"application/javascript; charset=UTF-8", 'a'=>'b', 'e'=>'f'}, {'a'=>'b', 'c'=>'d'}] app.plugin :assets, :headers=>{RodaResponseHeaders::CONTENT_TYPE=>'C', 'e'=>'g'} asset_opts_must_equal.call [{RodaResponseHeaders::CONTENT_TYPE=>"C", 'a'=>'b', 'c'=>'d', 'e'=>'g'}, {RodaResponseHeaders::CONTENT_TYPE=>"C", 'a'=>'b', 'e'=>'f'}, {'a'=>'b', 'c'=>'d'}] app.plugin :assets, :css_headers=>{'a'=>'b1'}, :js_headers=>{'e'=>'f1'}, :dependencies=>{'c'=>'d1'} asset_opts_must_equal.call [{RodaResponseHeaders::CONTENT_TYPE=>"C", 'a'=>'b1', 'c'=>'d', 'e'=>'g'}, {RodaResponseHeaders::CONTENT_TYPE=>"C", 'a'=>'b', 'e'=>'f1'}, {'a'=>'b', 'c'=>'d1'}] end it 'assets_paths should return arrays of source paths' do html = body('/paths_test') html.scan('css:Array:2:/assets/css/app.str,/assets/css/raw.css').length.must_equal 1 html.scan('js:Array:1:/assets/js/head/app.js').length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 end it 'assets_paths should return the compiled path in an array' do app.compile_assets html = body('/paths_test') css_hash = app.assets_opts[:compiled]['css'] js_hash = app.assets_opts[:compiled]['js.head'] html.scan("css:Array:1:/assets/app.#{css_hash}.css").length.must_equal 1 html.scan("js:Array:1:/assets/app.head.#{js_hash}.js").length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 end it 'assets_paths should return arrays of relative source paths if the :relative_paths plugin option is used' do app.plugin :assets, :relative_paths=>true html = body('/paths_test') html.scan('css:Array:2:./assets/css/app.str,./assets/css/raw.css').length.must_equal 1 html.scan('js:Array:1:./assets/js/head/app.js').length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 html = body('/paths_test/foo') html.scan('css:Array:2:../assets/css/app.str,../assets/css/raw.css').length.must_equal 1 html.scan('js:Array:1:../assets/js/head/app.js').length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 html = body('/paths_test/foo/') html.scan('css:Array:2:../../assets/css/app.str,../../assets/css/raw.css').length.must_equal 1 html.scan('js:Array:1:../../assets/js/head/app.js').length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 html = body('/paths_test/foo/bar') html.scan('css:Array:2:../../assets/css/app.str,../../assets/css/raw.css').length.must_equal 1 html.scan('js:Array:1:../../assets/js/head/app.js').length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 end it 'assets_paths should use relative paths for compiled paths if the :relative_paths plugin option is used' do app.plugin :assets, :relative_paths=>true app.compile_assets css_hash = app.assets_opts[:compiled]['css'] js_hash = app.assets_opts[:compiled]['js.head'] html = body('/paths_test') html.scan("css:Array:1:./assets/app.#{css_hash}.css").length.must_equal 1 html.scan("js:Array:1:./assets/app.head.#{js_hash}.js").length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 html = body('/paths_test/foo') html.scan("css:Array:1:../assets/app.#{css_hash}.css").length.must_equal 1 html.scan("js:Array:1:../assets/app.head.#{js_hash}.js").length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 html = body('/paths_test/foo/') html.scan("css:Array:1:../../assets/app.#{css_hash}.css").length.must_equal 1 html.scan("js:Array:1:../../assets/app.head.#{js_hash}.js").length.must_equal 1 html.scan('empty:Array:0').length.must_equal 1 end it 'should handle rendering assets, linking to them, and accepting requests for them when not compiling' do html = body('/test') html.scan(/true html = body('/test') html.scan(/'-' html = body('/test') html.scan(/'POST').must_equal 404 html =~ %r{href="(/assets/css/raw\.css)"} status($1, 'REQUEST_METHOD'=>'POST').must_equal 404 html.scan(/' end it '#assets must_include type given' do app.allocate.assets([:js, :head], type: 'module').must_equal '' end it '#assets should escape attribute values given' do app.allocate.assets([:js, :head], 'a'=>'b"e').must_equal '' end it 'requests for assets should return 304 if the asset has not been modified' do loc = '/assets/js/head/app.js' lm = header(RodaResponseHeaders::LAST_MODIFIED, loc) status(loc, 'HTTP_IF_MODIFIED_SINCE'=>lm).must_equal 304 body(loc, 'HTTP_IF_MODIFIED_SINCE'=>lm).must_equal '' end it 'requests for assets should not return 304 if the asset has been modified' do loc = '/assets/js/head/app.js' lm = header(RodaResponseHeaders::LAST_MODIFIED, loc) File.utime(@js_atime, @js_mtime+1, js_file) status(loc, 'HTTP_IF_MODIFIED_SINCE'=>lm).must_equal 200 body(loc, 'HTTP_IF_MODIFIED_SINCE'=>lm).must_include('console.log') end it 'requests for assets should return 304 if the dependency of an asset has not been modified' do app.plugin :assets, :dependencies=>{js_file=>css_file} loc = '/assets/js/head/app.js' lm = header(RodaResponseHeaders::LAST_MODIFIED, loc) status(loc, 'HTTP_IF_MODIFIED_SINCE'=>lm).must_equal 304 body(loc, 'HTTP_IF_MODIFIED_SINCE'=>lm).must_equal '' end it 'requests for assets should return 200 if the dependency of an asset has been modified' do app.plugin :assets, :dependencies=>{js_file=>css_file} loc = '/assets/js/head/app.js' lm = header(RodaResponseHeaders::LAST_MODIFIED, loc) File.utime(@css_atime, [@css_mtime+2, @js_mtime+2].max, css_file) status(loc, 'HTTP_IF_MODIFIED_SINCE'=>lm).must_equal 200 body(loc, 'HTTP_IF_MODIFIED_SINCE'=>lm).must_include('console.log') end it 'requests for assets should include modifications to content of dependencies' do File.open(File.join(assets_dir, 'css/importdep.str'), 'wb'){|f| f.write('body{color: blue;}')} app.plugin :assets, :css=>['import.str'], :dependencies=>{File.join(assets_dir, 'css/import.str')=>File.join(assets_dir, 'css/importdep.str')} app.plugin :render, :cache=>false 3.times do body('/assets/css/import.str').must_include('color: blue;') end File.open(File.join(assets_dir, 'css/importdep.str'), 'wb'){|f| f.write('body{color: red;}')} File.utime(Time.now+2, Time.now+4, File.join(assets_dir, 'css/importdep.str')) 3.times do body('/assets/css/import.str').must_include('color: red;') end end it 'should do a terminal match for assets' do status('/assets/css/app.str/foo').must_equal 404 end it 'should only allow files that you specify' do status('/assets/css/no_access.css').must_equal 404 end it 'should not add routes for empty asset types' do app.plugin :assets, :css=>nil a = app::RodaRequest.assets_matchers a.length.must_equal 1 a.first.length.must_equal 2 a.first.first.must_equal :js 'assets/js/head/app.js'.must_match a.first.last 'assets/js/head/app2.js'.wont_match a.first.last end it 'should not add routes if no asset types' do app.plugin :assets, :js=>nil, :css=>nil app::RodaRequest.assets_matchers.must_equal [] end it 'should support :postprocessor option' do postprocessor = lambda do |file, type, content| "file=#{file} type=#{type} tc=#{type.class} #{content.sub('color', 'font')}" end app.plugin :assets, :path=>pid_dir, :js_dir=>nil, :css_dir=>nil, :prefix=>nil, :postprocessor=>postprocessor, :css=>{:assets=>{:css=>%w'app.str'}} app.route do |r| r.assets r.is 'test' do "#{assets([:css, :assets, :css])}" end end html = body('/test') html.scan(/metadata_file File.exist?(metadata_file).must_equal false app.allocate.assets([:js, :head]).must_equal '' app.compile_assets File.exist?(metadata_file).must_equal true app.allocate.assets([:js, :head]).must_match %r{src="(/assets/app\.head\.[a-f0-9]{64}\.js)"} app.plugin :assets, :compiled=>false, :precompiled=>false app.allocate.assets([:js, :head]).must_equal '' app.plugin :assets, :precompiled=>metadata_file app.allocate.assets([:js, :head]).must_match %r{src="(/assets/app\.head\.[a-f0-9]{64}\.js)"} end it 'should work correctly with json plugin when r.assets is the last method called' do app.plugin :assets app.plugin :json app.route do |r| r.assets end status.must_equal 404 end end describe 'assets plugin' do it "app :root option affects :views default" do app.plugin :assets app.assets_opts[:path].must_equal File.join(Dir.pwd, 'assets') app.assets_opts[:js_path].must_equal File.join(Dir.pwd, 'assets/js/') app.assets_opts[:css_path].must_equal File.join(Dir.pwd, 'assets/css/') app.opts[:root] = '/foo' app.plugin :assets # Work around for Windows app.assets_opts[:path].sub(/\A\w:/, '').must_equal '/foo/assets' app.assets_opts[:js_path].sub(/\A\w:/, '').must_equal '/foo/assets/js/' app.assets_opts[:css_path].sub(/\A\w:/, '').must_equal '/foo/assets/css/' app.opts[:root] = '/foo/bar' app.plugin :assets app.assets_opts[:path].sub(/\A\w:/, '').must_equal '/foo/bar/assets' app.assets_opts[:js_path].sub(/\A\w:/, '').must_equal '/foo/bar/assets/js/' app.assets_opts[:css_path].sub(/\A\w:/, '').must_equal '/foo/bar/assets/css/' app.opts[:root] = nil app.plugin :assets app.assets_opts[:path].must_equal File.join(Dir.pwd, 'assets') app.assets_opts[:js_path].must_equal File.join(Dir.pwd, 'assets/js/') app.assets_opts[:css_path].must_equal File.join(Dir.pwd, 'assets/css/') end end end jeremyevans-roda-4f30bb3/spec/plugin/assume_ssl_spec.rb000066400000000000000000000003121516720775400233510ustar00rootroot00000000000000require_relative "../spec_helper" describe "assume_ssl plugin" do it "makes r.ssl? always return true" do app(:assume_ssl) do |r| r.ssl?.to_s end body.must_equal 'true' end end jeremyevans-roda-4f30bb3/spec/plugin/autoload_hash_branches_spec.rb000066400000000000000000000056351516720775400256700ustar00rootroot00000000000000require_relative "../spec_helper" describe "hash_branches plugin" do after do $roda_app = nil Dir['spec/autoload_hash_branches/**/*.rb'].each do |f| $LOADED_FEATURES.delete File.expand_path(f) $LOADED_FEATURES.delete File.realpath(f) end end def check_autoload_hash_branches @app.route do |r| r.hash_branches '-' end @app.opts[:loaded] = [] $roda_app = @app @app.opts[:loaded].must_equal [] body('/c').must_equal '-' @app.opts[:loaded].must_equal [] body('/b').must_equal 'b' @app.opts[:loaded].must_equal [:b] body('/a').must_equal 'a' @app.opts[:loaded].must_equal [:b, :a] status('/a/e').must_equal 404 @app.opts[:loaded].must_equal [:b, :a, :a_e] body('/a/d').must_equal 'a-d' @app.opts[:loaded].must_equal [:b, :a, :a_e, :a_d] body('/a/c').must_equal 'a-c' @app.opts[:loaded].must_equal [:b, :a, :a_e, :a_d, :a_c] body('/c').must_equal '-' body('/b').must_equal 'b' body('/a').must_equal 'a' status('/a/e').must_equal 404 body('/a/d').must_equal 'a-d' body('/a/c').must_equal 'a-c' @app.opts[:loaded].must_equal [:b, :a, :a_e, :a_d, :a_c] end it "should autoload hash branches on request when using autoload_hash_branch" do app(:bare) do plugin :autoload_hash_branches autoload_hash_branch('a', 'spec/autoload_hash_branches/a') autoload_hash_branch('b', 'spec/autoload_hash_branches/b') autoload_hash_branch('/a', 'c', 'spec/autoload_hash_branches/a/c') autoload_hash_branch('/a', 'd', 'spec/autoload_hash_branches/a/d') autoload_hash_branch('/a', 'e', 'spec/autoload_hash_branches/a/e') end check_autoload_hash_branches end it "should autoload hash branches on request when using autoload_hash_branch_dir" do app(:bare) do plugin :autoload_hash_branches autoload_hash_branch_dir('./spec/autoload_hash_branches') autoload_hash_branch_dir('/a', './spec/autoload_hash_branches/a') end check_autoload_hash_branches end it "should eager load autoload hash branches when freezing the application" do app(:bare) do plugin :autoload_hash_branches autoload_hash_branch('a', './spec/autoload_hash_branches/a') autoload_hash_branch('b', './spec/autoload_hash_branches/b') autoload_hash_branch('/a', 'c', './spec/autoload_hash_branches/a/c') autoload_hash_branch('/a', 'd', './spec/autoload_hash_branches/a/d') autoload_hash_branch('/a', 'e', './spec/autoload_hash_branches/a/e') route do |r| r.hash_branches '-' end end $roda_app = @app @app.opts[:loaded] = [] 2.times{@app.freeze} @app.opts[:loaded].must_equal [:a, :b, :a_c, :a_d, :a_e] body('/c').must_equal '-' body('/b').must_equal 'b' body('/a').must_equal 'a' status('/a/e').must_equal 404 body('/a/d').must_equal 'a-d' body('/a/c').must_equal 'a-c' end end jeremyevans-roda-4f30bb3/spec/plugin/autoload_named_routes_spec.rb000066400000000000000000000050641516720775400255610ustar00rootroot00000000000000require_relative "../spec_helper" describe "named_routes plugin" do after do $roda_app = nil Dir['spec/autoload_named_routes/**/*.rb'].each do |f| $LOADED_FEATURES.delete File.expand_path(f) $LOADED_FEATURES.delete File.realpath(f) end end it "should autoload hash branches on request when using autoload_named_route" do app(:bare) do $roda_app = self opts[:loaded] = [] plugin :autoload_named_routes autoload_named_route(:a, 'spec/autoload_named_routes/a') autoload_named_route(:b, 'spec/autoload_named_routes/b') autoload_named_route(:a, :c, 'spec/autoload_named_routes/a/c') autoload_named_route(:a, :d, 'spec/autoload_named_routes/a/d') autoload_named_route(:a, :e, 'spec/autoload_named_routes/a/e') route do |r| r.on('a'){r.route(:a)} r.on('b'){r.route(:b)} '-' end end @app.opts[:loaded].must_equal [] body('/c').must_equal '-' @app.opts[:loaded].must_equal [] body('/b').must_equal 'b' @app.opts[:loaded].must_equal [:b] body('/a').must_equal 'a' @app.opts[:loaded].must_equal [:b, :a] status('/a/e').must_equal 404 @app.opts[:loaded].must_equal [:b, :a, :a_e] body('/a/d').must_equal 'a-d' @app.opts[:loaded].must_equal [:b, :a, :a_e, :a_d] body('/a/c').must_equal 'a-c' @app.opts[:loaded].must_equal [:b, :a, :a_e, :a_d, :a_c] body('/c').must_equal '-' body('/b').must_equal 'b' body('/a').must_equal 'a' status('/a/e').must_equal 404 body('/a/d').must_equal 'a-d' body('/a/c').must_equal 'a-c' @app.opts[:loaded].must_equal [:b, :a, :a_e, :a_d, :a_c] end it "should eager load autoload hash branches when freezing the application" do app(:bare) do plugin :autoload_named_routes autoload_named_route(:a, './spec/autoload_named_routes/a') autoload_named_route(:b, './spec/autoload_named_routes/b') autoload_named_route(:a, :a, './spec/autoload_named_routes/a/c') autoload_named_route(:a, :d, './spec/autoload_named_routes/a/d') autoload_named_route(:a, :e, './spec/autoload_named_routes/a/e') route do |r| r.on('a'){r.route(:a)} r.on('b'){r.route(:b)} '-' end end $roda_app = @app @app.opts[:loaded] = [] 2.times{@app.freeze} @app.opts[:loaded].must_equal [:a, :b, :a_c, :a_d, :a_e] body('/c').must_equal '-' body('/b').must_equal 'b' body('/a').must_equal 'a' status('/a/e').must_equal 404 body('/a/d').must_equal 'a-d' body('/a/c').must_equal 'a-c' end end jeremyevans-roda-4f30bb3/spec/plugin/backtracking_array_spec.rb000066400000000000000000000015711516720775400250240ustar00rootroot00000000000000require_relative "../spec_helper" describe "backtracking_array plugin" do it "backtracks to next entry in array if later matcher fails" do app(:backtracking_array) do |r| r.is %w'a a/b' do |id| id end r.is %w'c c/d', %w'd e' do |a, b| "#{a}-#{b}" end r.is [%w'f f/g', %w'g g/h'] do |id| id end end status.must_equal 404 body("/a").must_equal 'a' body("/a/b").must_equal 'a/b' status("/a/b/").must_equal 404 body("/c/d").must_equal 'c-d' body("/c/e").must_equal 'c-e' body("/c/d/d").must_equal 'c/d-d' body("/c/d/e").must_equal 'c/d-e' status("/c/d/").must_equal 404 body("/f").must_equal 'f' body("/f/g").must_equal 'f/g' body("/g").must_equal 'g' body("/g/h").must_equal 'g/h' status("/f/g/").must_equal 404 status("/g/h/").must_equal 404 end end jeremyevans-roda-4f30bb3/spec/plugin/bearer_token_spec.rb000066400000000000000000000012161516720775400236370ustar00rootroot00000000000000require_relative "../spec_helper" describe "bearer_token plugin" do it "adds r.bearer_token method for bearer token access" do app(:bearer_token) do |r| r.bearer_token.to_s end body.must_equal '' body("HTTP_AUTHORIZATION" => "").must_equal '' body("HTTP_AUTHORIZATION" => "Foo bar").must_equal '' body("HTTP_AUTHORIZATION" => "Bearer: foo").must_equal '' body("HTTP_AUTHORIZATION" => "xBearer foo").must_equal '' body("HTTP_AUTHORIZATION" => "Bearer foo").must_equal 'foo' body("HTTP_AUTHORIZATION" => "bearer foo").must_equal 'foo' body("HTTP_AUTHORIZATION" => "BEArer foo").must_equal 'foo' end end jeremyevans-roda-4f30bb3/spec/plugin/branch_locals_spec.rb000066400000000000000000000066071516720775400240020ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping branch_locals plugin test" else describe "branch_locals plugin" do it "should set view and layout locals to use" do app(:branch_locals) do set_view_locals :title=>'About Roda' set_layout_locals :title=>'Home' view(:inline=>'

<%= title %>

', :layout=>{:inline=>"Alternative Layout: <%= title %>\n<%= yield %>"}) end body.strip.must_equal "Alternative Layout: Home\n

About Roda

" end it "should have set_view_locals work without set_layout_locals" do app(:branch_locals) do set_view_locals :title=>'About Roda' view(:inline=>'

<%= title %>

', :layout=>{:inline=>"Alternative Layout: <%= title %>\n<%= yield %>", :locals=>{:title=>'Home'}}) end body.strip.must_equal "Alternative Layout: Home\n

About Roda

" end it "should have set_layout_locals work without set_view_locals" do app(:branch_locals) do set_layout_locals :title=>'Home' view(:inline=>'

<%= title %>

', :locals=>{:title=>'About Roda'}, :layout=>{:inline=>"Alternative Layout: <%= title %>\n<%= yield %>"}) end body.strip.must_equal "Alternative Layout: Home\n

About Roda

" end it "should merge multiple calls to set view and layout locals" do app(:branch_locals) do set_layout_locals :title=>'About Roda' set_view_locals :title=>'Home' set_layout_locals :a=>'A' set_view_locals :b=>'B' view(:inline=>'<%= title %>:<%= b %>', :layout=>{:inline=>"<%= title %>:<%= a %>::<%= yield %>"}) end body.strip.must_equal "About Roda:A::Home:B" end it "should merge multiple calls in the correct order" do app(:branch_locals) do set_layout_locals :title=>'Roda' set_view_locals :title=>'H' set_layout_locals :a=>'A', :title=>'About Roda' set_view_locals :b=>'B', :title=>'Home' view(:inline=>'<%= title %>:<%= b %>', :layout=>{:inline=>"<%= title %>:<%= a %>::<%= yield %>"}) end body.strip.must_equal "About Roda:A::Home:B" end it "should have set_view_locals have more precedence than plugin options, but less than view/render method options" do app(:bare) do plugin :render, :views=>"./spec/views", :layout_opts=>{:template=>'multiple-layout'} plugin :render_locals, :render=>{:title=>'Home', :b=>'B'}, :layout=>{:title=>'About Roda', :a=>'A'} plugin :branch_locals route do |r| r.is 'c' do view(:multiple) end set_view_locals :b=>'BB' set_layout_locals :a=>'AA' r.on 'b' do set_view_locals :title=>'About' set_layout_locals :title=>'Roda' r.is 'a' do view(:multiple) end view("multiple", :locals=>{:b => "BBB"}, :layout_opts=>{:locals=>{:a=>'AAA'}}) end r.is 'a' do view(:multiple) end view("multiple", :locals=>{:b => "BBB"}, :layout_opts=>{:locals=>{:a=>'AAA'}}) end end body('/c').strip.must_equal "About Roda:A::Home:B" body('/b/a').strip.must_equal "Roda:AA::About:BB" body('/b').strip.must_equal "Roda:AAA::About:BBB" body('/a').strip.must_equal "About Roda:AA::Home:BB" body.strip.must_equal "About Roda:AAA::Home:BBB" end end end jeremyevans-roda-4f30bb3/spec/plugin/break_spec.rb000066400000000000000000000011341516720775400222620ustar00rootroot00000000000000require_relative "../spec_helper" describe "break plugin" do it "skips the current block if break is called" do app(:break) do |r| r.root do break if env['FOO'] == 'true' 'root' end r.on :id do |id| break if id == 'foo' id end r.on :x, :y do |x, y| x + y end end body.must_equal 'root' status('FOO'=>'true').must_equal 404 body("/a").must_equal 'a' body("/a/b").must_equal 'a' body("/foo/a").must_equal 'fooa' body("/foo/a/b").must_equal 'fooa' status("/foo").must_equal 404 end end jeremyevans-roda-4f30bb3/spec/plugin/caching_spec.rb000066400000000000000000000262711516720775400226030ustar00rootroot00000000000000require_relative "../spec_helper" describe 'response.cache_control' do it 'sets the Cache-Control header' do app(:caching) do |r| response.cache_control :public=>true, :no_cache=>true, :max_age => 60 end header(RodaResponseHeaders::CACHE_CONTROL).split(', ').sort.must_equal ['max-age=60', 'no-cache', 'public'] end it 'does not add a Cache-Control header if it would be empty' do app(:caching) do |r| response.cache_control({}) end header(RodaResponseHeaders::CACHE_CONTROL).must_be_nil end it 'skips Cache-Control nil parameters' do app(:caching) do |r| response.cache_control(:max_age=>nil) end header(RodaResponseHeaders::CACHE_CONTROL).must_be_nil end end describe 'response.expires' do it 'sets the Cache-Control and Expires header' do app(:caching) do |r| response.expires 60, :public=>true, :no_cache=>true end header(RodaResponseHeaders::CACHE_CONTROL).split(', ').sort.must_equal ['max-age=60', 'no-cache', 'public'] ((Time.httpdate(header(RodaResponseHeaders::EXPIRES)) - Time.now).round - 60).abs.must_be :<=, 1 end it 'can be called with only one argument' do app(:caching) do |r| response.expires 60 end header(RodaResponseHeaders::CACHE_CONTROL).split(', ').sort.must_equal ['max-age=60'] ((Time.httpdate(header(RodaResponseHeaders::EXPIRES)) - Time.now).round - 60).abs.must_be :<=, 1 end end describe 'response.finish' do it 'removes Content-Type and Content-Length for 304 responses' do app(:caching) do |r| response.status = 304 nil end header(RodaResponseHeaders::CONTENT_TYPE).must_be_nil header(RodaResponseHeaders::CONTENT_LENGTH).must_be_nil end it 'does not change non-304 responses' do app(:caching) do |r| response.status = 200 nil end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/html' header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '0' end end describe 'request.last_modified' do it 'ignores nil' do app(:caching) do |r| r.last_modified nil end header(RodaResponseHeaders::LAST_MODIFIED).must_be_nil end it 'does not change a status other than 200' do app(:caching) do |r| response.status = 201 r.last_modified Time.now end status.must_equal 201 status('HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2030 23:43:52 GMT').must_equal 201 status('HTTP_IF_MODIFIED_SINCE' => 'Sun, 26 Sep 2000 23:43:52 GMT').must_equal 201 end end describe 'request.last_modified' do def res(a={}) s, h, b = req(a) h[RodaResponseHeaders::LAST_MODIFIED].must_equal @last_modified.httpdate [s, b.join] end before(:all) do lm = @last_modified = Time.now app(:caching) do |r| r.last_modified lm 'ok' end end it 'just sets Last-Modified if no If-Modified-Since header' do res.must_equal [200, 'ok'] end it 'just sets Last-Modified if bogus If-Modified-Since header' do res('HTTP_IF_MODIFIED_SINCE' => 'a really weird date').must_equal [200, 'ok'] end it 'just sets Last-Modified if modified since If-Modified-Since header' do res('HTTP_IF_MODIFIED_SINCE' => (@last_modified - 1).httpdate).must_equal [200, 'ok'] end it 'sets Last-Modified and returns 304 if modified on If-Modified-Since header' do res('HTTP_IF_MODIFIED_SINCE' => @last_modified.httpdate).must_equal [304, ''] end it 'sets Last-Modified and returns 304 if modified before If-Modified-Since header' do res('HTTP_IF_MODIFIED_SINCE' => (@last_modified + 1).httpdate).must_equal [304, ''] end it 'sets Last-Modified if If-None-Match header present' do res('HTTP_IF_NONE_MATCH' => '*', 'HTTP_IF_MODIFIED_SINCE' => (@last_modified + 1).httpdate).must_equal [200, 'ok'] end it 'sets Last-Modified if modified before If-Unmodified-Since header' do res('HTTP_IF_UNMODIFIED_SINCE' => (@last_modified + 1).httpdate).must_equal [200, 'ok'] end it 'sets Last-Modified if modified on If-Unmodified-Since header' do res('HTTP_IF_UNMODIFIED_SINCE' => @last_modified.httpdate).must_equal [200, 'ok'] end it 'sets Last-Modified and returns 412 if modified after If-Unmodified-Since header' do res('HTTP_IF_UNMODIFIED_SINCE' => (@last_modified - 1).httpdate).must_equal [412, ''] end end describe 'request.etag' do before(:all) do app(:caching) do |r| r.is "" do response.status = r.env['status'].to_i if r.env['status'] etag_opts = {} etag_opts[:new_resource] = r.env['new_resource'] == 'true' if r.env.has_key?('new_resource') etag_opts[:weak] = r.env['weak'] == 'true' if r.env.has_key?('weak') r.etag 'foo', etag_opts 'ok' end end end def res(a={}) a['status'] = a['status'].to_s if a['status'] s, h, b = req(a) h[RodaResponseHeaders::ETAG].must_equal '"foo"' [s, b.join] end it 'uses a weak etag with the :weak option' do header(RodaResponseHeaders::ETAG, 'weak'=>'true').must_equal 'W/"foo"' end describe 'for GET requests' do it "sets etag if no If-None-Match" do res.must_equal [200, 'ok'] end it "sets etag and returns 304 if If-None-Match is *" do res('HTTP_IF_NONE_MATCH' => '*').must_equal [304, ''] end it "sets etag and if If-None-Match is * and it is a new resource" do res('HTTP_IF_NONE_MATCH' => '*', 'new_resource'=>'true').must_equal [200, 'ok'] end it "sets etag and returns 304 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"').must_equal [304, ''] end it "sets etag and returns 304 if If-None-Match includes etag" do res('HTTP_IF_NONE_MATCH' => '"bar", "foo"').must_equal [304, ''] end it "sets etag if If-None-Match does not include etag" do res('HTTP_IF_NONE_MATCH' => '"bar", "baz"').must_equal [200, 'ok'] end it "sets etag and does not change status code if status code set and not 2xx or 304 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>499).must_equal [499, 'ok'] end it "sets etag and returns 304 if status code set to 2xx if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>201).must_equal [304, ''] end it "sets etag and returns 304 if status code is already 304 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>304).must_equal [304, ''] end it "sets etag if If-Match is *" do res('HTTP_IF_MATCH' => '*').must_equal [200, 'ok'] end it "sets etag if If-Match is etag" do res('HTTP_IF_MATCH' => '"foo"').must_equal [200, 'ok'] end it "sets etag if If-Match includes etag" do res('HTTP_IF_MATCH' => '"bar", "foo"').must_equal [200, 'ok'] end it "sets etag and returns 412 if If-Match is * for new resources" do res('HTTP_IF_MATCH' => '*', 'new_resource'=>'true').must_equal [412, ''] end it "sets etag if If-Match does not include etag" do res('HTTP_IF_MATCH' => '"bar", "baz"', 'new_resource'=>'true').must_equal [412, ''] end end describe 'for PUT requests' do def res(a={}) super(a.merge('REQUEST_METHOD'=>'PUT')) end it "sets etag if no If-None-Match" do res.must_equal [200, 'ok'] end it "sets etag and returns 412 if If-None-Match is *" do res('HTTP_IF_NONE_MATCH' => '*').must_equal [412, ''] end it "sets etag and if If-None-Match is * and it is a new resource" do res('HTTP_IF_NONE_MATCH' => '*', 'new_resource'=>'true').must_equal [200, 'ok'] end it "sets etag and returns 412 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"').must_equal [412, ''] end it "sets etag and returns 412 if If-None-Match includes etag" do res('HTTP_IF_NONE_MATCH' => '"bar", "foo"').must_equal [412, ''] end it "sets etag if If-None-Match does not include etag" do res('HTTP_IF_NONE_MATCH' => '"bar", "baz"').must_equal [200, 'ok'] end it "sets etag and does not change status code if status code set and not 2xx or 304 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>499).must_equal [499, 'ok'] end it "sets etag and returns 304 if status code set to 2xx if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>201).must_equal [412, ''] end it "sets etag and returns 304 if status code is already 304 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>304).must_equal [412, ''] end it "sets etag if If-Match is *" do res('HTTP_IF_MATCH' => '*').must_equal [200, 'ok'] end it "sets etag if If-Match is etag" do res('HTTP_IF_MATCH' => '"foo"').must_equal [200, 'ok'] end it "sets etag if If-Match includes etag" do res('HTTP_IF_MATCH' => '"bar", "foo"').must_equal [200, 'ok'] end it "sets etag and returns 412 if If-Match is * for new resources" do res('HTTP_IF_MATCH' => '*', 'new_resource'=>'true').must_equal [412, ''] end it "sets etag if If-Match does not include etag" do res('HTTP_IF_MATCH' => '"bar", "baz"', 'new_resource'=>'true').must_equal [412, ''] end end describe 'for POST requests' do def res(a={}) super(a.merge('REQUEST_METHOD'=>'POST')) end it "sets etag if no If-None-Match" do res.must_equal [200, 'ok'] end it "sets etag and returns 412 if If-None-Match is * and it is not a new resource" do res('HTTP_IF_NONE_MATCH' => '*', 'new_resource'=>'false').must_equal [412, ''] end it "sets etag and if If-None-Match is *" do res('HTTP_IF_NONE_MATCH' => '*').must_equal [200, 'ok'] end it "sets etag and returns 412 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"').must_equal [412, ''] end it "sets etag and returns 412 if If-None-Match includes etag" do res('HTTP_IF_NONE_MATCH' => '"bar", "foo"').must_equal [412, ''] end it "sets etag if If-None-Match does not include etag" do res('HTTP_IF_NONE_MATCH' => '"bar", "baz"').must_equal [200, 'ok'] end it "sets etag and does not change status code if status code set and not 2xx or 304 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>499).must_equal [499, 'ok'] end it "sets etag and returns 304 if status code set to 2xx if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>201).must_equal [412, ''] end it "sets etag and returns 304 if status code is already 304 if If-None-Match is etag" do res('HTTP_IF_NONE_MATCH' => '"foo"', 'status'=>304).must_equal [412, ''] end it "sets etag if If-Match is * and this is not a new resource" do res('HTTP_IF_MATCH' => '*', 'new_resource'=>'false').must_equal [200, 'ok'] end it "sets etag if If-Match is etag" do res('HTTP_IF_MATCH' => '"foo"').must_equal [200, 'ok'] end it "sets etag if If-Match includes etag" do res('HTTP_IF_MATCH' => '"bar", "foo"').must_equal [200, 'ok'] end it "sets etag and returns 412 if If-Match is * for new resources" do res('HTTP_IF_MATCH' => '*').must_equal [412, ''] end it "sets etag if If-Match does not include etag" do res('HTTP_IF_MATCH' => '"bar", "baz"', 'new_resource'=>'true').must_equal [412, ''] end end end jeremyevans-roda-4f30bb3/spec/plugin/capture_erb_spec.rb000066400000000000000000000107031516720775400234730ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt' rescue LoadError warn "tilt not installed, skipping capture_erb plugin test" else describe "capture_erb plugin" do before do app(:bare) do plugin :render, :views => './spec/views' plugin :capture_erb plugin :inject_erb route do |r| r.root do render(:inline => "<% value = capture_erb do %>foo<% end %>bar<%= value %>") end r.get "nil" do render(:inline => "<% value = capture_erb do %>foo<% nil end %>bar<%= value %>") end r.get "returns", String do |x| @x = x render(:inline => "<% value = capture_erb(returns: @x.to_sym) do %>foo<% nil end %>bar<%= value %>") end r.get 'rescue' do render(:inline => "<% value = capture_erb do %>foo<% raise %><% end rescue (value = 'baz') %>bar<%= value %>") end r.get 'inject' do render(:inline => "<% some_method do %>foo<% end %>") end r.get 'monkey-patch' do render(:inline => "<% def (instance_variable_get(render_opts[:template_opts][:outvar])).capture(x); end ; value = capture_erb do %>foo<% end %>bar<%= value %>") end r.get 'outside' do capture_erb{1} end end def some_method(&block) inject_erb "bar" inject_erb capture_erb(&block).upcase inject_erb "baz" end end end it "should capture erb output" do body.strip.must_equal "barfoo" end it "should return block value by default" do body("/nil").strip.must_equal "bar" end it "should return buffer value if returns: :buffer plugin option is given" do app.plugin :capture_erb, returns: :buffer body("/nil").strip.must_equal "barfoo" end it "should return buffer value if returns: :buffer method option is given" do body("/returns/other").strip.must_equal "bar" body("/returns/buffer").strip.must_equal "barfoo" app.plugin :capture_erb, returns: :buffer body("/returns/other").strip.must_equal "bar" body("/returns/buffer").strip.must_equal "barfoo" end it "should handle exceptions in captured blocks" do body('/rescue').strip.must_equal "barbaz" end it "should work with the inject_erb plugin" do body('/inject').strip.must_equal "barFOObaz" end it "should work if buffer String instance defines capture" do body('/monkey-patch').must_equal "barfoo" end it "should return result of block converted to string when used outside template" do body('/outside').must_equal "1" end end end begin require 'tilt/erubi' require 'erubi/capture_block' rescue LoadError warn "tilt/erubi or erubi/capture not installed, skipping capture_erb plugin test for erubi/capture_block" else describe "capture_erb plugin with erubi/capture_block" do before do app(:bare) do plugin :render, :views => './spec/views', template_opts: {engine_class: Erubi::CaptureBlockEngine} plugin :capture_erb plugin :inject_erb route do |r| r.root do render(:inline => "<% value = capture_erb do %>foo<% end %>bar<%= value %>") end r.get "nil" do render(:inline => "<% value = capture_erb do %>foo<% nil end %>bar<%= value %>") end r.get "returns", String do |x| @x = x render(:inline => "<% value = capture_erb(returns: @x.to_sym) do %>foo<% nil end %>bar<%= value %>") end r.get 'rescue' do render(:inline => "<% value = capture_erb do %>foo<% raise %><% end rescue (value = 'baz') %>bar<%= value %>") end r.get 'inject' do render(:inline => "<%= some_method do %>foo<% end %>") end r.get 'outside' do capture_erb{1} end end def some_method(&block) "bar#{capture_erb(&block).upcase}baz" end end end it "should capture erb output" do body.strip.must_equal "barfoo" end it "should return buffer value always" do body("/nil").strip.must_equal "barfoo" body("/returns/other").strip.must_equal "barfoo" body("/returns/buffer").strip.must_equal "barfoo" end it "should handle exceptions in captured blocks" do body('/rescue').strip.must_equal "barbaz" end it "should work with the inject_erb plugin" do body('/inject').strip.must_equal "barFOObaz" end it "should return result of block converted to string when used outside template" do body('/outside').must_equal "1" end end end jeremyevans-roda-4f30bb3/spec/plugin/chunked_spec.rb000066400000000000000000000273641516720775400226340ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping chunked plugin test" else describe "chunked plugin with :force_chunked_encoding" do def cbody(env={}) body({'HTTP_VERSION'=>'HTTP/1.1'}.merge(env)) end force_chunked_encoding = {:force_chunked_encoding=>true}.freeze it "streams templates in chunked encoding only if HTTP 1.1 is used" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end cbody.must_equal "1\r\nh\r\n1\r\nm\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "hmt" end it "hex encodes chunk sizes" do m = 'm' * 31 app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked(:inline=>m.dup, :layout=>{:inline=>'h<%= yield %>t'}) end end cbody.must_equal "1\r\nh\r\n1f\r\n#{m}\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "h#{m}t" end it "accepts a block that is called after layout yielding but before content when streaming" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| @h = nil chunked(:inline=>'m<%= @h %>', :layout=>{:inline=>'<%= @h %><%= yield %>t'}) do @h = 'h' end end end cbody.must_equal "2\r\nmh\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "hmht" end it "has delay accept block that is called after layout yielding but before content when streaming" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| delay do @h << 'i' end @h = String.new('h') chunked(:inline=>'m<%= @h %>', :layout=>{:inline=>'<%= @h %><%= yield %>t'}) do @h << 'j' end end end cbody.must_equal "1\r\nh\r\n4\r\nmhij\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "hijmhijt" end it "has delay raise if not given a block" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| delay end end proc{body}.must_raise(Roda::RodaError) end it "works when a layout is not used" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked(:inline=>'m', :layout=>nil) end end cbody.must_equal "1\r\nm\r\n0\r\n\r\n" body.must_equal "m" end it "streams partial template responses if flush is used in content template" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked(:inline=>'m<%= flush %>n', :layout=>{:inline=>'h<%= yield %>t'}) end end cbody.must_equal "1\r\nh\r\n1\r\nm\r\n1\r\nn\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "hmnt" end it "streams partial template responses if flush is used in layout template" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= flush %>i<%= yield %>t'}) end end cbody.must_equal "1\r\nh\r\n1\r\ni\r\n1\r\nm\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "himt" end it "does not stream if no_chunk! is used" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| no_chunk! chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end cbody.must_equal "hmt" body.must_equal "hmt" end it "streams existing response body before call" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| response.write('a') response.write chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end cbody.must_equal "1\r\na\r\n1\r\nh\r\n1\r\nm\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "ahmt" end it "should not include Content-Length header even if body is already written to" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| response.write('a') response.write chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end header(RodaResponseHeaders::CONTENT_LENGTH, 'HTTP_VERSION'=>'HTTP/1.1').must_be_nil header(RodaResponseHeaders::CONTENT_LENGTH, 'HTTP_VERSION'=>'HTTP/1.0').must_equal '4' end it "stream template responses for view if :chunk_by_default is used" do app(:bare) do plugin :chunked, force_chunked_encoding.merge(:chunk_by_default=>true) route do |r| view(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end cbody.must_equal "1\r\nh\r\n1\r\nm\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "hmt" end it "uses Transfer-Encoding header when chunking" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end header(RodaResponseHeaders::TRANSFER_ENCODING, 'HTTP_VERSION'=>'HTTP/1.1').must_equal 'chunked' header(RodaResponseHeaders::TRANSFER_ENCODING, 'HTTP_VERSION'=>'HTTP/1.0').must_be_nil end it "uses given :headers when chunking" do app(:bare) do plugin :chunked, force_chunked_encoding.merge(:headers=>{'foo'=>'bar'}) route do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end header('foo', 'HTTP_VERSION'=>'HTTP/1.1').must_equal 'bar' header('foo', 'HTTP_VERSION'=>'HTTP/1.0').must_be_nil end it "handles multiple arguments to chunked" do app(:bare) do plugin :chunked, force_chunked_encoding.merge(:chunk_by_default=>true) plugin :render, :views => "./spec/views" route do |r| chunked('about', :locals=>{:title=>'m'}, :layout=>{:inline=>'h<%= yield %>t'}) end end cbody.must_equal "1\r\nh\r\nb\r\n

m

\n\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "h

m

\nt" end it "handles multiple hash arguments to chunked" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked({:inline=>'m'}, :layout=>{:inline=>'h<%= yield %>t'}) end end cbody.must_equal "1\r\nh\r\n1\r\nm\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "hmt" end it "handles :layout_opts option" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked(:inline=>'m', :layout=>{:inline=>'<%= h %><%= yield %>t'}, :layout_opts=>{:locals=>{:h=>'h'}}) end end cbody.must_equal "1\r\nh\r\n1\r\nm\r\n1\r\nt\r\n0\r\n\r\n" body.must_equal "hmt" end it "uses handle_chunk_error for handling errors when chunking" do app(:bare) do plugin :chunked, force_chunked_encoding route do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %><% raise %>'}) end end proc{cbody}.must_raise RuntimeError proc{body}.must_raise RuntimeError app.send(:define_method, :handle_chunk_error) do |v| @_out_buf = 'e' flush end cbody.must_equal "1\r\nh\r\n1\r\nm\r\n1\r\ne\r\n0\r\n\r\n" proc{body}.must_raise RuntimeError end end describe "chunked plugin without :force_chunked_encoding" do def body req[2].to_enum(:each).to_a end it "streams templates in chunks" do app(:chunked) do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end body.must_equal %w'h m t' end it "accepts a block that is called after layout yielding but before content when streaming" do app(:chunked) do |r| @h = nil chunked(:inline=>'m<%= @h %>', :layout=>{:inline=>'h<%= @h %>i<%= yield %>t'}) do @h = 'h' end end body.must_equal %w'hi mh t' end it "has delay accept block that is called after layout yielding but before content when streaming" do app(:chunked) do |r| delay do @h << 'i' end @h = String.new('h') chunked(:inline=>'m<%= @h %>', :layout=>{:inline=>'<%= @h %>k<%= yield %>t'}) do @h << 'j' end end body.must_equal %w'hk mhij t' end it "has delay raise if not given a block" do app(:chunked){|r| delay} proc{body}.must_raise(Roda::RodaError) end it "works when a layout is not used" do app(:chunked) do |r| chunked(:inline=>'m', :layout=>nil) end body.must_equal ['m'] end it "streams partial template responses if flush is used in content template" do app(:chunked) do |r| chunked(:inline=>'m<%= flush %>n', :layout=>{:inline=>'h<%= yield %>t'}) end body.must_equal %w'h m n t' end it "streams partial template responses if flush is used in layout template" do app(:chunked) do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= flush %>i<%= yield %>t'}) end body.must_equal %w'h i m t' end it "does not stream if no_chunk! is used" do app(:chunked) do |r| no_chunk! chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end body.must_equal ["hmt"] end it "streams existing response body before call" do app(:chunked) do |r| response.write('a') response.write chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end body.must_equal %w'a h m t' end it "should not include Content-Length header even if body is already written to" do app(:chunked) do |r| response.write('a') response.write chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end header(RodaResponseHeaders::CONTENT_LENGTH).must_be_nil end it "stream template responses for view if :chunk_by_default is used" do app(:bare) do plugin :chunked, :chunk_by_default=>true route do |r| view(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end body.must_equal %w'h m t' end it "does not stream template responses for view with a block if :chunk_by_default is used" do app(:bare) do plugin :chunked, :chunk_by_default=>true route do |r| view(:inline=>'<%= yield %>', :layout=>{:inline=>'h<%= yield %>t'}){'m'} end end body.must_equal %w'hmt' end it "does not uses Transfer-Encoding header when streaming chunks" do app(:chunked) do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end header(RodaResponseHeaders::TRANSFER_ENCODING).must_be_nil end it "uses given :headers when chunking" do app(:bare) do plugin :chunked, :headers=>{'foo'=>'bar'} route do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end header('foo').must_equal 'bar' end it "does not use given :headers when not chunking" do app(:bare) do plugin :chunked, :headers=>{'foo'=>'bar'}, :chunk_by_default=>true route do |r| no_chunk! view(:inline=>'m', :layout=>{:inline=>'h<%= yield %>t'}) end end header('foo').must_be_nil end it "handles multiple arguments to chunked" do app(:bare) do plugin :chunked plugin :render, :views => "./spec/views" route do |r| chunked('about', :locals=>{:title=>'m'}, :layout=>{:inline=>'h<%= yield %>t'}) end end body.must_equal ['h', "

m

\n", 't'] end it "handles multiple hash arguments to chunked" do app(:chunked) do |r| chunked({:inline=>'m'}, :layout=>{:inline=>'h<%= yield %>t'}) end body.must_equal %w'h m t' end it "handles :layout_opts option" do app(:chunked) do |r| chunked(:inline=>'m', :layout=>{:inline=>'<%= h %><%= yield %>t'}, :layout_opts=>{:locals=>{:h=>'h'}}) end body.must_equal %w'h m t' end it "uses handle_chunk_error for handling errors when chunking" do app(:chunked) do |r| chunked(:inline=>'m', :layout=>{:inline=>'h<%= yield %><% raise %>'}) end proc{body}.must_raise RuntimeError app.send(:define_method, :handle_chunk_error) do |v| @_out_buf = 'e' flush end body.must_equal %w'h m e' end end end jeremyevans-roda-4f30bb3/spec/plugin/class_level_routing_spec.rb000066400000000000000000000132721516720775400252470ustar00rootroot00000000000000require_relative "../spec_helper" describe "class_level_routing plugin" do before do app(:bare) do plugin :class_level_routing plugin :all_verbs root do 'root' end on "foo" do request.get "bar" do "foobar" end "foo" end is "d", :d do |x| request.get do "bazget#{x}" end request.post do "bazpost#{x}" end end meths = %w'get post delete head options patch put trace' meths.concat(%w'link unlink') if ::Rack::Request.method_defined?(:link?) meths.each do |meth| send(meth, :d) do |m| if meth == 'head' response['x'] = "x-#{meth}-#{m}" '' else "x-#{meth}-#{m}" end end end end end it "adds class methods for setting up routes" do body.must_equal 'root' body('/foo').must_equal 'foo' body('/foo/bar').must_equal 'foobar' body('/d/go').must_equal 'bazgetgo' body('/d/go', 'REQUEST_METHOD'=>'POST').must_equal 'bazpostgo' body('/bar').must_equal "x-get-bar" body('/bar', 'REQUEST_METHOD'=>'POST').must_equal "x-post-bar" body('/bar', 'REQUEST_METHOD'=>'DELETE').must_equal "x-delete-bar" header('x', '/bar', 'REQUEST_METHOD'=>'HEAD').must_equal "x-head-bar" body('/bar', 'REQUEST_METHOD'=>'OPTIONS').must_equal "x-options-bar" body('/bar', 'REQUEST_METHOD'=>'PATCH').must_equal "x-patch-bar" body('/bar', 'REQUEST_METHOD'=>'PUT').must_equal "x-put-bar" body('/bar', 'REQUEST_METHOD'=>'TRACE').must_equal "x-trace-bar" if ::Rack::Request.method_defined?(:link?) body('/bar', 'REQUEST_METHOD'=>'LINK').must_equal "x-link-bar" body('/bar', 'REQUEST_METHOD'=>'UNLINK').must_equal "x-unlink-bar" end status.must_equal 200 status("/asdfa/asdf").must_equal 404 @app = Class.new(app) body.must_equal 'root' body('/foo').must_equal 'foo' body('/foo/bar').must_equal 'foobar' body('/d/go').must_equal 'bazgetgo' body('/d/go', 'REQUEST_METHOD'=>'POST').must_equal 'bazpostgo' body('/bar').must_equal "x-get-bar" body('/bar', 'REQUEST_METHOD'=>'POST').must_equal "x-post-bar" body('/bar', 'REQUEST_METHOD'=>'DELETE').must_equal "x-delete-bar" header('x', '/bar', 'REQUEST_METHOD'=>'HEAD').must_equal "x-head-bar" body('/bar', 'REQUEST_METHOD'=>'OPTIONS').must_equal "x-options-bar" body('/bar', 'REQUEST_METHOD'=>'PATCH').must_equal "x-patch-bar" body('/bar', 'REQUEST_METHOD'=>'PUT').must_equal "x-put-bar" body('/bar', 'REQUEST_METHOD'=>'TRACE').must_equal "x-trace-bar" end it "only calls class level routes if routing tree doesn't handle request" do app.route do |r| r.root do 'iroot' end r.get 'foo' do 'ifoo' end r.on 'bar' do r.get true do response.status = 404 '' end r.post true do 'ibar' end end end body.must_equal 'iroot' body('/foo').must_equal 'ifoo' body('/foo/bar').must_equal 'foobar' body('/d/go').must_equal 'bazgetgo' body('/d/go', 'REQUEST_METHOD'=>'POST').must_equal 'bazpostgo' body('/bar').must_equal "" body('/bar', 'REQUEST_METHOD'=>'POST').must_equal "ibar" body('/bar', 'REQUEST_METHOD'=>'DELETE').must_equal "x-delete-bar" header('x', '/bar', 'REQUEST_METHOD'=>'HEAD').must_equal "x-head-bar" body('/bar', 'REQUEST_METHOD'=>'OPTIONS').must_equal "x-options-bar" body('/bar', 'REQUEST_METHOD'=>'PATCH').must_equal "x-patch-bar" body('/bar', 'REQUEST_METHOD'=>'PUT').must_equal "x-put-bar" body('/bar', 'REQUEST_METHOD'=>'TRACE').must_equal "x-trace-bar" end it "works with the not_found plugin if loaded before" do app.plugin :not_found do "nf" end body.must_equal 'root' body('/foo').must_equal 'foo' body('/foo/bar').must_equal 'foobar' body('/d/go').must_equal 'bazgetgo' body('/d/go', 'REQUEST_METHOD'=>'POST').must_equal 'bazpostgo' body('/bar').must_equal "x-get-bar" body('/bar', 'REQUEST_METHOD'=>'POST').must_equal "x-post-bar" body('/bar', 'REQUEST_METHOD'=>'DELETE').must_equal "x-delete-bar" header('x', '/bar', 'REQUEST_METHOD'=>'HEAD').must_equal "x-head-bar" body('/bar', 'REQUEST_METHOD'=>'OPTIONS').must_equal "x-options-bar" body('/bar', 'REQUEST_METHOD'=>'PATCH').must_equal "x-patch-bar" body('/bar', 'REQUEST_METHOD'=>'PUT').must_equal "x-put-bar" body('/bar', 'REQUEST_METHOD'=>'TRACE').must_equal "x-trace-bar" status.must_equal 200 status("/asdfa/asdf").must_equal 404 body("/asdfa/asdf").must_equal "nf" end it "works when freezing the app" do app.freeze body.must_equal 'root' body('/foo').must_equal 'foo' body('/foo/bar').must_equal 'foobar' body('/d/go').must_equal 'bazgetgo' body('/d/go', 'REQUEST_METHOD'=>'POST').must_equal 'bazpostgo' body('/bar').must_equal "x-get-bar" body('/bar', 'REQUEST_METHOD'=>'POST').must_equal "x-post-bar" body('/bar', 'REQUEST_METHOD'=>'DELETE').must_equal "x-delete-bar" header('x', '/bar', 'REQUEST_METHOD'=>'HEAD').must_equal "x-head-bar" body('/bar', 'REQUEST_METHOD'=>'OPTIONS').must_equal "x-options-bar" body('/bar', 'REQUEST_METHOD'=>'PATCH').must_equal "x-patch-bar" body('/bar', 'REQUEST_METHOD'=>'PUT').must_equal "x-put-bar" body('/bar', 'REQUEST_METHOD'=>'TRACE').must_equal "x-trace-bar" if ::Rack::Request.method_defined?(:link?) body('/bar', 'REQUEST_METHOD'=>'LINK').must_equal "x-link-bar" body('/bar', 'REQUEST_METHOD'=>'UNLINK').must_equal "x-unlink-bar" end status.must_equal 200 status("/asdfa/asdf").must_equal 404 proc{app.on{}}.must_raise end end jeremyevans-roda-4f30bb3/spec/plugin/class_matchers_spec.rb000066400000000000000000000142451516720775400242000ustar00rootroot00000000000000require_relative "../spec_helper" require 'date' describe "class_matchers plugin" do it "allows class specific regexps with type conversion for class matchers" do app(:bare) do plugin :class_matchers class_matcher(Date, /(\d\d\d\d)-(\d\d)-(\d\d)/){|y,m,d| Date.new(y.to_i, m.to_i, d.to_i) if Date.valid_date?(y.to_i, m.to_i, d.to_i)} class_matcher(Array, /(\w+)\/(\w+)/){|a, b| [[a, 1], [b, 2]]} class_matcher(Hash, /(\d+)\/(\d+)/){|a, b| [{a.to_i=>b.to_i}]} klass = Class.new def klass.to_s; "klass" end class_matcher(klass, Integer){|i| i*2 unless i == 10} plugin :symbol_matchers symbol_matcher(:i, /i(\d+)/, &:to_i) klass2 = Class.new def klass2.to_s; "klass2" end class_matcher(klass2, :i){|i| i*3} symbol_matcher(:j, /j(\d+)/) klass3 = Class.new def klass3.to_s; "klass3" end class_matcher(klass3, :j){|j| j*3} klass4 = Class.new def klass4.to_s; "klass4" end class_matcher(klass4, String){|i| i*2} klass5 = Class.new def klass5.to_s; "klass5" end class_matcher(klass5, klass){|i| i*3} klass6 = Class.new def klass6.to_s; "klass6" end class_matcher(klass6, klass) klass7 = Class.new def klass7.to_s; "klass7" end class_matcher(klass7, String) klass8 = Class.new def klass8.to_s; "klass8" end class_matcher(klass8, :d){|i| i*2} klass9 = Class.new def klass9.to_s; "klass9" end class_matcher(klass9, :d) route do |r| r.on Array do |(a,b), (c,d)| r.get 'X', klass5 do |i| [a, b, c, d, i].join('-') end r.get 'Y', [klass6, klass7] do |i| [a, b, c, d, i].join('-') end r.get 'Z1', klass8 do |i| [a, b, c, d, i].join('-') end r.get 'Z2', klass9 do |i| [a, b, c, d, i].join('-') end r.get Date do |date| [date.year, date.month, date.day, a, b, c, d].join('-') end r.get Hash do |h| [h.to_a.inspect, a, b, c, d].join('-') end r.get Array do |(a1,b1), (c1,d1)| [a1, b1, c1, d1, a, b, c, d].join('-') end r.get klass do |i| [a, b, c, d, i].join('-') + '-1' end r.get klass2 do |i| [a, b, c, d, i].join('-') + '-2' end r.get klass3 do |i| [a, b, c, d, i].join('-') + '-3' end r.get klass4 do |i| [a, b, c, d, i].join('-') + '-4' end r.is do [a, b, c, d].join('-') end "array" end "" end end body("/c").must_equal '' body("/c/d").must_equal 'c-1-d-2' body("/c/d/e/f/g").must_equal 'array' body("/c/d/2009-10-a").must_equal 'c-1-d-2-2009-10-a2009-10-a-4' body("/c/d/2009-10-01").must_equal '2009-10-1-c-1-d-2' body("/c/d/2009-13-01").must_equal "c-1-d-2-2009-13-012009-13-01-4" body("/c/d/1/2").must_equal '[[1, 2]]-c-1-d-2' body("/c/d/e/f").must_equal 'e-1-f-2-c-1-d-2' body("/c/d/3").must_equal 'c-1-d-2-6-1' body("/c/d/10").must_equal 'c-1-d-2-1010-4' body("/c/d/i3").must_equal 'c-1-d-2-9-2' body("/c/d/j3").must_equal 'c-1-d-2-333-3' body("/c/d/i").must_equal 'c-1-d-2-ii-4' body("/c/d/X/3").must_equal 'c-1-d-2-18' body("/c/d/X/10").must_equal 'X-1-10-2-c-1-d-2' body("/c/d/Y/3").must_equal 'c-1-d-2-6' body("/c/d/Y/a").must_equal 'c-1-d-2-a' body("/c/d/Z1/3").must_equal 'c-1-d-2-33' body("/c/d/Z2/3").must_equal 'c-1-d-2-3' end it "raises errors for unsupported calls to class matcher" do app(:class_matchers){} c = Class.new proc{app.class_matcher(:foo, /a/)}.must_raise Roda::RodaError proc{app.class_matcher(c, Hash)}.must_raise Roda::RodaError proc{app.class_matcher(c, :foo)}.must_raise Roda::RodaError app.plugin :symbol_matchers proc{app.class_matcher(c, :foo)}.must_raise Roda::RodaError proc{app.class_matcher(c, Object.new)}.must_raise Roda::RodaError end it "respects Integer_matcher_max plugin when using class_matcher with Integer matcher" do c = Class.new app(:class_matchers){|r| r.is(c){|x| (x*3).to_s}} app.class_matcher(c, Integer) body("/4").must_equal "12" body("/1000000000000000000000").must_equal "3000000000000000000000" app.plugin :Integer_matcher_max body("/1000000000000000000000").must_equal "" app.plugin :Integer_matcher_max, 1000000000000000000000 body("/1000000000000000000000").must_equal "3000000000000000000000" body("/1000000000000000000001").must_equal "" end it "respects Integer_matcher_max plugin when loaded first" do c = Class.new app(:bare) do plugin :Integer_matcher_max plugin :class_matchers route{|r| r.is(c){|x| (x*3).to_s}} end app.class_matcher(c, Integer) body("/4").must_equal "12" body("/1000000000000000000000").must_equal "" app.plugin :Integer_matcher_max, 1000000000000000000000 body("/1000000000000000000000").must_equal "3000000000000000000000" body("/1000000000000000000001").must_equal "" end it "handles conversion block returning falsey for matcher based on String " do app(:class_matchers) do |r| r.is(Array){|s| s} r.remaining_path end app.class_matcher(Array, String){|s| s*2 unless s == 'a'} body('/a').must_equal '/a' body('/b').must_equal 'bb' end it "yields multiple arguments in matcher based on String " do app(:class_matchers) do |r| r.is(Array){|s, s2| "#{s}-#{s2}"} end app.class_matcher(Array, String){|s| [s, s*2]} body('/a').must_equal 'a-aa' end it "yields hash instances as single arguments" do app(:class_matchers) do |r| r.is('a', Array){|h| h.to_a.join(',')} r.is('h', Hash){|h| h.to_a.join('-')} end app.class_matcher(Hash, /(\w)(\w)/){|k,v| {k=>v}} app.class_matcher(Array, Hash){|h| h['c'] = 'd'; h } body('/h/ab').must_equal 'a-b' body('/a/ab').must_equal 'a,b,c,d' end it "freezes :class_matchers option when freezing app" do app(:class_matchers){|r| } app.freeze app.opts[:class_matchers].frozen?.must_equal true end end jeremyevans-roda-4f30bb3/spec/plugin/common_logger_spec.rb000066400000000000000000000104761516720775400240360ustar00rootroot00000000000000require_relative "../spec_helper" describe "common_logger plugin" do def cl_app(&block) app(:common_logger, &block) @logger = rack_input @app.plugin :common_logger, @logger end it 'logs requests to given logger/stream' do cl_app(&:path_info) body("HTTP_VERSION"=>'HTTP/1.0').must_equal '/' @logger.rewind @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 1 0.\d\d\d\d\n\z/) unless ENV["LINT"] @logger.rewind @logger.truncate(0) body("HTTP_VERSION"=>"HTTP\n1.0").must_equal '/' @logger.rewind @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\\xa1.0" 200 1 0.\d\d\d\d\n\z/) end @logger.rewind @logger.truncate(0) body('/b', 'REMOTE_ADDR'=>'1.1.1.2', 'QUERY_STRING'=>'foo=bar', "HTTP_VERSION"=>'HTTP/1.0').must_equal '/b' @logger.rewind @logger.read.must_match(/\A1\.1\.1\.2 - - \[\d\d\/[A-Z][a-z]{2}\/\d\d\d\d:\d\d:\d\d:\d\d [-+]\d\d\d\d\] "GET \/b\?foo=bar HTTP\/1.0" 200 2 0.\d\d\d\d\n\z/) @logger.rewind @logger.truncate(0) body('/b', 'REMOTE_ADDR'=>'1.1.1.2', 'QUERY_STRING'=>'foo=bar', "HTTP_VERSION"=>'HTTP/1.0', "SCRIPT_NAME"=>"/a").must_equal '/b' @logger.rewind @logger.read.must_match(/\A1\.1\.1\.2 - - \[\d\d\/[A-Z][a-z]{2}\/\d\d\d\d:\d\d:\d\d:\d\d [-+]\d\d\d\d\] "GET \/a\/b\?foo=bar HTTP\/1.0" 200 2 0.\d\d\d\d\n\z/) logger = Struct.new(:logger) do def <<(m) logger << m; end end.new(@logger) @app.plugin :common_logger, logger @logger.rewind @logger.truncate(0) body("HTTP_VERSION"=>'HTTP/1.0').must_equal '/' @logger.rewind @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 1 0.\d\d\d\d\n\z/) end it 'handles empty PATH_INFO' do cl_app(&:path_info) body('', 'HTTP_X_FORWARDED_FOR'=>'1.1.1.1', 'REMOTE_USER'=>'je', 'REQUEST_METHOD'=>'POST', 'QUERY_STRING'=>'', "HTTP_VERSION"=>'HTTP/1.1').must_equal '' @logger.rewind @logger.read.must_match(/\A1\.1\.1\.1 - je \[\d\d\/[A-Z][a-z]{2}\/\d\d\d\d:\d\d:\d\d:\d\d [-+]\d\d\d\d\] "POST HTTP\/1.1" 200 - 0.\d\d\d\d\n\z/) end unless ENV['LINT'] it 'skips timer information if not available' do cl_app do |r| @_request_timer = nil r.path_info end body("HTTP_VERSION"=>'HTTP/1.0').must_equal '/' @logger.rewind @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 1 -\n\z/) end it 'skips length information if not available' do cl_app do |r| r.halt [500, {}, []] end body("HTTP_VERSION"=>'HTTP/1.0').must_equal '' @logger.rewind @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" 500 - 0.\d\d\d\d\n\z/) end it 'does not log if an error is raised' do cl_app do |r| raise "foo" end begin body rescue => e end e.must_be_instance_of(RuntimeError) e.message.must_equal 'foo' end it 'logs errors if used with error_handler' do cl_app do |r| raise "foo" end @app.plugin :error_handler do |_| "bad" end body("HTTP_VERSION"=>'HTTP/1.0').must_equal 'bad' @logger.rewind @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" 500 3 0.\d\d\d\d\n\z/) end it 'escapes' do cl_app(&:path_info) body("HTTP_VERSION"=>"HTTP/\x801.0".dup.force_encoding('BINARY')).must_equal '/' @logger.rewind @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\/\\x801.0" 200 1 0.\d\d\d\d\n\z/) end unless ENV['LINT'] def cl_app_meth(&block) app(:common_logger, &block) @logger = (Class.new(SimpleDelegator) do def debug(str) write "DEBUG #{str}" end end).new(rack_input) @app.plugin :common_logger, @logger, :method=>:debug end it 'logs using the given method' do cl_app_meth do |r| r.halt [500, {}, []] end body("HTTP_VERSION"=>'HTTP/1.0').must_equal '' @logger.rewind @logger.read.must_match(/\ADEBUG - - - \[\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" 500 - 0.\d\d\d\d\n\z/) end end jeremyevans-roda-4f30bb3/spec/plugin/conditional_sessions_spec.rb000066400000000000000000000042211516720775400254270ustar00rootroot00000000000000require_relative "../spec_helper" if RUBY_VERSION >= '2' describe "conditional_sessions plugin" do include CookieJar before do allow = @allow = String.new('f') app(:bare) do plugin :conditional_sessions, :secret=>'1'*64 do allow != 'f' end route do |r| r.get('s', String, String){|k, v| v.force_encoding('UTF-8'); session[k] = v} r.get('g', String){|k| session[k].to_s} r.get('cat'){r.session_created_at.to_i.to_s} r.get('uat'){r.session_updated_at.to_i.to_s} r.get('cs'){clear_session.to_s} r.get('ps', String, String){|k, v| r.persist_session(response.headers, k => v); env.delete("rack.session"); nil} '' end end end it "allows sessions if allowed" do @allow.replace('t') body('/s/foo/bar').must_equal 'bar' body('/g/foo').must_equal 'bar' body('/cat').to_i.must_be(:>=, Time.now.to_i - 1) body('/uat').to_i.must_be(:>=, Time.now.to_i - 1) body('/s/foo/baz').must_equal 'baz' body('/g/foo').must_equal 'baz' body('/ps/foo/quux').must_equal '' body('/g/foo').must_equal 'quux' body('/cs').must_equal '' body('/g/foo').must_equal '' end it "raises on session if sessions not allowed" do proc{body('/s/foo/bar')}.must_raise Roda::RodaError end it "raises on session_created_at if sessions not allowed" do proc{body('/cat')}.must_raise Roda::RodaError end it "raises on session_updated_at if sessions not allowed" do proc{body('/uat')}.must_raise Roda::RodaError end it "has clear_session do nothing if sessions are not alowed" do @allow.replace('t') body('/s/foo/bar').must_equal 'bar' @allow.replace('f') body('/cs').must_equal '' @allow.replace('t') body('/g/foo').must_equal 'bar' end it "has persist_session do nothing if sessions are not alowed" do @allow.replace('t') body('/s/foo/bar').must_equal 'bar' @allow.replace('f') body('/ps/foo/quux').must_equal '' @allow.replace('t') body('/g/foo').must_equal 'bar' end end end jeremyevans-roda-4f30bb3/spec/plugin/content_for_spec.rb000066400000000000000000000076141516720775400235270ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping content_for plugin test" else describe "content_for plugin with erb" do before do app(:bare) do plugin :render, :views => './spec/views' plugin :content_for route do |r| r.root do view(:inline => "<% content_for :foo do %>foo<% end %>bar", :layout => { :inline => '<%= yield %> <%= content_for(:foo) %>' }) end r.get 'a' do view(:inline => "bar", :layout => { :inline => '<%= content_for(:foo) %> <%= yield %>' }) end r.get 'b' do view(:inline => '<% content_for(:foo, "foo") %>bar', :layout => { :inline => '<%= yield %> <%= content_for(:foo) %>' }) end r.get 'e' do view(:inline => 'a<% content_for :foo do %><% end %>b', :layout => { :inline => 'c<%= yield %>d<%= content_for(:foo) %>e' }) end r.get 'f' do view(:inline => 'a<% content_for :foo do "f" end %>b', :layout => { :inline => 'c<%= yield %>d<%= content_for(:foo) %>e' }) end r.get 'g' do view(:inline => 'a<% content_for :foo do "<" + "%= 1 %" + ">" end %>b', :layout => { :inline => 'c<%= yield %>d<%= content_for(:foo) %>e' }) end end end end it "should be able to set content in template and get that content in the layout" do body.strip.must_equal "bar foo" end it "should work if content is not set by the template" do body('/a').strip.must_equal "bar" end it "should work if a raw string is set" do body('/b').strip.must_equal "bar foo" end it "should work for an empty content_for" do body('/e').strip.must_equal "cabde" end it "should work when content_for uses a regular block" do body('/f').strip.must_equal "cabdfe" end it "should use content_for output directly" do body('/g').strip.must_equal "cabd<%= 1 %>e" end end describe "content_for plugin with multiple calls to the same key" do before do app(:bare) do plugin :render, :views => './spec/views' plugin :content_for route do |r| r.root do view(:inline => "<% content_for :foo do %>foo<% end %><% content_for :foo do %>baz<% end %>bar", :layout => { :inline => '<%= yield %> <%= content_for(:foo) %>' }) end end end end it "should replace with multiple calls to the same key if :append=>false plugin option is used" do app.plugin :content_for, :append => false body.strip.must_equal "bar baz" end it "should append with multiple calls to the same key if :append=>true plugin option is used" do app.plugin :content_for body.strip.must_equal "bar foobaz" end end describe "content_for plugin with mixed template engines" do before do app(:bare) do plugin :render, :layout_opts=>{:engine => 'str', :inline => '#{yield}\n#{content_for :foo}' } plugin :content_for route do |r| r.root do view(:inline => "<% content_for :foo do %>foo<% end %>bar") end r.get 'a' do view(:inline => "<% content_for :foo, 'foo' %>bar") end end end end it "should work with alternate rendering engines" do body.strip.must_equal "bar\nfoo" body('/a').strip.must_equal "bar\nfoo" end end describe "content_for plugin when overriding :engine" do before do app(:bare) do plugin :render, :engine => 'str', :layout_opts=>{:inline => '#{yield}\n#{content_for :foo}' } plugin :content_for route do |r| r.root do view(:inline => "<% content_for :foo do %>foo<% end %>bar", :engine=>:erb) end r.get 'a' do view(:inline => "<% content_for :foo, 'foo' %>bar", :engine=>:erb) end end end end it "should work with alternate rendering engines" do body.strip.must_equal "bar\nfoo" body('/a').strip.must_equal "bar\nfoo" end end end jeremyevans-roda-4f30bb3/spec/plugin/content_security_policy_spec.rb000066400000000000000000000136701516720775400261660ustar00rootroot00000000000000require_relative "../spec_helper" describe "content_security_policy plugin" do it "does not add header if no options are set" do app(:content_security_policy){'a'} header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/a").must_be_nil end it "sets Content-Security-Policy header" do app(:bare) do plugin :content_security_policy do |csp| csp.default_src :self csp.img_src :self, 'example.com' csp.style_src [:sha256, 'abc'] end route do |r| r.get 'ro' do content_security_policy.report_only '' end r.get 'nro' do content_security_policy.report_only content_security_policy.report_only(false) content_security_policy.report_only?.inspect end r.get 'get' do content_security_policy.get_default_src.inspect end r.get 'add' do content_security_policy.add_default_src('foo.com', 'bar.com') '' end r.get 'empty' do content_security_policy.add_default_src '' end r.get 'set' do content_security_policy.default_src('foo.com', 'bar.com') '' end r.get 'bool' do content_security_policy.block_all_mixed_content content_security_policy.upgrade_insecure_requests(false) content_security_policy.block_all_mixed_content?.inspect end r.get 'block' do content_security_policy do |csp| csp.block_all_mixed_content csp.add_default_src('foo.com', 'bar.com') csp.img_src :none csp.style_src csp.report_only end '' end r.get 'clear' do content_security_policy do |csp| csp.clear csp.add_default_src('foo.com', 'bar.com') end '' end 'a' end end v = "default-src 'self'; img-src 'self' example.com; style-src 'sha256-abc'; " header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/a").must_equal v header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/nro").must_equal v header(RodaResponseHeaders::CONTENT_SECURITY_POLICY_REPORT_ONLY, "/nro").must_be_nil body("/nro").must_equal 'false' header(RodaResponseHeaders::CONTENT_SECURITY_POLICY_REPORT_ONLY, "/ro").must_equal v header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/ro").must_be_nil body('/get').must_equal '[:self]' header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/add").must_equal "default-src 'self' foo.com bar.com; img-src 'self' example.com; style-src 'sha256-abc'; " header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/empty").must_equal "default-src 'self'; img-src 'self' example.com; style-src 'sha256-abc'; " header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/set").must_equal "default-src foo.com bar.com; img-src 'self' example.com; style-src 'sha256-abc'; " body('/bool').must_equal 'true' header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/bool").must_equal "default-src 'self'; img-src 'self' example.com; style-src 'sha256-abc'; block-all-mixed-content; " header(RodaResponseHeaders::CONTENT_SECURITY_POLICY_REPORT_ONLY, "/block").must_equal "default-src 'self' foo.com bar.com; img-src 'none'; block-all-mixed-content; " header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/clear").must_equal "default-src foo.com bar.com; " end it "raises error for unsupported CSP values" do app{} proc{app.plugin(:content_security_policy){|csp| csp.default_src Object.new}}.must_raise Roda::RodaError proc{app.plugin(:content_security_policy){|csp| csp.default_src []}}.must_raise Roda::RodaError proc{app.plugin(:content_security_policy){|csp| csp.default_src [:a]}}.must_raise Roda::RodaError proc{app.plugin(:content_security_policy){|csp| csp.default_src [:a, :b, :c]}}.must_raise Roda::RodaError end it "supports all documented settings" do app(:content_security_policy) do |r| content_security_policy.send(r.path[1..-1], :self) end ' base_uri child_src connect_src default_src font_src form_action frame_ancestors frame_src img_src manifest_src media_src object_src plugin_types report_to report_uri require_sri_for sandbox script_src style_src worker_src '.split.each do |setting| header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, "/#{setting}").must_equal "#{setting.tr('_', '-')} 'self'; " end end it "does not override existing heading" do app(:content_security_policy) do |r| content_security_policy.default_src :self response[RodaResponseHeaders::CONTENT_SECURITY_POLICY] = "default_src 'none';" '' end header(RodaResponseHeaders::CONTENT_SECURITY_POLICY).must_equal "default_src 'none';" end it "should not set header when using response.skip_content_security_policy!" do app(:bare) do plugin :content_security_policy do |csp| csp.default_src :self end route do |r| response.skip_content_security_policy! '' end end header(RodaResponseHeaders::CONTENT_SECURITY_POLICY).must_be_nil end it "works with error_handler" do app(:bare) do plugin(:error_handler){|_| ''} plugin :content_security_policy do |csp| csp.default_src :self csp.img_src :self, 'example.com' csp.style_src [:sha256, 'abc'] end route do |r| r.get 'a' do content_security_policy.default_src 'foo.com' raise end raise end end header(RodaResponseHeaders::CONTENT_SECURITY_POLICY).must_equal "default-src 'self'; img-src 'self' example.com; style-src 'sha256-abc'; " # Don't include updates before the error header(RodaResponseHeaders::CONTENT_SECURITY_POLICY, '/a').must_equal "default-src 'self'; img-src 'self' example.com; style-src 'sha256-abc'; " end end jeremyevans-roda-4f30bb3/spec/plugin/cookie_flags_spec.rb000066400000000000000000000151061516720775400236270ustar00rootroot00000000000000require_relative "../spec_helper" describe "cookie_flags plugin" do exception_class = Class.new(StandardError) before do app(:bare) do plugin :cookies plugin :cookie_flags route do |r| r.get String, String, String do |secure, httponly, samesite| h = {:value=>'b', :secure=>secure == 'secure', :httponly=>httponly == 'httponly'} h[:same_site] = samesite.to_sym unless samesite == 'none' response.set_cookie('a', h) body = response.headers[RodaResponseHeaders::SET_COOKIE] response.set_cookie('b', h) if r.GET['2'] body end r.get 'raise' do raise exception_class end '' end end end it "does not modify flags if they are set correctly" do _, h, b = req('/secure/httponly/strict') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) b = b.join h.must_match(/secure/i) h.must_match(/httponly/i) h.must_match(/samesite=strict/i) h.must_equal b if Rack.release >= '1.6.5' end it "does not modify flags if they are set correctly when using multiple cookies" do _, h, b = req('/secure/httponly/strict', 'QUERY_STRING'=>'2=2') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) b = b.join h.must_match(/secure/i) h.must_match(/httponly/i) h.must_match(/samesite=strict/i) end it "modifies flags if they are set correctly" do _, h, b = req('/nosecure/nohttponly/lax') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.must_match(/secure/i) h.must_match(/httponly/i) h.must_match(/samesite=strict/i) b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.wont_match(/samesite=strict/i) b.must_match(/samesite=lax/i) if Rack.release >= '1.6.5' end it "modifies flags if they are set correctly when using multiple cookies" do _, h, b = req('/nosecure/nohttponly/lax') h[RodaResponseHeaders::SET_COOKIE].each do |h| h.must_match(/secure/i) h.must_match(/httponly/i) h.must_match(/samesite=strict/i) end b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.wont_match(/samesite=strict/i) b.must_match(/samesite=lax/i) end if Rack.release >= '3' it "adds samesite entry if configured and not present" do _, h, b = req('/nosecure/nohttponly/none') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.must_match(/secure/i) h.must_match(/httponly/i) h.must_match(/samesite=strict/i) b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.wont_match(/samesite/i) end it "supports checking only secure flag" do @app.plugin :cookie_flags, :httponly=>false, :same_site=>nil _, h, b = req('/nosecure/nohttponly/none') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.must_match(/secure/i) h.wont_match(/httponly/i) h.wont_match(/samesite=strict/i) b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.wont_match(/samesite/i) end it "supports checking only httponly flag" do @app.plugin :cookie_flags, :secure=>false, :same_site=>nil _, h, b = req('/nosecure/nohttponly/none') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.wont_match(/secure/i) h.must_match(/httponly/i) h.wont_match(/samesite=strict/i) b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.wont_match(/samesite/i) end it "supports checking only samesite flag" do @app.plugin :cookie_flags, :httponly=>false, :secure=>nil _, h, b = req('/nosecure/nohttponly/none') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.wont_match(/secure/i) h.wont_match(/httponly/i) h.must_match(/samesite=strict/i) b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.wont_match(/samesite/i) end it "supports enforcing samesite=lax" do @app.plugin :cookie_flags, :httponly=>false, :secure=>nil, :same_site=>:lax _, h, b = req('/nosecure/nohttponly/none') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.wont_match(/secure/i) h.wont_match(/httponly/i) h.must_match(/samesite=lax/i) b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.wont_match(/samesite/i) end it "supports enforcing samesite=none, which also turns on secure" do @app.plugin :cookie_flags, :httponly=>false, :secure=>nil, :same_site=>:none _, h, b = req('/nosecure/nohttponly/none') h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.must_match(/secure/i) h.wont_match(/httponly/i) h.must_match(/samesite=none/i) b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.wont_match(/samesite/i) end it "supports :warn_and_modify action" do s = nil @app.plugin :cookie_flags, :action=>:warn_and_modify @app.send(:define_method, :warn){|msg| s = msg} _, h, b = req('/nosecure/nohttponly/strict') s.must_match(/Response contains cookie with unexpected flags:.*Expecting the following cookie flags: secure httponly/) h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.must_match(/secure/i) h.must_match(/httponly/i) h.must_match(/samesite=strict/i) if Rack.release >= '1.6.5' b = b.join b.wont_match(/secure/i) b.wont_match(/httponly/i) b.must_match(/samesite=strict/i) if Rack.release >= '1.6.5' end it "supports :warn action" do s = nil @app.plugin :cookie_flags, :action=>:warn @app.send(:define_method, :warn){|msg| s = msg} _, h, b = req('/secure/httponly/lax') s.must_match(/Response contains cookie with unexpected flags:.*Expecting the following cookie flags: samesite=strict/) h = h[RodaResponseHeaders::SET_COOKIE] h = h[0] if h.is_a?(Array) h.must_match(/secure/i) h.must_match(/httponly/i) h.wont_match(/samesite=strict/i) h.must_equal b.join end it "supports :error action" do @app.plugin :cookie_flags, :action=>:raise e = proc{req('/secure/httponly/none')}.must_raise(Roda::RodaPlugins::CookieFlags::Error) e.message.must_match(/Response contains cookie with unexpected flags:.*Expecting the following cookie flags: samesite=strict/) end it "should not break when exceptions are raised by app" do proc{req('/raise')}.must_raise(exception_class) end it "should handle response without cookies set" do s, h, b = req s.must_equal 200 h[RodaResponseHeaders::SET_COOKIE].must_be_nil b.must_equal [''] end end jeremyevans-roda-4f30bb3/spec/plugin/cookies_spec.rb000066400000000000000000000042171516720775400226370ustar00rootroot00000000000000require_relative "../spec_helper" describe "cookies plugin" do it "should set cookies on response" do app(:cookies) do |r| response.set_cookie("foo", "bar") response.set_cookie("bar", "baz") "Hello" end ["foo=bar\nbar=baz", %w"foo=bar bar=baz"].must_include header(RodaResponseHeaders::SET_COOKIE) body.must_equal 'Hello' end it "should delete cookies on response" do app(:cookies) do |r| response.set_cookie("foo", "bar") response.delete_cookie("foo") "Hello" end cookie = header(RodaResponseHeaders::SET_COOKIE) if Rack.release >= '2.3' cookie[0].must_match(/foo=bar/) cookie[1].must_match(/foo=; (max-age=0; )?expires=Thu, 01[ -]Jan[ -]1970 00:00:00 (-0000|GMT)/) else if cookie.is_a?(Array) cookie.length.must_equal 1 cookie = cookie[0] end cookie.must_match(/foo=; (max-age=0; )?expires=Thu, 01[ -]Jan[ -]1970 00:00:00 (-0000|GMT)/) end body.must_equal 'Hello' end it "should pass default cookie options when setting" do app.plugin :cookies, :path => '/foo' app.route { response.set_cookie("foo", "bar") } header(RodaResponseHeaders::SET_COOKIE).must_equal "foo=bar; path=/foo" app.route { response.set_cookie("foo", :value=>"bar", :path=>'/baz') } header(RodaResponseHeaders::SET_COOKIE).must_equal "foo=bar; path=/baz" end it "should pass default cookie options when deleting" do app.plugin :cookies, :domain => 'example.com' app.route { response.delete_cookie("foo") } header(RodaResponseHeaders::SET_COOKIE).must_match(/foo=; domain=example.com; (max-age=0; )?expires=Thu, 01[ -]Jan[ -]1970 00:00:00 (-0000|GMT)/) app.route { response.delete_cookie("foo", :domain=>'bar.com') } header(RodaResponseHeaders::SET_COOKIE).must_match(/foo=; domain=bar.com; (max-age=0; )?expires=Thu, 01[ -]Jan[ -]1970 00:00:00 (-0000|GMT)/) end it "should not override existing default cookie options" do app.plugin :cookies, :path => '/foo' app.plugin :cookies app.route { response.set_cookie("foo", "bar") } header(RodaResponseHeaders::SET_COOKIE).must_equal "foo=bar; path=/foo" end end jeremyevans-roda-4f30bb3/spec/plugin/csrf_spec.rb000066400000000000000000000063121516720775400221360ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'rack/csrf' rescue LoadError warn "rack_csrf not installed, skipping csrf plugin test" else begin require 'rack/csrf/version' rescue LoadError end describe "csrf plugin" do include CookieJar it "adds csrf protection and csrf helper methods" do app(:bare) do use(*DEFAULT_SESSION_MIDDLEWARE_ARGS) plugin :csrf, :skip=>['POST:/foo'] route do |r| r.get do response['tag'] = csrf_tag response['metatag'] = csrf_metatag response['token'] = csrf_token response['field'] = csrf_field response['header'] = csrf_header 'g' end r.post 'foo' do 'bar' end r.post do 'p' end end end io = rack_input status('REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 403 body('/foo', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 'bar' s, h, b = req s.must_equal 200 field = h['field'] token = Regexp.escape(h['token']) h['tag'].must_match(/\A\z/) h['metatag'].must_match(/\A\z/) b.must_equal ['g'] s, _, b = req('REQUEST_METHOD'=>'POST', 'rack.input'=>io, "HTTP_#{h['header']}"=>h['token']) s.must_equal 200 b.must_equal ['p'] app.plugin :csrf body('/foo', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 'bar' end it "can optionally skip setting up the middleware" do sub_app = Class.new(Roda) sub_app.class_eval do plugin :csrf, :skip_middleware=>true route do |r| r.get do response['tag'] = csrf_tag response['metatag'] = csrf_metatag response['token'] = csrf_token response['field'] = csrf_field response['header'] = csrf_header 'g' end r.post 'bar' do 'foobar' end r.post do 'p' end end end app(:bare) do use(*DEFAULT_SESSION_MIDDLEWARE_ARGS) plugin :csrf, :skip=>['POST:/foo/bar'] route do |r| r.on 'foo' do r.run sub_app end end end io = rack_input status('/foo', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 403 body('/foo/bar', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 'foobar' s, h, b = req('/foo') s.must_equal 200 field = h['field'] token = Regexp.escape(h['token']) h['tag'].must_match(/\A\z/) h['metatag'].must_match(/\A\z/) b.must_equal ['g'] s, _, b = req('/foo', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io, "HTTP_#{h['header']}"=>h['token']) s.must_equal 200 b.must_equal ['p'] sub_app.plugin :csrf, :skip_middleware=>true body('/foo/bar', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).must_equal 'foobar' @app = sub_app s, _, b = req('/bar', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io) s.must_equal 200 b.must_equal ['foobar'] end end unless Rack.release >= '2.3' && defined?(Rack::Csrf::VERSION) && Rack::Csrf::VERSION < '2.7' end jeremyevans-roda-4f30bb3/spec/plugin/custom_block_results_spec.rb000066400000000000000000000021071516720775400254440ustar00rootroot00000000000000require_relative "../spec_helper" describe "custom_block_results plugin" do before do app(:custom_block_results) do :sym end end it "should handle classes" do 2.times do @app.handle_block_result(Symbol) do |s| "v#{s}" end body.must_equal "vsym" end end it "should handle blocks that do not return strings" do 2.times do @app.handle_block_result(Symbol) do |s| response.status = 201 end status.must_equal 201 body.must_be_empty end end it "should handle other objects supporting ===" do @app.handle_block_result(/sy/) do |s| "x#{s}" end body.must_equal "xsym" end it "should work in subclasses" do @app = Class.new(@app) @app.handle_block_result(/sy/) do |s| "x#{s}" end body.must_equal "xsym" end it "should handle frozen applications" do @app.freeze proc do @app.handle_block_result(Symbol){|s| } end.must_raise end it "should still raise for unhandled types" do proc{body}.must_raise Roda::RodaError end end jeremyevans-roda-4f30bb3/spec/plugin/custom_matchers_spec.rb000066400000000000000000000042551516720775400244050ustar00rootroot00000000000000require_relative "../spec_helper" require 'set' describe "custom_matchers plugin" do it "supports matching for custom classes and objects" do app(:bare) do plugin :custom_matchers custom_matcher(Set){|set| set.any?{|i| match(i)}} c = Struct.new(:meth, :start) custom_matcher(c) do |s| if request_method == s.meth && remaining_path.start_with?(s.start) captures << self.GET['foo'] end end o = Object.new o2 = Object.new o.define_singleton_method(:===){|other| other == o2} custom_matcher(o){|s| match(/o3-(\d+)/)} route do |r| r.is(Set.new(['a', 'b'])){'c'} r.on(c.new('GET', '/d')){|x| "e#{x.inspect}"} r.is(o2){|x| "f#{x}"} r.is("x", Object.new){'v'} 'g' end end body.must_equal 'g' body('/a').must_equal 'c' body('/b').must_equal 'c' body('/a/').must_equal 'g' body('/d').must_equal 'enil' body('/d', 'QUERY_STRING'=>'foo=bar').must_equal 'e"bar"' body('/d', 'REQUEST_METHOD'=>'POST').must_equal 'g' body('/o3').must_equal 'g' body('/o3-').must_equal 'g' body('/o3-123').must_equal 'f123' body('/o3-123/').must_equal 'g' body('/o3-a').must_equal 'g' proc{body('/x')}.must_raise Roda::RodaError end it "works when overriding methods in subclasses" do c = Struct.new(:meth, :start) app(:bare) do plugin :custom_matchers custom_matcher(c) do |s| if request_method == s.meth && remaining_path.start_with?(s.start) captures << self.GET['foo'] end end route do |r| r.on(c.new('GET', '/d')){|x| "e#{x.inspect}"} end end body('/d', 'QUERY_STRING'=>'foo=bar').must_equal 'e"bar"' body('/d', 'QUERY_STRING'=>'bar=foo').must_equal 'enil' @app = Class.new(@app) app.custom_matcher(c) do |s| if request_method == s.meth && remaining_path.start_with?(s.start) captures << self.GET['bar'] end end @app.freeze proc{app.custom_matcher(c){|s|}}.must_raise(RuntimeError) body('/d', 'QUERY_STRING'=>'foo=bar').must_equal 'enil' body('/d', 'QUERY_STRING'=>'bar=foo').must_equal 'e"foo"' end end jeremyevans-roda-4f30bb3/spec/plugin/default_headers_spec.rb000066400000000000000000000130311516720775400243140ustar00rootroot00000000000000require_relative "../spec_helper" describe "default_headers plugin" do h = {RodaResponseHeaders::CONTENT_TYPE=>'text/json', 'foo'=>'bar'}.freeze rh_html = {RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar'}.freeze it "sets the default headers to use for the response" do app(:bare) do plugin :default_headers, h route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal h req[1].wont_be_same_as h end it "should not override existing default headers" do app(:bare) do plugin :default_headers, h plugin :default_headers route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal h end it "should handle case insensitive headers" do app(:bare) do plugin :default_headers, 'Content-Type' => 'application/json' route do |r| '' end end header(RodaResponseHeaders::CONTENT_TYPE).must_equal "application/json" end it "should allow modifying the default headers by reloading the plugin" do app(:bare) do plugin :default_headers, RodaResponseHeaders::CONTENT_TYPE => 'text/json' plugin :default_headers, 'foo' => 'bar' route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal h end it "should have a default Content-Type header" do app(:bare) do plugin :default_headers, 'foo'=>'bar' route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal rh_html end it "should work correctly in subclasses" do app(:bare) do plugin :default_headers, h route do |r| r.halt response.finish_with_body([]) end end @app = Class.new(@app) req[1].must_equal h end it "should offer default_headers method on class and response instance" do app.plugin :default_headers, h app.default_headers.must_equal h app::RodaResponse.new.default_headers.must_equal h end it "should work correctly when frozen" do app(:bare) do plugin :default_headers, h route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal h req[1].wont_be_same_as h app.freeze req[1].must_equal h req[1].wont_be_same_as h end it "supports setting non-strings as header values" do headers = {RodaResponseHeaders::CONTENT_TYPE=>'text/json', 'foo'=>:bar} app(:bare) do plugin :default_headers, h route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal h req[1].wont_be_same_as headers end unless ENV['LINT'] it "should work when freezing" do app(:bare) do plugin :default_headers, 'foo'=>'bar' route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal rh_html app.freeze req[1].must_equal rh_html end it "should work when freezing when not all headers are strings" do app(:bare) do plugin :default_headers, 'foo'=>:bar route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>:bar) app.freeze req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>:bar) end unless ENV['LINT'] it "should work when subclassing and redefining" do app(:bare) do plugin :default_headers, 'foo'=>'bar' route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal rh_html app = self.app app2 = Class.new(app) app2.plugin(:default_headers, 'foo'=>'bar2') req[1].must_equal rh_html @app = app2 req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar2') app.plugin(:default_headers, 'foo'=>'bar3') req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar2') @app = app req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar3') end [true, false].each do |freeze| [true, false].each do |after| it "should work with content_security_policy plugin if loaded #{after ? 'after' : 'before'}#{' when freezing' if freeze}" do headers = {'foo'=>'bar'} app(:bare) do plugin :default_headers, headers if after plugin :content_security_policy do |csp| csp.default_src :none end plugin :default_headers, headers unless after route do |r| r.halt response.finish_with_body([]) end end req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar', RodaResponseHeaders::CONTENT_SECURITY_POLICY=>"default-src 'none'; ") app = self.app app2 = Class.new(app) app2.plugin(:default_headers, 'foo'=>'bar2') req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar', RodaResponseHeaders::CONTENT_SECURITY_POLICY=>"default-src 'none'; ") @app.freeze if freeze req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar', RodaResponseHeaders::CONTENT_SECURITY_POLICY=>"default-src 'none'; ") @app = app2 req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar2', RodaResponseHeaders::CONTENT_SECURITY_POLICY=>"default-src 'none'; ") @app.freeze if freeze req[1].must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html', 'foo'=>'bar2', RodaResponseHeaders::CONTENT_SECURITY_POLICY=>"default-src 'none'; ") end end end end jeremyevans-roda-4f30bb3/spec/plugin/default_status_spec.rb000066400000000000000000000036721516720775400242360ustar00rootroot00000000000000require_relative "../spec_helper" describe "default_status plugin" do it "sets the default response status to use for the response" do app(:bare) do plugin :default_status do 201 end route do |r| r.halt response.finish_with_body([]) end end status.must_equal 201 end it "should exec the plugin block in the context of the instance" do app(:bare) do plugin :default_status do 200 + @body[0].length end route do |r| r.path_info end end status.must_equal 201 status('/foo/bar').must_equal 208 end it "should not override existing response" do app(:bare) do plugin :default_status do 201 end route do |r| response.status = 202 r.halt response.finish_with_body([]) end end status.must_equal 202 end it "should work correctly in subclasses" do app(:bare) do plugin :default_status do 201 end route do |r| r.halt response.finish_with_body([]) end end @app = Class.new(@app) status.must_equal 201 end it "should raise if not given a block" do proc{app(:default_status)}.must_raise Roda::RodaError end [true, false].each do |warn_arity| send(warn_arity ? :deprecated : :it, "works with blocks with invalid arity") do app(:bare) do opts[:check_arity] = :warn if warn_arity plugin :default_status do |r| 201 end route do |r| r.halt response.finish_with_body([]) end end status.must_equal 201 end end it "does not work with blocks with invalid arity if :check_arity app option is false" do app(:bare) do opts[:check_arity] = false plugin :default_status do |r| 201 end route do |r| r.halt response.finish_with_body([]) end end proc{status}.must_raise ArgumentError end end jeremyevans-roda-4f30bb3/spec/plugin/delay_build_spec.rb000066400000000000000000000011161516720775400234530ustar00rootroot00000000000000require_relative "../spec_helper" describe "delay_build plugin" do it "does not build rack app until app is called" do app(:delay_build){"a"} app.instance_variable_get(:@app).must_be_nil body.must_equal "a" # Work around minitest bug refute_equal app.instance_variable_get(:@app), nil end it "supports the build! method for backwards compatibility" do app(:delay_build){"a"} body.must_equal "a" c = Class.new do def initialize(_) end def call(_) [200, {}, ["b"]] end end app.use c app.build! body.must_equal "b" end end jeremyevans-roda-4f30bb3/spec/plugin/delegate_spec.rb000066400000000000000000000010321516720775400227450ustar00rootroot00000000000000require_relative "../spec_helper" describe "delegate plugin" do it "adds request_delegate and response_delegate class methods for delegating" do app(:bare) do plugin :delegate request_delegate :root response_delegate :headers def self.a; 'foo'; end class_delegate :a route do root do headers[RodaResponseHeaders::CONTENT_TYPE] = a end end end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'foo' status('/foo').must_equal 404 end end jeremyevans-roda-4f30bb3/spec/plugin/delete_empty_headers_spec.rb000066400000000000000000000013551516720775400253560ustar00rootroot00000000000000require_relative "../spec_helper" describe "delete_empty_headers plugin" do it "automatically deletes headers that are empty" do app(:delete_empty_headers) do |r| response['foo'] = '' response[RodaResponseHeaders::CONTENT_TYPE] = '' response[RodaResponseHeaders::CONTENT_LENGTH] = '' response['bar'] = '1' 'a' end req[1].must_equal('bar'=>'1') end it "is called when finishing with a body" do app(:delete_empty_headers) do |r| response['foo'] = '' response[RodaResponseHeaders::CONTENT_TYPE] = '' response[RodaResponseHeaders::CONTENT_LENGTH] = '' response['bar'] = '1' r.halt response.finish_with_body(['a']) end req[1].must_equal('bar'=>'1') end end jeremyevans-roda-4f30bb3/spec/plugin/direct_call_spec.rb000066400000000000000000000012041516720775400234410ustar00rootroot00000000000000require_relative "../spec_helper" describe "direct_call plugin" do it "should have .call skip middleware" do app{'123'} app.use(Class.new do def initialize(_) end def call(env) [200, {}, ['321']] end end) body.must_equal '321' app.plugin :direct_call body.must_equal '123' end deprecated "should work when #call is overridden" do app.class_eval do def call; super end route{'123'} end app.use(Class.new do def initialize(_) end def call(env) [200, {}, ['321']] end end) body.must_equal '321' app.plugin :direct_call body.must_equal '123' end end jeremyevans-roda-4f30bb3/spec/plugin/disallow_file_uploads_spec.rb000066400000000000000000000017371516720775400255530ustar00rootroot00000000000000require_relative "../spec_helper" begin require_relative '../../lib/roda/plugins/disallow_file_uploads' rescue LoadError warn "Rack #{Rack.release} used, skipping disallow_file_uploads plugin test" else describe "disallow_file_uploads plugin" do it "disallows the uploading of files" do app do |r| r.params['foo'][:tempfile].read end 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") h = { 'rack.input'=>request_body, 'CONTENT_TYPE'=>'multipart/form-data; boundary=----WebKitFormBoundarymwHIM9XjTTVHn3YP', 'CONTENT_LENGTH'=>'184', 'REQUEST_METHOD'=>'POST' } body(h.dup).must_equal "foo\n" app.plugin :disallow_file_uploads request_body.rewind proc{body(h.dup)}.must_raise Roda::RodaPlugins::DisallowFileUploads::Error end end end jeremyevans-roda-4f30bb3/spec/plugin/drop_body_spec.rb000066400000000000000000000017511516720775400231640ustar00rootroot00000000000000require_relative "../spec_helper" describe "drop_body plugin" do it "automatically drops body and Content-Type/Content-Length headers for responses without a body" do app(:drop_body) do |r| response.status = r.path[1, 1000].to_i response.write('a') end [100 + rand(100), 204, 304].each do |i| path = "/#{i.to_s}" body(path).must_equal '' header(RodaResponseHeaders::CONTENT_TYPE, path).must_be_nil header(RodaResponseHeaders::CONTENT_LENGTH, path).must_be_nil end body('/205').must_equal '' header(RodaResponseHeaders::CONTENT_TYPE, '/205').must_be_nil if Rack.release < '2.0.2' header(RodaResponseHeaders::CONTENT_LENGTH, '/205').must_be_nil else header(RodaResponseHeaders::CONTENT_LENGTH, '/205').must_equal '0' end body('/200').must_equal 'a' header(RodaResponseHeaders::CONTENT_TYPE, '/200').must_equal 'text/html' header(RodaResponseHeaders::CONTENT_LENGTH, '/200').must_equal '1' end end jeremyevans-roda-4f30bb3/spec/plugin/each_part_spec.rb000066400000000000000000000167751516720775400231450ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt' require 'tilt/string' require 'tilt/rdoc' require_relative '../../lib/roda/plugins/render' rescue LoadError warn "tilt not installed, skipping each_part plugin test" else describe "each_part plugin" do [true, false].each do |cache| it "calls render with each argument, returning joined string with all results in cache: #{cache} mode" do app(:bare) do plugin :render, :views=>'spec/views', :engine=>'str', :cache=>cache plugin :each_part o = Object.new def o.to_s; 'each' end route do |r| r.root do each_part([1,2,3], :each) end r.is 'c' do each_part([1,2,3], :each, :foo=>4) end r.is 'e' do each_part([1,2,3], o) end r.is 'g' do each_part([1,2,3], "each.foo") end r.is 'h' do body = String.new each_part([1,2,3], :each){|t| body << t << " "} body end end end 2.times do 3.times do body.must_equal "r-1-\nr-2-\nr-3-\n" body("/c").must_equal "r-1-4\nr-2-4\nr-3-4\n" body("/e").must_equal "r-1-\nr-2-\nr-3-\n" body("/g").must_equal "r-1-\nr-2-\nr-3-\n" body("/h").must_equal "r-1-\n r-2-\n r-3-\n " end app.opts[:render] = app.opts[:render].dup app.opts[:render].delete(:template_method_cache) end end it "bases local name on basename of template in cache: #{cache} mode" do app(:bare) do plugin :render, :views=>'spec', :engine=>'str', :cache=>cache plugin :each_part route do |r| r.root do each_part([1,2,3], "views/each") end end end 3.times do body.must_equal "r-1-\nr-2-\nr-3-\n" end end if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT it "calls render with each argument, handling template engines that don't support compilation in cache: #{cache} mode" do app(:bare) do plugin :render, :views=>'spec/views', :engine=>'rdoc', :cache=>cache plugin :each_part route do |r| r.root do each_part([1], :a) end end end 3.times do body.strip.must_equal "

# a # * b

" end end end end if Roda::RodaPlugins::Render::FIXED_LOCALS_COMPILED_METHOD_SUPPORT [true, false].each do |cache_plugin_option| multiplier = cache_plugin_option ? 1 : 2 it "support fixed locals in layout templates with plugin option :cache=>#{cache_plugin_option}" do template = "comp_each_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :layout_opts=>{:locals=>{:title=>"Home"}}, :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true} plugin :each_part route do each_part([1], template) end end key = [:_render_locals, "comp_each_test"] app.render_opts[:template_method_cache][key].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end it "support fixed locals in render templates with plugin option :cache=>#{cache_plugin_option}" do template = "local_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true} plugin :each_part route do each_part([1], template, title: 'ct') end end key = [:_render_locals, template] app.render_opts[:template_method_cache][key].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end [true, false].each do |assume_fixed_locals_option| [true, false].each do |freeze_app| it "caches expectedly for cache: #{cache_plugin_option}, assume_fixed_locals: #{assume_fixed_locals_option} options, with #{'un' unless freeze_app}frozen app" do template = "opt_local_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true}, :assume_fixed_locals=>assume_fixed_locals_option, :layout=>false plugin :each_part route do |r| r.is 'a' do each_part([1], template) end r.is 'b' do s = String.new each_part([1], template){|v| s << v} s end each_part([1], template, title: 'ct') end freeze if freeze_app end cache_size = 1 key = if assume_fixed_locals_option template else [:_render_locals, template] end cache = app.render_opts[:template_method_cache] cache[key].must_be_nil body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * cache_size) body('/b').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/b').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/b').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * cache_size) end end end end end end end jeremyevans-roda-4f30bb3/spec/plugin/early_hints_spec.rb000066400000000000000000000007631516720775400235260ustar00rootroot00000000000000require_relative "../spec_helper" describe "early_hints plugin" do it "allows sending early hints to rack.early_hints" do queue = [] app(:early_hints) do |r| send_early_hints('link'=>'; rel=preload; as=script') queue << 'OK' 'OK' end body.must_equal 'OK' queue.must_equal ['OK'] queue = [] body('rack.early_hints'=>proc{|h| queue << h}).must_equal 'OK' queue.must_equal [{'link'=>'; rel=preload; as=script'}, 'OK'] end end jeremyevans-roda-4f30bb3/spec/plugin/empty_root_spec.rb000066400000000000000000000004741516720775400234050ustar00rootroot00000000000000require_relative "../spec_helper" describe "empty_root plugin" do it "makes root match on emtpy path" do app(:empty_root) do |r| r.root{"root"} "notroot" end body.must_equal 'root' unless_lint do body("").must_equal 'root' end body("/a").must_equal 'notroot' end end jeremyevans-roda-4f30bb3/spec/plugin/environments_spec.rb000066400000000000000000000021411516720775400237240ustar00rootroot00000000000000require_relative "../spec_helper" describe "environments plugin" do before do app app.plugin :environments, :development end it "adds environment accessor for getting/setting the environment" do app.environment.must_equal :development app.environment = :test app.environment.must_equal :test app.plugin :environments, :production app.environment.must_equal :production end it "adds predicates for testing the environment" do app.development?.must_equal true app.test?.must_equal false app.production?.must_equal false end it "adds configure method which yields if no arguments are given or an environment matches" do a = [] app.configure{a << 1} app.configure(:development){|ap| a << ap} app.configure(:test, :production){a << 2} a.must_equal [1, app] end it "defaults environment to RACK_ENV" do with_rack_env('test') do app(:environments){} end app.test?.must_equal true app.development?.must_equal false app(:environments){} app.test?.must_equal false app.development?.must_equal true end end jeremyevans-roda-4f30bb3/spec/plugin/erb_h_spec.rb000066400000000000000000000016261516720775400222630ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'erb/escape' rescue LoadError #warn "erb/escape not installed, skipping erb_h plugin test" else describe "erb_h plugin" do it "adds h method for html escaping" do app(:erb_h) do |r| h("
") + h(:form) + h("test&<>/'") end end it "does not allocate object if escaping not needed" do app(:erb_h) do |r| s1 = 'a' s2 = h(s1) "#{s1}-#{s2}-#{s1.equal?(s2)}" end body.must_equal 'a-a-true' end it "works even if loading h plugin after" do app(:bare) do plugin :erb_h plugin :h route do |r| r.get 'a' do s1 = 'a' s2 = h(s1) "#{s1}-#{s2}-#{s1.equal?(s2)}" end h("") + h(:form) + h("test&<>/'") end end body.must_equal '<form>formtest&<>/'' body('/a').must_equal 'a-a-true' end end end jeremyevans-roda-4f30bb3/spec/plugin/error_email_spec.rb000066400000000000000000000104731516720775400235040ustar00rootroot00000000000000require_relative "../spec_helper" describe "error_email plugin" do def app(opts={}) @emails = emails = [] unless defined?(@emails) @app ||= super(:bare) do plugin :error_email, {:to=>'t', :from=>'f', :emailer=>lambda{|h| emails << h}}.merge(opts) route do |r| r.get('noerror'){error_email("Problem"); 'g'} raise ArgumentError, 'bad foo' rescue error_email($!) 'e' end end end def email @emails.last end it "adds error_email method for emailing exceptions" do app body('rack.input'=>rack_input, 'QUERY_STRING'=>'b=c', 'rack.session'=>{'d'=>'e'}).must_equal 'e' email[:to].must_equal 't' email[:from].must_equal 'f' email[:host].must_equal 'localhost' email[:message].must_match(/^Subject: ArgumentError: bad foo/) email[:message].must_match(/^Backtrace:$.+^ENV:$.+^"rack\.input" => .+^Params:$\s+^"b" => "c"$\s+^Session:$\s+^"d" => "e"$/m) end it "have error_email method support string arguments" do app body('/noerror', 'rack.input'=>rack_input, 'QUERY_STRING'=>'b=c', 'rack.session'=>{'d'=>'e'}).must_equal 'g' email[:to].must_equal 't' email[:from].must_equal 'f' email[:host].must_equal 'localhost' email[:message].must_match(/^Subject: Problem/) email[:message].must_match(/^ENV:$.+^"rack\.input" => .+^Params:$\s+^"b" => "c"$\s+^Session:$\s+^"d" => "e"$/m) email[:message].wont_include('Backtrace') end it "supports error_email_content for the content of the email" do app.route do |r| raise ArgumentError, 'bad foo' rescue error_email_content($!) end b = body('rack.input'=>rack_input, 'QUERY_STRING'=>'b=c', 'rack.session'=>{'d'=>'e'}) b.must_match(/^Subject: ArgumentError: bad foo/) b.must_match(/^Backtrace:$.+^ENV:$.+^"rack\.input" => .+^Params:$\s+^"b" => "c"$\s+^Session:$\s+^"d" => "e"$/m) end it "supports :filter plugin option for filtering parameters, environment variables, and session values" do app.route do |r| raise ArgumentError, 'bad foo' rescue error_email_content($!) end app.plugin :error_email, :filter=>proc{|k, v| k == 'b' || k == 'd' || k == 'rack.input'} b = body('rack.input'=>rack_input, 'QUERY_STRING'=>'b=c&f=g', 'rack.session'=>{'d'=>'e', 'h'=>'i'}) b.must_match(/^Subject: ArgumentError: bad foo/) b.must_match(/^Backtrace:.+^ENV:.+^"rack\.input" => FILTERED.+^Params:\s+^"b" => FILTERED\s+"f" => "g"\s+^Session:\s+^"d" => FILTERED\s+"h" => "i"/m) end it "handles invalid parameters in error_email_content" do app.route do |r| raise ArgumentError, 'bad foo' rescue error_email_content($!) end b = body('rack.input'=>rack_input, 'QUERY_STRING'=>"b=%c", 'rack.session'=>{'d'=>'e'}) b.must_match(/^Subject: ArgumentError: bad foo/) b.must_match(/^Backtrace:$.+^ENV:$.+^"rack\.input" => .+^Params:$\s+^Invalid Parameters!$\s+^Session:$\s+^"d" => "e"$/m) end it "uses :host option" do app(:host=>'foo.bar.com') body('rack.input'=>rack_input).must_equal 'e' email[:host].must_equal 'foo.bar.com' end it "handles error messages with new lines" do app.route do |r| raise "foo\nbar\nbaz" rescue error_email($!) 'e' end body('rack.input'=>rack_input).must_equal 'e' email[:message].must_match %r{From: f\r\nSubject: RuntimeError: foo\r\n bar\r\n baz\r\nTo: t\r\n\r\n} end it "adds :prefix option to subject line" do app(:prefix=>'TEST ') body('rack.input'=>rack_input).must_equal 'e' email[:message].must_match(/^Subject: TEST ArgumentError/) end it "uses :headers option for additional headers" do app(:headers=>{'Foo'=>'Bar', 'Baz'=>'Quux'}) body('rack.input'=>rack_input).must_equal 'e' email[:message].must_match(/^Foo: Bar/) email[:message].must_match(/^Baz: Quux/) end it "requires the :to and :from options" do proc{app :from=>nil}.must_raise(Roda::RodaError) proc{app :to=>nil}.must_raise(Roda::RodaError) end it "works correctly in subclasses" do @app = Class.new(app) @app.route do |r| raise ArgumentError rescue error_email($!) 'e' end body('rack.input'=>rack_input).must_equal 'e' email[:to].must_equal 't' email[:from].must_equal 'f' email[:host].must_equal 'localhost' email[:message].must_match(/^Subject: ArgumentError/) email[:message].must_match(/Backtrace.*ENV/m) end end jeremyevans-roda-4f30bb3/spec/plugin/error_handler_spec.rb000066400000000000000000000077551516720775400240430ustar00rootroot00000000000000require_relative "../spec_helper" describe "error_handler plugin" do it "executes only if error raised" do app(:bare) do plugin :error_handler error do |e| e.message end route do |r| r.on "a" do "found" end raise ArgumentError, "bad idea" end end body("/a").must_equal 'found' status("/a").must_equal 200 body.must_equal 'bad idea' status.must_equal 500 end deprecated "works if #call is overridden" do app(:bare) do plugin :error_handler def call super end error do |e| e.message end route do |r| r.on "a" do "found" end raise ArgumentError, "bad idea" end end body("/a").must_equal 'found' status("/a").must_equal 200 body.must_equal 'bad idea' status.must_equal 500 end it "executes on SyntaxError exceptions" do app(:bare) do plugin :error_handler error do |e| e.message end route do |r| r.on "a" do "found" end raise SyntaxError, 'bad idea' end end body("/a").must_equal 'found' status("/a").must_equal 200 body.must_equal 'bad idea' status.must_equal 500 end it "executes on custom exception classes" do app(:bare) do plugin :error_handler, :classes=>[StandardError] error do |e| e.message end route do |r| r.on "a" do raise 'foo' end raise SyntaxError, 'bad idea' end end proc{body}.must_raise SyntaxError body("/a").must_equal 'foo' @app = Class.new(@app) proc{body}.must_raise SyntaxError body("/a").must_equal 'foo' end it "can override status inside error block" do app(:bare) do plugin :error_handler do |e| response.status = 501 e.message end route do |r| raise ArgumentError, "bad idea" end end status.must_equal 501 end it "calculates correct Content-Length" do app(:bare) do plugin :error_handler do |e| "a" end route do |r| raise ArgumentError, "bad idea" end end header(RodaResponseHeaders::CONTENT_LENGTH).must_equal "1" end it "clears existing headers" do app(:bare) do plugin :error_handler do |e| "a" end route do |r| response[RodaResponseHeaders::CONTENT_TYPE] = 'text/pdf' response['foo'] = 'bar' raise ArgumentError, "bad idea" end end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/html' header('foo').must_be_nil end it "can set error via the plugin block" do app(:bare) do plugin :error_handler do |e| e.message end route do |r| raise ArgumentError, "bad idea" end end body.must_equal 'bad idea' end it "has default error handler also raise" do app(:bare) do plugin :error_handler route do |r| raise ArgumentError, "bad idea" end end proc{req}.must_raise(ArgumentError) end it "logs exceptions during after processing of error handler" do app(:bare) do plugin :error_handler do |e| e.message * 2 end plugin :hooks after do raise "foo" end route do |r| '' end end errors = rack_input body('rack.errors'=>errors).must_equal 'foofoo' errors.rewind errors.read.split("\n").first.must_equal "Error in after hook processing of error handler: RuntimeError: foo" end it "has access to current remaining_path" do app(:bare) do plugin :error_handler do |e| request.remaining_path end route do |r| r.on('a') do raise ArgumentError, "bad idea" end raise ArgumentError, "bad idea" end end body.must_equal '/' body('/b').must_equal '/b' body('/a').must_equal '' body('/a/c').must_equal '/c' end end jeremyevans-roda-4f30bb3/spec/plugin/error_mail_spec.rb000066400000000000000000000077171516720775400233460ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'mail' rescue LoadError warn "mail not installed, skipping mail plugin test" else Mail.defaults do delivery_method :test end describe "error_mail plugin" do def app(opts={}) @emails = [] unless defined?(@emails) @app ||= super(:bare) do plugin :error_mail, {:to=>'t', :from=>'f'}.merge(opts) route do |r| r.get('noerror'){error_mail("Problem"); 'g'} raise ArgumentError, 'bad foo' rescue error_mail($!) 'e' end end end after do Mail::TestMailer.deliveries.clear end def email Mail::TestMailer.deliveries.last end it "adds error_mail method for emailing exceptions" do app body('rack.input'=>rack_input, 'QUERY_STRING'=>'b=c', 'rack.session'=>{'d'=>'e'}).must_equal 'e' email.to.must_equal ['t'] email.from.must_equal ['f'] email.header.to_s.must_match(/^Subject: ArgumentError: bad foo/) email.body.to_s.must_match(/^Backtrace:$.+^ENV:$.+^"rack\.input" => .+^Params:$\s+^"b" => "c"$\s+^Session:$\s+^"d" => "e"$/m) end it "have error_mail method support string arguments" do app body('/noerror', 'rack.input'=>rack_input, 'QUERY_STRING'=>'b=c', 'rack.session'=>{'d'=>'e'}).must_equal 'g' email.to.must_equal ['t'] email.from.must_equal ['f'] email.header.to_s.must_match(/^Subject: Problem/) email.body.to_s.must_match(/^ENV:$.+^"rack\.input" => .+^Params:$\s+^"b" => "c"$\s+^Session:$\s+^"d" => "e"$/m) email.body.to_s.wont_include('Backtrace') end it "supports error_mail_content for the content of the email" do app.route do |r| raise ArgumentError, 'bad foo' rescue error_mail_content($!) end b = body('rack.input'=>rack_input, 'QUERY_STRING'=>'b=c', 'rack.session'=>{'d'=>'e'}) b.must_match(/^Subject: ArgumentError: bad foo/) b.must_match(/^Backtrace:.+^ENV:.+^"rack\.input" => .+^Params:\s+^"b" => "c"\s+^Session:\s+^"d" => "e"/m) end it "supports :filter plugin option for filtering parameters, environment variables, and session values" do app.route do |r| raise ArgumentError, 'bad foo' rescue error_mail_content($!) end app.plugin :error_mail, :filter=>proc{|k, v| k == 'b' || k == 'd' || k == 'rack.input'} b = body('rack.input'=>rack_input, 'QUERY_STRING'=>'b=c&f=g', 'rack.session'=>{'d'=>'e', 'h'=>'i'}) b.must_match(/^Subject: ArgumentError: bad foo/) b.must_match(/^Backtrace:.+^ENV:.+^"rack\.input" => FILTERED.+^Params:\s+^"b" => FILTERED\s+"f" => "g"\s+^Session:\s+^"d" => FILTERED\s+"h" => "i"/m) end it "handles invalid parameters in error_mail_content" do app.route do |r| raise ArgumentError, 'bad foo' rescue error_mail_content($!) end b = body('rack.input'=>rack_input, 'QUERY_STRING'=>'b=%c', 'rack.session'=>{'d'=>'e'}) b.must_match(/^Subject: ArgumentError: bad foo/) b.must_match(/^Backtrace:.+^ENV:.+^"rack\.input" => .+^Params:\s+^Invalid Parameters!\s+^Session:\s+^"d" => "e"/m) end it "adds :prefix option to subject line" do app(:prefix=>'TEST ') body('rack.input'=>rack_input).must_equal 'e' email.header.to_s.must_match(/^Subject: TEST ArgumentError/) end it "uses :headers option for additional headers" do app(:headers=>{'Foo'=>'Bar', 'Baz'=>'Quux'}) body('rack.input'=>rack_input).must_equal 'e' email.header.to_s.must_match(/^Foo: Bar/) email.header.to_s.must_match(/^Baz: Quux/) end it "requires the :to and :from options" do proc{app :from=>nil}.must_raise(Roda::RodaError) proc{app :to=>nil}.must_raise(Roda::RodaError) end it "works correctly in subclasses" do @app = Class.new(app) @app.route do |r| raise ArgumentError rescue error_mail($!) 'e' end body('rack.input'=>rack_input).must_equal 'e' email.to.must_equal ['t'] email.from.must_equal ['f'] email.header.to_s.must_match(/^Subject: ArgumentError: ArgumentError/) email.body.to_s.must_match(/^Backtrace:$.+^ENV:$.+^"rack\.input" => .+/m) end end end jeremyevans-roda-4f30bb3/spec/plugin/exception_page_spec.rb000066400000000000000000000203101516720775400241650ustar00rootroot00000000000000require_relative "../spec_helper" describe "exception_page plugin" do def ep_app(&block) app(:exception_page) do |r| raise "foo" rescue block ? instance_exec($!, &block) : exception_page($!) end end def req(path = '/', headers={}) if path.is_a?(Hash) super({'rack.input'=>rack_input}.merge(path)) else super(path, {'rack.input'=>rack_input}.merge(headers)) end end message = Exception.method_defined?(:detailed_message) ? "foo (RuntimeError)" : "foo" it "returns HTML page with exception information if text/html is accepted" do ep_app s, h, body = req('HTTP_ACCEPT'=>'text/html') s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/html' body = body.join body.must_include "RuntimeError at /" body.must_include "<h1>RuntimeError at /</h1>" body.must_include "<h2>#{message}</h2>" body.must_include __FILE__ body.must_include "No GET data" body.must_include "No POST data" body.must_include "No cookie data" body.must_include "Rack ENV" body.must_include "HTTP_ACCEPT" body.must_include "text/html" body.must_include "table td.code" body.must_include "function toggle()" body.wont_include "\"/exception_page.css\"" body.wont_include "\"/exception_page.js\"" s, h, body = req('HTTP_ACCEPT'=>'text/html', 'REQUEST_METHOD'=>'POST', 'rack.input'=>rack_input('(%bad-params%)')) s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/html' body.join.must_include "Invalid POST data" size = body.size ep_app{|e| exception_page(e, :context=>10)} body('HTTP_ACCEPT'=>'text/html').size.must_be :>, size ep_app{|e| exception_page(e, :assets=>true, :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.wont_include "table td.code" body.wont_include "function toggle()" body.wont_include "id=\"bc" body.wont_include "id=\"ac" body.must_include "\"/exception_page.css\"" body.must_include "\"/exception_page.js\"" ep_app{|e| exception_page(e, :assets=>"/static", :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.wont_include "table td.code" body.wont_include "function toggle()" body.must_include "\"/static/exception_page.css\"" body.must_include "\"/static/exception_page.js\"" ep_app{|e| exception_page(e, :css_file=>"/foo.css", :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.wont_include "table td.code" body.must_include "function toggle()" body.must_include "\"/foo.css\"" ep_app{|e| exception_page(e, :js_file=>"/foo.js", :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.must_include "table td.code" body.wont_include "function toggle()" body.must_include "\"/foo.js\"" ep_app{|e| exception_page(e, :assets=>false, :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.wont_include "table td.code" body.wont_include "function toggle()" body.wont_include "\"/exception_page.css\"" body.wont_include "\"/exception_page.js\"" ep_app{|e| exception_page(e, :assets=>false, :css_file=>"/foo.css", :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.wont_include "table td.code" body.wont_include "function toggle()" body.must_include "\"/foo.css\"" ep_app{|e| exception_page(e, :assets=>false, :js_file=>"/foo.js", :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.wont_include "table td.code" body.wont_include "function toggle()" body.must_include "\"/foo.js\"" ep_app{|e| exception_page(e, :css_file=>false, :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.wont_include "table td.code" body.must_include "function toggle()" body.wont_include "\"/exception_page.css\"" body.wont_include "\"/exception_page.js\"" ep_app{|e| exception_page(e, :js_file=>false, :context=>0)} body = body('HTTP_ACCEPT'=>'text/html') body.must_include "table td.code" body.wont_include "function toggle()" body.wont_include "\"/exception_page.css\"" body.wont_include "\"/exception_page.js\"" end it "returns plain text page with exception information if text/html is not accepted" do ep_app s, h, body = req s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/plain' body = body.join first, *bt = body.split("\n") first.must_equal "RuntimeError: #{message}" bt.first.must_include __FILE__ end it "handles exceptions without a backtrace" do app(:exception_page) do |r| e = RuntimeError.new("foo") e.set_backtrace([]) raise e rescue exception_page($!) end s, h, body = req('HTTP_ACCEPT'=>'text/html') s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/html' body = body.join body.must_include "<title>RuntimeError at /" body.must_include "<h1>RuntimeError at /</h1>" body.must_include "<h2>#{message}</h2>" body.must_include "unknown location" body.must_include "No GET data" body.must_include "No POST data" body.must_include "No cookie data" body.must_include "Rack ENV" body.must_include "HTTP_ACCEPT" body.must_include "text/html" body.must_include "table td.code" body.must_include "function toggle()" body.wont_include "\"/exception_page.css\"" body.wont_include "\"/exception_page.js\"" end it "handles exceptions with invalid line numbers in a backtrace" do app(:exception_page) do |r| e = RuntimeError.new("foo") e.set_backtrace(["#{__FILE__}:10000:in `foo'"]) raise e rescue exception_page($!) end s, h, body = req('HTTP_ACCEPT'=>'text/html') s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/html' body = body.join body.must_include "<title>RuntimeError at /" body.must_include "<h1>RuntimeError at /</h1>" body.must_include "<h2>#{message}</h2>" body.must_include "No GET data" body.must_include "No POST data" body.must_include "No cookie data" body.must_include "Rack ENV" body.must_include "HTTP_ACCEPT" body.must_include "text/html" body.must_include "table td.code" body.must_include "function toggle()" body.wont_include 'class="context"' body.wont_include "\"/exception_page.css\"" body.wont_include "\"/exception_page.js\"" end it "returns JSON with exception information if :json information is used" do ep_app{|e| exception_page(e, :json=>true)} @app.plugin :json s, h, body = req s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'application/json' hash = JSON.parse(body.join) bt = hash["exception"].delete("backtrace") hash.must_equal("exception"=>{"class"=>"RuntimeError", "message"=>message}) bt.must_be_kind_of Array bt.each{|line| line.must_be_kind_of String} end it "should handle backtrace lines in unexpected forms" do ep_app do |e| e.backtrace.first.upcase! e.backtrace[0] = '' exception_page(e) end body = body('HTTP_ACCEPT'=>'text/html') body.must_include "<h1>RuntimeError at /</h1>" body.must_include "<h2>#{message}</h2>" body.must_include __FILE__ body.wont_include 'id="c0"' body.must_include 'id="c1"' end it "should still show line numbers if the line content cannot be displayed" do app(:exception_page) do |r| instance_eval('raise "foo"', 'foo-bar.rb', 4200+42) rescue exception_page($!) end body = body('HTTP_ACCEPT'=>'text/html') body.must_include(RUBY_VERSION >= '3.4' ? "foo (RuntimeError)" : "RuntimeError: foo") body.must_include "foo-bar.rb:#{4200+42}" body.must_include __FILE__ body.wont_include 'id="c0"' # On JRuby, instance_eval uses 2-3 frames depending on version body.must_match(/id="c[123]"/) end it "should serve exception page assets" do app(:exception_page) do |r| r.exception_page_assets end s, h, b = req('/exception_page.css') s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/css' b.join.must_equal Roda::RodaPlugins::ExceptionPage.css s, h, b = req('/exception_page.js') s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'application/javascript' b.join.must_equal Roda::RodaPlugins::ExceptionPage.js end end ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/filter_common_logger_spec.rb�����������������������������������0000664�0000000�0000000�00000002341�15167207754�0025373�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "filter_common_logger plugin" do it 'allows skipping logging of certain requests' do logger = rack_input app(:bare) do plugin :common_logger, logger plugin :filter_common_logger do |result| return false if result[0] >= 300 && result[0] < 400 return false if request.path_info.start_with?('/foo/') true end route do |r| r.on 'foo' do 'aa' end r.on 'redir' do r.redirect '/' end r.on 'err' do raise 'foo' end 'bbb' end end body("HTTP_VERSION"=>'HTTP/1.0').must_equal 'bbb' logger.rewind 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/) logger.rewind logger.truncate(0) body('/foo/bar').must_equal 'aa' logger.rewind logger.read.must_be_empty logger.rewind logger.truncate(0) body('/redir').must_equal '' logger.rewind logger.read.must_be_empty logger.rewind logger.truncate(0) proc do body('/err') end.must_raise(RuntimeError) logger.rewind logger.read.must_be_empty end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/flash_spec.rb��������������������������������������������������0000664�0000000�0000000�00000005523�15167207754�0022301�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "flash plugin" do include CookieJar [lambda{send(*DEFAULT_SESSION_ARGS); plugin :flash}, lambda{plugin :flash; send(*DEFAULT_SESSION_ARGS)}].each do |config| it "flash.now[] sets flash for current page" do app(:bare) do instance_exec(&config) route do |r| r.on do flash.now['a'] = 'b' flash['a'] end end end body.must_equal 'b' end it "flash[] sets flash for next page" do app(:bare) do instance_exec(&config) route do |r| r.get('a'){"c#{flash['a']}"} r.get('f'){flash; session['_flash'].inspect} flash['a'] = "b#{flash['a']}" flash['a'] || '' end end body.must_equal '' body.must_equal 'b' body.must_equal 'bb' body('/a').must_equal 'cbbb' body.must_equal '' body.must_equal 'b' body.must_equal 'bb' body('/f').must_match(/\A\{"a" ?=> ?"bbb"\}\z/) body('/f').must_equal 'nil' end it "works correctly if flash not accessed" do app(:bare) do instance_exec(&config) route{'a'} end body.must_equal 'a' end end end describe "FlashHash" do before do require 'roda/plugins/flash' @h = Roda::RodaPlugins::Flash::FlashHash.new end it ".new should accept nil for empty hash" do @h = Roda::RodaPlugins::Flash::FlashHash.new(nil) @h.now.must_equal({}) @h.next.must_equal({}) end it ".new should accept a hash" do @h = Roda::RodaPlugins::Flash::FlashHash.new(1=>2) @h.now.must_equal(1=>2) @h.next.must_equal({}) end it "#[]= assigns to next flash" do @h[1] = 2 @h.now.must_equal({}) @h.next.must_equal(1=>2) end it "#discard removes given key from next hash" do @h[1] = 2 @h[nil] = 3 @h.next.must_equal(1=>2, nil=>3) @h.discard(nil) @h.next.must_equal(1=>2) @h.discard(1) @h.next.must_equal({}) end it "#discard removes all entries from next hash with no arguments" do @h[1] = 2 @h[nil] = 3 @h.next.must_equal(1=>2, nil=>3) @h.discard @h.next.must_equal({}) end it "#keep copies entry for key from current hash to next hash" do @h.now[1] = 2 @h.now[nil] = 3 @h.next.must_equal({}) @h.keep(nil) @h.next.must_equal(nil=>3) @h.keep(1) @h.next.must_equal(1=>2, nil=>3) end it "#keep copies all entries from current hash to next hash" do @h.now[1] = 2 @h.now[nil] = 3 @h.next.must_equal({}) @h.keep @h.next.must_equal(1=>2, nil=>3) end it "#sweep replaces current hash with next hash" do @h[1] = 2 @h[nil] = 3 @h.next.must_equal(1=>2, nil=>3) @h.now.must_equal({}) @h.sweep.must_equal(1=>2, nil=>3) @h.next.must_equal({}) @h.now.must_equal(1=>2, nil=>3) end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/h_spec.rb������������������������������������������������������0000664�0000000�0000000�00000000367�15167207754�0021434�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "h plugin" do it "adds h method for html escaping" do app(:h) do |r| h("<form>") + h(:form) + h("test&<>/'") end body.must_equal '<form>formtest&<>/'' end end �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/halt_spec.rb���������������������������������������������������0000664�0000000�0000000�00000005124�15167207754�0022131�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "halt plugin" do it "should still have halt return current response if no arguments given" do app(:halt) do |r| response.write 'foo' r.halt end body.must_equal "foo" end it "should still have halt return rack response as argument given it as argument" do app(:halt) do |r| r.halt [200, {}, ['foo']] end body.must_equal "foo" end it "should consider string argument as response body" do app(:halt) do |r| r.halt "foo" end body.must_equal "foo" end it "should consider integer argument as response status" do app(:halt) do |r| r.halt 300 end status.must_equal 300 end it "should consider other single arguments similar to block bodies" do app(:bare) do plugin :halt plugin :json route do |r| r.halt({'a'=>1}) end end body.must_equal '{"a":1}' end it "should consider 2 arguments as response status and body" do app(:halt) do |r| r.halt 300, "foo" end status.must_equal 300 body.must_equal "foo" end it "should handle 2nd of 2 arguments similar to block bodies" do app(:bare) do plugin :halt plugin :json route do |r| r.halt(300, {'a'=>1}) end end status.must_equal 300 body.must_equal '{"a":1}' end it "should consider 3 arguments as response" do app(:halt) do |r| r.halt 300, {'a'=>'b'}, "foo" end status.must_equal 300 header('a').must_equal 'b' body.must_equal "foo" end it "should consider an array as a rack response" do app(:halt) do |r| r.halt [300, {'a'=>'b'}, ["foo"]] end status.must_equal 300 header('a').must_equal 'b' body.must_equal "foo" end it "should handle 3rd of 3 arguments similar to block bodies" do app(:bare) do plugin :halt plugin :json route do |r| r.halt(300, {'a'=>'b'}, {'a'=>1}) end end status.must_equal 300 header('a').must_equal 'b' body.must_equal '{"a":1}' end it "should raise an error for too many arguments" do app(:halt) do |r| r.halt 300, {'a'=>'b'}, "foo", 1 end proc{req}.must_raise(Roda::RodaError) end it "should raise an error for single argument not integer, String, or Array" do app(:halt) do |r| r.halt nil end proc{req}.must_raise(Roda::RodaError) end it "should raise an error for single argument not integer, String, or Array" do app(:halt) do |r| r.halt('a'=>'b') end proc{req}.must_raise(Roda::RodaError) end end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hash_branch_view_subdir_spec.rb��������������������������������0000664�0000000�0000000�00000005645�15167207754�0026053�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping hash_branch_view_subdir test" else describe "hash_branch_view_subdir plugin" do before do app(:bare) do plugin :render, :views=>'spec/views', :layout=>'./layout-yield' plugin :hash_branch_view_subdir hash_branch 'about' do |_| view 'comp_test' end route do |r| r.hash_branches view 'comp_test' end end end it "supports appending view subdirectories for each successful hash branch" do body('/about').gsub(/\s+/, '').must_equal "Headerabout-ctFooter" body('/foo').gsub(/\s+/, '').must_equal "HeaderctFooter" end it "supports removing already configured branches" do body('/about').gsub(/\s+/, '').must_equal "Headerabout-ctFooter" body('/foo').gsub(/\s+/, '').must_equal "HeaderctFooter" 2.times do app.hash_branch 'about' body('/about').gsub(/\s+/, '').must_equal "HeaderctFooter" end end it "supports use in subclasses" do sub_app = Class.new(app) sub_app.hash_branch('about') do 'a' end body('/about').gsub(/\s+/, '').must_equal "Headerabout-ctFooter" @app = sub_app body('/about').must_equal 'a' sub_app.hash_branch('about') do view 'comp_test' end body('/about').gsub(/\s+/, '').must_equal "Headerabout-ctFooter" end it "doesn't allow modifications after freezing" do app.freeze body('/about').gsub(/\s+/, '').must_equal "Headerabout-ctFooter" body('/foo').gsub(/\s+/, '').must_equal "HeaderctFooter" proc{app.hash_branch('about')}.must_raise end end describe "hash_branch_view_subdir plugin" do it "supports appending view subdirectories for each successful hash branch" do app(:bare) do plugin :render, :views=>'.', :layout=>'spec/views/layout-yield' plugin :hash_branch_view_subdir hash_branch 'spec' do |r| r.hash_branches('spec') end hash_branch 'spec', 'views' do |r| r.hash_branches('spec_views') view 'comp_test' end hash_branch 'spec_views', 'about' do |_| view 'comp_test' end route do |r| r.hash_branches end end body('/spec/views/about').gsub(/\s+/, '').must_equal "Headerabout-ctFooter" body('/spec/views/foo').gsub(/\s+/, '').must_equal "HeaderctFooter" end it "works with route_block_args plugin" do app(:bare) do plugin :render, :views=>'spec/views', :layout=>'./layout-yield' plugin :hash_branch_view_subdir plugin :route_block_args do [request, response] end hash_branch 'about' do |_, res| res.write(view 'comp_test') end route do |r, _| r.hash_branches view 'comp_test' end end body('/about').gsub(/\s+/, '').must_equal "Headerabout-ctFooter" body('/foo').gsub(/\s+/, '').must_equal "HeaderctFooter" end end end �������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hash_branches_spec.rb������������������������������������������0000664�0000000�0000000�00000005601�15167207754�0023771�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "hash_branches plugin" do before do app(:bare) do plugin :hash_branches hash_branch("a") do |r| r.is "" do "a0" end r.is "a" do "a1" end r.hash_branches "a2" end hash_branch("/a", "b") do |r| r.is "" do "ab0" end r.is "a" do "ab1" end r.hash_branches "ab2" end hash_branch("", "b") do |r| r.is "" do "b0" end r.is "a" do "b1" end "b2" end hash_branch(:p, "x") do |r| r.is do 'px' end 'pnx' end route do |r| r.hash_branches r.on 'p' do r.hash_branches(:p) r.hash_branches("") "p" end "n" end end end it "adds support for routing via r.hash_branches" do body.must_equal 'n' body('/a').must_equal 'a2' body('/a/').must_equal 'a0' body('/a/a').must_equal 'a1' body('/a/b').must_equal 'ab2' body('/a/b/').must_equal 'ab0' body('/a/b/a').must_equal 'ab1' body('/b').must_equal 'b2' body('/b/').must_equal 'b0' body('/b/a').must_equal 'b1' body('/p').must_equal 'p' body('/p/x').must_equal 'px' body('/p/x/1').must_equal 'pnx' body('/p/a').must_equal 'a2' body('/p/a/').must_equal 'a0' body('/p/a/a').must_equal 'a1' body('/p/a/b').must_equal 'a2' body('/p/a/b/').must_equal 'a2' body('/p/a/b/a').must_equal 'a2' body('/p/b').must_equal 'b2' body('/p/b/').must_equal 'b0' body('/p/b/a').must_equal 'b1' body('/p/p').must_equal 'p' body('/p/p/x').must_equal 'p' body('/p/p/x/1').must_equal 'p' end it "works when freezing the app" do app.freeze body.must_equal 'n' body('/a').must_equal 'a2' body('/a/').must_equal 'a0' proc{app.hash_branch("foo"){}}.must_raise end it "allows removing a hash branch" do 2.times do app.hash_branch('a') body.must_equal 'n' body('/a').must_equal 'n' body('/a/').must_equal 'n' body('/p/x').must_equal 'px' end end it "works when subclassing the app" do old_app = app @app = Class.new(app) @app.route(&old_app.route_block) body.must_equal 'n' body('/a').must_equal 'a2' body('/a/').must_equal 'a0' body('/p/x').must_equal 'px' end it "handles loading the plugin multiple times correctly" do app.plugin :hash_routes body.must_equal 'n' body('/a').must_equal 'a2' body('/a/').must_equal 'a0' body('/p/x').must_equal 'px' end it "r.hash_branch handles loading the same route more than once" do app.hash_branch(:p, "x") do |r| 'px' end body('/p').must_equal 'p' body('/p/x').must_equal 'px' end end �������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hash_matcher_spec.rb�������������������������������������������0000664�0000000�0000000�00000001264�15167207754�0023630�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "hash_matcher plugin" do it "should enable the handling of arbitrary hash keys" do app(:bare) do plugin :hash_matcher hash_matcher(:foos){|v| consume(self.class.cached_matcher(:"foos-#{v}"){/((?:foo){#{v}})/})} route do |r| r.is :foos=>1 do |f| "1#{f}" end r.is :foos=>2 do |f| "2#{f}" end r.is :foos=>3 do |f| "3#{f}" end end end body("/foo").must_equal '1foo' body("/foofoo").must_equal '2foofoo' body("/foofoofoo").must_equal '3foofoofoo' status("/foofoofoofoo").must_equal 404 status.must_equal 404 end end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hash_paths_spec.rb���������������������������������������������0000664�0000000�0000000�00000004257�15167207754�0023331�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "hash_paths plugin" do before do app(:bare) do plugin :hash_paths hash_path("/a") do |r| r.get{"a"} r.post{"ap"} end hash_path("", "/b") do |_| "b" end hash_path("/c", "/b") do |_| "cb" end hash_path(:p, "/x") do |_| 'px' end route do |r| r.hash_paths r.on 'p' do r.hash_paths(:p) r.hash_paths("") "p" end r.on "c" do r.hash_paths "c" end "n" end end end it "adds support for routing via r.hash_paths" do body.must_equal 'n' body('/a').must_equal 'a' body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'ap' body('/a/').must_equal 'n' body('/b').must_equal 'b' body('/b/').must_equal 'n' body('/c').must_equal 'c' body('/c/').must_equal 'c' body('/c/b').must_equal 'cb' body('/c/b/').must_equal 'c' body('/p').must_equal 'p' body('/p/x').must_equal 'px' body('/p/x/1').must_equal 'p' end it "works when freezing the app" do app.freeze body.must_equal 'n' body('/a').must_equal 'a' body('/a/').must_equal 'n' body('/p/x').must_equal 'px' proc{app.hash_path("foo"){}}.must_raise end it "works when subclassing the app" do old_app = app @app = Class.new(app) @app.route(&old_app.route_block) body.must_equal 'n' body('/a').must_equal 'a' body('/a/').must_equal 'n' body('/p/x').must_equal 'px' end it "allows removing a hash path" do 2.times do app.hash_path('/a') body.must_equal 'n' body('/a').must_equal 'n' body('/a/').must_equal 'n' body('/p/x').must_equal 'px' end end it "handles loading the plugin multiple times correctly" do app.plugin :hash_routes body.must_equal 'n' body('/a').must_equal 'a' body('/a/').must_equal 'n' body('/p/x').must_equal 'px' end it "r.hash_path handles loading the same route more than once" do app.hash_path(:p, "x") do |r| 'px' end body('/p').must_equal 'p' body('/p/x').must_equal 'px' end end �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hash_public_spec.rb��������������������������������������������0000664�0000000�0000000�00000010026�15167207754�0023457�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative '../spec_helper' describe 'hash_public plugin' do it 'adds r.hash_public for serving static files with content-hash-based paths' do app(:bare) do plugin :hash_public, root: 'spec/views' route do |r| r.hash_public end end status("/about/_test.erb\0").must_equal 404 status('/about/_test.erb').must_equal 404 status("/static/a/about/_test.erb\0").must_equal 404 body('/static/a/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it 'supports the :prefix option' do app(:bare) do plugin :hash_public, root: 'spec/views', prefix: 'foo' route do |r| r.hash_public end end body('/foo/a/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it 'returns the correct hash_path with content hash' do app(:bare) do plugin :hash_public, root: 'spec/plugin' route do |r| r.hash_public hash_path('../views/about/_test.erb') end end body.must_equal "/static/q12-OV-wZhMcEiJj7V60pITfJeGWgmIvBpSjMjef4UY/../views/about/_test.erb" status('/static/a/../views/about/_test.erb').must_equal 404 end it 'returns the correct hash_path with content hash when app is frozen' do app(:bare) do plugin :hash_public, root: 'spec/plugin' route do |r| r.hash_public hash_path('../views/about/_test.erb') end freeze end body.must_equal "/static/q12-OV-wZhMcEiJj7V60pITfJeGWgmIvBpSjMjef4UY/../views/about/_test.erb" status('/static/a/../views/about/_test.erb').must_equal 404 end it "respects the application's :root option" do app(:bare) do opts[:root] = File.expand_path('..', File.dirname(__FILE__)) plugin :hash_public, root: 'views' route do |r| r.hash_public end end body('/static/a/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it 'handles serving gzip files in gzip mode if client supports gzip' do app(:bare) do plugin :hash_public, root: 'spec/views', gzip: true route do |r| r.hash_public end end body('/static/a/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_ENCODING, '/about/_test.erb').must_be_nil body('/static/a/about.erb').must_equal File.read('spec/views/about.erb') header(RodaResponseHeaders::CONTENT_ENCODING, '/about.erb').must_be_nil body('/static/a/about/_test.erb', 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip').must_equal File.binread('spec/views/about/_test.erb.gz') h = req('/static/a/about/_test.erb', 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip')[1] h[RodaResponseHeaders::CONTENT_ENCODING].must_equal 'gzip' h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/plain' body('/static/a/about/_test.css', 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip').must_equal File.binread('spec/views/about/_test.css.gz') h = req('/static/a/about/_test.css', 'HTTP_ACCEPT_ENCODING' => 'deflate, gzip')[1] h[RodaResponseHeaders::CONTENT_ENCODING].must_equal 'gzip' h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/css' end it 'returns 404 for non-GET requests' do app(:bare) do plugin :hash_public, root: 'spec/views', prefix: 'foo' route do |r| r.hash_public end end status('/foo/a/about/_test.erb', 'REQUEST_METHOD' => 'POST').must_equal 404 end it 'supports the :length option to truncate the hash' do app(:bare) do plugin :hash_public, root: 'spec/plugin', length: 16 route do |r| r.hash_public hash_path('../views/about/_test.erb') end end body.must_equal "/static/q12-OV-wZhMcEiJj/../views/about/_test.erb" end it 'caches hash values for the same file' do app(:bare) do plugin :hash_public, root: 'spec/plugin' route do |r| r.hash_public hash_path('../views/about/_test.erb') end end result1 = body result2 = body result1.must_equal result2 end end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hash_routes_spec.rb��������������������������������������������0000664�0000000�0000000�00000015011�15167207754�0023521�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "hash_routes plugin - hash_routes DSL" do before do app(:bare) do plugin :hash_routes hash_routes "" do on "a" do |r| r.is "" do "a0" end r.is "a" do "a1" end r.hash_branches "a2" end on "b" do |r| r.is "" do "b0" end r.is "a" do "b1" end "b2" end is "c" do |r| "c#{r.request_method}" end get 'd' do 'dg' end post 'e' do 'ep' end end hash_routes "/a" do |hr| hr.dispatch_from(:p, "r") hr.dispatch_from(:p, "s") do |r| r.is "a" do "psa1" end end hr.on "b" do |r| r.is "" do "ab0" end r.is "a" do "ab1" end r.hash_branches "ab2" end end phr = hash_routes(:p) phr.is true do "pi" end phr.on "x" do |r| r.is do 'px' end 'pnx' end route do |r| r.hash_routes r.on 'p' do r.hash_routes(:p) r.hash_routes("") "p" end "n" end end end it "adds support for routing via r.hash_routes" do body.must_equal 'n' body('/a').must_equal 'a2' body('/a/').must_equal 'a0' body('/a/a').must_equal 'a1' body('/a/b').must_equal 'ab2' body('/a/b/').must_equal 'ab0' body('/a/b/a').must_equal 'ab1' body('/b').must_equal 'b2' body('/b/').must_equal 'b0' body('/b/a').must_equal 'b1' body('/c').must_equal 'cGET' body('/c', 'REQUEST_METHOD'=>'POST').must_equal 'cPOST' body('/c/').must_equal 'n' body('/d').must_equal 'dg' body('/d', 'REQUEST_METHOD'=>'POST').must_equal '' body('/d/').must_equal 'n' body('/e').must_equal '' body('/e', 'REQUEST_METHOD'=>'POST').must_equal 'ep' body('/e/').must_equal 'n' body('/p').must_equal 'pi' body('/p/x').must_equal 'px' body('/p/x/1').must_equal 'pnx' body('/p/a').must_equal 'a2' body('/p/a/').must_equal 'a0' body('/p/a/a').must_equal 'a1' body('/p/a/b').must_equal 'a2' body('/p/a/b/').must_equal 'a2' body('/p/a/b/a').must_equal 'a2' body('/p/b').must_equal 'b2' body('/p/b/').must_equal 'b0' body('/p/b/a').must_equal 'b1' body('/p/c').must_equal 'cGET' body('/p/c', 'REQUEST_METHOD'=>'POST').must_equal 'cPOST' body('/p/c/').must_equal 'p' body('/p/d').must_equal 'dg' body('/p/d', 'REQUEST_METHOD'=>'POST').must_equal '' body('/p/d/').must_equal 'p' body('/p/e').must_equal '' body('/p/e', 'REQUEST_METHOD'=>'POST').must_equal 'ep' body('/p/e/').must_equal 'p' body('/p/p').must_equal 'p' body('/p/p/x').must_equal 'p' body('/p/p/x/1').must_equal 'p' body('/p/r/b').must_equal 'ab2' body('/p/r/b/').must_equal 'ab0' body('/p/r/b/a').must_equal 'ab1' body('/p/s/a').must_equal 'psa1' body('/p/s/b').must_equal 'ab2' body('/p/s/b/').must_equal 'ab0' body('/p/s/b/a').must_equal 'ab1' end it "works when freezing the app" do app.freeze body.must_equal 'n' body('/a').must_equal 'a2' body('/a/').must_equal 'a0' proc{app.hash_branch("foo"){}}.must_raise end it "works when subclassing the app" do old_app = app @app = Class.new(app) @app.route(&old_app.route_block) body.must_equal 'n' body('/a').must_equal 'a2' body('/a/').must_equal 'a0' body('/p/x').must_equal 'px' end it "handles loading the plugin multiple times correctly" do app.plugin :hash_routes body.must_equal 'n' body('/a').must_equal 'a2' body('/a/').must_equal 'a0' body('/p/x').must_equal 'px' end it "r.hash_routes with verb handles loading the same route more than once" do app.hash_routes "" do get 'd' do 'dg' end end body('/d').must_equal 'dg' body('/d', 'REQUEST_METHOD'=>'POST').must_equal '' body('/d/').must_equal 'n' end it "r.hash_routes with verb handles true" do app.hash_routes "" do get true do 'dg' end end unless_lint do body('').must_equal 'dg' body('', 'REQUEST_METHOD'=>'POST').must_equal '' end body('/').must_equal 'n' end end describe "hash_routes plugin" do it "should work with route_block_args" do app(:bare) do plugin :hash_routes plugin :route_block_args do [request, response] end hash_branch 'a' do |r, res| r.hash_paths res.write('a') end hash_path '/a', '/b' do |r, res| res.write('b') end route do |r| r.hash_branches 'n' end end body.must_equal 'n' body('/a').must_equal 'a' body('/a/').must_equal 'a' body('/a/b').must_equal 'b' body('/a/b/').must_equal 'a' end it "should have r.hash_routes dispatch to both hash_paths and hash_branches" do app(:bare) do plugin :hash_routes plugin :route_block_args do [request, response] end hash_branch 'a' do |r| r.root do 'ar' end 'ab' end hash_path '/a' do |_| 'ap' end hash_branch 'b' do |_| 'b' end hash_path '/c' do |_| 'c' end route do |r| r.hash_routes 'n' end end body.must_equal 'n' body('/a').must_equal 'ap' body('/a/').must_equal 'ar' body('/a/b').must_equal 'ab' body('/a').must_equal 'ap' body('/b').must_equal 'b' body('/b/').must_equal 'b' body('/c').must_equal 'c' body('/c/').must_equal 'n' end end begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping hash_routes plugin views test" else describe "hash routes plugin" do it "supports easy rendering of multiple views by name" do app(:bare) do plugin :render, :views=>'spec/views', :layout=>'layout-yield' plugin :hash_routes hash_routes '/d' do view true, 'a' view '', 'b' views %w'c' end route do |r| r.on 'd' do r.hash_routes end end end body('/d').gsub(/\s+/, '').must_equal "HeaderaFooter" body('/d/').gsub(/\s+/, '').must_equal "HeaderbFooter" body('/d/c').gsub(/\s+/, '').must_equal "HeadercFooter" end end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/head_spec.rb���������������������������������������������������0000664�0000000�0000000�00000002303�15167207754�0022076�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "head plugin" do it "considers HEAD requests as GET requests which return no body" do app(:head) do |r| r.root do 'root' end r.get 'a' do 'a' end r.is 'b', :method=>[:get, :post] do 'b' end end s, h, b = req s.must_equal 200 h[RodaResponseHeaders::CONTENT_LENGTH].must_equal '4' b.must_equal ['root'] s, h, b = req('REQUEST_METHOD' => 'HEAD') s.must_equal 200 h[RodaResponseHeaders::CONTENT_LENGTH].must_equal '4' b.must_equal [] body('/a').must_equal 'a' status('/a', 'REQUEST_METHOD' => 'HEAD').must_equal 200 body('/b').must_equal 'b' status('/b', 'REQUEST_METHOD' => 'HEAD').must_equal 200 end it "releases resources via body.close" do body = rack_input('') app(:head) do |r| r.root do r.halt [ 200, {}, body ] end end s, _, b = req('REQUEST_METHOD' => 'HEAD') s.must_equal 200 res = String.new unless_lint do body.closed?.must_equal false end b.each { |buf| res << buf } unless_lint do b.close end body.closed?.must_equal true res.must_equal '' end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/header_matchers_spec.rb����������������������������������������0000664�0000000�0000000�00000005355�15167207754�0024325�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "accept matcher" do it "should accept mimetypes and set response Content-Type" do app(:header_matchers) do |r| r.on :accept=>"application/xml" do response[RodaResponseHeaders::CONTENT_TYPE] end end body("HTTP_ACCEPT" => "application/xml").must_equal "application/xml" status.must_equal 404 end end describe "header matcher" do it "should match if header present" do app(:header_matchers) do |r| r.on :header=>"accept" do "bar" end end body("HTTP_ACCEPT" => "application/xml").must_equal "bar" status("HTTP_HTTP_ACCEPT" => "application/xml").must_equal 404 status.must_equal 404 end it "should yield the header value" do app(:header_matchers) do |r| r.on :header=>"accept" do |v| "bar-#{v}" end end app.opts[:header_matcher_prefix] = true body("HTTP_ACCEPT" => "application/xml").must_equal "bar-application/xml" status.must_equal 404 end it "should match content-type and content-length headers" do app(:header_matchers) do |r| r.on :header=>"content-type" do |x| r.on :header=>"content-length" do |y| "bar-#{x}-#{y}" end end end body("CONTENT_TYPE" => "application/xml", "CONTENT_LENGTH" => "1234").must_equal "bar-application/xml-1234" status.must_equal 404 end end describe "host matcher" do it "should match a host" do app(:header_matchers) do |r| r.on :host=>"example.com" do "worked" end end body("HTTP_HOST" => "example.com").must_equal 'worked' status("HTTP_HOST" => "foo.com").must_equal 404 end it "should match a host with a regexp" do app(:header_matchers) do |r| r.on :host=>/example/ do "worked" end end body("HTTP_HOST" => "example.com").must_equal 'worked' status("HTTP_HOST" => "foo.com").must_equal 404 end it "doesn't yield host if given a string" do app(:header_matchers) do |r| r.on :host=>"example.com" do |*args| args.size.to_s end end body("HTTP_HOST" => "example.com").must_equal '0' end it "yields host if passed a regexp and opts[:host_matcher_captures] is set" do app(:header_matchers) do |r| r.on :host=>/\A(.*)\.example\.com\z/ do |*m| m.empty? ? '0' : m[0] end end body("HTTP_HOST" => "foo.example.com").must_equal 'foo' end end describe "user_agent matcher" do it "should accept pattern and match against user-agent" do app(:header_matchers) do |r| r.on :user_agent=>/(chrome)(\d+)/ do |agent, num| "a-#{agent}-#{num}" end end body("HTTP_USER_AGENT" => "chrome31").must_equal "a-chrome-31" status.must_equal 404 end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/heartbeat_spec.rb����������������������������������������������0000664�0000000�0000000�00000002716�15167207754�0023144�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "heartbeat plugin" do it "should return heartbeat response for heartbeat paths only" do app(:bare) do plugin :heartbeat route do |r| r.on 'a' do "a" end end end body('/a').must_equal 'a' status.must_equal 404 status('/heartbeat').must_equal 200 body('/heartbeat').must_equal 'OK' end it "should support custom heartbeat paths" do app(:bare) do plugin :heartbeat, :path=>'/heartbeat2' route do |r| r.on 'a' do "a" end end end body('/a').must_equal 'a' status.must_equal 404 status('/heartbeat').must_equal 404 status('/heartbeat2').must_equal 200 body('/heartbeat2').must_equal 'OK' end it "should work when using sessions" do app(:bare) do send(*DEFAULT_SESSION_ARGS) plugin :heartbeat route do |r| session.clear r.on "a" do "a" end end end body('/a').must_equal 'a' status.must_equal 404 status('/heartbeat').must_equal 200 body('/heartbeat').must_equal 'OK' end it "should work when redirecting" do app(:bare) do plugin :heartbeat route do |r| r.on "a" do "a" end r.redirect '/a' end end body('/a').must_equal 'a' status.must_equal 302 status('/heartbeat').must_equal 200 body('/heartbeat').must_equal 'OK' end end ��������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hmac_paths_spec.rb���������������������������������������������0000664�0000000�0000000�00000071623�15167207754�0023317�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "hmac_paths plugin" do def hmac_paths_app(&block) app(:bare) do plugin :hmac_paths, secret: '1'*32 route(&block) end end it "plugin requires :secret option be string at least 32 bytes for HMAC secret" do proc{app.plugin :hmac_paths}.must_raise Roda::RodaError proc{app.plugin :hmac_paths, secret: 1}.must_raise Roda::RodaError proc{app.plugin :hmac_paths, secret: '1'}.must_raise Roda::RodaError proc{app.plugin :hmac_paths, secret: '1'*31}.must_raise Roda::RodaError end it "plugin requires :old_secret option be string at least 32 bytes for HMAC secret if given" do proc{app.plugin :hmac_paths, secret: '1'*32, old_secret: 1}.must_raise Roda::RodaError proc{app.plugin :hmac_paths, secret: '1'*32, old_secret: '1'}.must_raise Roda::RodaError proc{app.plugin :hmac_paths, secret: '1'*32, old_secret: '1'*31}.must_raise Roda::RodaError end it "hmac_path method requires path argument be a string" do hmac_paths_app{|r| hmac_path(1)} proc{body}.must_raise Roda::RodaError end it "hmac_path method requires path argument must start with /" do hmac_paths_app{|r| hmac_path('1')} proc{body}.must_raise Roda::RodaError end it "hmac_path method returns path with HMAC" do hmac_paths_app{|r| hmac_path('/1')} body.must_equal "/1dd95fe7d0dbe409f852e81a7c2cc4c93971c04542a150c6baefe00876e28f13/0/1" end it "hmac_path HMAC depends on path argument" do hmac_paths_app do |r| r.get('a'){hmac_path('/2')} hmac_path('/1') end body.must_equal "/1dd95fe7d0dbe409f852e81a7c2cc4c93971c04542a150c6baefe00876e28f13/0/1" body('/a').must_equal "/adf4707dcc97605cdaeea144bba055ed330ae2d1d23025150d7cbf181af6cd63/0/2" end it "hmac_path method requires :root option be a string" do hmac_paths_app{|r| hmac_path('/1', root: 1)} proc{body}.must_raise Roda::RodaError end it "hmac_path method requires :root option must start with /" do hmac_paths_app{|r| hmac_path('/1', root: '1')} proc{body}.must_raise Roda::RodaError end it "hmac_path uses :root option for path prefix" do hmac_paths_app do |r| hmac_path('/1', root: '/foo') end body.must_equal "/foo/ee408306c4e9a5ba5e57d295fe5cbb2b82e252b636d67c2a15039cc265611210/0/1" end it "hmac_path HMAC depends on :root option" do hmac_paths_app do |r| r.get('a'){hmac_path('/1', root: '/bar')} hmac_path('/1', root: '/foo') end body.must_equal "/foo/ee408306c4e9a5ba5e57d295fe5cbb2b82e252b636d67c2a15039cc265611210/0/1" body('/a').must_equal "/bar/5ba0452a430fca1802eb93b52f2d994b58d768374ba318253cbc50c326e46f95/0/1" end it "hmac_path sets m flag for :method option" do hmac_paths_app do |r| hmac_path('/1', method: :get) end body.must_equal "/e21ef288ee8316f0d8e67eae55a2f27edda28316c3c231f4425281f6d291bccc/m/1" end it "hmac_path HMAC depends on :method option" do hmac_paths_app do |r| r.get('a'){hmac_path('/1', method: :post)} hmac_path('/1', method: :get) end body.must_equal "/e21ef288ee8316f0d8e67eae55a2f27edda28316c3c231f4425281f6d291bccc/m/1" body('/a').must_equal "/72644ba25f4253acbe5f26370ca948d31f8fa0041fc77ddc9dc92da22415d1cd/m/1" end it "hmac_path sets p flag and adds query string parameters for :params option" do hmac_paths_app do |r| hmac_path('/1', params: {foo: :bar}) end body.must_equal "/503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443c/p/1?foo=bar" end it "hmac_path HMAC depends on :params option" do hmac_paths_app do |r| r.get('a'){hmac_path('/1', params: {bar: :foo})} hmac_path('/1', params: {foo: :bar}) end body.must_equal "/503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443c/p/1?foo=bar" body('/a').must_equal "/1785c857e23dfd04c127162d292f557cd2db4b0a6ac9c39515c85ce9ff404165/p/1?bar=foo" end it "hmac_path sets t flag for :seconds and :until options" do t = Time.utc(2100).to_i seconds = t - Time.now.to_i hmac_paths_app do |r| r.get('s'){hmac_path('/1', seconds: seconds)} hmac_path('/1', until: 3) end body.must_equal "/78a56ddf0e081ca127ab1bc704c8a4d5e7e62ccf327dec5c3189a5c72057334c/t/3/1" body('/s').must_match %r"/64e31560c6df065b6116599c370aca918cfcbde092724b94f2770192ae513a28/t/4102444800/1|/f0206d644636000f733df59dcff98c763ca56d209ee4737c72e1b0fe35013913/t/4102444801/1" end it "hmac_path HMAC depends on :seconds and :until options" do t = Time.utc(2100).to_i seconds = t - Time.now.to_i hmac_paths_app do |r| r.on(Integer) do |v| r.get('s'){hmac_path('/1', seconds: seconds - v)} hmac_path('/1', until: v) end end body('/3').must_equal "/78a56ddf0e081ca127ab1bc704c8a4d5e7e62ccf327dec5c3189a5c72057334c/t/3/1" body('/3/s').must_match %r"(/9ffb7ce6b6ae76664aeebc53955c7c1d8d09e6908a512a6b5f22e7a24fffdc15/t/4102444797/1|/534c367b6c6154fb84e8a6ce6118a560b261306a7c35cb210e1cee98dd5d431a/t/4102444798/1)" body('/4').must_equal "/f04e6d0571e2f3322bfa03b4b251070b7cdbde1004726fd69ded8cb40d1fb4ae/t/4/1" body('/4/s').must_match %r"(/ecc641d021a3cccd6f3931e41f75f3bbdc9b05791b0ac939278b306e40571c86/t/4102444796/1|/9ffb7ce6b6ae76664aeebc53955c7c1d8d09e6908a512a6b5f22e7a24fffdc15/t/4102444797/1)" end it "hmac_path gives priority to :until option over seconds option" do hmac_paths_app do |r| hmac_path('/1', until: 3, seconds: 2) end body.must_equal "/78a56ddf0e081ca127ab1bc704c8a4d5e7e62ccf327dec5c3189a5c72057334c/t/3/1" end it "hmac_path HMAC depends on :namespace option" do hmac_paths_app do |r| r.is String, String do |ns, path| hmac_path("/#{path}", namespace: ns) end end body('/1/1').must_equal "/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd777/n/1" body('/1/2').must_equal "/7e34d4cbe1d20878f3cc1db93d18eda19690b9ba985344057e847c2447d285ac/n/2" body('/2/1').must_equal "/4107c5423d997ea30266f666907e703bbe7e83e1e0b1fc3d5d8bdf0e85aa84d7/n/1" body('/2/2').must_equal "/602cd4704d8c7ec0af4bcd640f0dfb3a16460b1c6115ad09e3aed71c4ebdd6c6/n/2" end it "hmac_path HMAC depends on :namespace and :root option" do hmac_paths_app do |r| r.is String, String, String do |root, ns, path| hmac_path("/#{path}", namespace: ns, root: "/#{root}") end end body('/1/1/1').must_equal "/1/6a8eb2d9a041cbec93bf1228d16065c30990979c8708d95fc7f598ce33582bf5/n/1" body('/1/1/2').must_equal "/1/9fda9fa5aeddaa182e578f9ff021c8144719278f73c9bf262437d1cd914f60a9/n/2" body('/1/2/1').must_equal "/1/bd0f071316f51dc663cbed519f480642fbda34b8fddd4e5741dc85ff47f67e3c/n/1" body('/1/2/2').must_equal "/1/2d8ee8168bbc3bd421f9e2e87a61394d849598c0c7616e0648027decd2168e3a/n/2" body('/2/1/1').must_equal "/2/9f3cf91195e00ccdd49dad755c63faeab69df4b4aa28c1204b46732009f63660/n/1" body('/2/1/2').must_equal "/2/bc8774a718a5e5b900956f4cd7c68e69fb21191efcc200bb6e239c330b1ef0cf/n/2" body('/2/2/1').must_equal "/2/aff7e70387307b017b8b560325c3f6cfe9fbe3f92434a32279410427086b50ca/n/1" body('/2/2/2').must_equal "/2/39a307cf0cb83d29cf7fa18831978eb7de8ace532e052c6fa760564fbb2324a9/n/2" end it "hmac_path HMAC depends on default namespace via :namespace_session_key" do session = {} app(:bare) do define_method(:session){session} plugin :hmac_paths, secret: '1'*32, namespace_session_key: 'nsk' route do |r| r.is String do |path| hmac_path("/#{path}") end end end session['nsk'] = 1 body('/1').must_equal "/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd777/n/1" body('/2').must_equal "/7e34d4cbe1d20878f3cc1db93d18eda19690b9ba985344057e847c2447d285ac/n/2" session['nsk'] = 2 body('/1').must_equal "/4107c5423d997ea30266f666907e703bbe7e83e1e0b1fc3d5d8bdf0e85aa84d7/n/1" body('/2').must_equal "/602cd4704d8c7ec0af4bcd640f0dfb3a16460b1c6115ad09e3aed71c4ebdd6c6/n/2" end it "hmac_path allows overriding default namespace via explicit namespace option" do session = {} app(:bare) do define_method(:session){session} plugin :hmac_paths, secret: '1'*32, namespace_session_key: 'nsk' route do |r| r.is String, String do |ns, path| hmac_path("/#{path}", namespace: ns) end hmac_path(r.remaining_path, namespace: nil) end end session['nsk'] = 1 body('/1/1').must_equal "/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd777/n/1" body('/1/2').must_equal "/7e34d4cbe1d20878f3cc1db93d18eda19690b9ba985344057e847c2447d285ac/n/2" body('/2/1').must_equal "/4107c5423d997ea30266f666907e703bbe7e83e1e0b1fc3d5d8bdf0e85aa84d7/n/1" body('/2/2').must_equal "/602cd4704d8c7ec0af4bcd640f0dfb3a16460b1c6115ad09e3aed71c4ebdd6c6/n/2" body('/1').must_equal "/1dd95fe7d0dbe409f852e81a7c2cc4c93971c04542a150c6baefe00876e28f13/0/1" body('/2').must_equal "/adf4707dcc97605cdaeea144bba055ed330ae2d1d23025150d7cbf181af6cd63/0/2" end it "r.hmac_path does not yield if remaining path does not start with /" do hmac_paths_app{|r| r.hmac_path{r.remaining_path}} status('503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443c').must_equal 404 end unless ENV['LINT'] it "r.hmac_path does not yield if hmac segment is not 64 bytes" do hmac_paths_app{|r| r.hmac_path{r.remaining_path}} status('/503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443/0/').must_equal 404 end it "r.hmac_path does not yield if there is no flags or empty flags segment" do hmac_paths_app{|r| r.hmac_path{r.remaining_path}} status('/503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443c').must_equal 404 status('/503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443c/').must_equal 404 status('/503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443c//').must_equal 404 end it "r.hmac_path does not yield if there is no remaining path after flags segment" do hmac_paths_app{|r| r.hmac_path{r.remaining_path}} status('/503907ffeb039fa93b0e6d0728d30c2fef4b10d655aef6e1ac23347b2159443c/m').must_equal 404 end it "r.hmac_path does not yield if a namespace is provided and not required or not provided and required" do hmac_paths_app{|r| r.hmac_path(namespace: r.GET['ns']){r.remaining_path}} status('/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd777/n/1').must_equal 404 status('/1dd95fe7d0dbe409f852e81a7c2cc4c93971c04542a150c6baefe00876e28f13/0/1', 'QUERY_STRING'=>'ns=1').must_equal 404 end it "r.hmac_path does not yield if there is a namespace provided and required but it doesn't match" do hmac_paths_app{|r| r.hmac_path(namespace: r.GET['ns']){r.remaining_path}} status('/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd776/n/1', 'QUERY_STRING'=>'ns=1').must_equal 404 status('/7e34d4cbe1d20878f3cc1db93d18eda19690b9ba985344057e847c2447d285ab/n/2', 'QUERY_STRING'=>'ns=1').must_equal 404 status('/4107c5423d997ea30266f666907e703bbe7e83e1e0b1fc3d5d8bdf0e85aa84d6/n/1', 'QUERY_STRING'=>'ns=2').must_equal 404 status('/602cd4704d8c7ec0af4bcd640f0dfb3a16460b1c6115ad09e3aed71c4ebdd6c5/n/2', 'QUERY_STRING'=>'ns=2').must_equal 404 end it "r.hmac_path does not yield if there is a namespace provided and required but it doesn't match, when not at the root" do hmac_paths_app do |r| r.on String do r.hmac_path(namespace: r.GET['ns']){r.remaining_path} end end status("/1/6a8eb2d9a041cbec93bf1228d16065c30990979c8708d95fc7f598ce33582bf4/n/1", 'QUERY_STRING'=>'ns=1').must_equal 404 status("/1/9fda9fa5aeddaa182e578f9ff021c8144719278f73c9bf262437d1cd914f60a8/n/2", 'QUERY_STRING'=>'ns=1').must_equal 404 status("/1/bd0f071316f51dc663cbed519f480642fbda34b8fddd4e5741dc85ff47f67e3b/n/1", 'QUERY_STRING'=>'ns=2').must_equal 404 status("/1/2d8ee8168bbc3bd421f9e2e87a61394d849598c0c7616e0648027decd2168e39/n/2", 'QUERY_STRING'=>'ns=2').must_equal 404 status("/2/9f3cf91195e00ccdd49dad755c63faeab69df4b4aa28c1204b46732009f6366f/n/1", 'QUERY_STRING'=>'ns=1').must_equal 404 status("/2/bc8774a718a5e5b900956f4cd7c68e69fb21191efcc200bb6e239c330b1ef0ce/n/2", 'QUERY_STRING'=>'ns=1').must_equal 404 status("/2/aff7e70387307b017b8b560325c3f6cfe9fbe3f92434a32279410427086b50c9/n/1", 'QUERY_STRING'=>'ns=2').must_equal 404 status("/2/39a307cf0cb83d29cf7fa18831978eb7de8ace532e052c6fa760564fbb2324a8/n/2", 'QUERY_STRING'=>'ns=2').must_equal 404 end it "r.hmac_path does not yield if there is a default namespace provided via :namespace_session_key and required but it doesn't match" do session = {} app(:bare) do define_method(:session){session} plugin :hmac_paths, secret: '1'*32, namespace_session_key: 'nsk' route do |r| r.hmac_path{r.remaining_path} end end session['nsk'] = 1 status('/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd776/n/1').must_equal 404 status('/7e34d4cbe1d20878f3cc1db93d18eda19690b9ba985344057e847c2447d285ab/n/2').must_equal 404 session['nsk'] = 2 status('/4107c5423d997ea30266f666907e703bbe7e83e1e0b1fc3d5d8bdf0e85aa84d6/n/1').must_equal 404 status('/602cd4704d8c7ec0af4bcd640f0dfb3a16460b1c6115ad09e3aed71c4ebdd6c5/n/2').must_equal 404 end it "r.hmac_path only yields if hmac path matches" do hmac_paths_app do |r| r.get 'path', String do |path| hmac_path("/#{path}", root: '') end r.hmac_path do r.remaining_path end end path = body('/path/1') body(path).must_equal '/1' status(path.chop + '2').must_equal 404 status(path.sub(%r|./0/1\z|, '/0/1')).must_equal 404 status(path.sub(%r|0/1\z|, '1/1')).must_equal 404 end it "r.hmac_path does not support using path designed for one root with a different root" do hmac_paths_app do |r| r.get 'root', String do |root| hmac_path("/1", root: "/#{root}") end r.on Integer do r.hmac_path do r.remaining_path end end end root1_path = body('/root/1') root2_path = body('/root/2') status(root1_path.sub(/\A\/1/, '/2')).must_equal 404 status(root2_path.sub(/\A\/2/, '/1')).must_equal 404 end it "r.hmac_path for non-method-specific path works for any request method" do hmac_paths_app do |r| r.get 'path' do hmac_path("/1") end r.hmac_path do r.remaining_path end end path = body('/path') body(path).must_equal '/1' body(path, 'REQUEST_METHOD'=>'POST').must_equal '/1' end it "r.hmac_path for method-specific path only works for specified request method" do hmac_paths_app do |r| r.get 'method', String do |meth| hmac_path("/1", method: meth) end r.hmac_path do r.remaining_path end end get_path = body('/method/get') post_path = body('/method/post') body(get_path).must_equal '/1' body(post_path, 'REQUEST_METHOD'=>'POST').must_equal '/1' status(get_path, 'REQUEST_METHOD'=>'POST').must_equal 404 status(post_path).must_equal 404 end it "r.hmac_path for non-param-specific path only works with any query string" do hmac_paths_app do |r| r.get 'path' do hmac_path("/1") end r.hmac_path do r.remaining_path end end path = body('/path') body(path).must_equal '/1' body(path, 'QUERY_STRING'=>'a=b').must_equal '/1' end it "r.hmac_path for param-specific path only works for specified query string" do hmac_paths_app do |r| r.get 'params', String, String, String, String do |k1, v1, k2, v2| hmac_path("/1", params: {k1=>v1, k2=>v2}) end r.hmac_path do r.remaining_path end end params1_path = body('/params/a/b/c/d') params2_path = body('/params/c/d/a/b') p1, qs1 = params1_path.split('?', 2) p2, qs2 = params2_path.split('?', 2) status(p1).must_equal 404 status(p2).must_equal 404 body(p1, 'QUERY_STRING'=>qs1).must_equal '/1' body(p2, 'QUERY_STRING'=>qs2).must_equal '/1' status(p1, 'QUERY_STRING'=>qs2).must_equal 404 status(p2, 'QUERY_STRING'=>qs1).must_equal 404 end it "r.hmac_path yields if the path is timestamped, hmac matches, and before the timestamp" do hmac_paths_app{|r| r.hmac_path{r.remaining_path}} body('/ecc641d021a3cccd6f3931e41f75f3bbdc9b05791b0ac939278b306e40571c86/t/4102444796/1').must_equal '/1' end it "r.hmac_path does not yield if the path is timestamped, hmac matches, and not before the timestamp" do hmac_paths_app{|r| r.hmac_path{r.remaining_path}} status("/78a56ddf0e081ca127ab1bc704c8a4d5e7e62ccf327dec5c3189a5c72057334c/t/3/1").must_equal 404 end it "r.hmac_path does not yield if the path is timestamped and hmac does not match" do hmac_paths_app{|r| r.hmac_path{r.remaining_path}} status('/ecc641d021a3cccd6f3931e41f75f3bbdc9b05791b0ac939278b306e40571c85/t/4102444796/1').must_equal 404 end it "r.hmac_path yields if there is a namespace provided and required and it matches" do hmac_paths_app{|r| r.hmac_path(namespace: r.GET['ns']){r.remaining_path}} body('/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd777/n/1', 'QUERY_STRING'=>'ns=1').must_equal '/1' body('/7e34d4cbe1d20878f3cc1db93d18eda19690b9ba985344057e847c2447d285ac/n/2', 'QUERY_STRING'=>'ns=1').must_equal '/2' body('/4107c5423d997ea30266f666907e703bbe7e83e1e0b1fc3d5d8bdf0e85aa84d7/n/1', 'QUERY_STRING'=>'ns=2').must_equal '/1' body('/602cd4704d8c7ec0af4bcd640f0dfb3a16460b1c6115ad09e3aed71c4ebdd6c6/n/2', 'QUERY_STRING'=>'ns=2').must_equal '/2' end it "r.hmac_path yields if there is a namespace provided and required and it matches, when not at the root" do hmac_paths_app do |r| r.on String do r.hmac_path(namespace: r.GET['ns']){r.remaining_path} end end body("/1/6a8eb2d9a041cbec93bf1228d16065c30990979c8708d95fc7f598ce33582bf5/n/1", 'QUERY_STRING'=>'ns=1').must_equal '/1' body("/1/9fda9fa5aeddaa182e578f9ff021c8144719278f73c9bf262437d1cd914f60a9/n/2", 'QUERY_STRING'=>'ns=1').must_equal '/2' body("/1/bd0f071316f51dc663cbed519f480642fbda34b8fddd4e5741dc85ff47f67e3c/n/1", 'QUERY_STRING'=>'ns=2').must_equal '/1' body("/1/2d8ee8168bbc3bd421f9e2e87a61394d849598c0c7616e0648027decd2168e3a/n/2", 'QUERY_STRING'=>'ns=2').must_equal '/2' body("/2/9f3cf91195e00ccdd49dad755c63faeab69df4b4aa28c1204b46732009f63660/n/1", 'QUERY_STRING'=>'ns=1').must_equal '/1' body("/2/bc8774a718a5e5b900956f4cd7c68e69fb21191efcc200bb6e239c330b1ef0cf/n/2", 'QUERY_STRING'=>'ns=1').must_equal '/2' body("/2/aff7e70387307b017b8b560325c3f6cfe9fbe3f92434a32279410427086b50ca/n/1", 'QUERY_STRING'=>'ns=2').must_equal '/1' body("/2/39a307cf0cb83d29cf7fa18831978eb7de8ace532e052c6fa760564fbb2324a9/n/2", 'QUERY_STRING'=>'ns=2').must_equal '/2' end it "r.hmac_path yields if there is a namespace provided via :namespace_session_key and it matches" do session = {} app(:bare) do define_method(:session){session} plugin :hmac_paths, secret: '1'*32, namespace_session_key: 'nsk' route do |r| r.hmac_path{r.remaining_path} end end session['nsk'] = 1 body('/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd777/n/1').must_equal '/1' body('/7e34d4cbe1d20878f3cc1db93d18eda19690b9ba985344057e847c2447d285ac/n/2').must_equal '/2' session['nsk'] = 2 body('/4107c5423d997ea30266f666907e703bbe7e83e1e0b1fc3d5d8bdf0e85aa84d7/n/1').must_equal '/1' body('/602cd4704d8c7ec0af4bcd640f0dfb3a16460b1c6115ad09e3aed71c4ebdd6c6/n/2').must_equal '/2' end it "r.hmac_path yields if there is a namespace provided via :namespace_session_key and it matches, when not at the root" do session = {} app(:bare) do define_method(:session){session} plugin :hmac_paths, secret: '1'*32, namespace_session_key: 'nsk' route do |r| r.on String do r.hmac_path{r.remaining_path} end end end session['nsk'] = 1 body("/1/6a8eb2d9a041cbec93bf1228d16065c30990979c8708d95fc7f598ce33582bf5/n/1").must_equal '/1' body("/1/9fda9fa5aeddaa182e578f9ff021c8144719278f73c9bf262437d1cd914f60a9/n/2").must_equal '/2' body("/2/9f3cf91195e00ccdd49dad755c63faeab69df4b4aa28c1204b46732009f63660/n/1").must_equal '/1' body("/2/bc8774a718a5e5b900956f4cd7c68e69fb21191efcc200bb6e239c330b1ef0cf/n/2").must_equal '/2' session['nsk'] = 2 body("/1/bd0f071316f51dc663cbed519f480642fbda34b8fddd4e5741dc85ff47f67e3c/n/1").must_equal '/1' body("/1/2d8ee8168bbc3bd421f9e2e87a61394d849598c0c7616e0648027decd2168e3a/n/2").must_equal '/2' body("/2/aff7e70387307b017b8b560325c3f6cfe9fbe3f92434a32279410427086b50ca/n/1").must_equal '/1' body("/2/39a307cf0cb83d29cf7fa18831978eb7de8ace532e052c6fa760564fbb2324a9/n/2").must_equal '/2' end it "r.hmac_path with :namespace option overrides namespace provided via :namespace_session_key" do session = {} app(:bare) do define_method(:session){session} plugin :hmac_paths, secret: '1'*32, namespace_session_key: 'nsk' route do |r| r.hmac_path(namespace: r.GET['ns']){r.remaining_path} end end session['nsk'] = 2 body('/4ac78addcebf8b8e00c901e127934c6e4dd4ac0b76dcc9d837099bea01afd777/n/1', 'QUERY_STRING'=>'ns=1').must_equal '/1' body('/7e34d4cbe1d20878f3cc1db93d18eda19690b9ba985344057e847c2447d285ac/n/2', 'QUERY_STRING'=>'ns=1').must_equal '/2' session['nsk'] = 1 body('/4107c5423d997ea30266f666907e703bbe7e83e1e0b1fc3d5d8bdf0e85aa84d7/n/1', 'QUERY_STRING'=>'ns=2').must_equal '/1' body('/602cd4704d8c7ec0af4bcd640f0dfb3a16460b1c6115ad09e3aed71c4ebdd6c6/n/2', 'QUERY_STRING'=>'ns=2').must_equal '/2' end it "r.hmac_path works as expected with :root, :method, and :params options" do hmac_paths_app do |r| r.get 'path', String, String, String, String do |r, m, k1, v1| hmac_path("/1", root: "/#{r}", method: m, params: {k1=>v1}) end r.on Integer do r.hmac_path do r.remaining_path end end end path1 = body('/path/1/get/c/d') path2 = body('/path/2/post/a/b') p1, qs1 = path1.split('?', 2) p2, qs2 = path2.split('?', 2) body(p1, 'QUERY_STRING'=>qs1).must_equal '/1' body(p2, 'QUERY_STRING'=>qs2, 'REQUEST_METHOD'=>'POST').must_equal '/1' # No query string status(p1).must_equal 404 status(p2).must_equal 404 # Query string mismatch status(p1, 'QUERY_STRING'=>qs2).must_equal 404 status(p2, 'QUERY_STRING'=>qs1, 'REQUEST_METHOD'=>'POST').must_equal 404 # Request method mismatch status(p1, 'QUERY_STRING'=>qs1, 'REQUEST_METHOD'=>'POST').must_equal 404 status(p2, 'QUERY_STRING'=>qs2).must_equal 404 # Root mismatch status(p1.sub(/\A\/1/, '/2'), 'QUERY_STRING'=>qs1).must_equal 404 status(p2.sub(/\A\/2/, '/1'), 'QUERY_STRING'=>qs2, 'REQUEST_METHOD'=>'POST').must_equal 404 end it "r.hmac_path works as expected with :root, :method, :params, and :namespace options" do hmac_paths_app do |r| r.get 'path', String, String, String, String do |root, m, k1, v1| hmac_path("/1", root: "/#{root}", method: m, params: {k1=>v1}, namespace: r.GET['ns']) end r.on Integer do ns = r.GET['ns'] env['QUERY_STRING'] = env['QUERY_STRING'].sub('&ns=1', '') if env['QUERY_STRING'] r.hmac_path(namespace: ns) do r.remaining_path end end end path1 = body('/path/1/get/c/d', 'QUERY_STRING'=>'ns=1') path2 = body('/path/2/post/a/b', 'QUERY_STRING'=>'ns=1') p1, qs1 = path1.split('?', 2) p2, qs2 = path2.split('?', 2) qs1 += '&ns=1' qs2 += '&ns=1' body(p1, 'QUERY_STRING'=>qs1).must_equal '/1' body(p2, 'QUERY_STRING'=>qs2, 'REQUEST_METHOD'=>'POST').must_equal '/1' # No query string status(p1).must_equal 404 status(p2).must_equal 404 # Query string mismatch status(p1, 'QUERY_STRING'=>qs2).must_equal 404 status(p2, 'QUERY_STRING'=>qs1, 'REQUEST_METHOD'=>'POST').must_equal 404 # Request method mismatch status(p1, 'QUERY_STRING'=>qs1, 'REQUEST_METHOD'=>'POST').must_equal 404 status(p2, 'QUERY_STRING'=>qs2).must_equal 404 # Root mismatch status(p1.sub(/\A\/1/, '/2'), 'QUERY_STRING'=>qs1).must_equal 404 status(p2.sub(/\A\/2/, '/1'), 'QUERY_STRING'=>qs2, 'REQUEST_METHOD'=>'POST').must_equal 404 # Namespace mismatch qs1[-1] = '2' qs2[-1] = '2' status(p1.sub(/\A\/1/, '/2'), 'QUERY_STRING'=>qs1).must_equal 404 status(p2.sub(/\A\/2/, '/1'), 'QUERY_STRING'=>qs2, 'REQUEST_METHOD'=>'POST').must_equal 404 end it "r.hmac_path works as expected with :root, :method, and :params options and default namespace via :namespace_session_key" do session = {} app(:bare) do define_method(:session){session} plugin :hmac_paths, secret: '1'*32, namespace_session_key: 'nsk' route do |r| r.get 'path', String, String, String, String do |root, m, k1, v1| hmac_path("/1", root: "/#{root}", method: m, params: {k1=>v1}) end r.on Integer do r.hmac_path do r.remaining_path end end end end [nil, 1, 2].each do |nsk| session['nsk'] = nsk path1 = body('/path/1/get/c/d') path2 = body('/path/2/post/a/b') p1, qs1 = path1.split('?', 2) p2, qs2 = path2.split('?', 2) body(p1, 'QUERY_STRING'=>qs1).must_equal '/1' body(p2, 'QUERY_STRING'=>qs2, 'REQUEST_METHOD'=>'POST').must_equal '/1' # No query string status(p1).must_equal 404 status(p2).must_equal 404 # Query string mismatch status(p1, 'QUERY_STRING'=>qs2).must_equal 404 status(p2, 'QUERY_STRING'=>qs1, 'REQUEST_METHOD'=>'POST').must_equal 404 # Request method mismatch status(p1, 'QUERY_STRING'=>qs1, 'REQUEST_METHOD'=>'POST').must_equal 404 status(p2, 'QUERY_STRING'=>qs2).must_equal 404 # Root mismatch status(p1.sub(/\A\/1/, '/2'), 'QUERY_STRING'=>qs1).must_equal 404 status(p2.sub(/\A\/2/, '/1'), 'QUERY_STRING'=>qs2, 'REQUEST_METHOD'=>'POST').must_equal 404 # Namespace mismatch session['nsk'] = 3 status(p1.sub(/\A\/1/, '/2'), 'QUERY_STRING'=>qs1).must_equal 404 status(p2.sub(/\A\/2/, '/1'), 'QUERY_STRING'=>qs2, 'REQUEST_METHOD'=>'POST').must_equal 404 end end it "r.hmac_path handles secret rotation using :old_secret option" do hmac_paths_app do |r| r.get 'path', String do |path| hmac_path("/#{path}", root: '') end r.hmac_path do r.remaining_path end end path = body('/path/1') body(path).must_equal '/1' app.plugin :hmac_paths, secret: '2'*32 status(path).must_equal 404 app.plugin :hmac_paths, secret: '2'*32, old_secret: '1'*32 body(path).must_equal '/1' app.plugin :hmac_paths, secret: '2'*32, old_secret: '3'*32 status(path).must_equal 404 end it "example code in documation is accurate" do app(:bare) do plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes' route do |r| r.on 'root', String do |root| hmac_path(r.remaining_path, root: "/#{root}") end r.on 'method', String do |method| hmac_path(r.remaining_path, method: method) end r.on 'params', String, String do |k, v| hmac_path(r.remaining_path, params: {k=>v}) end r.on 'until' do hmac_path(r.remaining_path, until: Time.utc(2100)) end r.on 'seconds' do hmac_path(r.remaining_path, seconds: Time.utc(2100).to_i - Time.now.to_i) end r.on 'namespace', String do |ns| hmac_path(r.remaining_path, namespace: ns) end r.on 'all', String, String, String, String, String do |root, method, k, v, ns| hmac_path(r.remaining_path, root: "/#{root}", method: method, params: {k=>v}, namespace: ns) end hmac_path(r.remaining_path) end end body('/widget/1').must_equal "/0c2feaefdfc80cc73da19b060c713d4193c57022815238c6657ce2d99b5925eb/0/widget/1" body('/root/widget/1').must_equal "/widget/daccafce3ce0df52e5ce774626779eaa7286085fcbde1e4681c74175ff0bbacd/0/1" body('/root/foobar/1').must_equal "/foobar/c5fdaf482771d4f9f38cc13a1b2832929026a4ceb05e98ed6a0cd5a00bf180b7/0/1" body('/method/get/widget/1').must_equal "/d38c1e634ecf9a3c0ab9d0832555b035d91b35069efcbf2670b0dfefd4b62fdd/m/widget/1" body('/params/foo/bar/widget/1').must_equal "/fe8d03f9572d5af6c2866295bd3c12c2ea11d290b1cbd016c3b68ee36a678139/p/widget/1?foo=bar" body('/until/widget/1').must_equal "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1" body('/seconds/widget/1').must_equal "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1" body('/namespace/1/widget/1').must_equal "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1" body('/namespace/2/widget/1').must_equal "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1" body('/all/widget/get/foo/bar/1/1').must_equal "/widget/c14c78a81d34d766cf334a3ddbb7a6b231bc2092ef50a77ded0028586027b14e/mpn/1?foo=bar" end end �������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hooks_spec.rb��������������������������������������������������0000664�0000000�0000000�00000006254�15167207754�0022331�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "hooks plugin" do before do a = @a = [] app(:bare) do plugin :hooks before do response['foo'] = 'bar' end after do |r| if r a << [r[0], r[1]['foo'], r[2]] r[0] += 1 else a << r end end route do |r| f = response['foo'] response['foo'] = 'baz' f end end end it "adds before and after hooks for running code before and after requests" do s, h, b = req s.must_equal 201 h['foo'].must_equal 'baz' b.join.must_equal 'bar' @a.must_equal [[200, 'baz', ['bar']]] end it "multiple plugin calls do not override existing hooks" do app.plugin :hooks s, h, b = req s.must_equal 201 h['foo'].must_equal 'baz' b.join.must_equal 'bar' @a.must_equal [[200, 'baz', ['bar']]] end it "works when freezing the app" do app.freeze s, h, b = req s.must_equal 201 h['foo'].must_equal 'baz' b.join.must_equal 'bar' @a.must_equal [[200, 'baz', ['bar']]] end it "after hooks are still called if an exception is raised" do a = @a @app.before do raise Roda::RodaError, "foo" end @app.after do |r| a << r a << $! end proc{req}.must_raise(Roda::RodaError) a.pop.must_be_kind_of(Roda::RodaError) a.pop.must_be_nil end it "handles multiple before and after blocks correctly" do a = @a @app.before do response['bar'] = "foo#{response['foo']}" end @app.after do |r| a << r[1]['bar'] r[0] *= 2 end s, h, b = req s.must_equal 402 h['foo'].must_equal 'baz' h['bar'].must_equal 'foo' b.join.must_equal 'bar' a.must_equal [[200, 'baz', ['bar']], 'foo'] end it "copies before and after blocks when subclassing" do @app = Class.new(@app) @app.route do |r| r.on do "foo" end end s, h, b = req s.must_equal 201 h['foo'].must_equal 'bar' b.join.must_equal 'foo' @a.must_equal [[200, 'bar', ['foo']]] end it "handles halt in before blocks" do app.before do response.status = 200 request.halt end status.must_equal 201 end it "works with error plugin when loaded first" do app.plugin(:error_handler){|e| "error"} app.before do raise "before" if @_request.path == '/b' end app.after do raise "after" if @_request.path == '/a' end body('/a').must_equal "error" body('/b').must_equal "error" end it "works with error plugin when loaded after" do app(:bare) do plugin(:error_handler){|e| "error"} plugin :hooks before do raise "before" if @_request.path == '/b' end after do raise "after" if @_request.path == '/a' end route{} end body('/a').must_equal "error" body('/b').must_equal "error" end deprecated "should work if #call is overridden" do app.class_eval do def call; super end end app.route(&app.route_block) s, h, b = req s.must_equal 201 h['foo'].must_equal 'baz' b.join.must_equal 'bar' @a.must_equal [[200, 'baz', ['bar']]] end end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/host_authorization_spec.rb�������������������������������������0000664�0000000�0000000�00000007174�15167207754�0025145�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "host_authorization plugin" do it "allows configuring authorized hosts" do app do |r| r.get 'x' do '2' end check_host_authorization! '1' end app.plugin :host_authorization, 'foo.example.com' status.must_equal 403 body.must_equal '' status('/x').must_equal 200 body('/x').must_equal '2' status('HTTP_HOST'=>'foo.example.com').must_equal 200 status('HTTP_HOST'=>'bar.example.com').must_equal 403 status('HTTP_HOST'=>'foo.example.com:80').must_equal 200 status('HTTP_HOST'=>'bar.example.com:80').must_equal 403 app.plugin :host_authorization, /\A(foo|bar)\.example\.com\z/ status.must_equal 403 body.must_equal '' status('HTTP_HOST'=>'foo.example.com').must_equal 200 status('HTTP_HOST'=>'bar.example.com').must_equal 200 status('HTTP_HOST'=>'baz.example.com').must_equal 403 app.plugin :host_authorization, %w'foo.example.com bar.example.com' status.must_equal 403 body.must_equal '' status('HTTP_HOST'=>'foo.example.com').must_equal 200 status('HTTP_HOST'=>'bar.example.com').must_equal 200 status('HTTP_HOST'=>'baz.example.com').must_equal 403 app.plugin :host_authorization, %w'foo.example.com bar.example.com' status.must_equal 403 body.must_equal '' status('HTTP_HOST'=>'foo.example.com').must_equal 200 status('HTTP_HOST'=>'bar.example.com').must_equal 200 status('HTTP_HOST'=>'baz.example.com').must_equal 403 status('HTTP_HOST'=>'foo.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com').must_equal 200 status('HTTP_HOST'=>'bar.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com').must_equal 200 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com').must_equal 403 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'bar.example.com').must_equal 403 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com, bar.example.com').must_equal 403 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'bar.example.com, x.example.com').must_equal 403 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com, bar.example.com:80').must_equal 403 app.plugin :host_authorization, %w'foo.example.com bar.example.com', :check_forwarded=>true status('HTTP_HOST'=>'foo.example.com').must_equal 200 status('HTTP_HOST'=>'bar.example.com').must_equal 200 status('HTTP_HOST'=>'baz.example.com').must_equal 403 status('HTTP_HOST'=>'foo.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com').must_equal 200 status('HTTP_HOST'=>'bar.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com').must_equal 200 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com').must_equal 403 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'bar.example.com').must_equal 200 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com, bar.example.com').must_equal 200 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'bar.example.com, x.example.com').must_equal 403 status('HTTP_HOST'=>'baz.example.com', 'HTTP_X_FORWARDED_HOST'=>'x.example.com, bar.example.com:80').must_equal 200 app.plugin :host_authorization, 'foo.example.com' do |r| response.status = 401 '2' end status.must_equal 401 body.must_equal '2' status('HTTP_HOST'=>'foo.example.com').must_equal 200 status('HTTP_HOST'=>'bar.example.com').must_equal 401 status('HTTP_HOST'=>'foo.example.com:80').must_equal 200 status('HTTP_HOST'=>'bar.example.com:80').must_equal 401 end end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/host_routing_spec.rb�������������������������������������������0000664�0000000�0000000�00000006316�15167207754�0023731�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "host_routing plugin" do it "adds support for routing based on host name" do app(:bare) do plugin :host_routing do |hosts| hosts.to :t1, "t1.example.com" hosts.to :t2, "t2.example.com", "tx.example.com" hosts.default :t1 end route do |r| r.t1 do "t1-#{r.t1?}-#{r.t2?}" end r.t2 do "t2-#{r.t1?}-#{r.t2?}" end end end 2.times do body.must_equal 't1-true-false' body('HTTP_HOST'=>"t1.example.com").must_equal 't1-true-false' body('HTTP_HOST'=>"t2.example.com").must_equal 't2-false-true' body('HTTP_HOST'=>"tx.example.com").must_equal 't2-false-true' @app = Class.new(@app) end end it "hosts.default accepts a block evaluated in route block scope" do app(:bare) do plugin :host_routing do |hosts| hosts.register :t2, :t3 hosts.default :t1 do |host| if host.start_with?('t2.example.com') :t2 elsif request.GET['b'] :t3 end end end route do |r| r.t1 do "t1-#{r.t1?}-#{r.t2?}-#{r.t3?}" end r.t2 do "t2-#{r.t1?}-#{r.t2?}-#{r.t3?}" end r.t3 do "t3-#{r.t1?}-#{r.t2?}-#{r.t3?}" end end end body.must_equal 't1-true-false-false' body('HTTP_HOST'=>"t2.example.com").must_equal 't2-false-true-false' body('QUERY_STRING'=>"b=1").must_equal 't3-false-false-true' body('SERVER_NAME'=>"t2.example.com").must_equal 't2-false-true-false' body('HTTP_X_FORWARDED_HOST'=>"t2.example.com").must_equal 't2-false-true-false' end it "supports :scope_predicates option for also defining predicates in route block scope" do app(:bare) do plugin :host_routing, :scope_predicates=>true do |hosts| hosts.to :t2, "t2.example.com" hosts.default :t1 end route do |r| r.t1 do "t1-#{t1?}-#{t2?}" end r.t2 do "t2-#{t1?}-#{t2?}" end end end body('HTTP_HOST'=>"t2.example.com").must_equal 't2-false-true' body('HTTP_HOST'=>"t1.example.com").must_equal 't1-true-false' end it "uses empty string for missing host" do app(:bare) do plugin :host_routing, :scope_predicates=>true do |hosts| hosts.to :t2, "" hosts.default :t1 end route do |r| r.t1 do "t1-#{r.t1?}-#{r.t2?}" end r.t2 do "t2-#{r.t1?}-#{r.t2?}" end end end if Rack.release >= '2.1' && !ENV["LINT"] # Old rack versions would return host ":" for no SERVER_NAME and no SERVER_PORT # Rack::Lint support in spec/helper forces SERVER_NAME=example.com body.must_equal 't2-false-true' end unless Rack.release =~ /\A2\.2/ # Rack 2.2 uses ":80" host in this case body('SERVER_NAME'=>'', 'SERVER_PORT'=>"80").must_equal 't2-false-true' end body('HTTP_HOST'=>"t1.example.com").must_equal 't1-true-false' end it "errors if default host is not provided" do proc{app.plugin(:host_routing){}}.must_raise Roda::RodaError app.plugin(:host_routing){|x| x.default :x} end end ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/hsts_spec.rb���������������������������������������������������0000664�0000000�0000000�00000001317�15167207754�0022162�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "default_headers plugin" do def app(opts={}) super(:bare) do plugin :hsts, opts route do |r| '' end end end it "sets appropriate headers for the response" do app req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000; includeSubDomains" end it "supports :preload option" do app(preload: true) req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000; includeSubDomains; preload" end it "supports subdomains: false option" do app(subdomains: false) req[1][RodaResponseHeaders::STRICT_TRANSPORT_SECURITY].must_equal "max-age=63072000" end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/indifferent_params_spec.rb�������������������������������������0000664�0000000�0000000�00000000702�15167207754�0025036�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "indifferent_params plugin" do it "allows indifferent access to request params via params method" do app(:indifferent_params) do |r| r.on do "#{params[:a]}/#{params[:b][0][:c]}" end end body('QUERY_STRING'=>'a=2&b[][c]=3', 'rack.input'=>rack_input).must_equal '2/3' body('REQUEST_METHOD'=>'POST', 'rack.input'=>rack_input('a=2&b[][c]=3')).must_equal '2/3' end end ��������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/inject_erb_spec.rb���������������������������������������������0000664�0000000�0000000�00000001733�15167207754�0023307�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping inject_erb plugin test" else describe "inject_erb plugin" do before do app(:bare) do plugin :render, :views => './spec/views' plugin :inject_erb route do |r| r.root do render(:inline => "<% inject_erb('foo') %>") end r.get 'to_s' do render(:inline => "<% inject_erb(1) %>") end r.get 'inject' do render(:inline => "<% some_method do %>foo<% end %>") end end def some_method inject_erb "bar" yield inject_erb "baz" end end end it "should allow injecting into erb template" do body.strip.must_equal "foo" end it "should convert argument to string" do body('/to_s').strip.must_equal "1" end it "should work with the inject_erb plugin" do body('/inject').strip.must_equal "barfoobaz" end end end �������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/invalid_request_body_spec.rb�����������������������������������0000664�0000000�0000000�00000004262�15167207754�0025416�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "invalid_request_body plugin" do def invalid_request_body_app(*args, &block) app(:bare) do plugin :invalid_request_body, *args, &block route{|r| r.POST.to_a.inspect} end end content_type = 'multipart/form-data; boundary=foobar' valid_body = "--foobar\r\nContent-Disposition: form-data; name=\"x\"\r\n\r\ny\r\n--foobar--" define_method :valid_request_hash do {"REQUEST_METHOD"=>'POST', 'CONTENT_TYPE'=>content_type, 'CONTENT_LENGTH'=>valid_body.bytesize.to_s, 'rack.input'=>rack_input(valid_body)} end define_method :invalid_request_hash do {"REQUEST_METHOD"=>'POST', 'CONTENT_TYPE'=>content_type, 'CONTENT_LENGTH'=>'100', 'rack.input'=>rack_input} end it "supports :empty_400 plugin argument" do invalid_request_body_app(:empty_400) body(valid_request_hash).must_equal '[["x", "y"]]' req(invalid_request_hash).must_equal [400, {RodaResponseHeaders::CONTENT_TYPE=>'text/html', RodaResponseHeaders::CONTENT_LENGTH=>'0'}, []] end it "supports :empty_hash plugin argument" do invalid_request_body_app(:empty_hash) body(valid_request_hash).must_equal '[["x", "y"]]' req(invalid_request_hash).must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>'text/html', RodaResponseHeaders::CONTENT_LENGTH=>'2'}, ['[]']] end it "supports :raise plugin argument" do invalid_request_body_app(:raise) body(valid_request_hash).must_equal '[["x", "y"]]' proc{req(invalid_request_hash)}.must_raise Roda::RodaPlugins::InvalidRequestBody::Error end it "supports plugin block argument" do invalid_request_body_app{|e| {'y'=>"x"}} body(valid_request_hash).must_equal '[["x", "y"]]' body(invalid_request_hash).must_equal '[["y", "x"]]' end it "raises Error if configuring plugin with invalid plugin argument" do proc{invalid_request_body_app(:foo)}.must_raise Roda::RodaError end it "raises Error if configuring plugin with block and regular argument" do proc{invalid_request_body_app(:raise){}}.must_raise Roda::RodaError end it "raises Error if configuring plugin without block or regular argument" do proc{invalid_request_body_app}.must_raise Roda::RodaError end end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/ip_from_header_spec.rb�����������������������������������������0000664�0000000�0000000�00000000574�15167207754�0024150�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "ip_from_header plugin" do it "returns value in header if present, and default behavior otherwise" do app(:bare) do plugin :ip_from_header, "x-y" route(&:ip) end body("REMOTE_ADDR"=>"127.0.0.1").must_equal '127.0.0.1' body("REMOTE_ADDR"=>"127.0.0.1", "HTTP_X_Y"=>"1.2.3.4").must_equal '1.2.3.4' end end ������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/json_parser_spec.rb��������������������������������������������0000664�0000000�0000000�00000012014�15167207754�0023522�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "json_parser plugin" do before do app(:json_parser) do |r| r.params['a']['b'].to_s end end it "parses incoming json if content type specifies json" do body('rack.input'=>rack_input('{"a":{"b":1}}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '1' end it "Handles Rack::Request#POST being called in advance" do env = req_env('rack.input'=>rack_input('{"a":{"b":1}}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST') r = Rack::Request.new(env) r.POST body(env).must_equal '1' end it "doesn't affect parsing of non-json content type" do body('rack.input'=>rack_input('a[b]=1'), 'REQUEST_METHOD'=>'POST').must_equal '1' end it "parses incoming json if content type specifies json and body is already read" do @app.route do |r| r.body.read r.params['a']['b'].to_s end body('rack.input'=>rack_input('{"a":{"b":1}}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '1' end unless Rack.release >= '2.3' it "returns 400 for invalid json" do req('rack.input'=>rack_input('{"a":{"b":1}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal [400, {}, []] end it "returns 400 for invalid json when using params_capturing plugin" do @app.plugin :params_capturing req('rack.input'=>rack_input('{"a":{"b":1}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal [400, {}, []] end it "raises by default if r.params is called and a non-hash is submitted" do proc do req('rack.input'=>rack_input('[1]'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST') end.must_raise end end describe "json_parser plugin" do it "handles empty request bodies" do app(:json_parser) do |r| r.params.length.to_s end body('rack.input'=>rack_input(''), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '0' end it "handles arrays and other non-hash values using r.POST" do app(:json_parser) do |r| r.POST.inspect end body('rack.input'=>rack_input('[ 1 ]'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '[1]' end it "supports :wrap=>:always option" do app(:bare) do plugin(:json_parser, :wrap=>:always) route do |r| r.post 'a' do r.params['_json']['a']['b'].to_s end r.params['_json'][1].to_s end end body('/a', 'rack.input'=>rack_input('{"a":{"b":1}}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '1' body('rack.input'=>rack_input('[true, 2]'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '2' end it "supports :wrap=>:unless_hash option" do app(:bare) do plugin(:json_parser, :wrap=>:unless_hash) route do |r| r.post 'a' do r.params['a']['b'].to_s end r.params['_json'][1].to_s end end body('/a', 'rack.input'=>rack_input('{"a":{"b":1}}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '1' body('rack.input'=>rack_input('[true, 2]'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '2' end it "raises for unsupported :wrap option" do proc do app(:bare) do plugin(:json_parser, :wrap=>:foo) end end.must_raise Roda::RodaError end it "supports :error_handler option" do app(:bare) do plugin(:json_parser, :error_handler=>proc{|r| r.halt [401, {}, ['bad']]}) route do |r| r.params['a']['b'].to_s end end req('rack.input'=>rack_input('{"a":{"b":1}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal [401, {}, ['bad']] end it "works with bare POST" do app(:bare) do plugin(:json_parser, :error_handler=>proc{|r| r.halt [401, {}, ['bad']]}) route do |r| (r.POST['a']['b'] + r.POST['a']['c']).to_s end end body('rack.input'=>rack_input('{"a":{"b":1,"c":2}}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '3' end it "supports :parser option" do app(:bare) do plugin(:json_parser, :parser=>method(:eval)) route do |r| r.params['a']['b'].to_s end end body('rack.input'=>rack_input("{'a'=>{'b'=>1}}"), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '1' end it "supports :include_request option" do app(:bare) do plugin(:json_parser, :include_request => true, :parser => lambda{|s,r| {'a'=>s, 'b'=>r.path_info}}) route do |r| "#{r.params['a']}:#{r.params['b']}" end end body('rack.input'=>rack_input('{}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '{}:/' end it "supports resetting :include_request option to false" do app(:bare) do plugin :json_parser, :include_request => true plugin :json_parser, :include_request => false route do |r| r.params['a']['b'].to_s end end body('rack.input'=>rack_input('{"a":{"b":1}}'), 'CONTENT_TYPE'=>'text/json', 'REQUEST_METHOD'=>'POST').must_equal '1' end end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/json_spec.rb���������������������������������������������������0000664�0000000�0000000�00000004416�15167207754�0022155�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "json plugin" do c = Class.new do def to_json '[1]' end end before do app(:bare) do plugin :json, :classes=>[Array, Hash, c] route do |r| r.is 'array' do [1, 2, 3] end r.is "hash" do {'a'=>'b'} end r.is 'c' do c.new end r.is 'd' do response[RodaResponseHeaders::CONTENT_TYPE] = 'foo' c.new end end end end it "should use a json content type for a json response" do header(RodaResponseHeaders::CONTENT_TYPE, "/array").must_equal 'application/json' header(RodaResponseHeaders::CONTENT_TYPE, "/hash").must_equal 'application/json' header(RodaResponseHeaders::CONTENT_TYPE, "/c").must_equal 'application/json' header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/html' end it "should not override existing content type for a json response" do header(RodaResponseHeaders::CONTENT_TYPE, "/d").must_equal 'foo' end it "should convert objects to json" do body('/array').gsub(/\s/, '').must_equal '[1,2,3]' body('/hash').gsub(/\s/, '').must_equal '{"a":"b"}' body('/c').must_equal '[1]' body.must_equal '' end it "should work when subclassing" do @app = Class.new(app) app.route{[1]} body.must_equal '[1]' end it "should return classes that will be converted to JSON" do @app.json_result_classes.must_equal [Array, Hash, c] end it "should accept custom serializers" do app.plugin :json, :serializer => proc{|o| o.to_a.inspect} body("/hash").must_equal '[["a", "b"]]' end it "should give serializer the request if :include_request is set" do app.plugin :json, :include_request => true, :serializer => lambda{|o,r| "#{o['a']}:#{r.path_info}"} body("/hash").must_equal 'b:/hash' end it "should allow resetting :include_request to false" do app.plugin :json, :include_request => true app.plugin :json, :include_request => false body("/hash").must_equal '{"a":"b"}' end it "should allow custom content type for a response" do app.plugin :json, :content_type => "application/xml" header(RodaResponseHeaders::CONTENT_TYPE, "/array").must_equal 'application/xml' end end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/link_to_spec.rb������������������������������������������������0000664�0000000�0000000�00000002424�15167207754�0022640�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "link_to plugin" do it "support string values" do app(:bare) do plugin :link_to route{|r| link_to('a', '/b')} end body.must_equal '<a href="/b">a</a>' end it "support symbol values for named paths" do app(:bare) do plugin :link_to path :b, '/bar' route{|r| link_to('a', :b)} end body.must_equal '<a href="/bar">a</a>' end it "support instances for class paths" do c = Class.new app(:bare) do plugin :link_to path c do '/bar' end route{|r| link_to('a', c.new)} end body.must_equal '<a href="/bar">a</a>' end it "supports nil text values to use the same as the link" do app(:bare) do plugin :link_to route{|r| link_to(nil, '/b')} end body.must_equal '<a href="/b">/b</a>' end it "escapes paths but not body" do app(:bare) do plugin :link_to route{|r| link_to('<span>x</span>', '/foo?bar=baz&q=1')} end body.must_equal '<a href="/foo?bar=baz&q=1"><span>x</span></a>' end it "supports HTML options" do app(:bare) do plugin :link_to route{|r| link_to('a', '/b', 'foo'=>'"bar"', :baz=>1)} end body.must_equal '<a href="/b" foo=""bar"" baz="1">a</a>' end end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/mail_processor_spec.rb�����������������������������������������0000664�0000000�0000000�00000035633�15167207754�0024232�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" begin require 'mail' rescue LoadError warn "mail not installed, skipping mail_processor plugin test" else Mail.defaults do retriever_method :test end describe "mail_processor plugin" do def new_mail m = Mail.new(:to=>'a@example.com', :from=>'b@example.com', :cc=>'c@example.com', :bcc=>'d@example.com', :subject=>'Sub', :body=>'Bod') yield m if block_given? m end def check @processed.clear yield @processed end it "supports processing Mail instances via the routing tree using case insensitive address matchers" do @processed = processed = [] app(:mail_processor) do |r| r.to('a@example.com') do r.handle_from(/@example.com/) do processed << :to_a1 end r.handle_from(/\A(.+)@example(\d).com\z/i) do |pre, id| processed << :to_a2 << pre << id end r.handle_cc(['d@example.com', 'c@example.com']) do |ad| processed << :to_a3 << ad end r.handle do processed << :to_a4 end end r.handle_to('e@example.com') do processed << :to_e end r.handle_rcpt('f@example.com') do processed << :to_f end end check{app.process_mail(new_mail)}.must_equal [:to_a1] check{app.process_mail(new_mail{|m| m.from 'b2@example2.com'})}.must_equal [:to_a2, "b2", "2"] check{app.process_mail(new_mail{|m| m.from 'b2@example12.com'})}.must_equal [:to_a3, 'c@example.com'] check{app.process_mail(new_mail{|m| m.from 'b2@f.com'; m.cc []})}.must_equal [:to_a4] check{app.process_mail(new_mail{|m| m.to 'e@example.com'})}.must_equal [:to_e] check{app.process_mail(new_mail{|m| m.to 'f@example.com'})}.must_equal [:to_f] check{app.process_mail(new_mail{|m| m.to 'foo@example.com'; m.cc 'f@example.com'})}.must_equal [:to_f] app.freeze check{app.process_mail(new_mail{|m| m.to 'A@example.com'})}.must_equal [:to_a1] check{app.process_mail(new_mail{|m| m.from 'b2@Example2.com'})}.must_equal [:to_a2, "b2", "2"] check{app.process_mail(new_mail{|m| m.from 'b2@EXAMPLE12.com'})}.must_equal [:to_a3, 'c@example.com'] check{app.process_mail(new_mail{|m| m.from 'b2@f.COM'; m.cc []})}.must_equal [:to_a4] check{app.process_mail(new_mail{|m| m.to 'E@EXAmple.com'})}.must_equal [:to_e] check{app.process_mail(new_mail{|m| m.to 'f@exAMPLe.com'})}.must_equal [:to_f] check{app.process_mail(new_mail{|m| m.to 'FOo@eXAMPle.com'; m.cc 'f@eXAMPle.com'})}.must_equal [:to_f] end it "supports processing Mail instances via the routing tree using body and subject matchers" do @processed = processed = [] app(:mail_processor) do |r| r.handle_subject('Sub') do r.handle_body(/XID: (\d+)/) do |xid| processed << :sb << xid end processed << :s1 end r.handle_subject(['Su', 'Si']) do |sub| processed << :s2 << sub end r.subject(/S([ao])/) do |sub| r.handle do processed << :s3 << sub end end end check{app.process_mail(new_mail)}.must_equal [:s1] check{app.process_mail(new_mail{|m| m.subject 'Si'})}.must_equal [:s2, 'Si'] check{app.process_mail(new_mail{|m| m.subject 'Sa'})}.must_equal [:s3, 'a'] check{app.process_mail(new_mail{|m| m.body 'XID: 1234'})}.must_equal [:sb, '1234'] end it "supports processing Mail instances via the routing tree using header matchers" do @processed = processed = [] app(:mail_processor) do |r| r.handle_header('X-Test') do |v| processed << :x1 << v end r.handle_header('X-Test2', 'Foo') do processed << :x2 end r.handle_header('X-Test2', ['Foo', 'Bar']) do |val| processed << :x3 << val end r.header('X-Test2', /(\d+)/) do |i| r.handle do processed << :x4 << i end end r.handle do processed << :f end end check{app.process_mail(new_mail)}.must_equal [:f] check{app.process_mail(new_mail{|m| m.header['X-Test'] = 'Foo'})}.must_equal [:x1, 'Foo'] check{app.process_mail(new_mail{|m| m.header['X-Test2'] = 'Foo'})}.must_equal [:x2] check{app.process_mail(new_mail{|m| m.header['X-Test2'] = 'Bar'})}.must_equal [:x3, 'Bar'] check{app.process_mail(new_mail{|m| m.header['X-Test2'] = 'foo 3'})}.must_equal [:x4, '3'] end it "calls unhandled_mail block for email not handled by a routing block" do @processed = processed = [] app(:mail_processor) do |r| r.to('a@example.com') do processed << :on_to end processed << :miss end app.unhandled_mail do processed << :uh << mail.to.first end check{app.process_mail(new_mail)}.must_equal [:on_to, :uh, 'a@example.com'] check{app.process_mail(new_mail{|m| m.to 'b@example.com'})}.must_equal [:miss, :uh, 'b@example.com'] end it "calls handled_mail block for email handled by a routing block" do @processed = processed = [] app(:mail_processor) do |r| r.handle_to('a@example.com') do processed << :to end end app.handled_mail do processed << :h << mail.to.first end check{app.process_mail(new_mail)}.must_equal [:to, :h, 'a@example.com'] end it "raises by default for unhandled email" do @processed = processed = [] app(:mail_processor) do |r| processed << :miss end proc{app.process_mail(new_mail)}.must_raise Roda::RodaPlugins::MailProcessor::UnhandledMail processed.must_equal [:miss] end it "allows calling unhandled_mail directly, and not calling either implicitly if called directly" do @processed = processed = [] app(:mail_processor) do |r| r.handle_to('a@example.com') do r.handle_from('b@example.com') do processed << :quux end unhandled_mail "bar" processed << :foo end r.to('d@example.com') do r.handle do processed << :bar unhandled_mail "foo" end end r.handle do processed << :baz end end app.unhandled_mail do processed << :uh end app.handled_mail do processed << :h end app.after_mail do processed << :a end check{app.process_mail(new_mail)}.must_equal [:quux, :h, :a] check{app.process_mail(new_mail{|m| m.from 'c@example.com'})}.must_equal [:uh, :a] check{app.process_mail(new_mail{|m| m.to 'd@example.com'})}.must_equal [ :bar, :uh, :a] check{app.process_mail(new_mail{|m| m.to 'e@example.com'})}.must_equal [ :baz, :h, :a] end it "always calls after_mail after processing an email, even if the mail is not handled" do @processed = processed = [] app(:mail_processor) do |r| r.handle_to('a@example.com') do processed << :t end r.handle_to('b@example.com') do raise end end app.unhandled_mail do processed << :uh end app.after_mail do processed << :a end check{app.process_mail(new_mail)}.must_equal [:t, :a] check{app.process_mail(new_mail{|m| m.to 'd@example.com'})}.must_equal [:uh, :a] check{proc{app.process_mail(new_mail{|m| m.to 'b@example.com'})}.must_raise RuntimeError}.must_equal [:a] end it "always calls after_mail after processing an email, even if handled_mail or unhandled_mail hooks raise an exception" do @processed = processed = [] app(:mail_processor) do |r| r.handle_to('a@example.com') do processed << :t end end app.handled_mail do processed << :h raise "foo" end app.unhandled_mail do processed << :uh raise "foo" end app.after_mail do processed << :a end check{proc{app.process_mail(new_mail)}.must_raise RuntimeError}.must_equal [:t, :h, :a] check{proc{app.process_mail(new_mail{|m| m.to 'd@example.com'})}.must_raise RuntimeError}.must_equal [:uh, :a] end it "should raise RodaError for unsupported address and content matchers" do app(:mail_processor) do |r| r.subject('Sub') do r.subject(Object.new) do end end r.subject('Si') do r.from(Object.new) do end end end proc{app.process_mail(new_mail)}.must_raise Roda::RodaError proc{app.process_mail(new_mail{|m| m.subject 'Si'})}.must_raise Roda::RodaError end it "supports processing retrieved mail from a mailbox via the routing tree" do @processed = processed = [] app(:mail_processor) do |r| r.handle_to('a@example.com') do processed << :to_a end r.handle_to('c@example.com') do processed.concat(mail.to) end end Mail::TestRetriever.emails = [new_mail] check{app.process_mailbox}.must_equal [:to_a] Mail::TestRetriever.emails = [new_mail{|m| m.to 'c@example.com'}] check{app.process_mailbox}.must_equal ['c@example.com'] Mail::TestRetriever.emails = [new_mail] * 10 check{app.process_mailbox}.must_equal([:to_a]*10) Mail::TestRetriever.emails = Array.new(10){new_mail} check{app.process_mailbox(:count=>2)}.must_equal([:to_a]*2) check{app.process_mailbox}.must_equal([:to_a]*8) end it "supports processing retrieved mail from a mailbox with a custom :retreiver" do @processed = processed = [] emails = [] retriever = Class.new(Mail::Retriever) do define_method(:find) do |opts={}, &block| es = emails.dup emails.clear es.each(&block) if block es end end.new app(:mail_processor) do |r| r.handle_to('a@example.com') do processed << :to_a end end emails << new_mail check{app.process_mailbox}.must_equal [] emails.wont_be_empty check{app.process_mailbox(:retriever=>retriever)}.must_equal [:to_a] emails.must_be_empty check{app.process_mailbox(:retriever=>retriever)}.must_equal [] end it "supports rcpt class method to delegate to blocks by recipient address, falling back to main routing block" do @processed = processed = [] app(:mail_processor) do |r| r.handle do processed << :f end end app.rcpt('a@example.com') do |r| r.handle do processed << :a end end app.rcpt(/(.*[bcd])@example.com/, /(.*[bcd]2)@example.com/) do |r, x| r.handle do processed << :bcd << x end end app.rcpt(/([cde])@example.com(.*)/i) do |r, x, y| r.handle do processed << :cde << x << y end end app.rcpt('B@EXAMPLE.com', 'c@example.com') do |r| r.handle do processed << :bc end end app.rcpt('x@example.com') do |r| processed << :x end proc{app.rcpt(Object.new){}}.must_raise Roda::RodaError check{app.process_mail(new_mail)}.must_equal [:a] app.freeze check{app.process_mail(new_mail{|m| m.to 'b@example.com'})}.must_equal [:bc] check{app.process_mail(new_mail{|m| m.to 'C@example.com'; m.cc 'a@example.com'})}.must_equal [:bc] check{app.process_mail(new_mail{|m| m.to 'd@example.com'; m.cc 'a@example.com'})}.must_equal [:a] check{app.process_mail(new_mail{|m| m.to 'd@example.com'; m.cc []})}.must_equal [:bcd, 'd'] check{app.process_mail(new_mail{|m| m.to '123d@example.com123'; m.cc []})}.must_equal [:bcd, '123d'] check{app.process_mail(new_mail{|m| m.to 'd2@example.com'; m.cc []})}.must_equal [:bcd, 'd2'] check{app.process_mail(new_mail{|m| m.to '123d2@example.com123'; m.cc []})}.must_equal [:bcd, '123d2'] check{app.process_mail(new_mail{|m| m.to 'e@example.com'; m.cc []})}.must_equal [:cde, 'e', ''] check{app.process_mail(new_mail{|m| m.to '123e@example.com123'; m.cc []})}.must_equal [:cde, 'e', '123'] check{proc{app.process_mail(new_mail{|m| m.to 'x@example.com'})}.must_raise Roda::RodaPlugins::MailProcessor::UnhandledMail}.must_equal [:x] end it "supports mail_recipients class method to set recipients of mail, respected by rcpt methods" do @processed = processed = [] app(:mail_processor) do |r| r.handle_rcpt('a@example.com') do processed << :a end r.handle do processed << :f end end app.rcpt('b@example.com') do |r| r.handle do processed << :b end end check{app.process_mail(new_mail)}.must_equal [:a] check{app.process_mail(new_mail{|m| m.to 'b@example.com'})}.must_equal [:b] check{app.process_mail(new_mail{|m| m.to 'e@example.com'})}.must_equal [:f] app.mail_recipients do if smtp_rcpt = header['X-SMTP-To'] smtp_rcpt = smtp_rcpt.decoded end Array(smtp_rcpt) end check{app.process_mail(new_mail)}.must_equal [:f] check{app.process_mail(new_mail{|m| m.header['X-SMTP-To'] = 'a@example.com'})}.must_equal [:a] check{app.process_mail(new_mail{|m| m.header['X-SMTP-To'] = 'b@example.com'})}.must_equal [:b] end it "supports #mail_text, .mail_text, and r.text for allowing the ability to extract text from mails" do @processed = processed = [] app(:mail_processor) do |r| r.handle_text(/Found (foo|bar)/) do |x| processed << :f << x end r.text(/Found (baz|quux)/) do |x| r.handle do processed << :f2 << x << mail_text end end r.handle do processed << :nf << mail_text end end check{app.process_mail(new_mail)}.must_equal [:nf, 'Bod'] check{app.process_mail(new_mail{|m| m.body "Found bar\n--\nFound foo"})}.must_equal [:f, 'bar'] check{app.process_mail(new_mail{|m| m.body "> Found baz\nFound quux"})}.must_equal [:f2, 'baz', "> Found baz\nFound quux"] @app.mail_text do text = mail.body.decoded.gsub(/^>[^\r\n]*\r?\n/m, '') text.split(/\r?\n--\r?\n/).last end check{app.process_mail(new_mail)}.must_equal [:nf, 'Bod'] check{app.process_mail(new_mail{|m| m.body "Found bar\n--\nFound foo"})}.must_equal [:f, 'foo'] check{app.process_mail(new_mail{|m| m.body "> Found baz\nFound quux"})}.must_equal [:f2, 'quux', "Found quux"] end it "works with route_block_args plugin" do @processed = processed = [] app(:bare) do plugin :mail_processor plugin :route_block_args do [to, from] end route do |t, f| request.handle do processed << t << f end end handled_mail do # processed << :h << mail.to.first end end check{app.process_mail(new_mail)}.must_equal [["a@example.com"], ["b@example.com"]] end it "works with hooks plugin, calling after hook before *_mail hooks" do @processed = processed = [] app(:bare) do plugin :mail_processor plugin :hooks before do processed << 1 end after do processed << 2 end route do |r| processed << 3 r.handle_to('a@example.com') do end end handled_mail do processed << 4 end unhandled_mail do processed << 5 end after_mail do processed << 6 end end check{app.process_mail(new_mail)}.must_equal [1, 3, 2, 4, 6] check{app.process_mail(new_mail{|m| m.to 'x@example.com'})}.must_equal [1, 3, 2, 5, 6] end end end �����������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/mailer_spec.rb�������������������������������������������������0000664�0000000�0000000�00000022574�15167207754�0022462�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" begin require 'mail' rescue LoadError warn "mail not installed, skipping mail plugin test" else Mail.defaults do delivery_method :test end describe "mailer plugin" do def deliveries Mail::TestMailer.deliveries end after do deliveries.clear end setup_email = lambda do from "f@example.com" to "t@example.com" subject 's' end it "supports sending emails via the routing tree" do app(:mailer) do |r| r.mail do instance_exec(&setup_email) cc "c@example.com" bcc "b@example.com" response['X-Foo'] = 'Bar' "b" end end m = app.mail('/foo') deliveries.must_equal [] m.from.must_equal ['f@example.com'] m.to.must_equal ['t@example.com'] m.cc.must_equal ['c@example.com'] m.bcc.must_equal ['b@example.com'] m.subject.must_equal 's' m.body.must_be :==, 'b' m.header['X-Foo'].to_s.must_equal 'Bar' m.deliver! deliveries.must_equal [m] deliveries.clear m = app.sendmail('/foo') deliveries.must_equal [m] m.from.must_equal ['f@example.com'] m.to.must_equal ['t@example.com'] m.cc.must_equal ['c@example.com'] m.bcc.must_equal ['b@example.com'] m.subject.must_equal 's' m.body.must_be :==, 'b' m.header['X-Foo'].to_s.must_equal 'Bar' end it "supports arguments to mail/sendmail methods, yielding them to the route blocks" do app(:mailer) do |r| instance_exec(&setup_email) r.mail "foo" do |*args| "foo#{args.inspect}" end r.mail :d do |*args| args.inspect end end app.mail('/foo', 1, 2).body.must_be :==, 'foo[1, 2]' app.sendmail('/bar', 1, 2).body.must_be :==, '["bar", 1, 2]' end it "supports keywords arguments to mail/sendmail methods, yielding them to the route blocks" do instance_eval(<<-'RUBY', __FILE__, __LINE__ + 1) app(:mailer) do |r| instance_exec(&setup_email) r.mail "foo" do |*args, **kw| "foo#{args.inspect}#{kw.to_a.inspect}" end r.mail :d do |*args, **kw| [args, kw.to_a].inspect end end RUBY app.mail('/foo', 1, a: 2).body.must_be :==, 'foo[1][[:a, 2]]' app.sendmail('/bar', 1, a: 2).body.must_be :==, '[["bar", 1], [[:a, 2]]]' end if RUBY_VERSION >= "2" it "supports no_mail! method for skipping mailing" do app(:mailer) do |r| instance_exec(&setup_email) r.mail "foo" do |*args| no_mail! raise end end app.mail('/foo', 1, 2).must_be_nil app.sendmail('/foo', 1, 2).must_be_nil deliveries.must_equal [] end it "does not enforce terminal match by default" do app(:mailer) do |r| instance_exec(&setup_email) r.mail "foo" do |*args| "a" end end app.mail('/foo').body.must_be :==, 'a' app.mail('/foo/').body.must_be :==, 'a' end it "supports :terminal option for enforcing terminal match" do app(:bare) do plugin :mailer, :terminal=>true route do |r| instance_exec(&setup_email) r.mail "foo" do |*args| "a" end no_mail! end end app.mail('/foo').body.must_be :==, 'a' app.mail('/foo/').must_be_nil end it "supports attachments" do app(:mailer) do |r| r.mail do instance_exec(&setup_email) add_file __FILE__ end end m = app.mail('foo') m.attachments.length.must_equal 1 m.attachments.first.content_type.must_match(/mailer_spec\.rb/) m.content_type.must_match(/\Amultipart\/mixed/) m.parts.length.must_equal 1 m.parts.first.body.decoded.gsub("\r\n", "\n").must_equal File.read(__FILE__) end it "supports attachments with blocks" do app(:mailer) do |r| r.mail do instance_exec(&setup_email) add_file __FILE__ do response.mail.attachments.last.content_type = 'text/foo' end end end m = app.mail('foo') m.attachments.length.must_equal 1 m.attachments.first.content_type.must_equal 'text/foo' m.content_type.must_match(/\Amultipart\/mixed/) m.parts.length.must_equal 1 m.parts.first.body.decoded.gsub("\r\n", "\n").must_equal File.read(__FILE__) end it "supports plain-text attachments with an email body" do app(:mailer) do |r| r.mail do instance_exec(&setup_email) add_file :filename=>'a.txt', :content=>'b' 'c' end end m = app.mail('foo') m.parts.length.must_equal 2 m.encoded # necessary in mail 2.8.0+ to get content_type to return expected value m.parts.first.content_type.must_match(/text\/plain/) m.parts.first.body.must_be :==, 'c' m.parts.last.content_type.must_match(/text\/plain/) m.parts.last.body.must_be :==, 'b' m.attachments.length.must_equal 1 m.attachments.first.content_type.must_match(/a\.txt/) m.content_type.must_match(/\Amultipart\/mixed/) end it "does not override explicit content type for non-plain/text bodies in multipart emails" do app(:mailer) do |r| r.mail do instance_exec(&setup_email) response[RodaResponseHeaders::CONTENT_TYPE] = 'text/plain' response.mail.body 'c' response.mail.add_file :filename=>'a.html', :content=>'b' response.mail.parts.first.content_type = 'text/html' nil end end m = app.mail('foo') m.parts.length.must_equal 2 m.parts.first.content_type.must_match(/text\/html/) m.parts.first.body.must_be :==, 'c' m.parts.last.content_type.must_match(/text\/html/) m.parts.last.body.must_be :==, 'b' m.attachments.length.must_equal 1 m.attachments.first.content_type.must_match(/a\.html/) m.content_type.must_match(/\Amultipart\/mixed/) end it "supports regular web requests in same application" do app(:mailer) do |r| r.get "foo", :bar do |bar| "foo#{bar}" end r.mail "bar" do instance_exec(&setup_email) "b" end end body("/foo/baz", 'rack.input'=>rack_input).must_equal 'foobaz' app.mail('/bar').body.must_be :==, 'b' end it "should not have r.mail handle non-mail requests" do app(:mailer) do |r| r.mail "bar" do instance_exec(&setup_email) "b" end r.get "bar" do "foo" end end body("/bar").must_equal 'foo' end it "supports multipart email using text_part/html_pat" do app(:mailer) do |r| r.mail do instance_exec(&setup_email) text_part "t" html_part "h" end end m = app.mail('/foo') m.text_part.body.must_be :==, 't' m.html_part.body.must_be :==, 'h' m.content_type.must_match(/\Amultipart\/alternative/) end it "supports setting arbitrary email headers for multipart emails" do app(:mailer) do |r| r.mail do instance_exec(&setup_email) text_part "t", "X-Text"=>'T' html_part "h", "X-HTML"=>'H' end end m = app.mail('/foo') m.text_part.body.must_be :==, 't' m.text_part.header['X-Text'].to_s.must_equal 'T' m.html_part.body.must_be :==, 'h' m.html_part.header['X-HTML'].to_s.must_equal 'H' m.content_type.must_match(/\Amultipart\/alternative/) end it "raises error if mail object is not returned" do app(:mailer){} proc{app.mail('/')}.must_raise(Roda::RodaPlugins::Mailer::Error) end it "does not raise an error when using an explicitly empty body" do app(:mailer){""} app.mail('/') end it "supports setting the default content-type for emails when loading the plugin" do app(:bare) do plugin :mailer, :content_type=>'text/html' route{""} end app.mail('/').content_type.must_match(/\Atext\/html/) end it "supports loading the plugin multiple times" do app(:bare) do plugin :mailer, :content_type=>'text/html' plugin :mailer route{""} end app.mail('/').content_type.must_match(/\Atext\/html/) end it "supports manually overridding the default content-type for emails" do app(:bare) do plugin :mailer, :content_type=>'text/html' route do response[RodaResponseHeaders::CONTENT_TYPE] = 'text/foo' "" end end app.mail('/').content_type.must_match(/\Atext\/foo/) end it "supports setting the default content type when attachments are used" do app(:bare) do plugin :mailer, :content_type=>'text/html' route do add_file 'spec/assets/css/raw.css' "a" end end m = app.mail('/') m.content_type.must_match(/\Amultipart\/mixed/) m.parts.length.must_equal 2 m.parts.first.content_type.must_match(/\Atext\/html/) m.parts.first.body.must_be :==, "a" m.parts.last.content_type.must_match(/\Atext\/css/) m.parts.last.body.decoded.gsub("\r\n", "\n").must_equal File.read('spec/assets/css/raw.css') end it "works with route_block_args plugin" do app(:bare) do plugin :mailer plugin :route_block_args do [request.path] end route do |path| path end end app.mail('/').body.decoded.must_equal '/' app.mail('/foo').body.decoded.must_equal '/foo' end it "works with hooks plugin" do x = [] app(:bare) do plugin :mailer plugin :hooks before do x << 1 end after do x << 2 end route do x << 3 '' end end app.mail('/').body.decoded.must_equal '' x.must_equal [1, 3, 2] end end end ������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/map_matcher_spec.rb��������������������������������������������0000664�0000000�0000000�00000001226�15167207754�0023460�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "map_matcher plugin" do it "allows for matching next segment to a hash key, yielding hash value" do app(:bare) do plugin :map_matcher route do |r| r.is :map=>{'a'=>'x', 'b'=>'y'} do |f| f end r.is({:map=>{'a'=>'x', 'b'=>'y'}}, :map=>{'c'=>'z'}) do |*a| a.join('-') end r.remaining_path end end body("/").must_equal '/' body("/a").must_equal 'x' body("/b").must_equal 'y' body("/c").must_equal '/c' body("/a/c").must_equal 'x-z' body("/b/c").must_equal 'y-z' body("/a/d").must_equal '/a/d' end end ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/match_affix_spec.rb��������������������������������������������0000664�0000000�0000000�00000002546�15167207754�0023457�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "match_affix plugin" do it "allows changing the match prefix/suffix" do app(:bare) do plugin :match_affix, "", /(\/|\z)/ route do |r| r.on "/albums" do |b| r.on "b/:id" do |id, s| "b-#{b}-#{id}-#{s.inspect}" end "albums-#{b}" end end end body("/albums/a/1").must_equal 'albums-/' body("/albums/b/1").must_equal 'b-/-1-""' end it "handles extra trailing slash only" do app(:bare) do plugin :match_affix, nil, /(?:\/\z|(?=\/|\z))/ route do |r| r.on "albums" do r.on "b" do "albums/b:#{r.remaining_path}" end "albums:#{r.remaining_path}" end end end body("/albums/a").must_equal 'albums:/a' body("/albums/a/").must_equal 'albums:/a/' body("/albums/b").must_equal 'albums/b:' body("/albums/b/").must_equal 'albums/b:' end it "allows changing the match prefix without suffix" do app(:bare) do plugin :match_affix, "" route do |r| r.on "/albums" do r.on "/b" do "albums-b-#{r.remaining_path}" end "albums-#{r.remaining_path}" end end end body("/albums/a/1").must_equal 'albums-/a/1' body("/albums/b/1").must_equal 'albums-b-/1' end end ����������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/match_hook_args_spec.rb����������������������������������������0000664�0000000�0000000�00000004503�15167207754�0024331�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "match_hook_args plugin" do it "yields matchers and block args to match hooks" do matches = [] app(:bare) do plugin :match_hook_args add_match_hook do |matchers, block_args| matches << [matchers, block_args, request.matched_path, request.remaining_path] end route do |r| r.on "foo" do r.on "bar" do r.get "baz" do "fbb" end "fb" end "f" end r.get "bar" do "b" end r.get "baz", Integer do |id| "b-#{id}" end r.root do "r" end "n" end end term = app::RodaRequest::TERM body("/foo").must_equal 'f' matches.must_equal [[%w"foo", [], "/foo", ""]] matches.clear body("/foo/bar").must_equal 'fb' matches.must_equal [[%w"foo", [], "/foo", "/bar"], [%w"bar", [], "/foo/bar", ""]] matches.clear body("/foo/bar/baz").must_equal 'fbb' matches.must_equal [[%w"foo", [], "/foo", "/bar/baz"], [%w"bar", [], "/foo/bar", "/baz"], [["baz", term], [], "/foo/bar/baz", ""]] matches.clear body("/bar").must_equal 'b' matches.must_equal [[["bar", term], [], "/bar", ""]] matches.clear body("/baz/1").must_equal 'b-1' matches.must_equal [[["baz", Integer, term], [1], "/baz/1", ""]] matches.clear body.must_equal 'r' matches.must_equal [[nil, nil, "", "/"]] matches.clear body('/x').must_equal 'n' matches.must_be_empty matches.clear body("/foo/baz").must_equal 'f' matches.must_equal [[%w"foo", [], "/foo", "/baz"]] matches.clear body("/foo/bar/bar").must_equal 'fb' matches.must_equal [[%w"foo", [], "/foo", "/bar/bar"], [%w"bar", [], "/foo/bar", "/bar"]] app.add_match_hook{|_,_|matches << :x } matches.clear body("/foo/bar/baz").must_equal 'fbb' matches.must_equal [[%w"foo", [], "/foo", "/bar/baz"], :x, [%w"bar", [], "/foo/bar", "/baz"], :x, [["baz", term], [], "/foo/bar/baz", ""], :x] app.freeze matches.clear body("/foo/bar/baz").must_equal 'fbb' matches.must_equal [[%w"foo", [], "/foo", "/bar/baz"], :x, [%w"bar", [], "/foo/bar", "/baz"], :x, [["baz", term], [], "/foo/bar/baz", ""], :x] app.opts[:match_hook_args].must_be :frozen? end end ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/match_hook_spec.rb���������������������������������������������0000664�0000000�0000000�00000003327�15167207754�0023320�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "match hook plugin" do it "matches verbs" do matches = [] app(:bare) do plugin :match_hook match_hook do matches << [request.matched_path, request.remaining_path] end route do |r| r.on "foo" do r.on "bar" do r.get "baz" do "fbb" end "fb" end "f" end r.get "bar" do "b" end r.root do "r" end "n" end end body("/foo").must_equal 'f' matches.must_equal [["/foo", ""]] matches.clear body("/foo/bar").must_equal 'fb' matches.must_equal [["/foo", "/bar"], ["/foo/bar", ""]] matches.clear body("/foo/bar/baz").must_equal 'fbb' matches.must_equal [["/foo", "/bar/baz"], ["/foo/bar", "/baz"], ["/foo/bar/baz", ""]] matches.clear body("/bar").must_equal 'b' matches.must_equal [["/bar", ""]] matches.clear body.must_equal 'r' matches.must_equal [["", "/"]] matches.clear body('/x').must_equal 'n' matches.must_be_empty matches.clear body("/foo/baz").must_equal 'f' matches.must_equal [["/foo", "/baz"]] matches.clear body("/foo/bar/bar").must_equal 'fb' matches.must_equal [["/foo", "/bar/bar"], ["/foo/bar", "/bar"]] app.match_hook{matches << :x } matches.clear body("/foo/bar/baz").must_equal 'fbb' matches.must_equal [["/foo", "/bar/baz"], :x, ["/foo/bar", "/baz"], :x, ["/foo/bar/baz", ""], :x] app.freeze matches.clear body("/foo/bar/baz").must_equal 'fbb' matches.must_equal [["/foo", "/bar/baz"], :x, ["/foo/bar", "/baz"], :x, ["/foo/bar/baz", ""], :x] end end ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/middleware_spec.rb���������������������������������������������0000664�0000000�0000000�00000016676�15167207754�0023334�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "middleware plugin" do [false, true].each do |def_call| meth = def_call ? :deprecated : :it send meth, "turns Roda app into middlware" do a2 = app(:bare) do plugin :middleware if def_call def call super end end route do |r| r.is "a" do "a2" end r.post "b" do "b2" end end end a3 = app(:bare) do plugin :middleware route do |r| r.get "a" do "a3" end r.get "b" do "b3" end end end app(:bare) do use a3 use a2 route do |r| r.is "a" do "a1" end r.is "b" do "b1" end end end body('/a').must_equal 'a3' body('/b').must_equal 'b3' body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'a2' body('/b', 'REQUEST_METHOD'=>'POST').must_equal 'b2' body('/a', 'REQUEST_METHOD'=>'PATCH').must_equal 'a2' body('/b', 'REQUEST_METHOD'=>'PATCH').must_equal 'b1' end send meth, "supports :forward_response_headers middleware option" do mid1 = app(:bare) do plugin :middleware, :forward_response_headers=>true def call; super end if def_call route do |r| response['a'] = 'A1' response['b'] = 'B1' end end mid2 = app(:bare) do plugin :middleware def call; super end if def_call route do |r| response['c'] = 'C1' response['d'] = 'D1' end end app(:bare) do use mid1 use mid2 def call; super end if def_call route do |r| response['a'] = 'A2' response['c'] = 'C2' r.root do 'body' end end end header('a').must_equal 'A2' header('b').must_equal 'B1' header('c').must_equal 'C2' header('d').must_be_nil end end it "makes it still possible to use the Roda app normally" do app(:middleware) do "a" end body.must_equal 'a' end deprecated "makes it still possible to use the Roda app normally when #call is overwritten" do app(:bare) do plugin :middleware def call super end route do "a" end end app body.must_equal 'a' end it "makes middleware always use a subclass of the app" do app(:middleware) do |r| r.get{opts[:a]} end app.opts[:a] = 'a' a = app app(:bare) do use a route{} end body.must_equal 'a' a.opts[:a] = 'b' body.must_equal 'a' end it "sets temporary name of the subclass" do app(:middleware) do |r| r.get{self.class.name || "anonymous"} end a = app app(:bare) do use a route {} end body.must_include '::middleware_subclass' begin Object.const_set(:MyApp, a) app(:bare) do use a route {} end body.must_equal 'MyApp::middleware_subclass' ensure Object.send(:remove_const, :MyApp) end end if RUBY_VERSION >= "3.3" it "should raise error if attempting to use options for Roda application that does not support configurable middleware" do a1 = app(:bare){plugin :middleware} proc{app(:bare){use a1, :foo; route{}; build_rack_app}}.must_raise Roda::RodaError proc{app(:bare){use(a1){}; route{}; build_rack_app}}.must_raise Roda::RodaError end it "supports configuring middleware via a block" do a1 = app(:bare) do plugin :middleware do |mid, *args, &block| mid.opts[:a] = args.concat(block.call(:quux)).join(' ') end opts[:a] = 'a1' route do |r| r.is 'a' do opts[:a] end end end body('/a').must_equal 'a1' app(:bare) do use a1, :foo, :bar do |baz| [baz, :a1] end route do 'b1' end end body.must_equal 'b1' body('/a').must_equal 'foo bar quux a1' @app = a1 body('/a').must_equal 'a1' end it "is compatible with the multi_route plugin" do app(:bare) do plugin :multi_route plugin :middleware route("a") do |r| r.is "b" do "ab" end end route do |r| r.multi_route end end body('/a/b').must_equal 'ab' end it "uses the app's middleware if :include_middleware option is given" do mid = Struct.new(:app) do def call(env) env['foo'] = 'bar' app.call(env) end end app(:bare) do plugin :middleware, :include_middleware=>true use mid route{} end mid2 = app app(:bare) do use mid2 route{env['foo']} end body.must_equal 'bar' end it "calls :handle_result option with env and response" do app(:bare) do plugin :middleware, :handle_result=>(proc do |env, res| res[1].delete(RodaResponseHeaders::CONTENT_LENGTH) res[2] << env['foo'] end) route{} end mid2 = app app(:bare) do use mid2 route{env['foo'] = 'bar'; 'baz'} end body.must_equal 'bazbar' end it "works with the route_block_args block when loaded before" do app(:bare) do plugin :middleware plugin :route_block_args do [request.path, response] end route do |path, res| request.get 'a' do res.write(path + '2') end end end a = app app(:bare) do use a route{|r| 'b'} end body('/a').must_equal '/a2' body('/x').must_equal 'b' end it "works with the route_block_args block when loaded after" do app(:bare) do plugin :route_block_args do [request.path, response] end plugin :middleware route do |path, res| request.get 'a' do res.write(path + '2') end end end a = app app(:bare) do use a route{|r| 'b'} end body('/a').must_equal '/a2' body('/x').must_equal 'b' end it "supports :env_var middleware option" do a2 = app(:bare) do plugin :middleware, :env_var=>'roda.fn' route do |r| r.is "a" do "a2" end r.post "b" do "b2" end end end a3 = app(:bare) do plugin :middleware, :env_var=>'roda.fn2' route do |r| r.get "a" do "a3" end r.get "b" do "b3" end end end app(:bare) do use a3 use a2 route do |r| r.is "a" do "a1" end r.is "b" do "b1" end end end body('/a').must_equal 'a3' body('/b').must_equal 'b3' body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'a2' body('/b', 'REQUEST_METHOD'=>'POST').must_equal 'b2' body('/a', 'REQUEST_METHOD'=>'PATCH').must_equal 'a2' body('/b', 'REQUEST_METHOD'=>'PATCH').must_equal 'b1' end it "supports :next_if_not_found plugin option" do a2 = app(:bare) do plugin :middleware, :next_if_not_found=>true route do |r| r.on "a" do r.is "b" do 'a-b' end end end end app(:bare) do use a2 route do |r| r.on 'a' do 'a' end 'c' end end body('/a').must_equal 'a' body('/a/b').must_equal 'a-b' body('/d').must_equal 'c' end end ������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/middleware_stack_spec.rb���������������������������������������0000664�0000000�0000000�00000005414�15167207754�0024505�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "middleware_stack plugin" do it "adds middleware_stack method for removing and inserting into middleware stack" do make_middleware = lambda do |name| Class.new do define_singleton_method(:name){name} attr_reader :app attr_reader :args attr_reader :block def initialize(app, *args, &block) @app = app @args = args @block = block end def call(env) (env['rack.record'] ||= []) << [self.class.name, args, block] app.call(env) end end end recorded = nil app(:middleware_stack) do |r| recorded = env['rack.record'] nil end status.must_equal 404 recorded.must_be_nil called = false app.middleware_stack.before{called = true}.use(make_middleware[:m1], :a1).must_be_nil unless_lint do called.must_equal false end status.must_equal 404 recorded.must_equal [[:m1, [:a1], nil]] app.middleware_stack.before{|m, *a| m.name == :m1}.use(make_middleware[:m2]).must_be_nil status.must_equal 404 recorded.must_equal [[:m2, [], nil], [:m1, [:a1], nil]] b = lambda{} app.middleware_stack.before{|m, *a| m.name == :m1}.use(make_middleware[:m3], :a2, :a3, &b).must_be_nil status.must_equal 404 recorded.must_equal [[:m2, [], nil], [:m3, [:a2, :a3], b], [:m1, [:a1], nil]] app.middleware_stack.after{|m, *a| m.name == :m4}.use(make_middleware[:m4]).must_be_nil status.must_equal 404 recorded.must_equal [[:m2, [], nil], [:m3, [:a2, :a3], b], [:m1, [:a1], nil], [:m4, [], nil]] app.middleware_stack.after{|m, *a| m.name == :m4}.use(make_middleware[:m5]).must_be_nil status.must_equal 404 recorded.must_equal [[:m2, [], nil], [:m3, [:a2, :a3], b], [:m1, [:a1], nil], [:m4, [], nil], [:m5, [], nil]] app.middleware_stack.after{|m, *a| a == [:a1]}.use(make_middleware[:m6]).must_be_nil status.must_equal 404 recorded.must_equal [[:m2, [], nil], [:m3, [:a2, :a3], b], [:m1, [:a1], nil], [:m6, [], nil], [:m4, [], nil], [:m5, [], nil]] app.middleware_stack.remove{|m, *a| a.empty?}.must_be_nil status.must_equal 404 recorded.must_equal [[:m3, [:a2, :a3], b], [:m1, [:a1], nil]] sp = app.middleware_stack.after{|m, *a| m.name == :m3} sp.use(make_middleware[:m7]) sp.use(make_middleware[:m8]) status.must_equal 404 recorded.must_equal [[:m3, [:a2, :a3], b], [:m7, [], nil], [:m8, [], nil], [:m1, [:a1], nil]] sp = app.middleware_stack.before{|m, *a| m.name == :m8} sp.use(make_middleware[:m9]) sp.use(make_middleware[:m10]) status.must_equal 404 recorded.must_equal [[:m3, [:a2, :a3], b], [:m7, [], nil], [:m9, [], nil], [:m10, [], nil], [:m8, [], nil], [:m1, [:a1], nil]] end end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/module_include_spec.rb�����������������������������������������0000664�0000000�0000000�00000002617�15167207754�0024175�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "module_include plugin" do it "must_include given module in request or response class" do app(:bare) do plugin :module_include request_module(Module.new{def h; halt response.finish end}) response_module(Module.new{def finish; [212, {}, []] end}) route do |r| r.h end end req.must_equal [212, {}, []] end it "should accept blocks and turn them into modules" do app(:bare) do plugin :module_include request_module{def h; halt response.finish end} response_module{def finish; [212, {}, []] end} route do |r| r.h end end req.must_equal [212, {}, []] end it "should work if called multiple times with a block" do app(:bare) do plugin :module_include request_module{def h; halt response.f end} request_module{def i; h end} response_module{def f; finish end} response_module{def finish; [212, {}, []] end} route do |r| r.i end end req.must_equal [212, {}, []] end it "should not allow both blocks and modules to be passed in single call" do app(:bare){} @app.plugin :module_include proc{@app.request_module(Module.new){}}.must_raise Roda::RodaError end it "allows calling without block or module" do app(:bare){} @app.plugin :module_include @app.request_module end end �����������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/multi_public_spec.rb�������������������������������������������0000664�0000000�0000000�00000013331�15167207754�0023670�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "multi_public plugin" do it "adds r.multi_public for serving static files from public folder" do app(:bare) do plugin :multi_public, :a => 'spec/views', :b => 'spec' route do |r| r.on 'static' do r.multi_public(:b) end r.multi_public(:a) end end status("/about/_test.erb\0").must_equal 404 body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') body('/static/views/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') body('/foo/.././/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it "respects the application's :root option" do app(:bare) do opts[:root] = File.expand_path('../../', __FILE__) plugin :multi_public, :a => 'views', :b => '.' route do |r| r.on 'static' do r.multi_public(:b) end r.multi_public(:a) end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') body('/static/views/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it "appends to directories if loaded a second time" do app(:bare) do plugin :multi_public, :a => 'spec/views' plugin :multi_public, :b => 'spec' route do |r| r.on 'static' do r.multi_public(:b) end r.multi_public(:a) end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') body('/static/views/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') body('/foo/.././/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it "support headers and default mime types per directory" do app(:bare) do plugin :multi_public, :a => ['spec/views', {'x-foo' => 'bar'}, nil], :b => ['spec', nil, 'foo/bar'] route do |r| r.on 'static' do r.multi_public(:b) end r.multi_public(:a) end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_TYPE, '/about/_test.erb').must_equal 'text/plain' header('x-foo', '/about/_test.erb').must_equal 'bar' body('/static/views/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_TYPE, '/static/views/about/_test.erb').must_equal 'foo/bar' header('x-foo', '/static/views/about/_test.erb').must_be_nil end it "loads the public plugin with the given options" do app(:bare) do plugin :multi_public, {}, :root=>'spec/views', :headers=>{'x-foo' => 'bar'}, :default_mime=>'foo/bar' route do |r| r.public end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_TYPE, '/about/_test.erb').must_equal 'foo/bar' header('x-foo', '/about/_test.erb').must_equal 'bar' end types = [ [:gzip, 'gzip', '.gz'], [:brotli, 'br', '.br'], [:zstd, 'zstd', '.zst'], ] types.each do |type, accept, ext| [true, false].each do |use_encodings| opts = {} if use_encodings opts[:encodings] = [[accept, ext]] opts[:encodings] << ['zstd', '.zst'] if type == :brotli opts[:encodings] << ['gzip', '.gz'] unless type == :gzip else opts[:gzip] = opts[type] = true end it "handles serving files with #{ext} extension if client supports accepts #{accept} encoding when :encodings is #{'not ' unless use_encodings}given" do app(:bare) do plugin :multi_public, {:a => 'spec/views', :b => 'spec'}, opts route do |r| r.on 'static' do r.multi_public(:b) end r.multi_public(:a) end end ['', '/static/views'].each do |prefix| body("#{prefix}/about/_test.erb").must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_ENCODING, '/about/_test.erb').must_be_nil body("#{prefix}/about.erb").must_equal File.read('spec/views/about.erb') header(RodaResponseHeaders::CONTENT_ENCODING, '/about.erb').must_be_nil accept_encoding = "deflate,#{' gzip,' unless type == :gzip} #{accept}" body("#{prefix}/about/_test.erb", 'HTTP_ACCEPT_ENCODING'=>accept_encoding).must_equal File.binread("spec/views/about/_test.erb.gz") h = req("#{prefix}/about/_test.erb", 'HTTP_ACCEPT_ENCODING'=>accept_encoding)[1] h[RodaResponseHeaders::CONTENT_ENCODING].must_equal 'gzip' h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/plain' body("#{prefix}/about/_test2.css", 'HTTP_ACCEPT_ENCODING'=>accept_encoding).must_equal File.binread("spec/views/about/_test2.css#{ext}") h = req("#{prefix}/about/_test2.css", 'HTTP_ACCEPT_ENCODING'=>accept_encoding)[1] h[RodaResponseHeaders::CONTENT_ENCODING].must_equal accept h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/css' s, h, b = req("#{prefix}/about/_test2.css", 'HTTP_IF_MODIFIED_SINCE'=>h[RodaResponseHeaders::LAST_MODIFIED], 'HTTP_ACCEPT_ENCODING'=>accept_encoding) s.must_equal 304 h[RodaResponseHeaders::CONTENT_ENCODING].must_be_nil h[RodaResponseHeaders::CONTENT_TYPE].must_be_nil b.must_equal [] end end end end it "does not handle non-GET requests" do app(:bare) do plugin :multi_public, :a => 'spec/views' route do |r| r.multi_public(:a) end end status("/about/_test.erb", "REQUEST_METHOD"=>"POST").must_equal 404 end end �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/multi_route_spec.rb��������������������������������������������0000664�0000000�0000000�00000014165�15167207754�0023556�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "multi_route plugin" do before do app(:bare) do plugin :multi_route route("get") do |r| r.is "" do "get" end r.is "a" do "geta" end "getd" end route("post") do |r| r.is "" do "post" end r.is "a" do "posta" end "postd" end route(:p) do |r| r.is do 'p' end end route do |r| r.on 'foo' do r.multi_route do "foo" end r.on "p" do r.route(:p) end end r.get do r.route("get") r.is "b" do "getb" end end r.post do r.route("post") r.is "b" do "postb" end end end end end it "adds named routing support" do body.must_equal 'get' body('REQUEST_METHOD'=>'POST').must_equal 'post' body('/a').must_equal 'geta' body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'posta' body('/b').must_equal 'getb' body('/b', 'REQUEST_METHOD'=>'POST').must_equal 'postb' status('/c').must_equal 404 status('/c', 'REQUEST_METHOD'=>'POST').must_equal 404 end it "works when freezing the app" do app.freeze.must_equal app body.must_equal 'get' body('REQUEST_METHOD'=>'POST').must_equal 'post' body('/a').must_equal 'geta' body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'posta' body('/b').must_equal 'getb' body('/b', 'REQUEST_METHOD'=>'POST').must_equal 'postb' status('/c').must_equal 404 status('/c', 'REQUEST_METHOD'=>'POST').must_equal 404 proc{app.route("foo"){}}.must_raise end it "uses multi_route to dispatch to any named route" do status('/foo').must_equal 404 body('/foo/get/').must_equal 'get' body('/foo/get/a').must_equal 'geta' body('/foo/post/').must_equal 'post' body('/foo/post/a').must_equal 'posta' body('/foo/post/b').must_equal 'foo' end it "does not have multi_route match non-String named routes" do body('/foo/p').must_equal 'p' status('/foo/p/2').must_equal 404 end it "has multi_route pick up routes newly added" do body('/foo/get/').must_equal 'get' status('/foo/delete').must_equal 404 app.route('delete'){|r| r.on{'delete'}} body('/foo/delete').must_equal 'delete' end it "makes multi_route match longest route if multiple routes have the same prefix" do app.route("post/a"){|r| r.on{"pa2"}} app.route("get/a"){|r| r.on{"ga2"}} status('/foo').must_equal 404 body('/foo/get/').must_equal 'get' body('/foo/get/a').must_equal 'ga2' body('/foo/post/').must_equal 'post' body('/foo/post/a').must_equal 'pa2' body('/foo/post/b').must_equal 'foo' end it "handles loading the plugin multiple times correctly" do app.plugin :multi_route body.must_equal 'get' body('REQUEST_METHOD'=>'POST').must_equal 'post' body('/a').must_equal 'geta' body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'posta' body('/b').must_equal 'getb' body('/b', 'REQUEST_METHOD'=>'POST').must_equal 'postb' status('/c').must_equal 404 status('/c', 'REQUEST_METHOD'=>'POST').must_equal 404 end it "handles subclassing correctly" do @app = Class.new(@app) @app.route do |r| r.get do r.route("post") r.is "b" do "1b" end end r.post do r.route("get") r.is "b" do "2b" end end end body.must_equal 'post' body('REQUEST_METHOD'=>'POST').must_equal 'get' body('/a').must_equal 'posta' body('/a', 'REQUEST_METHOD'=>'POST').must_equal 'geta' body('/b').must_equal '1b' body('/b', 'REQUEST_METHOD'=>'POST').must_equal '2b' status('/c').must_equal 404 status('/c', 'REQUEST_METHOD'=>'POST').must_equal 404 end it "uses the named route return value in multi_route if no block is given" do app.route{|r| r.multi_route} body('/get').must_equal 'getd' body('/post').must_equal 'postd' end end describe "multi_route plugin" do it "r.multi_route handles loading the same route more than once" do app(:multi_route) do |r| r.multi_route end app.route('foo'){'bar'} body('/foo').must_equal 'bar' app.route('foo'){'baz'} body('/foo').must_equal 'baz' end end describe "multi_route plugin" do it "r.multi_route raises error for invalid namespace" do app(:multi_route) do |r| r.is('a'){r.multi_route('foo')} r.multi_route 'a' end proc{body}.must_raise Roda::RodaError proc{body('/a')}.must_raise Roda::RodaError end end describe "multi_route plugin" do before do app(:bare) do plugin :multi_route route("foo", "foo") do |r| "#{@p}ff" end route("bar", "foo") do |r| "#{@p}fb" end route("foo", "bar") do |r| "#{@p}bf" end route("bar", "bar") do |r| "#{@p}bb" end end end it "handles namespaces in r.route" do app.route("foo") do |r| @p = 'f' r.on("foo"){r.route("foo", "foo")} r.on("bar"){r.route("bar", "foo")} @p end app.route("bar") do |r| @p = 'b' r.on("foo"){r.route("foo", "bar")} r.on("bar"){r.route("bar", "bar")} @p end app.route do |r| r.on("foo"){r.route("foo")} r.on("bar"){r.route("bar")} end body('/foo').must_equal 'f' body('/foo/foo').must_equal 'fff' body('/foo/bar').must_equal 'ffb' body('/bar').must_equal 'b' body('/bar/foo').must_equal 'bbf' body('/bar/bar').must_equal 'bbb' end it "handles namespaces in r.multi_route" do app(:multi_route) do |path| request.multi_route path end app.plugin :route_block_args do [request.path, request] end app.route("foo") do |path, r| r.multi_route("foo") "f-#{path}" end app.route("bar", "foo") do |path| "b-#{path}" end body.must_equal '/' body('/foo').must_equal 'f-/foo' body('/foo/bar').must_equal 'b-/foo/bar' end end �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/multi_run_spec.rb����������������������������������������������0000664�0000000�0000000�00000013510�15167207754�0023215�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "multi_run plugin" do it "adds Roda.run method for setting up prefix delegations to other rack apps" do app(:multi_run) do |r| r.multi_run "c" end app.run "a", Class.new(Roda).class_eval{route{"a1"}; app} body("/a").must_equal 'a1' body("/b").must_equal 'c' body("/b/a").must_equal 'c' body.must_equal 'c' app.run "b", Class.new(Roda).class_eval{route{"b1"}; app} body("/a").must_equal 'a1' body("/b").must_equal 'b1' body("/b/a").must_equal 'b1' body.must_equal 'c' app.run "b/a", Class.new(Roda).class_eval{route{"b2"}; app} body("/a").must_equal 'a1' body("/b").must_equal 'b1' body("/b/a").must_equal 'b2' body.must_equal 'c' end it "works when freezing the app" do app(:multi_run) do |r| r.multi_run "c" end app.run "a", Class.new(Roda).class_eval{route{"a1"}; app} app.run "b", Class.new(Roda).class_eval{route{"b1"}; app} app.run "b/a", Class.new(Roda).class_eval{route{"b2"}; app} app.freeze body("/a").must_equal 'a1' body("/b").must_equal 'b1' body("/b/a").must_equal 'b2' body.must_equal 'c' proc{app.run "a", Class.new(Roda).class_eval{route{"a1"}; app}}.must_raise end it "supports multi_run_apps" do app(:multi_run){|r|} app.multi_run_apps.must_equal({}) a = Class.new(Roda).class_eval{route{"a1"}; app} app.run :a, a app.multi_run_apps.must_equal('a'=>a) end it "works when subclassing" do app(:multi_run) do |r| r.multi_run "c" end app.run "a", Class.new(Roda).class_eval{route{"a1"}; app} body("/a").must_equal 'a1' a = app @app = Class.new(a) a.run "b", Class.new(Roda).class_eval{route{"b2"}; app} app.run "b", Class.new(Roda).class_eval{route{"b1"}; app} body("/a").must_equal 'a1' body("/b").must_equal 'b1' @app = a body("/b").must_equal 'b2' end it "yields prefix" do yielded = false app(:multi_run) do |r| r.multi_run do |prefix| yielded = prefix end end app.run "a", Class.new(Roda).class_eval{route{"a1"}; app} body("/a").must_equal "a1" yielded.must_equal "a" end it "allows removing dispatching to apps" do app(:multi_run) do |r| r.multi_run "c" end app.run "a", Class.new(Roda).class_eval{route{"a1"}; app} body("/a").must_equal 'a1' app.run "a" body("/a").must_equal 'c' end end describe "multi_run plugin with blocks for Roda.run" do it "adds Roda.run method for setting up prefix delegations to other rack apps" do app(:multi_run) do |r| r.multi_run "c" end loaded = [] app.run("a"){loaded << :a1; Class.new(Roda).class_eval{route{"a1"}; app}} body("/a").must_equal 'a1' body("/b").must_equal 'c' body("/b/a").must_equal 'c' body.must_equal 'c' loaded.must_equal [:a1] app.run("b"){loaded << :b1; Class.new(Roda).class_eval{route{"b1"}; app}} body("/a").must_equal 'a1' body("/b").must_equal 'b1' body("/b/a").must_equal 'b1' body.must_equal 'c' loaded.must_equal [:a1, :a1, :b1, :b1] app.run("b/a"){loaded << :b2; Class.new(Roda).class_eval{route{"b2"}; app}} body("/a").must_equal 'a1' body("/b").must_equal 'b1' body("/b/a").must_equal 'b2' body.must_equal 'c' loaded.must_equal [:a1, :a1, :b1, :b1, :a1, :b1, :b2] end it "works when freezing the app" do app(:multi_run) do |r| r.multi_run "c" end app.run("a"){Class.new(Roda).class_eval{route{"a1"}; app}} app.run("b"){Class.new(Roda).class_eval{route{"b1"}; app}} app.run("b/a"){Class.new(Roda).class_eval{route{"b2"}; app}} app.freeze body("/a").must_equal 'a1' body("/b").must_equal 'b1' body("/b/a").must_equal 'b2' body.must_equal 'c' proc{app.run "a", Class.new(Roda).class_eval{route{"a1"}; app}}.must_raise end it "works when subclassing" do app(:multi_run) do |r| r.multi_run "c" end app.run("a"){Class.new(Roda).class_eval{route{"a1"}; app}} body("/a").must_equal 'a1' a = app @app = Class.new(a) a.run("b"){Class.new(Roda).class_eval{route{"b2"}; app}} app.run("b"){Class.new(Roda).class_eval{route{"b1"}; app}} body("/a").must_equal 'a1' body("/b").must_equal 'b1' @app = a body("/b").must_equal 'b2' end it "yields prefix" do yielded = false app(:multi_run) do |r| r.multi_run do |prefix| yielded = prefix end end app.run("a"){Class.new(Roda).class_eval{route{"a1"}; app}} body("/a").must_equal "a1" yielded.must_equal "a" end it "allows removing dispatching to apps" do app(:multi_run) do |r| r.multi_run "c" end app.run("a"){Class.new(Roda).class_eval{route{"a1"}; app}} body("/a").must_equal 'a1' app.run "a" body("/a").must_equal 'c' end it "does not allow registering both app and app block in same call" do app(:multi_run) do |r| r.multi_run "c" end proc{app.run("a", Class.new){}}.must_raise Roda::RodaError end it "registering app removes app block and vice versa" do app(:multi_run) do |r| r.multi_run "c" end body("/a").must_equal 'c' app.run "a", Class.new(Roda).class_eval{route{"a1"}; app} body("/a").must_equal 'a1' app.run("a"){Class.new(Roda).class_eval{route{"a2"}; app}} body("/a").must_equal 'a2' app.run "a", Class.new(Roda).class_eval{route{"a3"}; app} body("/a").must_equal 'a3' app.run "a" body("/a").must_equal 'c' end it "supports both apps and app blocks" do app(:multi_run) do |r| r.multi_run "c" end app.run "a", Class.new(Roda).class_eval{route{"a"}; app} app.run("b"){Class.new(Roda).class_eval{route{"b"}; app}} body("/a").must_equal 'a' body("/b").must_equal 'b' end end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/multi_view_spec.rb���������������������������������������������0000664�0000000�0000000�00000002527�15167207754�0023371�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping multi_view plugin test" else describe "multi_view plugin" do before do app(:bare) do plugin :render, :views=>'spec/views', :layout=>'layout-yield' plugin :multi_view route do |r| r.multi_view(['a', 'b', 'c']) end end end it "supports easy rendering of multiple views by name" do body('/a').gsub(/\s+/, '').must_equal "HeaderaFooter" body('/b').gsub(/\s+/, '').must_equal "HeaderbFooter" body('/c').gsub(/\s+/, '').must_equal "HeadercFooter" status('/d').must_equal 404 status('/a', 'REQUEST_METHOD'=>'POST').must_equal 404 end end describe "multi_view plugin multi_view_compile method " do before do app(:bare) do plugin :render, :views=>'spec/views', :layout=>'layout-yield' plugin :multi_view regexp = multi_view_compile(['a', 'b', 'c']) route do |r| r.multi_view(regexp) end end end it "supports easy rendering of multiple views by name" do body('/a').gsub(/\s+/, '').must_equal "HeaderaFooter" body('/b').gsub(/\s+/, '').must_equal "HeaderbFooter" body('/c').gsub(/\s+/, '').must_equal "HeadercFooter" status('/d').must_equal 404 status('/a', 'REQUEST_METHOD'=>'POST').must_equal 404 end end end �������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/multibyte_string_matcher_spec.rb�������������������������������0000664�0000000�0000000�00000002150�15167207754�0026304�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "multibyte_string_matcher plugin" do it "uses multibyte safe string matching" do str = "\xD0\xB8".dup.force_encoding('UTF-8') app(:unescape_path) do |r| r.is String do |s| s end r.is(Integer, /(#{str})/u) do |_, a| a end r.is(Integer, Integer, str) do 'm' end r.is(Integer, str, Integer) do 'n' end end body('/%D0%B8').must_equal str body('/1/%D0%B8').must_equal str status('/1/%D0%B82').must_equal 404 status('/1/2/%D0%B8').must_equal 404 status('/1/%D0%B8/2').must_equal 404 status('/1/%D0%B9').must_equal 404 status('/1/2/%D0%B9').must_equal 404 status('/1/%D0%B9/2').must_equal 404 @app.plugin :multibyte_string_matcher body('/%D0%B8').must_equal str body('/1/%D0%B8').must_equal str body('/1/2/%D0%B8').must_equal 'm' body('/1/%D0%B8/2').must_equal 'n' status('/1/%D0%B82').must_equal 404 status('/1/%D0%B9').must_equal 404 status('/1/2/%D0%B9').must_equal 404 status('/1/%D0%B9/2').must_equal 404 end end ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/named_routes_spec.rb�������������������������������������������0000664�0000000�0000000�00000002524�15167207754�0023667�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "named_routes plugin" do before do app(:bare) do plugin :named_routes route(:p) do |r| r.is do 'p' end end route(:q, :b) do |r| r.is do 'q' end end route do |r| r.on "p" do r.route(:p) end r.on "q" do r.route(:q, :b) end end end end it "adds named routing support" do body('/p').must_equal 'p' body('/q').must_equal 'q' status('/').must_equal 404 status('/b').must_equal 404 status('/p/').must_equal 404 status('/q/a').must_equal 404 end it "works when freezing the app" do app.freeze body('/p').must_equal 'p' body('/q').must_equal 'q' status('/').must_equal 404 status('/b').must_equal 404 status('/p/').must_equal 404 status('/q/a').must_equal 404 end it "works when subclassing the app" do @app = Class.new(@app) body('/p').must_equal 'p' body('/q').must_equal 'q' status('/').must_equal 404 status('/b').must_equal 404 status('/p/').must_equal 404 status('/q/a').must_equal 404 end it "allows removing a hash branch" do status('/p').must_equal 200 2.times do app.route(:p) proc{status('/p')}.must_raise TypeError end end end ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/named_templates_spec.rb����������������������������������������0000664�0000000�0000000�00000003710�15167207754�0024342�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping named_templates plugin test" else describe "named_templates plugin" do it "adds template method method for naming templates, and have render recognize it" do app(:bare) do plugin :named_templates template :foo do @b = 2 "foo<%= @a %><%= @b %>" end template :layout, :engine=>:str do @c = 3 'bar#{@a}#{@c}-#{yield}-baz' end route do |r| @a = 1 view(:foo) end end body.must_equal 'bar13-foo12-baz' @app = Class.new(@app) body.must_equal 'bar13-foo12-baz' end it "works when freezing the app" do app(:bare) do plugin :named_templates template :foo do @b = 2 "foo<%= @a %><%= @b %>" end template :layout, :engine=>:str do @c = 3 'bar#{@a}#{@c}-#{yield}-baz' end route do |r| @a = 1 view(:foo) end end app.freeze body.must_equal 'bar13-foo12-baz' proc{app.template(:b){"a"}}.must_raise end it "works with the view_options plugin" do app(:bare) do plugin :render plugin :view_options plugin :named_templates template "foo/bar" do @b = 2 "foobar<%= @a %><%= @b %>" end template "foo/layout", :engine=>:str do @c = 3 'foo#{@a}#{@c}-#{yield}-baz' end template "bar/layout", :engine=>:str do @c = 3 'bar#{@a}#{@c}-#{yield}-baz' end route do |r| r.is 'foo' do set_view_subdir :foo @a = 1 view(:bar) end r.is 'bar' do set_view_subdir :bar @a = 4 @b = 2 view(:inline=>"barfoo<%= @a %><%= @b %>") end end end body('/foo').must_equal 'foo13-foobar12-baz' body('/bar').must_equal 'bar43-barfoo42-baz' end end end ��������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/not_allowed_spec.rb��������������������������������������������0000664�0000000�0000000�00000003753�15167207754�0023516�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "not_allowed plugin" do it "skips the current block if pass is called" do app(:not_allowed) do |r| r.root do 'a' end r.is "c" do r.get do "cg" end r.post do "cp" end end r.on "q" do r.is do r.get do "q" end end end r.get do r.is 'b' do 'b' end r.is(/(d)/) do |s| s end r.get(/(e)/) do |s| s end end end body.must_equal 'a' s, h, b = req('REQUEST_METHOD'=>'POST') s.must_equal 405 h[RodaResponseHeaders::ALLOW].must_equal 'GET' b.must_be_empty body('/b').must_equal 'b' status('/b', 'REQUEST_METHOD'=>'POST').must_equal 404 body('/d').must_equal 'd' status('/d', 'REQUEST_METHOD'=>'POST').must_equal 404 body('/e').must_equal 'e' status('/e', 'REQUEST_METHOD'=>'POST').must_equal 404 body('/q').must_equal 'q' s, _, b = req('/q', 'REQUEST_METHOD'=>'POST') s.must_equal 405 b.must_be_empty body('/c').must_equal 'cg' body('/c', 'REQUEST_METHOD'=>'POST').must_equal 'cp' s, h, b = req('/c', 'REQUEST_METHOD'=>'PATCH') s.must_equal 405 h[RodaResponseHeaders::ALLOW].must_equal 'GET, POST' b.must_be_empty @app.plugin :head header(RodaResponseHeaders::ALLOW, 'REQUEST_METHOD'=>'POST').must_equal 'HEAD, GET' header(RodaResponseHeaders::ALLOW, '/c', 'REQUEST_METHOD'=>'PATCH').must_equal 'HEAD, GET, POST' @app.plugin :status_handler @app.status_handler(405, :keep_headers=>[RodaResponseHeaders::ALLOW]){'a'} s, h, b = req('REQUEST_METHOD'=>'POST') s.must_equal 405 h[RodaResponseHeaders::ALLOW].must_equal 'HEAD, GET' b.must_equal ['a'] s, h, b = req('/c', 'REQUEST_METHOD'=>'PATCH') s.must_equal 405 h[RodaResponseHeaders::ALLOW].must_equal 'HEAD, GET, POST' b.must_equal ['a'] end end ���������������������jeremyevans-roda-4f30bb3/spec/plugin/not_found_spec.rb����������������������������������������������0000664�0000000�0000000�00000004327�15167207754�0023200�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "not_found plugin" do it "executes on no arguments" do app(:bare) do plugin :not_found not_found do "not found" end route do |r| r.on "a" do "found" end end end body.must_equal 'not found' status.must_equal 404 body("/a").must_equal 'found' status("/a").must_equal 200 end it "allows overriding status inside not_found" do app(:bare) do plugin :not_found not_found do response.status = 403 "not found" end route do |r| end end status.must_equal 403 end it "calculates correct Content-Length" do app(:bare) do plugin :not_found do "a" end route{} end header(RodaResponseHeaders::CONTENT_LENGTH).must_equal "1" end it "clears existing headers" do app(:bare) do plugin :not_found do || "a" end route do |r| response[RodaResponseHeaders::CONTENT_TYPE] = 'text/pdf' response['foo'] = 'bar' nil end end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/html' header('foo').must_be_nil end it "does not modify behavior if not_found is not called" do app(:not_found) do |r| r.on "a" do "found" end end body.must_equal '' body("/a").must_equal 'found' end it "can set not_found via the plugin block" do app(:bare) do plugin :not_found do "not found" end route do |r| r.on "a" do "found" end end end body.must_equal 'not found' body("/a").must_equal 'found' end it "does not modify behavior if body is not an array" do app(:bare) do plugin :not_found do "not found" end o = Object.new def o.each(&_); end route do |r| r.halt [404, {}, o] end end body.must_equal '' end it "does not modify behavior if body is not an empty array" do app(:bare) do plugin :not_found do "not found" end route do |r| response.status = 404 response.write 'a' end end body.must_equal 'a' end end ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/optimized_segment_matchers_spec.rb�����������������������������0000664�0000000�0000000�00000001335�15167207754�0026615�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "optimized_segment_matchers plugin" do it "should support on_segment and is_segment match methods" do app(:optimized_segment_matchers) do |r| r.on_segment do |a| r.is_segment do |b| "x-#{a}-#{b}" end "y-#{a}" end "r" end unless_lint do body('a').must_equal 'r' end body.must_equal 'r' body('/a').must_equal 'y-a' body('/a/').must_equal 'y-a' body('/b').must_equal 'y-b' body('/b/').must_equal 'y-b' body('/a/b').must_equal 'x-a-b' body('/b/a').must_equal 'x-b-a' body('/a/b/').must_equal 'y-a' body('/a/b/c').must_equal 'y-a' body('/a/b/c/').must_equal 'y-a' end end ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/optimized_string_matchers_spec.rb������������������������������0000664�0000000�0000000�00000001614�15167207754�0026461�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" describe "optimized_string_matchers plugin" do it "should support on_branch and is_exactly match methods" do app(:optimized_string_matchers) do |r| r.on_branch "e" do r.is_exactly "f" do "ef" end "ee" end r.on_branch "a" do r.on_branch "b" do r.is_exactly "c" do "c" end "b" end "a" end "cc" end body.must_equal 'cc' body('/a').must_equal 'a' body('/a/').must_equal 'a' body('/a/b/').must_equal 'b' body('/a/b/c').must_equal 'c' body('/a/b/c/').must_equal 'b' body('/a/b/c/d').must_equal 'b' body('/e').must_equal 'ee' body('/eb').must_equal 'cc' body('/e/').must_equal 'ee' body('/e/f').must_equal 'ef' body('/e/f/').must_equal 'ee' body('/e/fe').must_equal 'ee' end end ��������������������������������������������������������������������������������������������������������������������jeremyevans-roda-4f30bb3/spec/plugin/padrino_render_spec.rb�����������������������������������������0000664�0000000�0000000�00000001452�15167207754�0024174�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping padrino_render plugin test" else describe "padrino_render plugin" do before do app(:bare) do plugin :padrino_render, :views=>"./spec/views" route do |r| r.is "render" do render(:content=>'bar', :layout_opts=>{:locals=>{:title=>"Home"}}) end r.is "render/nolayout" do render("about", :locals=>{:title => "No Layout"}, :layout=>nil) end end end end it "render uses layout by default" do body("/render").strip.must_equal "<title>Roda: Home\nbar" end it "render doesn't use layout if layout is nil" do body("/render/nolayout").strip.must_equal "

No Layout

" end end end jeremyevans-roda-4f30bb3/spec/plugin/param_matchers_spec.rb000066400000000000000000000053061516720775400241710ustar00rootroot00000000000000require_relative "../spec_helper" describe "param_matchers plugin" do it "param! matcher should yield a param only if given and not empty" do app(:param_matchers) do |r| r.get "signup", :param! => "email" do |email| email end "No email" end io = rack_input body("/signup", "rack.input" => io, "QUERY_STRING" => "email=john@doe.com").must_equal 'john@doe.com' body("/signup", "rack.input" => io, "QUERY_STRING" => "").must_equal 'No email' body("/signup", "rack.input" => io, "QUERY_STRING" => "email=").must_equal 'No email' end it "param matcher should yield a param only if given" do app(:param_matchers) do |r| r.get "signup", :param=>"email" do |email| email end "No email" end io = rack_input body("/signup", "rack.input" => io, "QUERY_STRING" => "email=john@doe.com").must_equal 'john@doe.com' body("/signup", "rack.input" => io, "QUERY_STRING" => "").must_equal 'No email' body("/signup", "rack.input" => io, "QUERY_STRING" => "email=").must_equal '' end it "params! matcher should yield the params only if all are given and not empty" do app(:param_matchers) do |r| r.get "signup", :params! => %w"em ail" do |em, ail| em + ail end "No email" end io = rack_input body("/signup", "rack.input" => io, "QUERY_STRING" => "em=foo&ail=john@doe.com").must_equal 'foojohn@doe.com' body("/signup", "rack.input" => io, "QUERY_STRING" => "em=&ail=john@doe.com").must_equal 'No email' body("/signup", "rack.input" => io, "QUERY_STRING" => "em=foo&ail=").must_equal 'No email' body("/signup", "rack.input" => io, "QUERY_STRING" => "em=&ail=").must_equal 'No email' body("/signup", "rack.input" => io, "QUERY_STRING" => "em=foo").must_equal 'No email' body("/signup", "rack.input" => io, "QUERY_STRING" => "ail=john@doe.com").must_equal 'No email' end it "params matcher should yield the params only if all are given" do app(:param_matchers) do |r| r.get "signup", :params=>%w"em ail" do |em, ail| em + ail end "No email" end io = rack_input body("/signup", "rack.input" => io, "QUERY_STRING" => "em=foo&ail=john@doe.com").must_equal 'foojohn@doe.com' body("/signup", "rack.input" => io, "QUERY_STRING" => "em=&ail=john@doe.com").must_equal 'john@doe.com' body("/signup", "rack.input" => io, "QUERY_STRING" => "em=foo&ail=").must_equal 'foo' body("/signup", "rack.input" => io, "QUERY_STRING" => "em=&ail=").must_equal '' body("/signup", "rack.input" => io, "QUERY_STRING" => "em=foo").must_equal 'No email' body("/signup", "rack.input" => io, "QUERY_STRING" => "ail=john@doe.com").must_equal 'No email' end end jeremyevans-roda-4f30bb3/spec/plugin/params_capturing_spec.rb000066400000000000000000000025701516720775400245420ustar00rootroot00000000000000require_relative "../spec_helper" describe "params_capturing plugin" do it "should add captures to r.params for symbol matchers" do app(:params_capturing) do |r| r.on('foo', :y, :z, :w) do |y, z, w| (r.params.values_at('y', 'z', 'w') + [y, z, w, r.params['captures'].length]).join('-') end r.on(/(quux)/, /(foo)(bar)/) do |q, foo, bar| "y-#{r.params['captures'].join}-#{q}-#{foo}-#{bar}" end r.on(/(quux)/, :y) do |q, y| r.on(:x) do |x| "y-#{r.params['y']}-#{r.params['x']}-#{q}-#{y}-#{x}-#{r.params['captures'].length}" end "y-#{r.params['y']}-#{q}-#{y}-#{r.params['captures'].length}" end r.on('z') do r.send(:match, :y) ? 'yes' : 'no' end r.on(:x) do |x| "x-#{x}-#{r.params['x']}-#{r.params['captures'].length}" end end body('/blarg', 'rack.input'=>rack_input).must_equal 'x-blarg-blarg-1' body('/foo/1/2/3', 'rack.input'=>rack_input).must_equal '1-2-3-1-2-3-3' body('/quux/foobar', 'rack.input'=>rack_input).must_equal 'y-quuxfoobar-quux-foo-bar' body('/quux/asdf', 'rack.input'=>rack_input).must_equal 'y--quux-asdf-2' body('/quux/asdf/890', 'rack.input'=>rack_input).must_equal 'y--890-quux-asdf-890-3' body('/z', 'rack.input'=>rack_input).must_equal 'no' body('/z/x', 'rack.input'=>rack_input).must_equal 'yes' end end jeremyevans-roda-4f30bb3/spec/plugin/part_spec.rb000066400000000000000000000147341516720775400221560ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt' require 'tilt/erb' require 'tilt/string' require_relative '../../lib/roda/plugins/render' rescue LoadError warn "tilt not installed, skipping part plugin test" else describe "part plugin" do before do app(:bare) do plugin :render, :views=>"./spec/views", :check_paths=>true plugin :part route do |r| r.on "home" do part('layout', :title=>"Home"){part("home", :name => "Agent Smith", :title => "Home")} end r.on "about" do part("about", :title => "About Roda") end r.on "inline" do part({:inline=>"Hello <%= name %>"}, :name => "Agent Smith") end r.on "path" do part({:path=>"./spec/views/about.erb"}, :title => "Path") end r.on "render-block" do part('layout', :title=>"Home"){part("about", :title => "About Roda")} end end end end it "default actions" do body("/about").strip.must_equal "

About Roda

" body("/home").strip.must_equal "Roda: Home\n

Home

\n

Hello Agent Smith

" body("/inline").strip.must_equal "Hello Agent Smith" body("/path").strip.must_equal "

Path

" body("/render-block").strip.must_equal "Roda: Home\n

About Roda

" end it "with str as engine" do app.plugin :render, :engine => "str" body("/about").strip.must_equal "

About Roda

" body("/home").strip.must_equal "Roda: Home\n

Home

\n

Hello Agent Smith

" body("/inline").strip.must_equal "Hello <%= name %>" end if Roda::RodaPlugins::Render::FIXED_LOCALS_COMPILED_METHOD_SUPPORT [true, false].each do |cache_plugin_option| multiplier = cache_plugin_option ? 1 : 2 it "support fixed locals in layout templates with plugin option :cache=>#{cache_plugin_option}" do template = "comp_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true} plugin :part route do part("layout", title: "Home"){part(template)} end end layout_key = [:_render_locals, "layout"] template_key = [:_render_locals, template] app.render_opts[:template_method_cache][template_key].must_be_nil app.render_opts[:template_method_cache][layout_key].must_be_nil body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template_key].must_be_kind_of(Array) app.render_opts[:template_method_cache][layout_key].must_be_kind_of(Array) body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template_key].must_be_kind_of(Array) app.render_opts[:template_method_cache][layout_key].must_be_kind_of(Array) body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template_key].must_be_kind_of(Array) app.render_opts[:template_method_cache][layout_key].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * 2) end it "support fixed locals in render templates with plugin option :cache=>#{cache_plugin_option}" do template = "local_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true} plugin :part route do part(template, title: 'ct') end end key = [:_render_locals, template] app.render_opts[:template_method_cache][key].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end [true, false].each do |assume_fixed_locals_option| [true, false].each do |freeze_app| it "caches expectedly for cache: #{cache_plugin_option}, assume_fixed_locals: #{assume_fixed_locals_option} options when #{'not ' unless freeze_app}freezing app" do template = "opt_local_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true}, :assume_fixed_locals=>assume_fixed_locals_option, :layout=>false plugin :part route do |r| r.is 'a' do render(template) end part(template, title: 'ct') end freeze if freeze_app end cache_size = 1 key = if assume_fixed_locals_option template else [:_render_locals, template] end cache = app.render_opts[:template_method_cache] cache[key].must_be_nil body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier cache_size = 2 unless assume_fixed_locals_option key = template body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * cache_size) end end end end end end end jeremyevans-roda-4f30bb3/spec/plugin/partials_spec.rb000066400000000000000000000061161516720775400230220ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping partials plugin test" else describe "partials plugin" do before do app(:bare) do plugin :partials, :views=>"./spec/views" route do |r| r.is "partial" do partial("test", :locals=>{:title => "About Roda"}) end r.is "partial/subdir" do partial("about/test", :locals=>{:title => "About Roda"}) end r.is "partial/inline" do partial(:inline=>"Hello <%= name %>", :locals=>{:name => "Agent Smith"}) end end end end it "partial renders without layout, and prepends _ to template" do body("/partial").strip.must_equal "

About Roda

" end it "partial renders without layout, and prepends _ to template" do body("/partial/subdir").strip.must_equal "

Subdir: About Roda

" end it "partial handles inline partials" do body("/partial/inline").strip.must_equal "Hello Agent Smith" end end end begin require 'tilt' require 'tilt/string' require 'tilt/rdoc' require_relative '../../lib/roda/plugins/render' rescue LoadError warn "tilt not installed, skipping each_partial test" else describe "each_partial method in partials plugin" do [true, false].each do |cache| it "calls render with each argument, returning joined string with all results in cache: #{cache} mode" do app(:bare) do plugin :render, :views=>'spec/views', :engine=>'str', :cache=>cache plugin :partials o = Object.new def o.to_s; 'each' end route do |r| r.root do each_partial([1,2,3], :each) end r.is 'a' do each_partial([1,2,3], :each, :local=>:foo, :bar=>4) end r.is 'b' do each_partial([1,2,3], :each, :local=>nil) end r.is 'c' do each_partial([1,2,3], :each, :locals=>{:foo=>4}) end r.is 'e' do each_partial([1,2,3], o) end r.is 'f' do each_partial([1,2,3], "views/each", :views=>'spec') end r.is 'g' do each_partial([1,2,3], "each.foo") end end end 3.times do body.must_equal "x-1-\nx-2-\nx-3-\n" body("/a").must_equal "x--1\nx--2\nx--3\n" body("/b").must_equal "x--\nx--\nx--\n" body("/c").must_equal "x-1-4\nx-2-4\nx-3-4\n" body("/e").must_equal "x-1-\nx-2-\nx-3-\n" body("/f").must_equal "x-1-\nx-2-\nx-3-\n" body("/g").must_equal "y-1-\ny-2-\ny-3-\n" end end it "bases local name on basename of template in cache: #{cache} mode" do app(:bare) do plugin :render, :views=>'spec', :engine=>'str', :cache=>cache plugin :partials route do |r| r.root do each_partial([1,2,3], "views/each") end end end 3.times do body.must_equal "x-1-\nx-2-\nx-3-\n" end end end end end jeremyevans-roda-4f30bb3/spec/plugin/pass_spec.rb000066400000000000000000000011331516720775400221430ustar00rootroot00000000000000require_relative "../spec_helper" describe "pass plugin" do it "skips the current block if pass is called" do app(:pass) do |r| r.root do r.pass if env['FOO'] == 'true' 'root' end r.on :id do |id| r.pass if id == 'foo' id end r.on :x, :y do |x, y| x + y end end body.must_equal 'root' status('FOO'=>'true').must_equal 404 body("/a").must_equal 'a' body("/a/b").must_equal 'a' body("/foo/a").must_equal 'fooa' body("/foo/a/b").must_equal 'fooa' status("/foo").must_equal 404 end end jeremyevans-roda-4f30bb3/spec/plugin/path_matchers_spec.rb000066400000000000000000000016541516720775400240270ustar00rootroot00000000000000require_relative "../spec_helper" describe "path_matchers plugin" do it ":extension matcher should match given file extension" do app(:path_matchers) do |r| r.on "css" do r.on :extension=>"css" do |file| file end end end body("/css/reset.css").must_equal 'reset' status("/css/reset.bar").must_equal 404 end it ":suffix matcher should match given suffix" do app(:path_matchers) do |r| r.on "css" do r.on :suffix=>".css" do |file| file end end end body("/css/reset.css").must_equal 'reset' status("/css/reset.bar").must_equal 404 end it ":prefix matcher should match given prefix" do app(:path_matchers) do |r| r.on "css" do r.on :prefix=>"reset" do |file| file end end end body("/css/reset.css").must_equal '.css' status("/css/foo.bar").must_equal 404 end end jeremyevans-roda-4f30bb3/spec/plugin/path_rewriter_spec.rb000066400000000000000000000032021516720775400240530ustar00rootroot00000000000000require_relative "../spec_helper" describe "path_rewriter plugin" do it "allows rewriting remaining path or PATH_INFO" do app(:bare) do plugin :path_rewriter rewrite_path '/1', '/a' rewrite_path '/a', '/b' rewrite_path '/c', '/d', :path_info=>true rewrite_path '/2', '/1', :path_info=>true rewrite_path '/3', '/h' rewrite_path '/3', '/g', :path_info=>true rewrite_path(/\A\/e\z/, '/f') rewrite_path(/\A\/(dynamic1)/){|match| "/#{match[1].capitalize}"} rewrite_path(/\A\/(dynamic2)/, :path_info=>true){|match| "/#{match[1].capitalize}"} proc{rewrite_path('/a', '/z'){|match| "/x"}}.must_raise(Roda::RodaError) proc{rewrite_path('/a', {:path_info=>true}, :path_info=>true)}.must_raise(Roda::RodaError) proc{rewrite_path('/a', {:path_info=>true}, :path_info=>true){|match| "/x"}}.must_raise(Roda::RodaError) route do |r| "#{r.path_info}:#{r.remaining_path}" end end body('/a').must_equal '/a:/b' body('/a/f').must_equal '/a/f:/b/f' body('/b').must_equal '/b:/b' body('/c').must_equal '/d:/d' body('/c/f').must_equal '/d/f:/d/f' body('/d').must_equal '/d:/d' body('/e').must_equal '/e:/f' body('/e/g').must_equal '/e/g:/e/g' body('/1').must_equal '/1:/b' body('/1/f').must_equal '/1/f:/b/f' body('/2').must_equal '/1:/b' body('/2/f').must_equal '/1/f:/b/f' body('/3').must_equal '/g:/g' body('/dynamic1').must_equal '/dynamic1:/Dynamic1' body('/dynamic2').must_equal '/Dynamic2:/Dynamic2' app.freeze body('/a').must_equal '/a:/b' proc{app.rewrite_path '/a', '/b'}.must_raise end end jeremyevans-roda-4f30bb3/spec/plugin/path_spec.rb000066400000000000000000000300671516720775400221410ustar00rootroot00000000000000require_relative "../spec_helper" describe "path plugin" do def path_app(*args, &block) app(:bare) do plugin :path path(*args, &block) route{|r| send(r.path_info[1, 1000])} end end def path_script_name_app(*args, &block) app(:bare) do opts[:add_script_name] = true plugin :path path(*args, &block) route{|r| send(r.path_info[1, 1000])} end end def path_block_app(b, *args, &block) path_app(*args, &block) app.route{|r| send(r.path_info[1, 1000], &b)} end it "adds path method for defining named paths" do app(:bare) do plugin :path path :foo, "/foo" path :bar do |o| "/bar/#{o}" end path :baz do |&block| "/baz/#{block.call}" end route do |r| "#{foo_path}#{bar_path('a')}#{baz_path{'b'}}" end end body.must_equal '/foo/bar/a/baz/b' end it "raises if both path and block are given" do app.plugin :path proc{app.path(:foo, '/foo'){}}.must_raise(Roda::RodaError) end it "raises if neither path nor block are given" do app.plugin :path proc{app.path(:foo)}.must_raise(Roda::RodaError) end it "raises if two options hashes are given" do app.plugin :path proc{app.path(:foo, {:name=>'a'}, :add_script_name=>true)}.must_raise(Roda::RodaError) end it "supports :name option for naming the method" do path_app(:foo, :name=>'foobar_route'){"/bar/foo"} body("/foobar_route").must_equal "/bar/foo" end it "supports :add_script_name option for automatically adding the script name" do path_app(:foo, :add_script_name=>true){"/bar/foo"} body("/foo_path", 'SCRIPT_NAME'=>'/baz').must_equal "/baz/bar/foo" end it "respects :add_script_name app option for automatically adding the script name" do path_script_name_app(:foo){"/bar/foo"} body("/foo_path", 'SCRIPT_NAME'=>'/baz').must_equal "/baz/bar/foo" end it "supports :add_script_name=>false option for not automatically adding the script name" do path_script_name_app(:foo, :add_script_name=>false){"/bar/foo"} body("/foo_path", 'SCRIPT_NAME'=>'/baz').must_equal "/bar/foo" end it "respects :add_script_name app option for automatically adding the script name for url methods" do path_script_name_app(:foo, :url=>true){"/bar/foo"} body("/foo_url", 'SCRIPT_NAME'=>'/baz', 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal "http://example.org/baz/bar/foo" end it "supports :add_script_name=>false option for not automatically adding the script name for url methods" do path_script_name_app(:foo, :add_script_name=>false, :url=>true){"/bar/foo"} body("/foo_url", 'SCRIPT_NAME'=>'/baz', 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal "http://example.org/bar/foo" end it "supports path method accepting a block when using :add_script_name" do path_block_app(lambda{"c"}, :foo, :add_script_name=>true){|&block| "/bar/foo/#{block.call}"} body("/foo_path", 'SCRIPT_NAME'=>'/baz').must_equal "/baz/bar/foo/c" end it "supports :relative option for returning paths relative to the current request" do app(:bare) do plugin :path path("bar", :relative=>true){"/bar/foo"} route{|r| bar_path} end body.must_equal "./bar/foo" body('/a').must_equal "./bar/foo" body('/a/').must_equal "../bar/foo" body('/a/b/c/d').must_equal "../../../bar/foo" body('/a/b/c/d', "SCRIPT_NAME"=>"/e").must_equal "../../../../e/bar/foo" end it "raises Error if :relative option to be used with :url or :url_only options" do app.plugin :path proc{app.path("bar", :relative=>true, :url=>true){"/bar/foo"}}.must_raise Roda::RodaError proc{app.path("bar", :relative=>true, :url_only=>true){"/bar/foo"}}.must_raise Roda::RodaError end it "supports :url option for also creating a *_url method" do path_app(:foo, :url=>true){"/bar/foo"} body("/foo_path", 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal "/bar/foo" body("/foo_url", 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal "http://example.org/bar/foo" end it "supports url method accepting a block when using :url" do path_block_app(lambda{"c"}, :foo, :url=>true){|&block| "/bar/foo/#{block.call}"} body("/foo_url", 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal "http://example.org/bar/foo/c" end it "supports url method name specified in :url option" do path_app(:foo, :url=>:foobar_uri){"/bar/foo"} body("/foo_path", 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal "/bar/foo" body("/foobar_uri", 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal "http://example.org/bar/foo" end it "supports :url_only option for not creating a path method" do path_app(:foo, :url_only=>true){"/bar/foo"} proc{body("/foo_path")}.must_raise(NoMethodError) body("/foo_url", 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal "http://example.org/bar/foo" end it "handles non-default ports in url methods" do path_app(:foo, :url=>true){"/bar/foo"} body("/foo_url", 'HTTP_HOST'=>'example.org:81', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'81').must_equal "http://example.org:81/bar/foo" end if RUBY_VERSION >= "2.1" it "supports keyword argument when opts[:add_script_name] is true" do eval(<<-'RUBY') app(:bare) do opts[:add_script_name] = true plugin :path path(:foo) {|bar:| "/foo/#{bar}"} route{|r| foo_path(bar: "bar")} end RUBY body.must_equal "/foo/bar" end it "supports keyword arguments when :relative is true" do eval(<<-'RUBY') app(:bare) do plugin :path path(:foo, relative: true) {|bar:| "/foo/#{bar}"} route{|r| foo_path(bar: "bar")} end RUBY body.must_equal "./foo/bar" end it "supports keyword arguments when :url is true" do eval(<<-'RUBY') app(:bare) do plugin :path path(:foo, url: true) {|bar:| "/foo/#{bar}"} route{|r| foo_url(bar: "bar")} end RUBY body("/", 'HTTP_HOST'=>'example.org:81', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'81').must_equal "http://example.org:81/foo/bar" end end end describe "path plugin" do before do app(:bare) do plugin :path route do |r| r.get("url"){url(*env['rack.path'])} path(*env['rack.path']) end end c = Class.new{attr_accessor :a} app.path(c){|obj, *args| "/d/#{obj.a}/#{File.join(*args)}"} @obj = c.new @obj.a = 1 end it "Roda#path respects classes and symbols registered via Roda.path" do # Strings body('rack.path'=>'/foo/bar').must_equal '/foo/bar' # Classes body('rack.path'=>@obj).must_equal '/d/1/' body('rack.path'=>[@obj, 'foo']).must_equal '/d/1/foo' body('rack.path'=>[@obj, 'foo', 'bar']).must_equal '/d/1/foo/bar' end it "Roda#path raises an error for an unrecognized class" do # Strings proc{body('rack.path'=>:foo)}.must_raise(Roda::RodaError) end it "Roda#path respects :add_script_name app option" do app.opts[:add_script_name] = true # Strings body('rack.path'=>'/foo/bar', 'SCRIPT_NAME'=>'/baz').must_equal '/baz/foo/bar' # Classes body('rack.path'=>@obj, 'SCRIPT_NAME'=>'/baz').must_equal '/baz/d/1/' body('rack.path'=>[@obj, 'foo'], 'SCRIPT_NAME'=>'/baz').must_equal '/baz/d/1/foo' body('rack.path'=>[@obj, 'foo', 'bar'], 'SCRIPT_NAME'=>'/baz').must_equal '/baz/d/1/foo/bar' end it "Roda#path works in subclasses" do old_app = @app @app = Class.new(@app) @app.route{|r| path('/a')} body.must_equal '/a' @app.path(String){|b| "/foo#{b}"} body.must_equal '/foo/a' @app = old_app body('rack.path'=>'/a').must_equal '/a' end it "Roda#url works similar to Roda#path but turns it into a full URL" do body("/url", 'rack.path'=>@obj, 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'80').must_equal 'http://example.org/d/1/' body("/url", 'rack.path'=>[@obj, 'foo'], 'HTTP_HOST'=>'example.org', "rack.url_scheme"=>'https', 'SERVER_PORT'=>'443').must_equal 'https://example.org/d/1/foo' body("/url", 'rack.path'=>[@obj, 'foo', 'bar'], 'HTTP_HOST'=>'example.org:81', "rack.url_scheme"=>'http', 'SERVER_PORT'=>'81').must_equal 'http://example.org:81/d/1/foo/bar' end it "registers classes by reference by default" do c1 = Class.new def c1.name; 'C'; end c2 = Class.new def c2.name; 'C'; end @app.path(c1){'/c'} @app.route{|r| path(r.env['rack.c'])} body('rack.c'=>c1.new).must_equal '/c' proc{body('rack.c'=>c2.new)}.must_raise(Roda::RodaError) end it ":by_name plugin option registers classes by name" do c1 = Class.new def c1.name; 'C'; end c2 = Class.new def c2.name; 'C'; end @app.plugin :path, :by_name=>true @app.path(c1){'/c'} @app.route{|r| path(r.env['rack.c'])} body('rack.c'=>c1.new).must_equal '/c' body('rack.c'=>c2.new).must_equal '/c' end it ":by_name plugin option works with string argument and the :class_name path method option" do c = Class.new def c.name; 'Roda::TestRodaPathPlugin'; end @app.plugin :path, :by_name=>true @app.path('Roda::TestRodaPathPlugin', :class_name=>true){'/c'} @app.route{|r| path(r.env['rack.c'])} body('rack.c'=>c.new).must_equal '/c' end it ":by_name plugin option works with symbol argument and the :class_name path method option" do c = Class.new def c.name; 'TestRodaPathPlugin'; end @app.plugin :path, :by_name=>true @app.path(:TestRodaPathPlugin, :class_name=>true){'/c'} @app.route{|r| path(r.env['rack.c'])} body('rack.c'=>c.new).must_equal '/c' end it "string argument and the :class_name path method option works without :by_name plugin option" do begin c = Class.new def c.name; 'Roda::TestRodaPathPlugin'; end Roda.const_set(:TestRodaPathPlugin, c) @app.plugin :path @app.path('Roda::TestRodaPathPlugin', :class_name=>true){'/c'} @app.route{|r| path(r.env['rack.c'])} body('rack.c'=>c.new).must_equal '/c' ensure Roda.send(:remove_const, :TestRodaPathPlugin) end end it "symbol argument and the :class_name path method option works without :by_name plugin option" do begin c = Class.new def c.name; 'TestRodaPathPlugin'; end Object.const_set(:TestRodaPathPlugin, c) @app.plugin :path @app.path(:TestRodaPathPlugin, :class_name=>true){'/c'} @app.route{|r| path(r.env['rack.c'])} body('rack.c'=>c.new).must_equal '/c' ensure Object.send(:remove_const, :TestRodaPathPlugin) end end it ":class_name path method option raises for invalid class names" do @app.plugin :path proc{@app.path(:testRodaPathPlugin, :class_name=>true){'/c'}}.must_raise Roda::RodaError end it ":by_name plugin option defaults to true in development" do with_rack_env('development') do app(:path){} end app.opts[:path_class_by_name].must_equal true app(:path){} app.opts[:path_class_by_name].must_equal false end it "Roda.path_block returns the block used" do c = Class.new b = proc{|x| x.to_s} @app.path(c, &b) # Work around minitest bug app.path_block(c).must_equal b end it "Roda.path doesn't work with classes without blocks" do proc{app.path(Class.new)}.must_raise(Roda::RodaError) end it "Roda.path doesn't work with classes with paths or options" do proc{app.path(Class.new, '/a'){}}.must_raise(Roda::RodaError) proc{app.path(Class.new, nil, :a=>1){}}.must_raise(Roda::RodaError) end it "Roda.path doesn't work after freezing the app" do app.freeze proc{app.path(Class.new){|obj| ''}}.must_raise end it "works if the plugin is loaded twice" do app(:bare) do plugin :path plugin :path path :foo, "/foo" route do |r| "#{foo_path}" end end body.must_equal '/foo' end end jeremyevans-roda-4f30bb3/spec/plugin/permissions_policy_spec.rb000066400000000000000000000133221516720775400251320ustar00rootroot00000000000000require_relative "../spec_helper" describe "permissions_policy plugin" do it "does not add header if no options are set" do app(:permissions_policy){'a'} header(RodaResponseHeaders::PERMISSIONS_POLICY, "/a").must_be_nil end it "sets Permissions-Policy header" do app(:bare) do plugin :permissions_policy do |pp| pp.camera :none pp.fullscreen :self pp.midi :self, 'http://example.com' pp.geolocation :all end route do |r| r.get 'ro' do permissions_policy.report_only '' end r.get 'nro' do permissions_policy.report_only permissions_policy.report_only(false) permissions_policy.report_only?.inspect end r.get 'get' do permissions_policy.get_geolocation.inspect end r.get 'add' do permissions_policy.add_camera('http://foo.com', 'https://bar.com') permissions_policy.add_geolocation('http://foo.com', 'https://bar.com') permissions_policy.add_fullscreen('https://foo.com', 'http://bar.com') permissions_policy.add_midi('https://foo.com') '' end r.get 'empty' do permissions_policy.add_geolocation '' end r.get 'set' do permissions_policy.fullscreen('http://foobar.com', 'https://barfoo.com') '' end r.get 'block' do permissions_policy do |pp| pp.geolocation(:src, 'http://foo.com', 'https://bar.com') pp.camera :all pp.add_midi pp.fullscreen pp.report_only end '' end r.get 'clear' do permissions_policy do |pp| pp.clear pp.add_geolocation('http://foo.com', 'https://bar.com') end '' end 'a' end end v = 'camera=(), fullscreen=(self), midi=(self "http://example.com"), geolocation=*' header(RodaResponseHeaders::PERMISSIONS_POLICY, "/a").must_equal v header(RodaResponseHeaders::PERMISSIONS_POLICY, "/nro").must_equal v header(RodaResponseHeaders::PERMISSIONS_POLICY_REPORT_ONLY, "/nro").must_be_nil body("/nro").must_equal 'false' header(RodaResponseHeaders::PERMISSIONS_POLICY_REPORT_ONLY, "/ro").must_equal v header(RodaResponseHeaders::PERMISSIONS_POLICY, "/ro").must_be_nil body('/get').must_equal ':all' header(RodaResponseHeaders::PERMISSIONS_POLICY, "/add").must_equal 'camera=("http://foo.com" "https://bar.com"), fullscreen=(self "https://foo.com" "http://bar.com"), midi=(self "http://example.com" "https://foo.com"), geolocation=*' header(RodaResponseHeaders::PERMISSIONS_POLICY, "/empty").must_equal 'camera=(), fullscreen=(self), midi=(self "http://example.com"), geolocation=*' header(RodaResponseHeaders::PERMISSIONS_POLICY, "/set").must_equal 'camera=(), fullscreen=("http://foobar.com" "https://barfoo.com"), midi=(self "http://example.com"), geolocation=*' header(RodaResponseHeaders::PERMISSIONS_POLICY_REPORT_ONLY, "/block").must_equal 'camera=*, midi=(self "http://example.com"), geolocation=(src "http://foo.com" "https://bar.com")' header(RodaResponseHeaders::PERMISSIONS_POLICY, "/clear").must_equal 'geolocation=("http://foo.com" "https://bar.com")' end it "raises error for unsupported Permission-Policy values" do app{} proc{app.plugin(:permissions_policy){|pp| pp.fullscreen Object.new}}.must_raise Roda::RodaError proc{app.plugin(:permissions_policy){|pp| pp.fullscreen []}}.must_raise Roda::RodaError proc{app.plugin(:permissions_policy){|pp| pp.fullscreen [:a]}}.must_raise Roda::RodaError proc{app.plugin(:permissions_policy){|pp| pp.fullscreen [:a, :b, :c]}}.must_raise Roda::RodaError end it "supports :default plugin option" do app(:bare) do plugin :permissions_policy, :default=>:none route do |r| '' end end header(RodaResponseHeaders::PERMISSIONS_POLICY).must_equal Roda::RodaPlugins::PermissionsPolicy.const_get(:SUPPORTED_SETTINGS).map{|s| "#{s}=()"}.join(', ') end it "supports all documented settings" do app(:permissions_policy) do |r| permissions_policy.send(r.path[1..-1], :self) end Roda::RodaPlugins::PermissionsPolicy.const_get(:SUPPORTED_SETTINGS).each do |setting| header(RodaResponseHeaders::PERMISSIONS_POLICY, "/#{setting.tr('-', '_')}").must_equal "#{setting}=(self)" end end it "does not override existing heading" do app(:permissions_policy) do |r| permissions_policy.fullscreen :self response[RodaResponseHeaders::PERMISSIONS_POLICY] = "foo" '' end header(RodaResponseHeaders::PERMISSIONS_POLICY).must_equal "foo" end it "should not set header when using response.skip_permissions_policy!" do app(:bare) do plugin :permissions_policy do |pp| pp.fullscreen :self end route do |r| response.skip_permissions_policy! '' end end header(RodaResponseHeaders::PERMISSIONS_POLICY).must_be_nil end it "works with error_handler" do app(:bare) do plugin(:error_handler){|_| ''} plugin :permissions_policy do |pp| pp.fullscreen :self pp.camera :self, 'https://example.com' pp.midi :none end route do |r| r.get 'a' do permissions_policy.fullscreen 'foo.com' raise end raise end end header(RodaResponseHeaders::PERMISSIONS_POLICY).must_equal 'fullscreen=(self), camera=(self "https://example.com"), midi=()' # Don't include updates before the error header(RodaResponseHeaders::PERMISSIONS_POLICY, '/a').must_equal 'fullscreen=(self), camera=(self "https://example.com"), midi=()' end end jeremyevans-roda-4f30bb3/spec/plugin/placeholder_string_matchers_spec.rb000066400000000000000000000072741516720775400267470ustar00rootroot00000000000000require_relative "../spec_helper" describe "placeholder_string_matchers plugin" do it "should handle string with embedded param" do app(:placeholder_string_matchers) do |r| r.on "posts/:id" do |id| id end r.on "responses-:id" do |id| id end end body('/posts/123').must_equal '123' status('/post/123').must_equal 404 body('/responses-123').must_equal '123' end it "should handle multiple params in single string" do app(:placeholder_string_matchers) do |r| r.on "u/:uid/posts/:id" do |uid, id| uid + id end end body("/u/jdoe/posts/123").must_equal 'jdoe123' status("/u/jdoe/pots/123").must_equal 404 end it "should escape regexp metacharaters in string" do app(:placeholder_string_matchers) do |r| r.on "u/:uid/posts?/:id" do |uid, id| uid + id end end body("/u/jdoe/posts?/123").must_equal 'jdoe123' status("/u/jdoe/post/123").must_equal 404 end it "should handle colons by themselves" do app(:bare) do plugin :placeholder_string_matchers plugin :unescape_path route do |r| r.on "u/:/:uid/posts/::id" do |uid, id| uid + id end end end body("/u/%3A/jdoe/posts/%3A123").must_equal 'jdoe123' status("/u/a/jdoe/post/b123").must_equal 404 end it "should work with params_capturing plugin to add captures to r.params for string matchers" do app(:bare) do plugin :placeholder_string_matchers plugin :params_capturing route do |r| r.on("bar/:foo") do |foo| "b-#{foo}-#{r.params['foo']}-#{r.params['captures'].length}" end r.on("baz/:bar", :foo) do |bar, foo| "b-#{bar}-#{foo}-#{r.params['bar']}-#{r.params['foo']}-#{r.params['captures'].length}" end end end body('/bar/banana', 'rack.input'=>rack_input).must_equal 'b-banana-banana-1' body('/baz/ban/ana', 'rack.input'=>rack_input).must_equal 'b-ban-ana-ban-ana-2' end it "works with symbol_matchers plugin" do app(:bare) do plugin :placeholder_string_matchers plugin :symbol_matchers symbol_matcher(:f, /(f+)/) plugin :class_matchers symbol_matcher(:s, String) symbol_matcher(:i, Integer) symbol_matcher(:j, :i) route do |r| r.on "X" do r.is "j/:j" do |i| "j-#{i}" end r.is "i/:i" do |i| "i-#{i}" end r.is "s/:s" do |s| "s-#{s}" end end r.is ":d" do |d| "d#{d}" end r.is "thing/:thing" do |d| "thing#{d}" end r.is "thing2", ":thing" do |d| "thing2#{d}" end r.is ":f" do |f| "f#{f}" end r.is 'q:rest' do |rest| "rest#{rest}" end r.is ":w" do |w| "w#{w}" end r.is ':d/:w/:f' do |d, w, f| "dwf#{d}#{w}#{f}" end end end status.must_equal 404 body("/1").must_equal 'd1' body("/11232135").must_equal 'd11232135' body("/a").must_equal 'wa' body("/1az0").must_equal 'w1az0' body("/f").must_equal 'ff' body("/ffffffffffffffff").must_equal 'fffffffffffffffff' status("/-").must_equal 404 body("/1/1a/f").must_equal 'dwf11af' body("/12/1azy/fffff").must_equal 'dwf121azyfffff' status("/1/f/a").must_equal 404 body("/qa/b/c/d//f/g").must_equal 'resta/b/c/d//f/g' body('/q').must_equal 'rest' body('/thing/q').must_equal 'thingq' body('/thing2/q').must_equal 'thing2q' body("/X/j/1").must_equal 'j-1' body("/X/i/3").must_equal 'i-3' body("/X/s/a").must_equal 's-a' end end jeremyevans-roda-4f30bb3/spec/plugin/plain_hash_response_headers_spec.rb000066400000000000000000000011261516720775400267160ustar00rootroot00000000000000require_relative "../spec_helper" describe "plain_hash_response_headers plugin" do it "uses plain hashes for response headers" do app(:plain_hash_response_headers) do |r| r.get 'up' do response.headers['UP'] = 'U' end response.headers['down'] = 'd' end if Rack.release >= '3' && ENV['LINT'] proc{req('/up')}.must_raise Rack::Lint::LintError else header('up', '/up').must_be_nil header('UP', '/up').must_equal 'U' end header('down').must_equal 'd' header('DOWN').must_be_nil req[1].must_be_instance_of Hash end end jeremyevans-roda-4f30bb3/spec/plugin/precompile_templates_spec.rb000066400000000000000000000131711516720775400254170ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping precompiled_templates plugin test" else describe "precompile_templates plugin - precompile_templates method" do it "adds support for template precompilation" do app(:bare) do plugin :render, :views=>'spec/views' plugin :precompile_templates route do |r| @a = 1 render('iv') end end app.render_opts[:cache][File.expand_path('spec/views/iv.erb')].must_be_nil app.precompile_templates 'spec/views/iv.erb' app.render_opts[:cache][File.expand_path('spec/views/iv.erb')].wont_equal nil app.render_opts[:cache][File.expand_path('spec/views/iv.erb')].instance_variable_get(:@compiled_method).length.must_equal 1 body.strip.must_equal '1' app.render_opts[:cache][File.expand_path('spec/views/iv.erb')].instance_variable_get(:@compiled_method).length.must_equal 1 end it "adds support for template precompilation with :locals" do app(:bare) do plugin :render, :views=>'spec/views' plugin :precompile_templates route do |r| render('about', :locals=>{:title=>'1'}) end end app.render_opts[:cache][File.expand_path('spec/views/about.erb')].must_be_nil app.precompile_templates 'spec/views/about.erb', :locals=>[:title] app.render_opts[:cache][File.expand_path('spec/views/about.erb')].wont_equal nil app.render_opts[:cache][File.expand_path('spec/views/about.erb')].instance_variable_get(:@compiled_method).length.must_equal 1 body.strip.must_equal '

1

' app.render_opts[:cache][File.expand_path('spec/views/about.erb')].instance_variable_get(:@compiled_method).length.must_equal 1 end it "adds support for template precompilation with :inline" do app(:bare) do plugin :render, :views=>'spec/views' plugin :precompile_templates route do |r| render(:inline=>'a', :cache_key=>'a') end end app.render_opts[:cache]['a'].must_be_nil app.precompile_templates :inline=>'a', :cache_key=>'a' app.render_opts[:cache]['a'].wont_equal nil app.render_opts[:cache]['a'].instance_variable_get(:@compiled_method).length.must_equal 1 body.strip.must_equal "a" app.render_opts[:cache]['a'].instance_variable_get(:@compiled_method).length.must_equal 1 end end describe "precompile_templates plugin - precompile_views method" do it "adds support for template precompilation without locals" do app(:bare) do plugin :render, :views=>'spec/views', :layout=>'layout-yield' plugin :precompile_templates route do |r| @a = ' 1 ' view('iv') end end app.render_opts[:cache][File.expand_path('spec/views/iv.erb')].must_be_nil app.render_opts[:cache][File.expand_path('spec/views/layout-yield.erb')].must_be_nil if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT app.render_opts[:template_method_cache]['iv'].must_be_nil end app.precompile_views ['iv'] app.freeze_template_caches! app.render_opts[:cache][File.expand_path('spec/views/iv.erb')].wont_be_nil app.render_opts[:cache][File.expand_path('spec/views/layout-yield.erb')].wont_be_nil app.render_opts[:cache].size.must_equal 2 if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT app.render_opts[:template_method_cache]['iv'].wont_be_nil app.render_opts[:template_method_cache].size.must_equal 2 end body.strip.gsub(/\s+/, ' ').must_equal 'Header 1 Footer' app.render_opts[:cache].size.must_equal 2 if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT app.render_opts[:template_method_cache].size.must_equal 2 end end it "adds support for template precompilation with :locals" do app(:bare) do plugin :render, :views=>'spec/views', :layout=>false plugin :precompile_templates route do |r| render('about', :locals=>{:title=>'1'}) end end app.render_opts[:cache][File.expand_path('spec/views/about.erb')].must_be_nil if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT app.render_opts[:template_method_cache]['about'].must_be_nil end app.precompile_views ['about'], [:title] app.freeze_template_caches! app.render_opts[:cache][File.expand_path('spec/views/about.erb')].wont_be_nil app.render_opts[:cache].size.must_equal 1 if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT app.render_opts[:template_method_cache][[:_render_locals, "about", [:title]]].wont_be_nil app.render_opts[:template_method_cache].size.must_equal 1 end body.strip.must_equal '

1

' app.render_opts[:cache].size.must_equal 1 if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT app.render_opts[:template_method_cache].size.must_equal 1 end end end end begin require 'tilt/plain' rescue LoadError warn "tilt/plain not installed, skipping precompiled_templates plugin test" else describe "precompile_templates plugin" do it "adds support for template precompilation for tilt template types that do not support precompilation" do app(:bare) do plugin :render, :views=>'spec/views' plugin :precompile_templates route do |r| render(:path=>File.expand_path('spec/assets/css/app.html'), :template_opts=>{:cache=>false}) end end key = [File.expand_path("spec/assets/css/app.html"), nil, nil, {:cache=>false}, nil] app.render_opts[:cache][key].must_be_nil app.precompile_templates(:path=>File.expand_path('spec/assets/css/app.html'), :template_opts=>{:cache=>false}) app.render_opts[:cache][key].wont_be_nil app.freeze_template_caches! body.must_match(/color: red;/) end end end jeremyevans-roda-4f30bb3/spec/plugin/public_spec.rb000066400000000000000000000113161516720775400224570ustar00rootroot00000000000000require_relative "../spec_helper" describe "public plugin" do it "adds r.public for serving static files from public folder" do app(:bare) do plugin :public, :root=>'spec/views' route do |r| r.public r.on 'static' do r.public end end end status("/about/_test.erb\0").must_equal 404 body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') body('/static/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') body('/foo/.././/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it "respects the application's :root option" do app(:bare) do opts[:root] = File.expand_path('../../', __FILE__) plugin :public, :root=>'views' route do |r| r.public end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it "keeps existing :root option if loaded a second time" do app(:bare) do plugin :public, :root=>'spec/views' plugin :public route do |r| r.public end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_TYPE, '/about/_test.erb').must_equal 'text/plain' header('x-foo', '/about/_test.erb').must_be_nil end it "support :headers options for custom headers" do app(:bare) do plugin :public, :root=>'spec/views', :headers=>{'x-foo' => 'bar'} route do |r| r.public end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_TYPE, '/about/_test.erb').must_equal 'text/plain' header('x-foo', '/about/_test.erb').must_equal 'bar' end it "support :default_mime options for default mime type" do app(:bare) do plugin :public, :root=>'spec/views', :default_mime=>'foo/bar' route do |r| r.public end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_TYPE, '/about/_test.erb').must_equal 'foo/bar' header('x-foo', '/about/_test.erb').must_be_nil end it "assumes public directory as default :root option" do app(:public){} app.opts[:public_root].must_equal File.expand_path('public') end types = [ [:gzip, 'gzip', '.gz'], [:brotli, 'br', '.br'], [:zstd, 'zstd', '.zst'], ] types.each do |type, accept, ext| [true, false].each do |use_encodings| opts = {:root=>'spec/views'} if use_encodings opts[:encodings] = [[accept, ext]] opts[:encodings] << ['zstd', '.zst'] if type == :brotli opts[:encodings] << ['gzip', '.gz'] unless type == :gzip else opts[:gzip] = opts[type] = true end it "handles serving files with #{ext} extension if client supports accepts #{accept} encoding when :encodings is #{'not ' unless use_encodings}given" do app(:bare) do plugin :public, opts route do |r| r.public end end body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_ENCODING, '/about/_test.erb').must_be_nil body('/about.erb').must_equal File.read('spec/views/about.erb') header(RodaResponseHeaders::CONTENT_ENCODING, '/about.erb').must_be_nil accept_encoding = "deflate,#{' gzip,' unless type == :gzip} #{accept}" body('/about/_test.erb', 'HTTP_ACCEPT_ENCODING'=>accept_encoding).must_equal File.binread("spec/views/about/_test.erb.gz") h = req('/about/_test.erb', 'HTTP_ACCEPT_ENCODING'=>accept_encoding)[1] h[RodaResponseHeaders::CONTENT_ENCODING].must_equal 'gzip' h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/plain' body('/about/_test2.css', 'HTTP_ACCEPT_ENCODING'=>accept_encoding).must_equal File.binread("spec/views/about/_test2.css#{ext}") h = req('/about/_test2.css', 'HTTP_ACCEPT_ENCODING'=>accept_encoding)[1] h[RodaResponseHeaders::CONTENT_ENCODING].must_equal accept h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/css' s, h, b = req('/about/_test2.css', 'HTTP_IF_MODIFIED_SINCE'=>h[RodaResponseHeaders::LAST_MODIFIED], 'HTTP_ACCEPT_ENCODING'=>accept_encoding) s.must_equal 304 h[RodaResponseHeaders::CONTENT_ENCODING].must_be_nil h[RodaResponseHeaders::CONTENT_TYPE].must_be_nil b.must_equal [] end end end it "does not handle non-GET requests" do app(:bare) do plugin :public, :root=>'spec/views' route do |r| r.public end end status("/about/_test.erb", "REQUEST_METHOD"=>"POST").must_equal 404 end end jeremyevans-roda-4f30bb3/spec/plugin/r_spec.rb000066400000000000000000000004041516720775400214360ustar00rootroot00000000000000require_relative "../spec_helper" describe "r plugin" do it "adds r method for request access" do app(:r) do |_| r.get "foo" do "foo" end "root" end body.must_equal 'root' body("/foo").must_equal 'foo' end end jeremyevans-roda-4f30bb3/spec/plugin/recheck_precompiled_assets_spec.rb000066400000000000000000000070301516720775400265500ustar00rootroot00000000000000require_relative "../spec_helper" require 'fileutils' run_tests = true begin begin require 'tilt' rescue LoadError warn "tilt not installed, skipping assets plugin test" run_tests = false end end if run_tests pid_dir = "spec/pid-#{$$}" assets_dir = File.join(pid_dir, "tmp") metadata_file = File.expand_path(File.join(assets_dir, 'precompiled.json')) describe 'recheck_precompiled_assets plugin' do define_method(:compile_assets) do |opts={}| Class.new(Roda) do plugin :assets, {:css => 'app.str', :path => assets_dir, :css_dir=>nil, :precompiled=>metadata_file, :public=>assets_dir, :prefix=>nil}.merge!(opts) compile_assets end end before do Dir.mkdir(pid_dir) unless File.directory?(pid_dir) Dir.mkdir(assets_dir) unless File.directory?(assets_dir) FileUtils.cp('spec/assets/css/app.str', assets_dir) FileUtils.cp('spec/assets/js/head/app.js', assets_dir) compile_assets File.utime(Time.now, Time.now - 20, metadata_file) app(:bare) do plugin :assets, :public => assets_dir, :prefix=>nil, :precompiled=>metadata_file plugin :recheck_precompiled_assets route do |r| r.assets "#{assets(:css)}\n#{assets(:js)}" end end end after do FileUtils.rm_r(pid_dir) if File.directory?(pid_dir) end it 'should support :recheck_precompiled option to recheck precompiled file for new precompilation data' do css_hash = app.assets_opts[:compiled]['css'] app.assets_opts[:compiled]['js'].must_be_nil body.scan("href=\"/app.#{css_hash}.css\"").length.must_equal 1 body("/app.#{css_hash}.css").must_match(/color:\s*red/) css_file = File.join(assets_dir, 'app.str') File.write(css_file, File.read(css_file).sub('red', 'blue')) compile_assets File.utime(Time.now, Time.now - 10, metadata_file) app.assets_opts[:compiled]['css'].must_equal css_hash body.scan("href=\"/app.#{css_hash}.css\"").length.must_equal 1 body("/app.#{css_hash}.css").must_match(/color:\s*red/) css2_hash = nil 2.times do app.recheck_precompiled_assets css2_hash = app.assets_opts[:compiled]['css'] css2_hash.wont_equal css_hash body.scan("href=\"/app.#{css2_hash}.css\"").length.must_equal 1 body("/app.#{css2_hash}.css").must_match(/color:\s*blue/) body("/app.#{css_hash}.css").must_match(/color:\s*red/) end compile_assets(:js=>'app.js', :js_dir=>nil) app.recheck_precompiled_assets js_hash = app.assets_opts[:compiled]['js'] body.scan("src=\"/app.#{js_hash}.js\"").length.must_equal 1 body("/app.#{js_hash}.js").must_match(/console\.log\(.test.\)/) app.assets_opts[:compiled].replace({}) app.compile_assets body.strip.must_be_empty app.plugin :assets, :css => 'app.str', :path => assets_dir, :css_dir=>nil, :css_opts => {:cache=>false} app.compile_assets body.scan("href=\"/app.#{css2_hash}.css\"").length.must_equal 1 body("/app.#{css2_hash}.css").must_match(/color:\s*blue/) end end describe 'recheck_precompiled_assets plugin' do it "should not allow loading if not using assets plugin" do proc{app(:recheck_precompiled_assets)}.must_raise Roda::RodaError end it "should not allow loading if using assets plugin without :precompiled option" do proc do app(:bare) do plugin :assets plugin :recheck_precompiled_assets end end.must_raise Roda::RodaError end end end jeremyevans-roda-4f30bb3/spec/plugin/redirect_http_to_https_spec.rb000066400000000000000000000061351516720775400257700ustar00rootroot00000000000000require_relative "../spec_helper" describe "redirect_http_to_https plugin" do before do app(:redirect_http_to_https) do |r| r.get 'a' do "a-#{r.ssl?}" end r.redirect_http_to_https "x-#{r.ssl?}" end end it "should not redirect before call to r.redirect_http_to_https" do body('/a').must_equal 'a-false' body('/a', 'HTTPS'=>'on').must_equal 'a-true' end it "r.redirect_http_to_https redirects HTTP requests to HTTP" do s, h, b = req('/b', 'HTTP_HOST'=>'foo.com') s.must_equal 301 h[RodaResponseHeaders::LOCATION].must_equal 'https://foo.com/b' b.must_be_empty body('/b', 'HTTPS'=>'on').must_equal 'x-true' end it "uses 301 for HEAD redirects by default" do status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'HEAD').must_equal 301 end it "uses 307 for POST redirects by default" do status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'POST').must_equal 307 end it "includes query string when redirecting" do header(RodaResponseHeaders::LOCATION, '/b', 'HTTP_HOST'=>'foo.com', 'QUERY_STRING'=>'foo=bar').must_equal 'https://foo.com/b?foo=bar' end it "supports :body option" do @app.plugin :redirect_http_to_https, :body=>'RTHS' s, h, b = req('/b', 'HTTP_HOST'=>'foo.com') s.must_equal 301 h[RodaResponseHeaders::LOCATION].must_equal 'https://foo.com/b' b.must_equal ['RTHS'] end it "supports :headers option" do @app.plugin :redirect_http_to_https, :headers=>{'foo'=>'bar'} s, h, b = req('/b', 'HTTP_HOST'=>'foo.com') s.must_equal 301 h[RodaResponseHeaders::LOCATION].must_equal 'https://foo.com/b' h['foo'].must_equal 'bar' b.must_be_empty end it "supports :host option" do @app.plugin :redirect_http_to_https, :host=>'bar.foo.com' s, h, b = req('/b', 'HTTP_HOST'=>'foo.com') s.must_equal 301 h[RodaResponseHeaders::LOCATION].must_equal 'https://bar.foo.com/b' b.must_be_empty end it "supports :port option" do @app.plugin :redirect_http_to_https, :port=>444 s, h, b = req('/b', 'HTTP_HOST'=>'foo.com') s.must_equal 301 h[RodaResponseHeaders::LOCATION].must_equal 'https://foo.com:444/b' b.must_be_empty end it "supports :host and :port options together" do @app.plugin :redirect_http_to_https, :host=>'bar.foo.com', :port=>444 s, h, b = req('/b', 'HTTP_HOST'=>'foo.com') s.must_equal 301 h[RodaResponseHeaders::LOCATION].must_equal 'https://bar.foo.com:444/b' b.must_be_empty end it "supports :status_map option" do map = Hash.new(302) map['GET'] = 301 @app.plugin :redirect_http_to_https, :status_map=>map status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'GET').must_equal 301 status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'HEAD').must_equal 302 end it "raise for :status_map that does not handle request mthod" do @app.plugin :redirect_http_to_https, :status_map=>{'GET'=>302} status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'GET').must_equal 302 proc{status('/b', 'HTTP_HOST'=>'foo.com', 'REQUEST_METHOD'=>'HEAD')}.must_raise Roda::RodaError end end jeremyevans-roda-4f30bb3/spec/plugin/redirect_path_spec.rb000066400000000000000000000022761516720775400240230ustar00rootroot00000000000000require_relative "../spec_helper" describe "redirect_path plugin" do before do app(:bare) do plugin :redirect_path foo = Struct.new(:id).new(1) path foo.class do |foo| "/foo/#{foo.id}" end route do|r| r.post("none"){r.redirect} r.get("string"){r.redirect "/string"} r.get("foo"){r.redirect foo} r.get("suffix"){r.redirect foo, "/status"} end end end it "allows normal use of redirect if given no arguments" do s, h = req("/none", "REQUEST_METHOD"=>"POST") s.must_equal 302 h[RodaResponseHeaders::LOCATION].must_equal "/none" end it "allows normal use of redirect if given a string argument" do s, h = req("/string") s.must_equal 302 h[RodaResponseHeaders::LOCATION].must_equal "/string" end it "uses path method to determine path if given a non-string argument" do s, h = req("/foo") s.must_equal 302 h[RodaResponseHeaders::LOCATION].must_equal "/foo/1" end it "supports suffix to path as a second argument if given a non-string first argument" do s, h = req("/suffix") s.must_equal 302 h[RodaResponseHeaders::LOCATION].must_equal "/foo/1/status" end end jeremyevans-roda-4f30bb3/spec/plugin/relative_path_spec.rb000066400000000000000000000036431516720775400240340ustar00rootroot00000000000000require_relative "../spec_helper" describe "relative_plath plugin" do it "supports relative_path method to turn absolute paths into relative paths" do app(:relative_path) do relative_path("/a") end body.must_equal './a' body('/a').must_equal './a' body('/a/').must_equal '../a' body('/a/b').must_equal '../a' body('/a/b/c').must_equal '../../a' body('/a/b/c', 'SCRIPT_NAME'=>'/d').must_equal '../../../a' unless_lint do body('', 'SCRIPT_NAME'=>'/d').must_equal './a' body('', 'SCRIPT_NAME'=>'').must_equal '/a' body('a', 'SCRIPT_NAME'=>'').must_equal '/a' body('/', 'SCRIPT_NAME'=>'d').must_equal '/a' end end it "supports relative_prefix method for prefix to turn absolute paths into relative paths" do app(:relative_path) do "#{relative_prefix}/a" end body.must_equal './a' body('/a').must_equal './a' body('/a/').must_equal '../a' body('/a/b').must_equal '../a' body('/a/b/c').must_equal '../../a' body('/a/b/c', 'SCRIPT_NAME'=>'/d').must_equal '../../../a' unless_lint do body('', 'SCRIPT_NAME'=>'/d').must_equal './a' body('', 'SCRIPT_NAME'=>'').must_equal '/a' body('a', 'SCRIPT_NAME'=>'').must_equal '/a' body('/', 'SCRIPT_NAME'=>'d').must_equal '/a' end end it "supports multiple calls to relative_prefix while routing same request" do app(:relative_path) do 3.times.map{"#{relative_prefix}/a"}[0] end body.must_equal './a' body('/a').must_equal './a' body('/a/').must_equal '../a' body('/a/b').must_equal '../a' body('/a/b/c').must_equal '../../a' body('/a/b/c', 'SCRIPT_NAME'=>'/d').must_equal '../../../a' unless_lint do body('', 'SCRIPT_NAME'=>'/d').must_equal './a' body('', 'SCRIPT_NAME'=>'').must_equal '/a' body('a', 'SCRIPT_NAME'=>'').must_equal '/a' body('/', 'SCRIPT_NAME'=>'d').must_equal '/a' end end end jeremyevans-roda-4f30bb3/spec/plugin/render_coverage_spec.rb000066400000000000000000000067221516720775400243400ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt' require 'tilt/erb' raise LoadError unless Tilt::Template.method_defined?(:compiled_path=) rescue LoadError warn "tilt 2.1+ not installed, skipping render_coverage plugin test" else require 'fileutils' describe "render_coverage plugin" do coverage_dir = "./spec/render_coverage-#{$$}" define_method(:setup_app) do |render_opts={}, render_coverage_opts={}, prefix=''| app(:bare) do if render_opts == :scope_class_and_fixed_locals render_opts = {:template_opts=>{:scope_class=>self, :default_fixed_locals=>'()'}} end plugin :render, {:views=>"./spec/views/about", :check_paths=>true, :layout=>false}.merge!(render_opts) plugin :render_coverage, {:dir=>coverage_dir}.merge!(render_coverage_opts) plugin :render_coverage route do |r| r.get "inline" do render(:inline=>"il") end r.get "path" do render(:path=>"./spec/views/about.erb", :locals=>{:title => "About Roda"}, :template_opts=>{:fixed_locals=>'(title: raise)'}) end r.get "not-exist" do template_path(find_template(parse_template_opts("#{prefix}not-exist", {}))) end r.get "view" do view("#{prefix}comp_test") end r.get "nested-rel" do render("#{prefix}nested/../nested/comp_test") end r.get "nested" do render("#{prefix}nested/comp_test") end r.get "root" do render("#{prefix}comp_test") end end end Object.const_set(:RodaRenderCoverage, @app) end after do Object.send(:remove_const, :RodaRenderCoverage) FileUtils.rm_r(coverage_dir) end { [] => "should store files in specified directory", [:scope_class_and_fixed_locals] => "should handle using of :scope_class and fixed_locals", [{:cache=>false}] => "should handle cache: false render plugin option", [{:views=>'./spec/views', :allowed_paths=>%w'./spec/views/about'}, {}, 'about/'] => "should strip paths based on render plugin :allowed_paths option", [{:views=>'./spec/views'}, {:strip_paths=>%w'./spec/views/about'}, 'about/'] => "should strip paths based on render_coverage plugin :strip_paths option" }.each do |args, desc| it desc do setup_app(*args) Dir["#{coverage_dir}/*"].sort.must_equal %w'' body("/root").strip.must_equal "about-ct" Dir["#{coverage_dir}/*"].map{|f| File.basename(f)}.sort.must_equal %w'comp_test.erb.rb' body("/nested").strip.must_equal "about-nested-ct" Dir["#{coverage_dir}/*"].map{|f| File.basename(f)}.sort.must_equal %w'comp_test.erb.rb nested-comp_test.erb.rb' body("/path").strip.must_equal "

About Roda

" Dir["#{coverage_dir}/*"].map{|f| File.basename(f)}.sort.must_equal %w'comp_test.erb.rb nested-comp_test.erb.rb' body("/inline").strip.must_equal "il" Dir["#{coverage_dir}/*"].map{|f| File.basename(f)}.sort.must_equal %w'comp_test.erb.rb nested-comp_test.erb.rb' Dir["#{coverage_dir}/*"].map{|f| File.delete(f)} body("/view").strip.must_equal "about-ct" Dir["#{coverage_dir}/*"].map{|f| File.basename(f)}.sort.must_equal %w'' body("/nested-rel").strip.must_equal "about-nested-ct" Dir["#{coverage_dir}/*"].map{|f| File.basename(f)}.sort.must_equal %w'' body("/not-exist").must_include 'spec/views/about/not-exist.erb' Dir["#{coverage_dir}/*"].map{|f| File.basename(f)}.sort.must_equal %w'' end end end end jeremyevans-roda-4f30bb3/spec/plugin/render_each_spec.rb000066400000000000000000000217421516720775400234440ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt' require 'tilt/string' require 'tilt/rdoc' require_relative '../../lib/roda/plugins/render' rescue LoadError warn "tilt not installed, skipping render_each plugin test" else describe "render_each plugin" do [true, false].each do |cache| it "calls render with each argument, returning joined string with all results in cache: #{cache} mode" do app(:bare) do plugin :render, :views=>'spec/views', :engine=>'str', :cache=>cache plugin :render_each o = Object.new def o.to_s; 'each' end route do |r| r.root do render_each([1,2,3], :each) end r.is 'a' do render_each([1,2,3], :each, :local=>:foo, :bar=>4) end r.is 'b' do render_each([1,2,3], :each, :local=>nil) end r.is 'c' do render_each([1,2,3], :each, :locals=>{:foo=>4}) end r.is 'd' do render_each([1,2,3], {:template=>:each}, :local=>:each) end r.is 'e' do render_each([1,2,3], o) end r.is 'f' do render_each([1,2,3], "views/each", :views=>'spec') end r.is 'g' do render_each([1,2,3], :"each.foo") end r.is 'h' do body = String.new render_each([1,2,3], :each){|t| body << t << " "} body end r.is 'i' do body = String.new render_each([1,2,3], {:template=>:each}, :local=>:each){|t| body << t << "|"} body end r.is 'j' do body = String.new render_each([1,2,3], :each, :local=>nil){|t| body << t << "|"} body end r.is 'k' do body = String.new render_each([1,2,3], {:template=>:each}, :local=>nil){|t| body << t << "/"} body end end end 2.times do 3.times do body.must_equal "r-1-\nr-2-\nr-3-\n" body("/a").must_equal "r--1\nr--2\nr--3\n" body("/b").must_equal "r--\nr--\nr--\n" body("/c").must_equal "r-1-4\nr-2-4\nr-3-4\n" body("/d").must_equal "r-1-\nr-2-\nr-3-\n" body("/e").must_equal "r-1-\nr-2-\nr-3-\n" body("/f").must_equal "r-1-\nr-2-\nr-3-\n" body("/g").must_equal "r-1-\nr-2-\nr-3-\n" body("/h").must_equal "r-1-\n r-2-\n r-3-\n " body("/i").must_equal "r-1-\n|r-2-\n|r-3-\n|" body("/j").must_equal "r--\n|r--\n|r--\n|" body("/k").must_equal "r--\n/r--\n/r--\n/" end app.opts[:render] = app.opts[:render].dup app.opts[:render].delete(:template_method_cache) end end it "bases local name on basename of template in cache: #{cache} mode" do app(:bare) do plugin :render, :views=>'spec', :engine=>'str', :cache=>cache plugin :render_each route do |r| r.root do render_each([1,2,3], "views/each") end end end 3.times do body.must_equal "r-1-\nr-2-\nr-3-\n" end end if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT it "calls render with each argument, handling template engines that don't support compilation in cache: #{cache} mode" do app(:bare) do plugin :render, :views=>'spec/views', :engine=>'rdoc', :cache=>cache plugin :render_each route do |r| r.root do render_each([1], :a) end r.is 'a' do render_each([1], :a, :local=>:b) end end end 3.times do body.strip.must_equal "

# a # * b

" body('/a').strip.must_equal "

# a # * b

" end end end end if Roda::RodaPlugins::Render::FIXED_LOCALS_COMPILED_METHOD_SUPPORT [true, false].each do |cache_plugin_option| multiplier = cache_plugin_option ? 1 : 2 it "support fixed locals in layout templates with plugin option :cache=>#{cache_plugin_option}" do template = "comp_each_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :layout_opts=>{:locals=>{:title=>"Home"}}, :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true} plugin :render_each route do render_each([1], template) end end key = [:_render_locals, "comp_each_test"] app.render_opts[:template_method_cache][key].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end it "support fixed locals in render templates with plugin option :cache=>#{cache_plugin_option}" do template = "local_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true} plugin :render_each route do render_each([1], template, locals: {title: 'ct'}) end end key = [:_render_locals, template] app.render_opts[:template_method_cache][key].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end [true, false].each do |assume_fixed_locals_option| [true, false].each do |freeze_app| it "caches expectedly for cache: #{cache_plugin_option}, assume_fixed_locals: #{assume_fixed_locals_option} options, with #{'un' unless freeze_app}frozen app" do template = "opt_local_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true}, :assume_fixed_locals=>assume_fixed_locals_option, :layout=>false plugin :render_each route do |r| r.is 'a' do render_each([1], template) end r.is 'b' do o = Object.new o.define_singleton_method(:to_s){template} render_each([1], o) end render_each([1], template, locals: {title: 'ct'}) end freeze if freeze_app end cache_size = 1 key = if assume_fixed_locals_option template else [:_render_locals, template] end cache = app.render_opts[:template_method_cache] cache[key].must_be_nil body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * cache_size) body('/b').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/b').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/b').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * cache_size) end end end end end end end jeremyevans-roda-4f30bb3/spec/plugin/render_locals_spec.rb000066400000000000000000000112311516720775400240110ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping render_locals plugin test" else describe "render_locals plugin with :merge option" do before do app(:bare) do plugin :render_locals, :render=>{:a=>1, :b=>2, :c=>3, :d=>4, :e=>5}, :layout=>{:a=>-1, :f=>6}, :merge=>true plugin :render, :views=>"./spec/views", :check_paths=>true, :layout_opts=>{:inline=>'<%= a %>|<%= b %>|<%= c %>|<%= d %>|<%= e %>|<%= f %>|<%= yield %>'} route do |r| r.on "base" do view(:inline=>'(<%= a %>|<%= b %>|<%= c %>|<%= d %>|<%= e %>)') end r.on "override" do view(:inline=>'(<%= a %>|<%= b %>|<%= c %>|<%= d %>|<%= e %>)', :locals=>{:b=>-2, :d=>-4, :f=>-6}, :layout_opts=>{:locals=>{:d=>0, :c=>-3, :e=>-5}}) end r.on "no_merge" do view(:inline=>'(<%= a %>|<%= b %>|<%= c %>|<%= d %>|<%= e %>)', :locals=>{:b=>-2, :d=>-4, :f=>-6}, :layout_opts=>{:merge_locals=>false, :locals=>{:d=>0, :c=>-3, :e=>-5}}) end end end end it "should choose method opts before plugin opts, and layout specific before locals" do body("/base").must_equal '-1|2|3|4|5|6|(1|2|3|4|5)' body("/override").must_equal '-1|-2|-3|0|-5|-6|(1|-2|3|-4|5)' body("/no_merge").must_equal '-1|2|-3|0|-5|6|(1|-2|3|-4|5)' end end describe "render_locals plugin" do it "locals overrides" do app(:bare) do plugin :render, :views=>"./spec/views", :layout_opts=>{:template=>'multiple-layout'} plugin :render_locals, :render=>{:title=>'Home', :b=>'B'}, :layout=>{:title=>'Roda', :a=>'A'} route do |r| view("multiple", :locals=>{:b=>"BB"}, :layout_opts=>{:locals=>{:a=>'AA'}}) end end body.strip.must_equal "Roda:AA::Home:BB" end it ":layout=>true/false/string/hash/not-present respects plugin layout switch and template" do app(:bare) do plugin :render, :views=>"./spec/views", :layout_opts=>{:template=>'layout-yield'} plugin :render_locals, :layout=>{:title=>'a'} route do |r| opts = {:content=>'bar'} opts[:layout] = true if r.path == '/' opts[:layout] = false if r.path == '/f' opts[:layout] = 'layout' if r.path == '/s' opts[:layout] = {:template=>'layout'} if r.path == '/h' view(opts) end end body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "HeaderbarFooter" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "HeaderbarFooter" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout=>true body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "HeaderbarFooter" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout=>'layout-alternative' body.delete("\n").must_equal "Alternative Layout: abar" body('/a').delete("\n").must_equal "Alternative Layout: abar" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout=>nil body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "bar" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout=>false body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "bar" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout_opts=>{:template=>'layout-alternative'} app.plugin :render_locals, :layout=>{:title=>'a'} body.delete("\n").must_equal "Alternative Layout: abar" body('/a').delete("\n").must_equal "bar" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" end end end jeremyevans-roda-4f30bb3/spec/plugin/render_spec.rb000066400000000000000000001240441516720775400224630ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt' require 'tilt/erb' require 'tilt/string' require_relative '../../lib/roda/plugins/render' rescue LoadError warn "tilt not installed, skipping render plugin test" else describe "render plugin" do before do app(:bare) do plugin :render, :views=>"./spec/views", :check_paths=>true route do |r| r.on "home" do view("home", :locals=>{:name => "Agent Smith", :title => "Home"}, :layout_opts=>{:locals=>{:title=>"Home"}}) end r.on "about" do render("about", :locals=>{:title => "About Roda"}) end r.on "inline" do view(:inline=>"Hello <%= name %>", :locals=>{:name => "Agent Smith"}, :layout=>nil) end r.on "path" do render(:path=>"./spec/views/about.erb", :locals=>{:title => "Path"}, :layout_opts=>{:locals=>{:title=>"Home"}}) end r.on "content" do view(:content=>'bar', :layout_opts=>{:locals=>{:title=>"Home"}}) end r.on "render-block" do render('layout', :locals=>{:title=>"Home"}){render("about", :locals=>{:title => "About Roda"})} end r.on "view-block" do view(:layout, :locals=>{:title=>"A"}, :layout_opts=>{:locals=>{:title=>"B"}}){render("about", :locals=>{:title => "About Roda"})} end end end end it "default actions" do body("/about").strip.must_equal "

About Roda

" body("/home").strip.must_equal "Roda: Home\n

Home

\n

Hello Agent Smith

" body("/inline").strip.must_equal "Hello Agent Smith" body("/path").strip.must_equal "

Path

" body("/content").strip.must_equal "Roda: Home\nbar" body("/render-block").strip.must_equal "Roda: Home\n

About Roda

" body("/view-block").strip.must_equal "Roda: B\nRoda: A\n

About Roda

" end it "with str as engine" do app.plugin :render, :engine => "str" body("/about").strip.must_equal "

About Roda

" body("/home").strip.must_equal "Roda: Home\n

Home

\n

Hello Agent Smith

" body("/inline").strip.must_equal "Hello <%= name %>" end it "custom default layout support" do app.plugin :render, :layout => "layout-alternative" body("/home").strip.must_equal "Alternative Layout: Home\n

Home

\n

Hello Agent Smith

" end it "using hash for :layout" do app.plugin :render, :layout => {:inline=> 'a<%= yield %>b'} body("/home").strip.must_equal "a

Home

\n

Hello Agent Smith

\nb" end end describe "render plugin" do iv = "iv-#{$$}" file = "spec/#{iv}.erb" dependent_file = "spec/tmp-#{$$}.txt" before do File.binwrite(file, File.binread("spec/views/iv.erb")) end after do [file, dependent_file].each do |f| File.delete(f) if File.file?(f) end end it "checks mtime of dependent files if :dependencies render method option is used" do File.write(dependent_file, '') app(:bare) do plugin :render, :views=>"./spec", :cache=>false route do |r| @a = 'a' render(iv, :dependencies=>[dependent_file]) end end t = Time.now+1 body.strip.must_equal "a" File.binwrite(file, File.binread(file) + "b") File.utime(t, t+1, dependent_file) body.delete("\n").must_equal "ab" end [{:cache=>false}, {:explicit_cache=>true}, {:check_template_mtime=>true}].each do |cache_plugin_opts| it "checks mtime if #{cache_plugin_opts} plugin option is used" do app(:bare) do plugin :render, {:views=>"./spec"}.merge!(cache_plugin_opts) route do |r| @a = 'a' render(iv) end end t = Time.now body.strip.must_equal "a" File.binwrite(file, File.binread(file) + "b") File.utime(t, t+1, file) body.delete("\n").must_equal "ab" File.binwrite(file, File.binread(file) + "c") File.utime(t, t+2, file) body.delete("\n").must_equal "abc" mtime = File.mtime(file) File.binwrite(file, File.binread(file) + "d") File.utime(t, mtime, file) body.delete("\n").must_equal "abc" File.delete(file) body.delete("\n").must_equal "abc" end end it "does not update mtime if there was an error rebuilding the template" do app(:bare) do plugin :render, :views=>"./spec", :cache=>false route do |r| @a = 'a' render(iv) end end t = Time.now body.strip.must_equal "a" content = File.binread(file) File.binwrite(file, content + "<% end %>") File.utime(t, t+1, file) proc{body}.must_raise SyntaxError proc{body}.must_raise SyntaxError File.binwrite(file, content + "b") File.utime(t, t+1, file) body.delete("\n").must_equal "ab" end it "does not check mtime if :cache render option is used" do app(:bare) do plugin :render, :views=>"./spec", :cache=>false route do |r| @a = 'a' render(iv, :cache=>true) end end t = Time.now+1 body.strip.must_equal "a" File.binwrite(file, File.binread(file) + "b") File.utime(t, t+1, file) body.delete("\n").must_equal "a" end end describe "render plugin" do it "simple layout support" do app(:bare) do plugin :render route do |r| render(:path=>"spec/views/layout-yield.erb") do render(:path=>"spec/views/content-yield.erb") end end end body.squeeze("\n").must_equal "Header\nThis is the actual content.\nFooter\n" end it "layout changing does not use cached template method" do app(:bare) do plugin :render, :views=>'spec/views', :layout=>'layout-yield' route do |r| view(:content=>'1') end end body.squeeze("\n").sub("\nFooter", 'Footer').must_equal "Header\n1Footer\n" app.plugin :render, :layout=>'layout-yield2' body.squeeze("\n").sub("\nFooter", 'Footer').must_equal "Header2\n1Footer2\n" end it "should have :layout_opts=>:views plugin option respect :root app option" do app(:bare) do self.opts[:root] = 'spec' plugin :render, :layout_opts=>{:views=>"views"}, :allowed_paths=>["spec/views"] route do |r| view(:content=>'a', :layout_opts=>{:locals=>{:title=>"Home"}}) end end body.strip.must_equal "Roda: Home\na" app.freeze body.strip.must_equal "Roda: Home\na" end it "views without default layouts" do app(:bare) do plugin :render, :views=>"./spec/views", :layout=>false route do |r| view("home", :locals=>{:name=>"Agent Smith", :title=>"Home"}) end end body.strip.must_equal "

Home

\n

Hello Agent Smith

" app.freeze body.strip.must_equal "

Home

\n

Hello Agent Smith

" end it "layout overrides" do app(:bare) do plugin :render, :views=>"./spec/views" route do |r| view("home", :locals=>{:name=>"Agent Smith", :title=>"Home" }, :layout=>"layout-alternative", :layout_opts=>{:locals=>{:title=>"Home"}}) end end body.strip.must_equal "Alternative Layout: Home\n

Home

\n

Hello Agent Smith

" end it ":layout=>true/false/string/hash/not-present respects plugin layout switch and template" do app(:bare) do plugin :render, :views=>"./spec/views", :layout_opts=>{:template=>'layout-yield'} route do |r| opts = {:content=>'bar'} opts[:layout] = true if r.path == '/' opts[:layout] = false if r.path == '/f' opts[:layout] = 'layout' if r.path == '/s' opts[:layout] = {:template=>'layout'} if r.path == '/h' opts[:layout_opts] = {:locals=>{:title=>'a'}} view(opts) end end body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "HeaderbarFooter" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "HeaderbarFooter" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout=>true body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "HeaderbarFooter" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout=>'layout-alternative' body.delete("\n").must_equal "Alternative Layout: abar" body('/a').delete("\n").must_equal "Alternative Layout: abar" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout=>nil body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "bar" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout=>false body.delete("\n").must_equal "HeaderbarFooter" body('/a').delete("\n").must_equal "bar" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" app.plugin :render, :layout_opts=>{:template=>'layout-alternative', :locals=>{:title=>'a'}} body.delete("\n").must_equal "Alternative Layout: abar" body('/a').delete("\n").must_equal "bar" body('/f').delete("\n").must_equal "bar" body('/s').delete("\n").must_equal "Roda: abar" body('/h').delete("\n").must_equal "Roda: abar" end it "app :root option affects :views default" do app app.plugin :render app.render_opts[:views].must_equal File.join(Dir.pwd, 'views') app.opts[:root] = '/foo' app.plugin :render # Work around for Windows app.render_opts[:views].sub(/\A\w:/, '').must_equal '/foo/views' app.opts[:root] = '/foo/bar' app.plugin :render app.render_opts[:views].sub(/\A\w:/, '').must_equal '/foo/bar/views' app.opts[:root] = nil app.plugin :render app.render_opts[:views].must_equal File.join(Dir.pwd, 'views') app.plugin :render, :views=>'bar' app.render_opts[:views].must_equal File.join(Dir.pwd, 'bar') end if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT [true, false].each do |cache_plugin_option| multiplier = cache_plugin_option ? 1 : 2 it "does not cache template renders when using a template library that doesn't support it with plugin option :cache=>#{cache_plugin_option}" do begin require 'tilt/rdoc' rescue next end app(:bare) do plugin :render, :views=>'spec/views', :engine=>'rdoc', :cache=>cache_plugin_option route do render('a') end end app.render_opts[:template_method_cache]['a'].must_be_nil body.strip.must_equal "

# a # * b

" app.render_opts[:template_method_cache]['a'].must_equal false body.strip.must_equal "

# a # * b

" app.render_opts[:template_method_cache]['a'].must_equal false body.strip.must_equal "

# a # * b

" app.render_opts[:template_method_cache]['a'].must_equal false app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0 end it "raises an exception and does not cache if trying to render a directory with :cache=>#{cache_plugin_option}" do app(:bare) do plugin :render, :views=>'spec', :cache=>cache_plugin_option route do render('views') end end app.render_opts[:template_method_cache]['views'].must_be_nil proc{body}.must_raise SystemCallError app.render_opts[:template_method_cache]['views'].must_be_nil end ['comp_test', :comp_test].each do |template| it "does not cache template renders when given a hash with #{template.class} value with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do plugin :render, :views=>'spec/views', :cache=>cache_plugin_option route do render(:template=>template) end end app.render_opts[:template_method_cache][template].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_nil app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0 end it "caches template renders when given a #{template.class} with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do plugin :render, :views=>'spec/views', :cache=>cache_plugin_option route do render(template) end end app.render_opts[:template_method_cache][template].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end it "does not cache template renders when there is no template method cache with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do plugin :render, :views=>'spec/views', :cache=>cache_plugin_option route do render(template) end end app.opts[:render] = app.opts[:render].dup app.opts[:render].delete(:template_method_cache) 3.times do body.strip.must_equal "ct" end app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0 end it "does not cache template renders with locals when there is no template method cache with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do plugin :render, :views=>'spec/views', :cache=>cache_plugin_option route do render(template, :locals => {:a=>1}) end end app.opts[:render] = app.opts[:render].dup app.opts[:render].delete(:template_method_cache) 3.times do body.strip.must_equal "ct" end app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0 end it "does not cache template views or layout when given a hash with #{template.class} value with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do layout = template.to_s.sub('test', 'layout') layout = layout.to_sym if template.is_a?(Symbol) plugin :render, :views=>'spec/views', :layout=>layout, :cache=>cache_plugin_option route do view(:template=>template) end end app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil app::RodaCompiledTemplates.private_instance_methods.length.must_equal 0 end it "caches template views with layout when given a #{template.class} with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do layout = template.to_s.sub('test', 'layout') layout = layout.to_sym if template.is_a?(Symbol) plugin :render, :views=>'spec/views', :layout=>layout, :cache=>cache_plugin_option route do view(template) end end app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Array) body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal(2*multiplier) end it "caches layout template automatically when freezeing when given a #{template.class} with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do layout = template.to_s.sub('test', 'layout') layout = layout.to_sym if template.is_a?(Symbol) plugin :render, :views=>'spec/views', :layout=>layout, :cache=>cache_plugin_option route do view(template) end end app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil app.freeze app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Array) body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Array) body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Array) body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal(2*multiplier) end it "caches template views with inline layout when given a #{template.class} with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do plugin :render, :views=>'spec/views', :layout=>{:inline=>"a<%= yield %>b"}, :cache=>cache_plugin_option route do view(template) end end app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier) end it "caches template views with inline layout when given a #{template.class} with explicitly not caching layout when using plugin option :cache=>#{cache_plugin_option}" do app(:bare) do layout = template.to_s.sub('test', 'layout') layout = layout.to_sym if template.is_a?(Symbol) plugin :render, :views=>'spec/views', :layout=>layout, :cache=>cache_plugin_option route do view(template) end end 3.times do app.render_opts[:template_method_cache][:_roda_layout] = false body.strip.must_equal "act\nb" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_equal false end app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier) end it "caches template views without layout when additional layout options given when given a #{template.class} with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do plugin :render, :views=>'spec/views', :layout=>nil, :cache=>cache_plugin_option route do view(template) end end app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end it "caches template views without layout when additional layout options given when given a #{template.class} with plugin option :cache=>#{cache_plugin_option}" do app(:bare) do plugin :render, :views=>'spec/views', :layout_opts=>{:locals=>{:title=>"Home"}}, :cache=>cache_plugin_option route do view(template) end end app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end end end end if Roda::RodaPlugins::Render::FIXED_LOCALS_COMPILED_METHOD_SUPPORT [true, false].each do |cache_plugin_option| multiplier = cache_plugin_option ? 1 : 2 it "support fixed locals in layout templates with plugin option :cache=>#{cache_plugin_option}" do template = "comp_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :layout_opts=>{:locals=>{:title=>"Home"}}, :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true} route do view(template) end end app.render_opts[:template_method_cache][template].must_be_nil app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil body.strip.must_equal "Roda: Home\nct" app.render_opts[:template_method_cache][template].must_be_kind_of(Array) app.render_opts[:template_method_cache][:_roda_layout].must_be_nil app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end it "support fixed locals in render templates with plugin option :cache=>#{cache_plugin_option}" do template = "local_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true} route do render(template, locals: {title: 'ct'}) end end key = [:_render_locals, template] app.render_opts[:template_method_cache][key].must_be_nil body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) body.strip.must_equal "ct" app.render_opts[:template_method_cache][key].must_be_kind_of(Array) app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier end [true, false].each do |assume_fixed_locals_option| [true, false].each do |freeze_app| it "caches expectedly for cache: #{cache_plugin_option}, assume_fixed_locals: #{assume_fixed_locals_option} options" do template = "opt_local_test" app(:bare) do plugin :render, :views=>'spec/views/fixed', :cache=>cache_plugin_option, :template_opts=>{:extract_fixed_locals=>true}, :assume_fixed_locals=>assume_fixed_locals_option, :layout=>false route do |r| r.is 'a' do render(template) end render(template, locals: {title: 'ct'}) end freeze if freeze_app end cache_size = 1 key = if assume_fixed_locals_option template else [:_render_locals, template] end cache = app.render_opts[:template_method_cache] cache[key].must_be_nil body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body.strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal multiplier cache_size = 2 unless assume_fixed_locals_option key = template body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size body('/a').strip.must_equal "ct" cache[key].must_be_kind_of(Array) cache.instance_variable_get(:@hash).length.must_equal cache_size app::RodaCompiledTemplates.private_instance_methods.length.must_equal(multiplier * cache_size) end end end end end it "inline layouts and inline views" do app(:render) do view({:inline=>'bar'}, :layout=>{:inline=>'Foo: <%= yield %>'}) end body.strip.must_equal "Foo: bar" app.freeze body.strip.must_equal "Foo: bar" end it "inline renders with opts" do app(:render) do render({:inline=>'<%= bar %>'}, {:engine=>'str'}) end body.strip.must_equal '<%= bar %>' app.freeze body.strip.must_equal '<%= bar %>' end it "template renders with :template opts" do app(:bare) do plugin :render, :views => "./spec/views" route do render(:template=>"about", :locals=>{:title => "About Roda"}) end end body.strip.must_equal "

About Roda

" end it "template renders with :template_class opts" do app(:render) do @a = 1 render(:inline=>'i#{@a}', :template_class=>::Tilt[:str]) end body.must_equal "i1" end it "can specify engine-specific options via :engine_opts" do app(:bare) do plugin :render, :engine_opts=>{'a.erb'=>{:outvar=>'@a'}} route do |r| @a = nil r.is('a') do render(:inline=>'<%= @a.class.name %>', :engine=>'a.erb') end render(:inline=>'<%= @a.class.name %>') end end body('/a').must_equal "String" body.must_equal "NilClass" end it "template cache respects :template_opts" do c = Class.new do def initialize(path, _, opts) @path = path @opts = opts end def render(*) "#{@path}-#{@opts[:foo]}" end end app(:render) do |r| r.is "a" do render(:inline=>"i", :template_class=>c, :template_opts=>{:foo=>'a'}) end r.is "b" do render(:inline=>"i", :template_class=>c, :template_opts=>{:foo=>'b'}) end end body('/a').must_equal "i-a" body('/b').must_equal "i-b" end it "template cache respects :template_block" do c = Class.new do def initialize(path, *, &block) @path = path @block = block end def render(*) "#{@path}-#{@block.call}" end end proca = proc{'a'} procb = proc{'b'} app(:render) do |r| r.is "a" do render(:path=>"i", :template_class=>c, :template_block=>proca) end r.is "b" do render(:path=>"i", :template_class=>c, :template_block=>procb) end end body('/a').must_equal "i-a" body('/b').must_equal "i-b" end it "template cache respects :locals" do template = '<%= @a ? b : c %>' app(:render) do |r| r.is "a" do @a = true render(:inline=>template.dup, :locals=>{:b=>1}) end r.is "b" do @a = true render(:inline=>template.dup, :locals=>{:b=>2, :c=>4}) end r.is "c" do @a = nil render(:inline=>template.dup, :locals=>{:c=>3}) end end body('/a').must_equal "1" body('/b').must_equal "2" body('/c').must_equal "3" end it "Default to :check_template_mtime=>true in development mode" do with_rack_env('development') do app(:render){} end app.render_opts[:check_template_mtime].must_equal true app(:render){} app.render_opts[:check_template_mtime].must_equal false end it "Support :cache=>false plugin option to disable template caching by default, except :cache=>true method option is given" do app(:bare) do plugin :render, :views=>"./spec/views", :cache=>false route do |r| @a = 'a' r.is('a'){render('iv', :cache=>false, :cache_key=>:a)} r.is('b'){render('iv', :cache=>true, :cache_key=>:a)} render('iv', :cache_key=>:a) end end body('/a').strip.must_equal "a" app.render_opts[:cache][:a].must_be_nil body('/b').strip.must_equal "a" app.render_opts[:cache][:a].wont_be_nil body('/c').strip.must_equal "a" app.render_opts[:cache][:a].wont_be_nil end it "Support :cache=>false option to disable template caching" do app(:bare) do plugin :render, :views=>"./spec/views" route do |r| @a = 'a' r.is('a'){render('iv', :cache=>false)} render('iv') end end body('/a').strip.must_equal "a" app.render_opts[:cache][File.expand_path('spec/views/iv.erb')].must_be_nil body('/b').strip.must_equal "a" app.render_opts[:cache][File.expand_path('spec/views/iv.erb')].wont_be_nil end it "Support :cache=>false option to disable template caching even when :cache_key is given" do app(:bare) do plugin :render, :views=>"./spec/views" route do |r| @a = 'a' r.is('a'){render('iv', :cache=>false, :cache_key=>:foo)} render('iv', :cache_key=>:foo) end end body('/a').strip.must_equal "a" app.render_opts[:cache][:foo].must_be_nil body('/b').strip.must_equal "a" app.render_opts[:cache][:foo].wont_be_nil end [{}, {:cache=>true}].each do |cache_val_opts| [{}, {:cache_key=>:foo}].each do |cache_key_opts| cache_opts = cache_key_opts.merge(cache_val_opts) it "Support :explicit_cache plugin option with #{cache_opts} render option" do app(:bare) do plugin :render, :views=>"./spec/views", :explicit_cache=>true route do |r| @a = 'a' render('iv', cache_opts) end end body('/a').strip.must_equal "a" template = app.render_opts[:cache][cache_opts[:cache_key] || File.expand_path("spec/views/iv.erb")] template.must_be_kind_of(cache_val_opts.empty? ? Roda::RodaPlugins::Render::TemplateMtimeWrapper : Tilt::Template) body('/a').strip.must_equal "a" end end end it "Support :cache=>true option to enable template caching when :template_block is used" do c = Class.new do def initialize(path, *, &block) @path = path @block = block end def render(*) "#{@path}-#{@block.call}" end end proca = proc{'a'} app(:bare) do plugin :render, :views=>"./spec/views" route do |r| @a = 'a' r.is('a'){render(:path=>'iv', :template_class=>c, :template_block=>proca)} render(:path=>'iv', :template_class=>c, :template_block=>proca, :cache=>true) end end body('/a').strip.must_equal "iv-a" app.render_opts[:cache][['iv', c, nil, nil, proca]].must_be_nil body('/b').strip.must_equal "iv-a" app.render_opts[:cache][['iv', c, nil, nil, proca]].wont_be_nil end it "Support :cache_key option to force the key used when caching, unless :cache=>false option is used" do app(:bare) do plugin :render, :views=>"./spec/views" route do |r| @a = 'a' r.is('a'){render('iv', :cache_key=>:a)} r.is('about'){render('about', :cache_key=>:a, :cache=>false, :locals=>{:title=>'a'})} render('about', :cache_key=>:a) end end body('/a').strip.must_equal "a" body('/b').strip.must_equal "a" body('/about').strip.must_equal "

a

" end it "Support :scope option to override object in which to evaluate the template" do app(:bare) do plugin :render, :views=>"./spec/views" route do |r| render(:inline=>'<%= first %>-<%= last %>', :scope=>[1,2]) end end body.must_equal "1-2" end it "should dup render_opts when subclassing" do c = Class.new(Roda) c.plugin :render, :foo=>:bar sc = Class.new(c) c.render_opts.wont_be_same_as(sc.render_opts) c.render_opts[:foo].must_equal :bar end it "should use a copy of superclass's cache when subclassing" do c = Class.new(Roda) c.plugin :render c.render_opts[:cache][:foo] = 1 sc = Class.new(c) c.render_opts.wont_be_same_as(sc.render_opts) c.render_opts[:cache].wont_be_same_as(sc.render_opts[:cache]) sc.render_opts[:cache][:foo].must_equal 1 end it "should not modifying existing cache if loading the plugin a separate time" do c = Class.new(Roda) c.plugin :render cache = c.render_opts[:cache] c.render_opts[:check_template_mtime].must_equal false c.plugin :render c.render_opts[:cache].must_be_same_as cache c.render_opts[:check_template_mtime].must_equal false c.plugin :render, :cache=>false c.render_opts[:cache].must_be_same_as cache c.render_opts[:check_template_mtime].must_equal true c.plugin :render c.render_opts[:cache].must_be_same_as cache c.render_opts[:check_template_mtime].must_equal true end it "render plugin call should not override existing options" do c = Class.new(Roda) c.plugin :render, :layout_opts=>{:template=>'foo'} c.plugin :render c.render_opts[:layout_opts][:template].must_equal 'foo' end it "should not use cache by default in subclass if not caching by default in superclass" do app(:bare) do plugin :render, :views=>"./spec/views", :cache=>false route do |r| view(:inline=>"Hello <%= name %>", :cache_key=>:a, :locals=>{:name => "Agent Smith"}, :layout=>nil) end end body("/inline").strip.must_equal "Hello Agent Smith" Class.new(app).render_opts[:check_template_mtime].must_equal true end it "with :check_paths=>true plugin option used" do render_opts = {} app(:bare) do plugin :render, :views=>"./spec/views", :check_paths=>true route do |r| r.get 'a' do render("a", render_opts) end r.get 'c' do render("about/_test", :locals=>{:title=>'a'}) end render("b", render_opts) end end body.strip.must_equal "b" req("/a") req("/c") @app = Class.new(app) app.plugin :render, :allowed_paths=>[] proc{req}.must_raise Roda::RodaError proc{req("/a")}.must_raise Roda::RodaError proc{req("/c")}.must_raise Roda::RodaError @app = Class.new(app) app.plugin :render, :allowed_paths=>['spec/views/about'] proc{req}.must_raise Roda::RodaError proc{req("/a")}.must_raise Roda::RodaError req("/c") @app = Class.new(app) app.plugin :render, :allowed_paths=>%w'spec/views/about spec/views/b' body.strip.must_equal "b" proc{req("/a")}.must_raise Roda::RodaError req("/c") render_opts[:check_paths] = true @app = Class.new(app) app.plugin :render, :check_paths=>false body.strip.must_equal "b" proc{req("/a")}.must_raise Roda::RodaError req("/c") render_opts.delete(:check_paths) app.plugin :render body.strip.must_equal "b" req("/a") req("/c") render_opts[:check_paths] = true body.strip.must_equal "b" proc{req("/a")}.must_raise Roda::RodaError req("/c") end it "with a cache_class set" do app(:bare) do test_cache = Class.new(Roda::RodaCache) do def [](key) super end def []=(key, value) super end def test_method end end plugin :render, :views=>"./spec/views", :cache=>true, :cache_class=>test_cache route do |r| view(:inline=>"foo", :layout=>nil) end end body("/inline").strip.must_equal "foo" Class.new(app).render_opts[:cache].must_respond_to :test_method end it "supports template_opts default_encoding option" do app(:bare){} app.plugin :render app.render_opts[:template_opts][:default_encoding].must_equal Encoding.default_external app.plugin :render, :template_opts=>{:default_encoding=>'ISO-8859-1'} app.render_opts[:template_opts][:default_encoding].must_equal 'ISO-8859-1' end end end begin require 'tilt' require 'tilt/erubi' rescue LoadError warn "tilt 2 or erubi not installed, skipping render :escape=>true test" else describe ":render plugin :escape option" do before do if defined?(Tilt::ErubiTemplate) && ::Tilt['erb'] != Tilt::ErubiTemplate # Set erubi as default erb template handler Tilt.register(Tilt::ErubiTemplate, 'erb') end end it "should escape inside <%= %> and not inside <%== %>, and handle postfix conditionals" do app(:bare) do plugin :render, :escape=>true route do |r| render(:inline=>'<%= "<>" %> <%== "<>" %><%= "<>" if false %>') end end body.must_equal '<> <>' end it "should allow for per-branch escaping via set_view options" do app(:bare) do plugin :render, :escape=>true plugin :view_options route do |r| set_view_options :template_opts=>{:escape=>false} r.is 'a' do set_view_options :template_opts=>{:engine_class=>render_opts[:template_opts][:engine_class]} render(:inline=>'<%= "<>" %>') end render(:inline=>'<%= "<>" %>') end end body('/a').must_equal '<>' body.must_equal '<>' end it "should accept :escape=>String to only escape for that template engine" do app(:bare) do plugin :render, :escape=>'erb' route do |r| r.is 'a' do render(:inline=>'<%= "<>" %> <%== "<>" %><%= "<>" if false %>', :engine=>'unescaped.erb') end render(:inline=>'<%= "<>" %> <%== "<>" %><%= "<>" if false %>') end end body.must_equal '<> <>' body('/a').must_equal '<> <>' end it "should accept :escape=>Array to only escape for those template engines" do app(:bare) do plugin :render, :escape=>%w'erb erubi', :engine_opts=>{'erubi'=>{:bufvar=>'_buf'}} route do |r| r.is 'a' do render(:inline=>'<%= "<>" %> <%== "<>" %><%= "<>" if false %>', :engine=>'unescaped.erb') end r.is 'b' do render(:inline=>'<%= _buf.length %><%= "<>" %> <%== "<>" %><%= "<>" if false %>', :engine=>'erubi') end render(:inline=>'<%= "<>" %> <%== "<>" %><%= "<>" if false %>') end end body.must_equal '<> <>' body('/a').must_equal '<> <>' body('/b').must_equal '0<> <>' end end end jeremyevans-roda-4f30bb3/spec/plugin/request_aref_spec.rb000066400000000000000000000026741516720775400236750ustar00rootroot00000000000000require_relative "../spec_helper" describe "request_aref plugin" do def request_aref_app(value) warning = @warning = String.new('') app(:bare) do plugin :request_aref, value self::RodaRequest.class_eval do define_method(:warn){|s| warning.replace(s)} private :warn end route do |r| r.get('set'){r['b'] = 'c'; r.params['b']} r['a'] end end end def aref_body body("QUERY_STRING" => 'a=d', 'rack.input'=>rack_input) end def aset_body body('/set', "QUERY_STRING" => 'a=d', 'rack.input'=>rack_input) end it "allows if given the :allow option" do request_aref_app(:allow) aref_body.must_equal 'd' @warning.must_equal '' aset_body.must_equal 'c' @warning.must_equal '' end it "warns if given the :warn option" do request_aref_app(:warn) aref_body.must_equal 'd' @warning.must_include('#[] is deprecated, use #params.[] instead') aset_body.must_equal 'c' @warning.must_include('#[]= is deprecated, use #params.[]= instead') end it "raises if given the :raise option" do request_aref_app(:raise) proc{aref_body}.must_raise Roda::RodaPlugins::RequestAref::Error @warning.must_equal '' proc{aset_body}.must_raise Roda::RodaPlugins::RequestAref::Error @warning.must_equal '' end it "raises when loading plugin if given other option" do proc{request_aref_app(:r)}.must_raise Roda::RodaError end end jeremyevans-roda-4f30bb3/spec/plugin/request_headers_spec.rb000066400000000000000000000020161516720775400243610ustar00rootroot00000000000000require_relative "../spec_helper" describe "request_headers plugin" do def header_app(header_name) app(:bare) do plugin :request_headers route do |r| r.on do # return the value of the request header in the response body, # or the static string 'not found' if it hasn't been supplied. r.headers[header_name] || 'not found' end end end end it "must add HTTP_ prefix when appropriate" do header_app('Foo') body('/', {'HTTP_FOO' => 'a'}).must_equal 'a' end it "must ignore HTTP_ prefix when appropriate" do header_app('Content-Type') body('/', {'CONTENT_TYPE' => 'a'}).must_equal 'a' end it "must return nil for non-existant headers" do header_app('X-Non-Existant') body('/').must_equal 'not found' end it "must be case-insensitive" do header_app('X-My-Header') body('/', {'HTTP_X_MY_HEADER' => 'a'}).must_equal 'a' header_app('x-my-header') body('/', {'HTTP_X_MY_HEADER' => 'a'}).must_equal 'a' end end jeremyevans-roda-4f30bb3/spec/plugin/response_attachment_spec.rb000066400000000000000000000040551516720775400252510ustar00rootroot00000000000000require_relative "../spec_helper" require 'uri' describe 'response_attachment plugin' do before do app(:response_attachment) do |r| response.attachment r.path[1, 1000] 'b' end end it 'sets the Content-Disposition header' do header(RodaResponseHeaders::CONTENT_DISPOSITION, '/foo/test.xml').must_equal 'attachment; filename="test.xml"' body.must_equal 'b' end it 'sets the Content-Disposition header with character set if filename contains characters that should be escaped' do filename = "/foo/a\u1023b.xml" app(:response_attachment) do |r| response.attachment filename 'b' end header(RodaResponseHeaders::CONTENT_DISPOSITION).must_equal 'attachment; filename="a-b.xml"; filename*=UTF-8\'\'a%E1%80%A3b.xml' body.must_equal 'b' filename = "/foo/a\255\255b.xml".dup.force_encoding('ISO-8859-1') header(RodaResponseHeaders::CONTENT_DISPOSITION).must_equal 'attachment; filename="a-b.xml"; filename*=ISO-8859-1\'\'a%AD%ADb.xml' body.must_equal 'b' filename = "/foo/a\255\255b.xml".dup.force_encoding('BINARY') header(RodaResponseHeaders::CONTENT_DISPOSITION).must_equal 'attachment; filename="a-b.xml"' body.must_equal 'b' end it 'sets the Content-Disposition header even when a filename is not given' do app(:response_attachment) do |r| response.attachment 'b' end header(RodaResponseHeaders::CONTENT_DISPOSITION, '/foo/test.xml').must_equal 'attachment' end it 'sets the Content-Type header' do header(RodaResponseHeaders::CONTENT_TYPE, '/test.xml').must_equal 'application/xml' end it 'does not modify the default Content-Type without a file extension' do header(RodaResponseHeaders::CONTENT_TYPE, '/README').must_equal 'text/html' end it 'should not modify the Content-Type if it is already set' do app(:response_attachment) do |r| response[RodaResponseHeaders::CONTENT_TYPE] = "foo" response.attachment r.path[1, 1000] 'b' end header(RodaResponseHeaders::CONTENT_TYPE, '/README').must_equal 'foo' end end jeremyevans-roda-4f30bb3/spec/plugin/response_content_type_spec.rb000066400000000000000000000034521516720775400256340ustar00rootroot00000000000000require_relative "../spec_helper" describe "response_content_type plugin" do it "allows getting and setting content-type" do app(:response_content_type) do |r| r.get "a" do response.content_type = "text/plain" "a:#{response.content_type}" end ":#{response.content_type}" end _, h, b = req h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/html' b.must_equal [":"] _, h, b = req("/a") h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/plain' b.must_equal ["a:text/plain"] end it "supports using symbols with plugin :mime_types option, and raises if an invalid symbol is provided" do app(:bare) do plugin :response_content_type, mime_types: {:txt => "text/plain"} route do |r| r.get "a" do response.content_type = :bad end response.content_type = :txt response.content_type end end _, h, b = req h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/plain' b.must_equal ["text/plain"] proc{req("/a")}.must_raise KeyError end it "supports plugin mime_types: :from_rack_mime option" do app(:bare) do plugin :response_content_type, mime_types: :from_rack_mime route do |r| r.get String do |s| response.content_type = s.to_sym "" end response.content_type = :txt response.content_type end end 2.times do _, h, b = req h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/plain' b.must_equal ["text/plain"] header(RodaResponseHeaders::CONTENT_TYPE, "/pdf").must_equal "application/pdf" proc{req("/invalid-mime-type")}.must_raise KeyError # test when loading the plugin more than once app.plugin :response_content_type end end end jeremyevans-roda-4f30bb3/spec/plugin/response_request_spec.rb000066400000000000000000000016251516720775400246110ustar00rootroot00000000000000require_relative "../spec_helper" describe "response_request plugin" do it "gives the response access to the request" do app(:response_request) do response.request.post? ? "b" : "a" end body.must_equal "a" body('REQUEST_METHOD'=>'POST').must_equal "b" end it "should work with error_handler plugin" do app(:bare) do plugin :response_request plugin :error_handler do |_| response.request.post? ? "b" : "a" end route{raise} end body.must_equal "a" body('REQUEST_METHOD'=>'POST').must_equal "b" end it "should work with class_level_routing plugin" do app(:bare) do plugin :response_request plugin :class_level_routing is '' do |_| response.request.post? ? "b" : "a" end route{} end body.must_equal "a" body('REQUEST_METHOD'=>'POST').must_equal "b" end end jeremyevans-roda-4f30bb3/spec/plugin/route_block_args_spec.rb000066400000000000000000000037261516720775400245330ustar00rootroot00000000000000require_relative "../spec_helper" describe "route_block_args plugin" do it "works with hooks when loaded last" do a = [] app(:bare) do plugin :hooks before { a << 1 } after { a << 2 } plugin :route_block_args do [request, response] end route do |req, res| response.status = 401 a << req.path << res.status "1" end end body.must_equal "1" a.must_equal [1, '/', 401, 2] end it "works with hooks when loaded first" do a = [] app(:bare) do plugin :route_block_args do [request, response] end plugin :hooks before { a << 1 } after { a << 2 } route do |req, res| response.status = 401 a << req.path << res.status "1" end end body.must_equal "1" a.must_equal [1, '/', 401, 2] end it "still supports a single route block argument" do app(:bare) do plugin :route_block_args do request end route { |r| "OK" } end status('/').must_equal(200) end it "supports many route block arguments" do app(:bare) do plugin :route_block_args do [request.params, request.env, response.headers] end route do |p, e, h| h['foo'] = "#{p['a']}-#{e['B']}" end end header('foo', 'rack.input'=>rack_input).must_equal('-') body('rack.input'=>rack_input).must_equal('-') header('foo', 'QUERY_STRING'=>'a=c', 'B'=>'D', 'rack.input'=>rack_input).must_equal('c-D') end it "works if given after the route block" do app(:bare) do route do |p, e, h| h['foo'] = "#{p['a']}-#{e['B']}" end plugin :route_block_args do [request.params, request.env, response.headers] end end header('foo', 'rack.input'=>rack_input).must_equal('-') body('rack.input'=>rack_input).must_equal('-') header('foo', 'QUERY_STRING'=>'a=c', 'B'=>'D', 'rack.input'=>rack_input).must_equal('c-D') end end jeremyevans-roda-4f30bb3/spec/plugin/route_csrf_spec.rb000066400000000000000000000501031516720775400233510ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'cgi/escape' rescue LoadError require 'cgi' end describe "route_csrf plugin" do include CookieJar def route_csrf_app(opts={}, &block) app(:bare) do send(*DEFAULT_SESSION_ARGS) unless opts[:no_sessions_plugin] plugin(:route_csrf, opts, &opts[:block]) route do |r| check_csrf! unless env['SKIP'] r.post('foo'){'f'} r.post('bar'){'b'} r.get "token", String do |s| csrf_token("/#{s}") end instance_exec(r, &block) if block end end end it "allows all GET requests and allows POST requests only if they have a correct token for the path" do route_csrf_app token = body("/token/foo") token.length.must_equal 84 body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f' proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken token = body("/token/bar") token.length.must_equal 84 body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'b' proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/bar", "REQUEST_METHOD"=>'DELETE', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken # Additional failure cases proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input)}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}a"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken t2 = token.dup t2.setbyte(1, t2.getbyte(1) ^ 1) proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(t2)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken t2 = token.dup t2.setbyte(61, t2.getbyte(61) ^ 1) proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(t2)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken t2 = token.dup t2[1] = '|' proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(t2)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken end it "supports :require_request_specific_tokens => false option to allow non-request-specific tokens" do route_csrf_app(:require_request_specific_tokens=>false){csrf_token} token = body("/token/foo") token.length.must_equal 84 body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f' proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken token = body token.length.must_equal 84 body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f' body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'b' end it "allows tokens submitted in both parameter and HTTP header if :check_header option is true" do route_csrf_app(:check_header=>true) token = body("/token/foo") token.length.must_equal 84 body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f' body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input, 'HTTP_X_CSRF_TOKEN'=>token).must_equal 'f' proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'HTTP_X_CSRF_TOKEN'=>token)}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken end it "allows tokens submitted in only HTTP header if :check_header option is :only" do route_csrf_app(:check_header=>:only) token = body("/token/foo") token.length.must_equal 84 body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input, 'HTTP_X_CSRF_TOKEN'=>token).must_equal 'f' proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'HTTP_X_CSRF_TOKEN'=>token)}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken end it "allows tokens specified via :token option to check_csrf" do app(:bare) do send(*DEFAULT_SESSION_ARGS) unless opts[:no_sessions_plugin] plugin(:route_csrf) route do |r| check_csrf!(:token=>env['TOKEN']) r.post('foo'){'f'} r.get "token", String do |s| csrf_token("/#{s}") end end end token = body("/token/foo") token.length.must_equal 84 body("/foo", "REQUEST_METHOD"=>'POST', 'TOKEN'=>token).must_equal 'f' proc{body("/foo", "REQUEST_METHOD"=>'POST', 'TOKEN'=>token+'1')}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken end it "allows configuring CSRF failure action with :csrf_failure => :empty_403 option" do route_csrf_app(:csrf_failure=>:empty_403) body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f' req("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input).must_equal [403, {RodaResponseHeaders::CONTENT_TYPE=>'text/html', RodaResponseHeaders::CONTENT_LENGTH=>'0'}, []] end it "allows configuring CSRF failure action with :csrf_failure => :clear_session option" do route_csrf_app(:csrf_failure=>:clear_session){session.inspect} body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f' body("/b", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/token/a'))}")).must_equal '{}' end it "allows configuring CSRF failure action with :csrf_failure => proc option" do route_csrf_app(:csrf_failure=>proc{|r| r.path + '2'}) body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f' body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input).must_equal '/foo2' end it "allows configuring CSRF failure action via a plugin block" do route_csrf_app(:block=>proc{|r| r.path + '2'}) body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f' body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input).must_equal '/foo2' end it "allows plugin block to integrate with route_block_args plugin" do app(:bare) do send(*DEFAULT_SESSION_ARGS) plugin :route_block_args do [request, request.path, response] end plugin(:route_csrf){|r, path, res| res.write(path); res.write('2')} route do |r| check_csrf! r.post('foo'){'f'} r.get "token", String do |s| csrf_token("/#{s}") end end end body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/foo"))}")).must_equal 'f' body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input).must_equal '/foo2' end it "raises Error if configuring plugin with invalid :csrf_failure option" do route_csrf_app(:csrf_failure=>:foo) proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input)}.must_raise Roda::RodaError end it "raises Error if configuring plugin with block and :csrf_failure option" do proc{route_csrf_app(:block=>proc{|r| r.path + '2'}, :csrf_failure=>:raise)}.must_raise Roda::RodaError end deprecated "supports check_csrf! :csrf_failure option as a Proc" do pr = proc{env['BAD'] == '1' ? 't' : 'f'} route_csrf_app{check_csrf!(:csrf_failure=>pr); ''} token = body("/token/foo") body("SKIP"=>"1", "BAD"=>'1', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 't' body("SKIP"=>"1", "BAD"=>'0', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f' end it "supports valid_csrf? method" do route_csrf_app{valid_csrf?.to_s} body("/a", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/a"))}")).must_equal 'true' body("/a", "REQUEST_METHOD"=>'POST', 'SKIP'=>'1', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/b"))}")).must_equal 'false' end it "supports valid_csrf? method" do route_csrf_app do check_csrf!{'nope'} 'yep' end body("/a", "REQUEST_METHOD"=>'POST', 'SKIP'=>'1', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/a"))}")).must_equal 'yep' body("/a", "REQUEST_METHOD"=>'POST', 'SKIP'=>'1', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body("/token/b"))}")).must_equal 'nope' end it "supports use_request_specific_csrf_tokens? method" do route_csrf_app{use_request_specific_csrf_tokens?.to_s} body.must_equal 'true' route_csrf_app(:require_request_specific_tokens=>false){use_request_specific_csrf_tokens?.to_s} body.must_equal 'false' end it "supports csrf_field method" do route_csrf_app{csrf_field} body.must_equal '_csrf' route_csrf_app(:field=>'foo'){csrf_field} body.must_equal 'foo' end it "supports csrf_header method" do route_csrf_app{csrf_header} body.must_equal 'X-CSRF-Token' route_csrf_app(:header=>'Foo'){csrf_header} body.must_equal 'Foo' end it "supports csrf_metatag method" do route_csrf_app(:require_request_specific_tokens=>false){csrf_metatag} body =~ /\A\z/ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape($1)}")).must_equal 'f' route_csrf_app(:require_request_specific_tokens=>false, :field=>'foo'){csrf_metatag} body =~ /\A\z/ body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("foo=#{Rack::Utils.escape($1)}")).must_equal 'b' end it "supports csrf_tag method" do route_csrf_app(:require_request_specific_tokens=>false){csrf_tag} body =~ /\A\z/ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape($1)}")).must_equal 'f' route_csrf_app(:require_request_specific_tokens=>false, :field=>'foo'){csrf_tag} body =~ /\A\z/ body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("foo=#{Rack::Utils.escape($1)}")).must_equal 'f' route_csrf_app{csrf_tag('/foo')} body =~ /\A\z/ token = $1 body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f' proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken route_csrf_app do |r| r.is 'foo', :method=>'PUT' do 'f2' end csrf_tag('/foo', 'PUT') end body =~ /\A\z/ token = $1 body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}").must_equal 'f2' proc{body("/bar", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken end it "supports csrf_formaction_tag method" do route_csrf_app{csrf_formaction_tag('/foo')} body =~ /\A\z/ field = CGI.unescapeHTML($1) token = $2 body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("#{Rack::Utils.escape(field)}=#{Rack::Utils.escape(token)}")).must_equal 'f' proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("#{Rack::Utils.escape(field)}=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"#{Rack::Utils.escape(field)}=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken route_csrf_app do |r| r.is 'foo', :method=>'PUT' do 'f2' end csrf_formaction_tag('/foo', 'PUT') end body =~ /\A\z/ field = CGI.unescapeHTML($1) token = $2 body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"#{Rack::Utils.escape(field)}=#{Rack::Utils.escape(token)}").must_equal 'f2' proc{body("/bar", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"#{Rack::Utils.escape(field)}=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("#{Rack::Utils.escape(field)}=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken end it "supports csrf_token method" do route_csrf_app(:require_request_specific_tokens=>false){csrf_token} body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body)}")).must_equal 'f' route_csrf_app(:require_request_specific_tokens=>false, :field=>'foo'){csrf_token} body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("foo=#{Rack::Utils.escape(body)}")).must_equal 'f' route_csrf_app{csrf_token('/foo')} token = body body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f' proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken route_csrf_app do |r| r.is 'foo', :method=>'PUT' do 'f2' end csrf_token('/foo', 'PUT') end token = body body("/foo", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}").must_equal 'f2' proc{body("/bar", "REQUEST_METHOD"=>'PUT', 'rack.input'=>rack_input, 'QUERY_STRING'=>"_csrf=#{Rack::Utils.escape(token)}")}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken end it "supports csrf_path method" do route_csrf_app do |r| r.post{r.path + '2'} csrf_token(csrf_path(env['CP'])) end body("REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body)}")).must_equal '/2' body("REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>''))}")).must_equal '/2' body("REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>'#foo'))}")).must_equal '/2' body("REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>'?foo'))}")).must_equal '/2' body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/a'))}")).must_equal '/a2' body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/a', 'CP'=>''))}")).must_equal '/a2' body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/a', 'CP'=>'?foo'))}")).must_equal '/a2' body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/a', 'CP'=>'#foo'))}")).must_equal '/a2' body("REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>'http://foo/'))}")).must_equal '/2' body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>'https://foo/a'))}")).must_equal '/a2' body('/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>'http://foo/a/b'))}")).must_equal '/a/b2' body("REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>'/'))}")).must_equal '/2' body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>'/a'))}")).must_equal '/a2' body('/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('CP'=>'/a/b'))}")).must_equal '/a/b2' body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a'))}")).must_equal '/a2' body('/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/b', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a'))}")).must_equal '/a2' body('/b/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/b/', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a'))}")).must_equal '/b/a2' body('/b/a', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/b/b', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a'))}")).must_equal '/b/a2' body('/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/b', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a/b'))}")).must_equal '/a/b2' body('/b/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/b/', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a/b'))}")).must_equal '/b/a/b2' body('/b/a/b', "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(body('/b/a', 'HTTPS'=>'on', 'HTTP_HOST'=>'foo.com', 'CP'=>'a/b'))}")).must_equal '/b/a/b2' end begin require 'rack/csrf' rescue LoadError warn "rack_csrf not installed, skipping route_csrf plugin test for rack_csrf upgrade" else it "supports upgrades from existing rack_csrf token" do route_csrf_app(:upgrade_from_rack_csrf_key=>'csrf.token', :no_sessions_plugin=>true) do |r| r.get 'clear' do session.clear '' end Rack::Csrf.token(env) end app.use(*DEFAULT_SESSION_MIDDLEWARE_ARGS) app.use Rack::Csrf, :skip=>['POST:/foo', 'POST:/bar'], :raise=>true token = body token.length.wont_equal 84 body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'f' body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}")).must_equal 'b' body('/clear').must_equal '' proc{body("/foo", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken proc{body("/bar", "REQUEST_METHOD"=>'POST', 'rack.input'=>rack_input("_csrf=#{Rack::Utils.escape(token)}"))}.must_raise Roda::RodaPlugins::RouteCsrf::InvalidToken end end end jeremyevans-roda-4f30bb3/spec/plugin/run_append_slash_spec.rb000066400000000000000000000044151516720775400245300ustar00rootroot00000000000000require_relative "../spec_helper" describe "run_append_slash plugin" do before do sub2 = app do |r| r.root do 'sub-bar-root' end r.get 'baz' do 'sub-bar-baz' end end sub1 = app(:run_append_slash) do |r| r.root do 'sub-root' end r.get 'foo' do 'sub-foo' end r.on 'bar' do r.run sub2 end end app(:bare) do route do |r| r.root do 'root' end r.on 'sub' do r.run sub1 end end end end it "without plugin does not append a missing trailing slash to #run sub apps" do body.must_equal 'root' status('/sub').must_equal 404 body('/sub/').must_equal 'sub-root' body('/sub/foo').must_equal 'sub-foo' status('/sub/foo/').must_equal 404 body('/sub/bar/').must_equal 'sub-bar-root' body('/sub/bar/baz').must_equal 'sub-bar-baz' status('/sub/bar/baz/').must_equal 404 end unless ENV['LINT'] it "internally appends a missing trailing slash to #run sub apps" do app.plugin :run_append_slash body('/sub').must_equal 'sub-root' body('/sub/').must_equal 'sub-root' body('/sub/foo').must_equal 'sub-foo' status('/sub/foo/').must_equal 404 body('/sub/bar').must_equal 'sub-bar-root' body('/sub/bar/').must_equal 'sub-bar-root' body('/sub/bar/baz').must_equal 'sub-bar-baz' status('/sub/bar/baz/').must_equal 404 end it "redirects #run sub apps when trailing slash is missing" do app.plugin :run_append_slash, :use_redirects => true status('/sub').must_equal 302 header(RodaResponseHeaders::LOCATION, '/sub').must_equal '/sub/' body('/sub/').must_equal 'sub-root' body('/sub/foo').must_equal 'sub-foo' status('/sub/foo/').must_equal 404 body('/sub/bar').must_equal 'sub-bar-root' body('/sub/bar/').must_equal 'sub-bar-root' body('/sub/bar/baz').must_equal 'sub-bar-baz' status('/sub/bar/baz/').must_equal 404 end it "works with run_handler plugin" do sub = app(:bare) { } app(:bare) do plugin :run_handler plugin :run_append_slash route do |r| r.run sub, not_found: :pass r.root do "main" end end end body("/").must_equal 'main' end end jeremyevans-roda-4f30bb3/spec/plugin/run_handler_spec.rb000066400000000000000000000034341516720775400235040ustar00rootroot00000000000000require_relative "../spec_helper" describe "run_handler plugin" do it "makes r.run :not_found=>:pass keep going on 404" do pr = proc{|env| [(env['PATH_INFO'] == '/a' ? 404 : 201), {}, ['b']]} app(:run_handler) do |r| r.run pr, :not_found=>:pass 'a' end status.must_equal 201 body.must_equal 'b' status('/a').must_equal 200 body('/a').must_equal 'a' end it "closes body when passing" do o = Object.new closed = false o.define_singleton_method(:close){closed = true} pr = proc{|env| [(env['PATH_INFO'] == '/a' ? 404 : 201), {}, (env['PATH_INFO'] == '/a' ? o : ['b'])]} app(:run_handler) do |r| r.run pr, :not_found=>:pass 'a' end body.must_equal 'b' closed.must_equal false body('/a').must_equal 'a' closed.must_equal true end it "makes r.run with a block yield rack app to block, and have it be thrown afterward" do pr = proc{|env| [(env['PATH_INFO'] == '/a' ? 404 : 201), {}, ['b']]} app(:run_handler) do |r| r.run(pr){|a| a[0] *= 2} 'a' end status.must_equal 402 status('/a').must_equal 808 end it "works when both :not_found=>:pass and block are given" do pr = proc{|env| [(env['PATH_INFO'] == '/a' ? 202 : 201), {}, ['b']]} app(:run_handler) do |r| r.run(pr, :not_found=>:pass){|a| a[0] *= 2} 'a' end status.must_equal 402 body.must_equal 'b' status('/a').must_equal 200 body('/a').must_equal 'a' end it "makes r.run work normally if not given an option or block" do pr = proc{|env| [(env['PATH_INFO'] == '/a' ? 404 : 201), {}, ['b']]} app(:run_handler) do |r| r.run pr 'a' end status.must_equal 201 body.must_equal 'b' status('/a').must_equal 404 body('/a').must_equal 'b' end end jeremyevans-roda-4f30bb3/spec/plugin/run_require_slash_spec.rb000066400000000000000000000015051516720775400247320ustar00rootroot00000000000000require_relative "../spec_helper" describe "run_require_slash plugin" do before do sub = app do |r| "sub-#{r.remaining_path}" end app(:bare) do plugin :match_affix, "", /(\/|\z)/ plugin :run_require_slash route do |r| r.on "/a" do |b| r.on "b" do |id, s| r.run sub "b-#{r.remaining_path}" end "albums-#{b}" end end end end it "dispatches to application for empty PATH_INFO" do body("/a/b").must_equal 'sub-' body("/a/b/").must_equal 'sub-' end unless ENV['LINT'] it "dispatches to application for PATH_INFO starting with /" do body("/a/b//").must_equal 'sub-/' end it "does not dispatch to application for PATH_INFO not starting with /" do body("/a/b/1").must_equal 'b-1' end end jeremyevans-roda-4f30bb3/spec/plugin/sec_fetch_site_csrf_spec.rb000066400000000000000000000123611516720775400251660ustar00rootroot00000000000000require_relative "../spec_helper" describe "sec_fetch_site_csrf plugin" do def sec_fetch_site_app(opts={}, &block) app(:bare) do plugin(:sec_fetch_site_csrf, opts, &block) route do |r| check_sec_fetch_site! r.post("session"){session[:a].inspect} "allowed" end end end it "allows all GET requests and allows POST requests only for same-origin requests" do sec_fetch_site_app body.must_equal 'allowed' body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'DELETE', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'PATCH', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'PUT', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' proc{body("REQUEST_METHOD"=>'POST')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure proc{body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-site')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure proc{body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'none')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure proc{body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'cross-site')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure proc{body("REQUEST_METHOD"=>'DELETE', 'HTTP_SEC_FETCH_SITE'=>'same-site')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure proc{body("REQUEST_METHOD"=>'PATCH', 'HTTP_SEC_FETCH_SITE'=>'same-site')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure proc{body("REQUEST_METHOD"=>'PUT', 'HTTP_SEC_FETCH_SITE'=>'same-site')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure end it "supports :allow_missing option" do sec_fetch_site_app(:allow_missing=>true) body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'POST').must_equal 'allowed' proc{body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-site')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure end it "supports :allow_none option" do sec_fetch_site_app(:allow_none=>true) body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'none').must_equal 'allowed' proc{body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-site')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure end it "supports :allow_same_site option" do sec_fetch_site_app(:allow_same_site=>true) body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-site').must_equal 'allowed' proc{body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'none')}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure end it "supports :check_request_methods option" do sec_fetch_site_app(:check_request_methods=>["GET"]) proc{body}.must_raise Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure body('HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'POST').must_equal 'allowed' end it "allows configuring CSRF failure action with :csrf_failure => :empty_403 option" do sec_fetch_site_app(:csrf_failure=>:empty_403) body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' req("REQUEST_METHOD"=>'POST').must_equal [403, {RodaResponseHeaders::CONTENT_TYPE=>'text/html', RodaResponseHeaders::CONTENT_LENGTH=>'0'}, []] end it "allows configuring CSRF failure action with :csrf_failure => :clear_session option" do sec_fetch_site_app(:csrf_failure=>:clear_session) body("/session", "REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin', 'rack.session'=>{a: 1}).must_equal '1' body("/session", "REQUEST_METHOD"=>'POST', 'rack.session'=>{a: 1}).must_equal 'nil' end it "allows configuring CSRF failure action via a plugin block" do sec_fetch_site_app{|r| r.path + '2'} body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'POST').must_equal '/2' end it "allows overriding failure behavior by passing block to check_sec_fetch_site! method" do app(:bare) do plugin(:sec_fetch_site_csrf) route do |r| check_sec_fetch_site!{r.path + '2'} "allowed" end end body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'POST').must_equal '/2' end it "allows plugin block to integrate with route_block_args plugin" do app(:bare) do plugin :route_block_args do [request, request.path, response] end plugin(:sec_fetch_site_csrf){|r, path, res| res.write(path); res.write('2')} route do |r| check_sec_fetch_site! "allowed" end end body("REQUEST_METHOD"=>'POST', 'HTTP_SEC_FETCH_SITE'=>'same-origin').must_equal 'allowed' body("REQUEST_METHOD"=>'POST').must_equal '/2' end it "raises Error if configuring plugin with invalid :csrf_failure option" do proc{sec_fetch_site_app(:csrf_failure=>:foo)}.must_raise Roda::RodaError end end jeremyevans-roda-4f30bb3/spec/plugin/send_file_spec.rb000066400000000000000000000071211516720775400231300ustar00rootroot00000000000000require_relative "../spec_helper" require 'uri' describe 'send_file plugin' do before do file = @file = 'spec/assets/css/raw.css' @content = File.read(@file) app(:send_file) do |r| send_file file, env['rack.OPTS'] || {} end end it "sends the contents of the file" do status.must_equal 200 body.must_equal @content end it "returns response body implementing to_path" do req[2].to_path.must_equal @file end if !ENV['LINT'] || Rack.release >= '3' it 'sets the Content-Type response header if a mime-type can be located' do header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/css' end it 'sets the Content-Type response header if type option is set to a file extension' do header(RodaResponseHeaders::CONTENT_TYPE, 'rack.OPTS'=>{:type => 'html'}).must_equal 'text/html' end it 'sets the Content-Type response header if type option is set to a mime type' do header(RodaResponseHeaders::CONTENT_TYPE, 'rack.OPTS'=>{:type => 'application/octet-stream'}).must_equal 'application/octet-stream' end it 'sets the Content-Length response header' do header(RodaResponseHeaders::CONTENT_LENGTH).must_equal @content.length.to_s end it 'sets the Last-Modified response header' do header(RodaResponseHeaders::LAST_MODIFIED).must_equal File.mtime(@file).httpdate end it 'allows passing in a different Last-Modified response header with :last_modified' do time = Time.now @app.plugin :caching header(RodaResponseHeaders::LAST_MODIFIED, 'rack.OPTS'=>{:last_modified => time}).must_equal time.httpdate end it "returns a 404 when not found" do app(:send_file) do |r| send_file 'this-file-does-not-exist.txt' end status.must_equal 404 end it "does not set the Content-Disposition header by default" do header(RodaResponseHeaders::CONTENT_DISPOSITION).must_be_nil end it "sets the Content-Disposition header when :disposition set to 'attachment'" do header(RodaResponseHeaders::CONTENT_DISPOSITION, 'rack.OPTS'=>{:disposition => 'attachment'}).must_equal 'attachment; filename="raw.css"' end it "does not set add a file name if filename is false" do header(RodaResponseHeaders::CONTENT_DISPOSITION, 'rack.OPTS'=>{:disposition => 'inline', :filename=>false}).must_equal 'inline' end it "sets the Content-Disposition header when :disposition set to 'inline'" do header(RodaResponseHeaders::CONTENT_DISPOSITION, 'rack.OPTS'=>{:disposition => 'inline'}).must_equal 'inline; filename="raw.css"' end it "sets the Content-Disposition header when :filename provided" do header(RodaResponseHeaders::CONTENT_DISPOSITION, 'rack.OPTS'=>{:filename => 'foo.txt'}).must_equal 'attachment; filename="foo.txt"' end it 'allows setting a custom status code' do status('rack.OPTS'=>{:status=>201}).must_equal 201 end it "is able to send files with unknown mime type" do header(RodaResponseHeaders::CONTENT_TYPE, 'rack.OPTS'=>{:type => '.foobar'}).must_equal 'application/octet-stream' end it "does not override Content-Type if already set and no explicit type is given" do file = @file app(:send_file) do |r| response[RodaResponseHeaders::CONTENT_TYPE] = "image/png" send_file file end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'image/png' end it "does override Content-Type even if already set, if explicit type is given" do file = @file app(:send_file) do |r| response[RodaResponseHeaders::CONTENT_TYPE] = "image/png" send_file file, :type => :gif end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'image/gif' end end jeremyevans-roda-4f30bb3/spec/plugin/sessions_spec.rb000066400000000000000000000505011516720775400230460ustar00rootroot00000000000000require_relative "../spec_helper" require_relative "../../lib/roda/plugins/_base64" base64 = Roda::RodaPlugins::Base64_ if RUBY_VERSION >= '2' [true, false].each do |per_cookie_cipher_secret| describe "sessions plugin with per_cookie_cipher_secret: #{per_cookie_cipher_secret}" do include CookieJar def req(path, opts={}) @errors ||= StringIO.new super(path, opts.merge('rack.errors'=>@errors)) end def errors @errors.rewind e = @errors.read.split("\n") @errors.rewind @errors.truncate(0) e end before do app(:bare) do plugin :sessions, :secret=>'1'*64, :per_cookie_cipher_secret=>per_cookie_cipher_secret route do |r| if r.GET['sut'] session env['roda.session.updated_at'] -= r.GET['sut'].to_i if r.GET['sut'] end r.get('s', String, String){|k, v| v.force_encoding('UTF-8'); session[k] = v} r.get('swc', String, String, String, String) do |k, v, ck, cv| Rack::Utils.set_cookie_header!(response.headers, ck, cv) v.force_encoding('UTF-8') session[k] = v end r.get('g', String){|k| session[k].to_s} r.get('e', String){|k| session; env[k].to_a.join('-')} r.get('sct'){|i| session; env['roda.session.created_at'].to_s} r.get('ssct', Integer){|i| session; (env['roda.session.created_at'] -= i).to_s} r.get('ssct2', Integer, String, String){|i, k, v| session[k] = v; (env['roda.session.created_at'] -= i).to_s} r.get('sc'){session.clear; 'c'} r.get('cs', String, String){|k, v| clear_session; session[k] = v} r.get('cat'){t = r.session_created_at; t.strftime("%F") if t} r.get('uat'){t = r.session_updated_at; t.strftime("%F") if t} '' end end end it "requires appropriate :secret option" do proc{app(:bare){plugin :sessions}}.must_raise Roda::RodaError proc{app(:bare){plugin :sessions, :secret=>Object.new}}.must_raise Roda::RodaError proc{app(:bare){plugin :sessions, :secret=>'1'*63}}.must_raise Roda::RodaError end it "has session store data between requests" do req('/').must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>"text/html", RodaResponseHeaders::CONTENT_LENGTH=>"0"}, [""]] body('/s/foo/bar').must_equal 'bar' body('/g/foo').must_equal 'bar' body('/s/foo/baz').must_equal 'baz' body('/g/foo').must_equal 'baz' body("/s/foo/\u1234".b).must_equal "\u1234" body("/g/foo").must_equal "\u1234" errors.must_equal [] end it "supports :env_key for the env key to use for the session" do body('/s/foo/bar').must_equal 'bar' body('/e/rack.session').must_equal 'foo-bar' body('/e/roda.session').must_equal '' @app.plugin(:sessions, :env_key=>'roda.session') body('/e/rack.session').must_equal '' body('/e/roda.session').must_equal 'foo-bar' body('/s/foo/baz').must_equal 'baz' body('/e/roda.session').must_equal 'foo-baz' end it "supports loading sessions created when per_cookie_cipher_secret: #{!per_cookie_cipher_secret} " do req('/').must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>"text/html", RodaResponseHeaders::CONTENT_LENGTH=>"0"}, [""]] body('/s/foo/bar').must_equal 'bar' body('/g/foo').must_equal 'bar' app.plugin :sessions, :per_cookie_cipher_secret=>!per_cookie_cipher_secret body('/s/foo/baz').must_equal 'baz' body('/g/foo').must_equal 'baz' errors.must_equal [] end it "allows accessing session creation and last update times" do status('/cat').must_equal 404 status('/uat').must_equal 404 status('/s/foo/bar').must_equal 200 body('/cat').must_equal Date.today.strftime("%F") body('/uat').must_equal Date.today.strftime("%F") status('/ssct2/172800/bar/baz').must_equal 200 body('/cat').must_equal((Date.today - 2).strftime("%F")) end it "does not add Set-Cookie header if session does not change, unless outside :skip_within seconds" do req('/').must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>"text/html", RodaResponseHeaders::CONTENT_LENGTH=>"0"}, [""]] _, h, b = req('/s/foo/bar') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda.session/) b.must_equal ["bar"] req('/g/foo').must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>"text/html", RodaResponseHeaders::CONTENT_LENGTH=>"3"}, ["bar"]] req('/s/foo/bar').must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>"text/html", RodaResponseHeaders::CONTENT_LENGTH=>"3"}, ["bar"]] _, h, b = req('/s/foo/baz') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda.session/) b.must_equal ["baz"] req('/g/foo').must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>"text/html", RodaResponseHeaders::CONTENT_LENGTH=>"3"}, ["baz"]] req('/g/foo', 'QUERY_STRING'=>'sut=3500').must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>"text/html", RodaResponseHeaders::CONTENT_LENGTH=>"3"}, ["baz"]] _, h, b = req('/g/foo', 'QUERY_STRING'=>'sut=3700') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda.session/) b.must_equal ["baz"] @app.plugin(:sessions, :skip_within=>3800) req('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal [200, {RodaResponseHeaders::CONTENT_TYPE=>"text/html", RodaResponseHeaders::CONTENT_LENGTH=>"3"}, ["baz"]] _, h, b = req('/g/foo', 'QUERY_STRING'=>'sut=3900') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda.session/) b.must_equal ["baz"] errors.must_equal [] end it "removes session cookie when session is submitted but empty after request" do body('/s/foo/bar').must_equal 'bar' body('/sct').to_i body('/g/foo').must_equal 'bar' _, h, b = req('/sc') # Parameters can come in any order, and only the final parameter may omit the ; ['roda.session=', 'max-age=0', 'path=/'].each do |param| h[RodaResponseHeaders::SET_COOKIE].must_match(/#{Regexp.escape(param)}(;|\z)/) end h[RodaResponseHeaders::SET_COOKIE].must_match(/expires=Thu, 01 Jan 1970 00:00:00 (-0000|GMT)(;|\z)/) b.must_equal ['c'] errors.must_equal [] end it "removes session cookie even when max-age and expires are in cookie options" do app.plugin :sessions, :cookie_options=>{:max_age=>'1000', :expires=>Time.now+1000} body('/s/foo/bar').must_equal 'bar' body('/sct').to_i body('/g/foo').must_equal 'bar' _, h, b = req('/sc') # Parameters can come in any order, and only the final parameter may omit the ; ['roda.session=', 'max-age=0', 'path=/'].each do |param| h[RodaResponseHeaders::SET_COOKIE].must_match(/#{Regexp.escape(param)}(;|\z)/) end h[RodaResponseHeaders::SET_COOKIE].must_match(/expires=Thu, 01 Jan 1970 00:00:00 (-0000|GMT)(;|\z)/) b.must_equal ['c'] errors.must_equal [] end it "sets new session create time when clear_session is called even when session is not empty when serializing" do body('/s/foo/bar').must_equal 'bar' sct = body('/sct').to_i body('/g/foo').must_equal 'bar' body('/sct').to_i.must_equal sct body('/ssct/10').to_i.must_equal(sct - 10) body('/cs/foo/baz').must_equal 'baz' body('/sct').to_i.must_be :>=, sct errors.must_equal [] end it "should include HttpOnly and secure cookie options appropriately" do h = header(RodaResponseHeaders::SET_COOKIE, '/s/foo/bar') h.must_match(/; HttpOnly/i) h.wont_include('; secure') h = header(RodaResponseHeaders::SET_COOKIE, '/s/foo/baz', 'HTTPS'=>'on') h.must_match(/; HttpOnly/i) h.must_include('; secure') @app.plugin(:sessions, :cookie_options=>{}) h = header(RodaResponseHeaders::SET_COOKIE, '/s/foo/bar') h.must_match(/; HttpOnly/i) h.wont_include('; secure') end it "should merge :cookie_options options into the default cookie options" do @app.plugin(:sessions, :cookie_options=>{:secure=>true}) h = header(RodaResponseHeaders::SET_COOKIE, '/s/foo/bar') h.must_match(/; HttpOnly/i) h.must_include('; path=/') h.must_include('; secure') end it "handles secret rotation using :old_secret option" do body('/s/foo/bar').must_equal 'bar' body('/g/foo').must_equal 'bar' old_cookie = @cookie @app.plugin(:sessions, :secret=>'2'*64, :old_secret=>'1'*64) body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar' @app.plugin(:sessions, :secret=>'2'*64, :old_secret=>nil) body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar' @cookie = old_cookie body('/g/foo').must_equal '' errors.must_equal ["Not decoding session: HMAC invalid"] proc{app(:bare){plugin :sessions, :old_secret=>'1'*63}}.must_raise Roda::RodaError proc{app(:bare){plugin :sessions, :old_secret=>Object.new}}.must_raise Roda::RodaError end it "handles secret rotation using :old_secret option when also changing :per_cookie_cipher_secret option" do body('/s/foo/bar').must_equal 'bar' body('/g/foo').must_equal 'bar' old_cookie = @cookie @app.plugin(:sessions, :secret=>'2'*64, :old_secret=>'1'*64, :per_cookie_cipher_secret=>!per_cookie_cipher_secret) body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar' @app.plugin(:sessions, :secret=>'2'*64, :old_secret=>nil) body('/g/foo', 'QUERY_STRING'=>'sut=3700').must_equal 'bar' @cookie = old_cookie body('/g/foo').must_equal '' errors.must_equal ["Not decoding session: HMAC invalid"] proc{app(:bare){plugin :sessions, :old_secret=>'1'*63}}.must_raise Roda::RodaError proc{app(:bare){plugin :sessions, :old_secret=>Object.new}}.must_raise Roda::RodaError end it "pads data by default to make it more difficult to guess session contents based on size" do long = "bar"*35 _, h1, b = req('/s/foo/bar') b.must_equal ['bar'] _, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700') b.must_equal ['bar'] _, h3, b = req('/s/foo/bar2') b.must_equal ['bar2'] _, h4, b = req("/s/foo/#{long}") b.must_equal [long] h1[RodaResponseHeaders::SET_COOKIE].length.must_equal h2[RodaResponseHeaders::SET_COOKIE].length h1[RodaResponseHeaders::SET_COOKIE].wont_equal h2[RodaResponseHeaders::SET_COOKIE] h1[RodaResponseHeaders::SET_COOKIE].length.must_equal h3[RodaResponseHeaders::SET_COOKIE].length h1[RodaResponseHeaders::SET_COOKIE].wont_equal h3[RodaResponseHeaders::SET_COOKIE] h1[RodaResponseHeaders::SET_COOKIE].length.wont_equal h4[RodaResponseHeaders::SET_COOKIE].length @app.plugin(:sessions, :pad_size=>256) _, h1, b = req('/s/foo/bar') b.must_equal ['bar'] _, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700') b.must_equal ['bar'] _, h3, b = req('/s/foo/bar2') b.must_equal ['bar2'] _, h4, b = req("/s/foo/#{long}") b.must_equal [long] h1[RodaResponseHeaders::SET_COOKIE].length.must_equal h2[RodaResponseHeaders::SET_COOKIE].length h1[RodaResponseHeaders::SET_COOKIE].wont_equal h2[RodaResponseHeaders::SET_COOKIE] h1[RodaResponseHeaders::SET_COOKIE].length.must_equal h3[RodaResponseHeaders::SET_COOKIE].length h1[RodaResponseHeaders::SET_COOKIE].wont_equal h3[RodaResponseHeaders::SET_COOKIE] h1[RodaResponseHeaders::SET_COOKIE].length.must_equal h4[RodaResponseHeaders::SET_COOKIE].length h1[RodaResponseHeaders::SET_COOKIE].wont_equal h4[RodaResponseHeaders::SET_COOKIE] @app.plugin(:sessions, :pad_size=>nil) _, h1, b = req('/s/foo/bar') b.must_equal ['bar'] _, h2, b = req('/s/foo/bar', 'QUERY_STRING'=>'sut=3700') b.must_equal ['bar'] _, h3, b = req('/s/foo/bar2') b.must_equal ['bar2'] h1[RodaResponseHeaders::SET_COOKIE].length.must_equal h2[RodaResponseHeaders::SET_COOKIE].length h1[RodaResponseHeaders::SET_COOKIE].wont_equal h2[RodaResponseHeaders::SET_COOKIE] if !defined?(JRUBY_VERSION) || JRUBY_VERSION >= '9.2' h1[RodaResponseHeaders::SET_COOKIE].length.wont_equal h3[RodaResponseHeaders::SET_COOKIE].length end proc{@app.plugin(:sessions, :pad_size=>0)}.must_raise Roda::RodaError proc{@app.plugin(:sessions, :pad_size=>1)}.must_raise Roda::RodaError proc{@app.plugin(:sessions, :pad_size=>Object.new)}.must_raise Roda::RodaError errors.must_equal [] end it "compresses data over a certain size by default" do long = 'b'*8192 proc{body("/s/foo/#{long}")}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge @app.plugin(:sessions, :gzip_over=>8000) body("/s/foo/#{long}").must_equal long body("/g/foo", 'QUERY_STRING'=>'sut=3700').must_equal long @app.plugin(:sessions, :gzip_over=>15000) proc{body("/g/foo", 'QUERY_STRING'=>'sut=3700')}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge errors.must_equal [] end it "raises CookieTooLarge if cookie value is too large" do @app.plugin(:sessions, :pad_size=>nil) bytes = 1502 bytes -= 16 if per_cookie_cipher_secret # Results in 4100 byte cookie value, fails earlier proc{req("/s/foo/#{SecureRandom.hex(bytes)}")}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge end it "raises CookieTooLarge if cookie value is too large" do @app.plugin(:sessions, :pad_size=>nil) bytes = 1500 bytes -= 16 if per_cookie_cipher_secret # Results in 4092 byte cookie value, but browser 4K limit includes # cookie name and attributes, fails later proc{req("/s/foo/#{SecureRandom.hex(bytes)}")}.must_raise Roda::RodaPlugins::Sessions::CookieTooLarge end it "handles large cookies that are under 4K limit" do @app.plugin(:sessions, :pad_size=>nil) bytes = 1481 bytes -= 16 if per_cookie_cipher_secret cookie_size = Array(header(RodaResponseHeaders::SET_COOKIE, "/s/foo/#{SecureRandom.hex(bytes)}")).join.bytesize cookie_size.must_be :>, (Rack.release < "2" ? 4078 : 4090) cookie_size.must_be :<=, 4096 end it "applies 4K limit to single cookie and not multiple cookies" do @app.plugin(:sessions, :pad_size=>nil) bytes = 1481 bytes -= 16 if per_cookie_cipher_secret cookie_size = Array(header(RodaResponseHeaders::SET_COOKIE, "/swc/foo/#{SecureRandom.hex(bytes)}/bar/#{"1"*2000}")).join.bytesize cookie_size.must_be :>, (Rack.release < "2" ? 6078 : 6090) cookie_size.must_be :<=, 6112 end it "ignores session cookies if session exceeds max time since create" do body("/s/foo/bar").must_equal 'bar' body("/g/foo").must_equal 'bar' @app.plugin(:sessions, :max_seconds=>-1) body("/g/foo").must_equal '' errors.must_equal ["Not returning session: maximum session time expired"] @app.plugin(:sessions, :max_seconds=>10) body("/s/foo/bar").must_equal 'bar' body("/g/foo").must_equal 'bar' errors.must_equal [] end it "ignores session cookies if session exceeds max idle time since update" do body("/s/foo/bar").must_equal 'bar' body("/g/foo").must_equal 'bar' @app.plugin(:sessions, :max_idle_seconds=>-1) body("/g/foo").must_equal '' errors.must_equal ["Not returning session: maximum session idle time expired"] @app.plugin(:sessions, :max_idle_seconds=>10) body("/s/foo/bar").must_equal 'bar' body("/g/foo").must_equal 'bar' errors.must_equal [] end it "supports :serializer and :parser options to override serializer/deserializer" do body('/s/foo/bar').must_equal 'bar' @app.plugin(:sessions, :parser=>proc{|s| JSON.parse("{#{s[1...-1].reverse}}")}) body('/g/rab').must_equal 'oof' @app.plugin(:sessions, :serializer=>proc{|s| s.to_json.upcase}) body('/s/foo/baz').must_equal 'baz' body('/g/ZAB').must_equal 'OOF' errors.must_equal [] end it "logs session decoding errors to rack.errors" do body('/s/foo/bar').must_equal 'bar' c = @cookie.dup k = c.split('=', 2)[0] + '=' @cookie[20] = '!' body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: invalid base64"] @cookie = k+base64.urlsafe_encode64('') body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: no data"] @cookie = k+base64.urlsafe_encode64("\0" * 60) body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: data too short"] @cookie = k+base64.urlsafe_encode64("\1" * 92) body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: data too short"] @cookie = k+base64.urlsafe_encode64('1'*75) body('/g/foo').must_equal '' errors.must_equal ["Unable to decode session: version marker unsupported"] @cookie = k+base64.urlsafe_encode64("\0"*75) body('/g/foo').must_equal '' errors.must_equal ["Not decoding session: HMAC invalid"] end end describe "sessions plugin" do include CookieJar def req(path, opts={}) @errors ||= StringIO.new super(path, opts.merge('rack.errors'=>@errors)) end def errors @errors.rewind e = @errors.read.split("\n") @errors.rewind @errors.truncate(0) e end it "supports transparent upgrade from Rack::Session::Cookie with default HMAC and coder" do app(:bare) do use Rack::Session::Cookie, :secret=>'1' plugin :middleware_stack route do |r| r.get('s', String, String){|k, v| session[k] = {:a=>v}; v} r.get('g', String){|k| session[k].inspect} '' end end _, h, b = req('/s/foo/bar') (h[RodaResponseHeaders::SET_COOKIE] =~ /\A(rack\.session=.*); path=\/; HttpOnly\z/i).must_equal 0 c = $1 b.must_equal ['bar'] _, h, b = req('/g/foo') h[RodaResponseHeaders::SET_COOKIE].must_be_nil [['{:a=>"bar"}'], ['{a: "bar"}']].must_include b @app.plugin :sessions, :secret=>'1'*64, :upgrade_from_rack_session_cookie_secret=>'1' @app.middleware_stack.remove{|m, *| m == Rack::Session::Cookie} @cookie = c.dup @cookie.slice!(15) body('/g/foo').must_equal 'nil' errors.must_equal ["Not decoding Rack::Session::Cookie session: HMAC invalid"] @cookie = c.split('--', 2)[0] body('/g/foo').must_equal 'nil' errors.must_equal ["Not decoding Rack::Session::Cookie session: invalid format"] @cookie = c.split('--', 2)[0][13..-1] @cookie = Rack::Utils.unescape(@cookie).unpack('m')[0] @cookie[2] = "^" @cookie = [@cookie].pack('m') cookie = String.new cookie << 'rack.session=' << @cookie << '--' << OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA1.new, '1', @cookie) @cookie = cookie body('/g/foo').must_equal 'nil' errors.must_equal ["Error decoding Rack::Session::Cookie session: not base64 encoded marshal dump"] @cookie = c _, h, b = req('/g/foo') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda\.session=(.*); path=\/; HttpOnly(; SameSite=Lax)?\nrack\.session=; path=\/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00/mi) [['{"a"=>"bar"}'], ['{"a" => "bar"}']].must_include b @app.plugin :sessions, :cookie_options=>{:path=>'/foo'}, :upgrade_from_rack_session_cookie_options=>{} @cookie = c _, h, b = req('/g/foo') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda\.session=(.*); path=\/foo; HttpOnly(; SameSite=Lax)?\nrack\.session=; path=\/foo; max-age=0; expires=Thu, 01 Jan 1970 00:00:00/mi) [['{"a"=>"bar"}'], ['{"a" => "bar"}']].must_include b @app.plugin :sessions, :upgrade_from_rack_session_cookie_options=>{:path=>'/baz'} @cookie = c _, h, b = req('/g/foo') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda\.session=(.*); path=\/foo; HttpOnly(; SameSite=Lax)?\nrack\.session=; path=\/baz; max-age=0; expires=Thu, 01 Jan 1970 00:00:00/mi) [['{"a"=>"bar"}'], ['{"a" => "bar"}']].must_include b @app.plugin :sessions, :upgrade_from_rack_session_cookie_key=>'quux.session' @cookie = c.sub(/\Arack/, 'quux') _, h, b = req('/g/foo') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda\.session=(.*); path=\/foo; HttpOnly(; SameSite=Lax)?\nquux\.session=; path=\/baz; max-age=0; expires=Thu, 01 Jan 1970 00:00:00/mi) [['{"a"=>"bar"}'], ['{"a" => "bar"}']].must_include b end end if Rack.release < '2.3' end end jeremyevans-roda-4f30bb3/spec/plugin/shared_vars_spec.rb000066400000000000000000000017651516720775400235110ustar00rootroot00000000000000require_relative "../spec_helper" describe "shared_vars plugin" do it "adds shared method for sharing variables across multiple apps" do app(:shared_vars) {|r| shared[:c]} old_app = app app(:shared_vars) do |r| shared[:c] = 'c' r.run old_app end body.must_equal 'c' end it "adds shared with hash merges the hash into the shared vars" do app(:shared_vars) do |r| shared(:c=>'c') shared[:c] end body.must_equal 'c' end it "calling shared with hash and a block sets shared variables only for that block" do app(:shared_vars) do |r| c = nil d = nil shared[:c] = 'b' shared(:c=>'c', :d=>'d') do c = shared[:c] d = shared[:d] end "#{shared[:c]}:#{shared[:d]}:#{c}:#{d}" end body.must_equal 'b::c:d' end it "calling shared with no arguments and a block raises an error" do app(:shared_vars) do |r| shared{} end proc{body}.must_raise(Roda::RodaError) end end jeremyevans-roda-4f30bb3/spec/plugin/sinatra_helpers_spec.rb000066400000000000000000000453311516720775400243700ustar00rootroot00000000000000require_relative "../spec_helper" require 'uri' describe "sinatra_helpers plugin" do def sin_app(&block) app = app(:sinatra_helpers, &block) app.plugin :drop_body app end def status_app(code, &block) block ||= proc{} case code when 204, 205, 304 code += 2 end sin_app do |r| status code instance_eval(&block).inspect end end it 'status returns the response status code if not given an argument' do status_app(207){status} body.must_equal "207" end it 'status sets the response status code if given an argument' do status_app 207 status.must_equal 207 end it 'not_found? is true only if status == 404' do status_app(404){not_found?} body.must_equal 'true' status_app(405){not_found?} body.must_equal 'false' status_app(403){not_found?} body.must_equal 'false' end it 'informational? is true only for 1xx status' do status_app(100 + rand(100)){response['x'] = informational?.inspect} header('x').must_equal 'true' status_app(200 + rand(400)){informational?} body.must_equal 'false' end it 'success? is true only for 2xx status' do status_app(200 + rand(100)){success?} body.must_equal 'true' status_app(100 + rand(100)){response['x'] = success?.inspect} header('x').must_equal 'false' status_app(300 + rand(300)){success?} body.must_equal 'false' end it 'redirect? is true only for 3xx status' do status_app(300 + rand(100)){redirect?} body.must_equal 'true' status_app(200 + rand(100)){redirect?} body.must_equal 'false' status_app(400 + rand(200)){redirect?} body.must_equal 'false' end it 'client_error? is true only for 4xx status' do status_app(400 + rand(100)){client_error?} body.must_equal 'true' status_app(200 + rand(200)){client_error?} body.must_equal 'false' status_app(500 + rand(100)){client_error?} body.must_equal 'false' end it 'server_error? is true only for 5xx status' do status_app(500 + rand(100)){server_error?} body.must_equal 'true' status_app(200 + rand(300)){server_error?} body.must_equal 'false' end it 'status predicate methods return nil if status is not set' do sin_app{informational?.inspect} body.must_equal 'nil' sin_app{success?.inspect} body.must_equal 'nil' sin_app{redirect?.inspect} body.must_equal 'nil' sin_app{client_error?.inspect} body.must_equal 'nil' sin_app{server_error?.inspect} body.must_equal 'nil' end describe 'body' do it 'takes a block for deferred body generation' do sin_app{body{'Hello World'}; nil} body.must_equal 'Hello World' header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '11' end it 'supports #join' do sin_app{body{'Hello World'}; nil} req[2].join.must_equal 'Hello World' end it 'takes a String, Array, or other object responding to #each' do sin_app{body 'Hello World'; nil} body.must_equal 'Hello World' header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '11' sin_app{body ['Hello ', 'World']; nil} body.must_equal 'Hello World' header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '11' o = Object.new def o.each; yield 'Hello World' end sin_app{body o; nil} body.must_equal 'Hello World' header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '11' end it 'returns previously set body' do sin_app do response.body 'Hello World' response.body.join.must_equal 'Hello World' end body.must_equal 'Hello World' header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '11' end end describe 'redirect' do it 'uses a 302 when only a path is given' do sin_app do redirect '/foo' fail 'redirect should halt' end status.must_equal 302 body.must_equal '' header(RodaResponseHeaders::LOCATION).must_equal '/foo' end it 'adds script_name if given a path' do sin_app{redirect "/foo"} header(RodaResponseHeaders::LOCATION, '/bar', 'SCRIPT_NAME'=>'/foo').must_equal '/foo' end it 'does not adds script_name if not given a path' do sin_app{redirect} header(RodaResponseHeaders::LOCATION, '/bar', 'SCRIPT_NAME'=>'/foo', 'REQUEST_METHOD'=>'POST').must_equal '/foo/bar' end it 'respects :absolute_redirects option' do sin_app{redirect} app.opts[:absolute_redirects] = true header(RodaResponseHeaders::LOCATION, '/bar', 'HTTP_HOST'=>'example.org', 'SCRIPT_NAME'=>'/foo', 'REQUEST_METHOD'=>'POST').must_equal 'http://example.org/foo/bar' end it 'respects :prefixed_redirects option' do sin_app{redirect "/bar"} app.opts[:prefixed_redirects] = true header(RodaResponseHeaders::LOCATION, 'SCRIPT_NAME'=>'/foo').must_equal '/foo/bar' end it 'ignores :prefix_redirects option if not given a path' do sin_app{redirect} app.opts[:prefix_redirects] = true header(RodaResponseHeaders::LOCATION, "/bar", 'SCRIPT_NAME'=>'/foo', 'REQUEST_METHOD'=>'POST').must_equal '/foo/bar' end it 'uses the code given when specified' do sin_app{redirect '/foo', 301} status.must_equal 301 end it 'redirects back to request.referer when passed back' do sin_app{redirect back} header(RodaResponseHeaders::LOCATION, 'HTTP_REFERER' => '/foo').must_equal '/foo' end it 'uses 303 for post requests if request is HTTP 1.1, 302 for 1.0' do sin_app{redirect '/foo'} status('HTTP_VERSION' => 'HTTP/1.1', 'REQUEST_METHOD'=>'POST').must_equal 303 status('HTTP_VERSION' => 'HTTP/1.0', 'REQUEST_METHOD'=>'POST').must_equal 302 end end describe 'error' do it 'sets a status code and halts' do sin_app do error fail 'error should halt' end status.must_equal 500 body.must_equal '' end it 'accepts status code' do sin_app{error 501} status.must_equal 501 body.must_equal '' end it 'accepts body' do sin_app{error '501'} status.must_equal 500 body.must_equal '501' end it 'accepts status code and body' do sin_app{error 502, '501'} status.must_equal 502 body.must_equal '501' end end describe 'not_found' do it 'halts with a 404 status' do sin_app do not_found fail 'not_found should halt' end status.must_equal 404 body.must_equal '' end it 'accepts optional body' do sin_app{not_found 'nf'} status.must_equal 404 body.must_equal 'nf' end end describe 'headers' do it 'sets headers on the response object when given a Hash' do sin_app do headers 'x-foo' => 'bar' 'kthx' end header('x-foo').must_equal 'bar' body.must_equal 'kthx' end it 'returns the response headers hash when no hash provided' do sin_app{headers['x-foo'] = 'bar'} header('x-foo').must_equal 'bar' end end describe 'mime_type' do before do sin_app{|r| mime_type((r.path[1,1000] unless r.path.empty?)).to_s} end it "looks up mime types in Rack's MIME registry" do Rack::Mime::MIME_TYPES['.foo'] = 'application/foo' body('/foo').must_equal 'application/foo' body('/.foo').must_equal 'application/foo' end it 'returns nil when given nil' do sin_app{|r| mime_type((r.path[2,1000] unless r.path.empty?)).to_s} body.must_equal '' end it 'returns nil when media type not registered' do body('/bizzle').must_equal '' end it 'returns the argument when given a media type string' do body('/text/plain').must_equal 'text/plain' end it 'supports mime types registered at the class level' do app.mime_type :foo, 'application/foo2' body('/foo').must_equal 'application/foo2' end end describe 'content_type' do it 'sets the Content-Type header' do sin_app do content_type 'text/plain' 'Hello World' end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/plain' body.must_equal 'Hello World' end it 'takes media type parameters (like charset=)' do sin_app{content_type 'text/html', :charset => 'latin1'} header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/html;charset=latin1' end it "looks up symbols in Rack's mime types dictionary" do sin_app{content_type :foo} Rack::Mime::MIME_TYPES['.foo'] = 'application/foo' header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'application/foo' end it 'fails when no mime type is registered for the argument provided' do sin_app{content_type :bizzle} proc{body}.must_raise(Roda::RodaError) end it 'handles already present params' do sin_app{content_type 'foo/bar;level=1', :charset => 'utf-8'} header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'foo/bar;level=1, charset=utf-8' end it 'does not add charset if present' do sin_app{content_type 'text/plain;charset=utf-16', :charset => 'utf-8'} header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/plain;charset=utf-16' end it 'properly encodes parameters with delimiter characters' do sin_app{|r| content_type 'image/png', :comment => r.path[1, 1000] } header(RodaResponseHeaders::CONTENT_TYPE, '/Hello, world!').must_equal 'image/png;comment="Hello, world!"' header(RodaResponseHeaders::CONTENT_TYPE, '/semi;colon').must_equal 'image/png;comment="semi;colon"' header(RodaResponseHeaders::CONTENT_TYPE, '/"Whatever."').must_equal 'image/png;comment="\"Whatever.\""' end end describe 'attachment' do before do sin_app{|r| attachment r.path[1, 1000]; 'b'} end it 'sets the Content-Disposition header' do header(RodaResponseHeaders::CONTENT_DISPOSITION, '/foo/test.xml').must_equal 'attachment; filename="test.xml"' body.must_equal 'b' end it 'sets the Content-Disposition header with character set if filename contains characters that should be escaped' do filename = "/foo/a\u1023b.xml" sin_app{|r| attachment filename; 'b'} header(RodaResponseHeaders::CONTENT_DISPOSITION).must_equal 'attachment; filename="a-b.xml"; filename*=UTF-8\'\'a%E1%80%A3b.xml' body.must_equal 'b' filename = "/foo/a\255\255b.xml".dup.force_encoding('ISO-8859-1') header(RodaResponseHeaders::CONTENT_DISPOSITION).must_equal 'attachment; filename="a-b.xml"; filename*=ISO-8859-1\'\'a%AD%ADb.xml' body.must_equal 'b' filename = "/foo/a\255\255b.xml".dup.force_encoding('BINARY') header(RodaResponseHeaders::CONTENT_DISPOSITION).must_equal 'attachment; filename="a-b.xml"' body.must_equal 'b' end it 'sets the Content-Disposition header even when a filename is not given' do sin_app{attachment} header(RodaResponseHeaders::CONTENT_DISPOSITION, '/foo/test.xml').must_equal 'attachment' end it 'sets the Content-Type header' do header(RodaResponseHeaders::CONTENT_TYPE, '/test.xml').must_equal 'application/xml' end it 'does not modify the default Content-Type without a file extension' do header(RodaResponseHeaders::CONTENT_TYPE, '/README').must_equal 'text/html' end it 'should not modify the Content-Type if it is already set' do sin_app do content_type :atom attachment 'test.xml' end header(RodaResponseHeaders::CONTENT_TYPE, '/README').must_equal 'application/atom+xml' end end describe 'send_file' do before(:all) do file = @file = 'spec/assets/css/raw.css' @content = File.read(@file) sin_app{request.send_file file, env['rack.OPTS'] || {}} end it "sends the contents of the file" do status.must_equal 200 body.must_equal @content end it "returns response body implementing to_path" do req[2].to_path.must_equal @file end if !ENV['LINT'] || Rack.release >= '3' it 'sets the Content-Type response header if a mime-type can be located' do header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/css' end it 'sets the Content-Type response header if type option is set to a file extension' do header(RodaResponseHeaders::CONTENT_TYPE, 'rack.OPTS'=>{:type => 'html'}).must_equal 'text/html' end it 'sets the Content-Type response header if type option is set to a mime type' do header(RodaResponseHeaders::CONTENT_TYPE, 'rack.OPTS'=>{:type => 'application/octet-stream'}).must_equal 'application/octet-stream' end it 'sets the Content-Length response header' do header(RodaResponseHeaders::CONTENT_LENGTH).must_equal @content.length.to_s end it 'sets the Last-Modified response header' do header(RodaResponseHeaders::LAST_MODIFIED).must_equal File.mtime(@file).httpdate end it 'allows passing in a different Last-Modified response header with :last_modified' do time = Time.now @app.plugin :caching header(RodaResponseHeaders::LAST_MODIFIED, 'rack.OPTS'=>{:last_modified => time}).must_equal time.httpdate end it "returns a 404 when not found" do sin_app{send_file 'this-file-does-not-exist.txt'} status.must_equal 404 end it "does not set the Content-Disposition header by default" do header(RodaResponseHeaders::CONTENT_DISPOSITION).must_be_nil end it "sets the Content-Disposition header when :disposition set to 'attachment'" do header(RodaResponseHeaders::CONTENT_DISPOSITION, 'rack.OPTS'=>{:disposition => 'attachment'}).must_equal 'attachment; filename="raw.css"' end it "does not set add a file name if filename is false" do header(RodaResponseHeaders::CONTENT_DISPOSITION, 'rack.OPTS'=>{:disposition => 'inline', :filename=>false}).must_equal 'inline' end it "sets the Content-Disposition header when :disposition set to 'inline'" do header(RodaResponseHeaders::CONTENT_DISPOSITION, 'rack.OPTS'=>{:disposition => 'inline'}).must_equal 'inline; filename="raw.css"' end it "sets the Content-Disposition header when :filename provided" do header(RodaResponseHeaders::CONTENT_DISPOSITION, 'rack.OPTS'=>{:filename => 'foo.txt'}).must_equal 'attachment; filename="foo.txt"' end it 'allows setting a custom status code' do status('rack.OPTS'=>{:status=>201}).must_equal 201 end it "is able to send files with unknown mime type" do header(RodaResponseHeaders::CONTENT_TYPE, 'rack.OPTS'=>{:type => '.foobar'}).must_equal 'application/octet-stream' end it "does not override Content-Type if already set and no explicit type is given" do file = @file sin_app do content_type :png send_file file end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'image/png' end it "does override Content-Type even if already set, if explicit type is given" do file = @file sin_app do content_type :png send_file file, :type => :gif end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'image/gif' end end describe 'uri' do describe "without arguments" do before do sin_app{uri} end it 'generates absolute urls' do body('HTTP_HOST'=>'example.org').must_equal 'http://example.org/' end it 'includes path_info' do body('/foo', 'HTTP_HOST'=>'example.org').must_equal 'http://example.org/foo' end it 'includes script_name' do body('/bar', 'HTTP_HOST'=>'example.org', "SCRIPT_NAME" => '/foo').must_equal 'http://example.org/foo/bar' end it 'handles standard HTTP and HTTPS ports' do body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '80').must_equal 'http://example.org/' body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '443', 'HTTPS'=>'on').must_equal 'https://example.org/' end it 'handles non-standard HTTP port' do body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '81').must_equal 'http://example.org:81/' body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '443').must_equal 'http://example.org:443/' end it 'handles non-standard HTTPS port' do body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '444', 'HTTPS'=>'on').must_equal 'https://example.org:444/' body('SERVER_NAME'=>'example.org', 'SERVER_PORT' => '80', 'HTTPS'=>'on').must_equal 'https://example.org:80/' end it 'handles reverse proxy' do body('SERVER_NAME'=>'example.org', 'HTTP_X_FORWARDED_HOST' => 'example.com', 'SERVER_PORT' => '8080').must_equal 'http://example.com/' end end it 'allows passing an alternative to path_info' do sin_app{uri '/bar'} body('HTTP_HOST'=>'example.org').must_equal 'http://example.org/bar' body('HTTP_HOST'=>'example.org', "SCRIPT_NAME" => '/foo').must_equal 'http://example.org/foo/bar' end it 'handles absolute URIs' do sin_app{uri 'http://google.com'} body('HTTP_HOST'=>'example.org').must_equal 'http://google.com' end it 'handles different protocols' do sin_app{uri 'mailto:jsmith@example.com'} body('HTTP_HOST'=>'example.org').must_equal 'mailto:jsmith@example.com' end it 'allows turning off host' do sin_app{uri '/foo', false} body('HTTP_HOST'=>'example.org').must_equal '/foo' body('HTTP_HOST'=>'example.org', "SCRIPT_NAME" => '/bar').must_equal '/bar/foo' end it 'allows turning off script_name' do sin_app{uri '/foo', true, false} body('HTTP_HOST'=>'example.org').must_equal 'http://example.org/foo' body('HTTP_HOST'=>'example.org', "SCRIPT_NAME" => '/bar').must_equal 'http://example.org/foo' end it 'is aliased to #url' do sin_app{url} body('HTTP_HOST'=>'example.org').must_equal 'http://example.org/' end it 'is aliased to #to' do sin_app{to} body('HTTP_HOST'=>'example.org').must_equal 'http://example.org/' end it 'accepts a URI object instead of a String' do sin_app{uri URI.parse('http://roda.jeremyevans.net')} body.must_equal 'http://roda.jeremyevans.net' end end it 'logger logs to rack.logger' do sin_app{logger.info "foo"; nil} o = Object.new def o.method_missing(*a) (@a ||= []) << a end %w'info debug warn error fatal'.each do |meth| o.define_singleton_method(meth){|*a| super(*a)} end def o.logs @a end status('rack.logger'=>o).must_equal 404 o.logs.must_equal [[:info, 'foo']] end it 'supports disabling delegation if :delegate=>false option is provided' do app(:bare) do plugin :sinatra_helpers, :delegate=>false route do |r| r.root{content_type} r.is("req"){r.ssl?.to_s} r.is("res"){response.not_found?.inspect} end end proc{body}.must_raise(NameError) body('/req').must_equal 'false' body('/res').must_equal 'nil' end end jeremyevans-roda-4f30bb3/spec/plugin/slash_path_empty_spec.rb000066400000000000000000000010361516720775400245430ustar00rootroot00000000000000require_relative "../spec_helper" describe "slash_path_empty" do it "considers a / path as empty" do app(:slash_path_empty) do |r| r.is{"1"} r.is("a"){"2"} r.get("b"){"3"} end unless_lint do body("").must_equal '1' body("a").must_equal '' body("b").must_equal '' end body.must_equal '1' body("/a").must_equal '2' body("/a/").must_equal '2' body("/a/b").must_equal '' body("/b").must_equal '3' body("/b/").must_equal '3' body("/b/c").must_equal '' end end jeremyevans-roda-4f30bb3/spec/plugin/static_routing_spec.rb000066400000000000000000000100301516720775400242270ustar00rootroot00000000000000require_relative "../spec_helper" describe "static_routing plugin" do it "adds support for static routes that are taken before normal routes" do app(:bare) do plugin :static_routing static_route "/foo" do |r| "#{r.path}:#{r.remaining_path}" end static_route "/bar" do |r| r.get{"GET:#{r.path}:#{r.remaining_path}"} r.post{"POST:#{r.path}:#{r.remaining_path}"} end static_get "/bar" do |r| r.get{"GET2:#{r.path}:#{r.remaining_path}"} end static_route "/quux" do |r| r.halt [500, {}, []] end route do |r| r.on 'foo' do r.get true do 'foo1' end r.root do 'foo2' end end r.get 'baz' do 'baz' end end end 2.times do body('/foo').must_equal '/foo:' body('/foo/').must_equal 'foo2' body('/bar').must_equal 'GET2:/bar:' body('/bar', 'REQUEST_METHOD'=>'POST').must_equal 'POST:/bar:' status('/bar', 'REQUEST_METHOD'=>'PATCH').must_equal 404 body('/baz').must_equal 'baz' status('/quux').must_equal 500 @app = Class.new(@app) end end it "works with hooks plugin if loaded after" do a = [] app(:bare) do plugin :hooks plugin :static_routing before{a << 1} after{a << 2} static_route "/foo" do |r| a << 3 "bar" end route{} end body('/foo').must_equal 'bar' a.must_equal [1,3,2] end it "works with hooks plugin if loaded before" do a = [] app(:bare) do plugin :static_routing plugin :hooks before{a << 1} after{a << 2} static_route "/foo" do |r| a << 3 "bar" end route{} end body('/foo').must_equal 'bar' a.must_equal [1,3,2] end it "supports overridding static routes" do app(:static_routing) do |r| end app.static_route('/foo'){'bar'} body('/foo').must_equal 'bar' app.static_route('/foo'){'baz'} body('/foo').must_equal 'baz' end it "keeps existing routes when loading the plugin" do app(:bare) do plugin :static_routing static_route "/foo" do |r| "#{r.path}:#{r.remaining_path}" end plugin :static_routing route{} end body('/foo').must_equal '/foo:' end it "does not allow placeholders in static routes" do app(:bare) do plugin :static_routing static_route "/:foo" do |r| "#{r.path}:#{r.remaining_path}" end route{} end body('/:foo').must_equal '/:foo:' status('/a').must_equal 404 end it "duplicates data structures in subclasses" do app(:bare) do plugin :static_routing static_route "/foo" do |r| 'foo' end route{} end old_app = @app @app = Class.new(old_app) old_app.static_route '/bar' do |r| 'bar1' end old_app.static_get '/foo' do |r| 'foop' end @app.static_route '/bar' do |r| 'bar2' end body('/foo').must_equal 'foo' body('/bar').must_equal 'bar2' body('/foo', 'REQUEST_METHOD'=>'POST').must_equal 'foo' @app = old_app body('/foo').must_equal 'foop' body('/bar').must_equal 'bar1' body('/foo', 'REQUEST_METHOD'=>'POST').must_equal 'foo' end it "freezes static routes when app is frozen" do app(:bare) do plugin :static_routing static_route("/foo"){} freeze proc do static_get("/foo"){} end.must_raise proc do static_route("/bar"){} end.must_raise end end it 'works with route_block_args plugin' do app(:bare) do plugin :static_routing plugin :route_block_args do [request.request_method, request.path] end static_route "/foo" do |meth, path| "#{path}-#{meth}" end static_get "/bar" do |meth, path| "#{path}-#{meth}-bar" end route{'a'} end body('/foo').must_equal '/foo-GET' body('/bar').must_equal '/bar-GET-bar' end end jeremyevans-roda-4f30bb3/spec/plugin/static_spec.rb000066400000000000000000000012451516720775400224700ustar00rootroot00000000000000require_relative "../spec_helper" describe "static plugin" do it "adds support for serving static files" do app(:bare) do plugin :static, ['/about'], :root=>'spec/views' route do 'a' end end body.must_equal 'a' body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it "respects the application's :root option" do app(:bare) do opts[:root] = File.expand_path('../../', __FILE__) plugin :static, ['/about'], :root=>'views' route do 'a' end end body.must_equal 'a' body('/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end end jeremyevans-roda-4f30bb3/spec/plugin/status_303_spec.rb000066400000000000000000000014031516720775400231050ustar00rootroot00000000000000require_relative "../spec_helper" describe "status_303 plugin" do it 'uses a 302 for get requests' do app(:status_303) do request.redirect '/foo' fail 'redirect should halt' end status.must_equal 302 body.must_equal '' header(RodaResponseHeaders::LOCATION).must_equal '/foo' end it 'uses the code given when specified' do app(:status_303) do request.redirect '/foo', 301 end status.must_equal 301 end it 'uses 303 for post requests if request is HTTP 1.1, 302 for 1.0' do app(:status_303) do request.redirect '/foo' end status('HTTP_VERSION' => 'HTTP/1.1', 'REQUEST_METHOD'=>'POST').must_equal 303 status('HTTP_VERSION' => 'HTTP/1.0', 'REQUEST_METHOD'=>'POST').must_equal 302 end end jeremyevans-roda-4f30bb3/spec/plugin/status_handler_spec.rb000066400000000000000000000067631516720775400242330ustar00rootroot00000000000000require_relative "../spec_helper" describe "status_handler plugin" do it "executes on no arguments" do app(:bare) do plugin :status_handler status_handler(404) do "not found" end route do |r| r.on "a" do "found" end end end body.must_equal 'not found' status.must_equal 404 body("/a").must_equal 'found' status("/a").must_equal 200 end it "passes request if block accepts argument" do app(:bare) do plugin :status_handler status_handler(404) do |r| r.path + 'foo' end route do |r| end end body('/').must_equal '/foo' body("/a").must_equal '/afoo' status("/").must_equal 404 end it "allows overriding status inside status_handler" do app(:bare) do plugin :status_handler status_handler(404) do response.status = 403 "not found" end route do |r| end end status.must_equal 403 end it "calculates correct Content-Length" do app(:bare) do plugin :status_handler status_handler(404) do "a" end route{} end header(RodaResponseHeaders::CONTENT_LENGTH).must_equal "1" end it "clears existing headers" do app(:bare) do plugin :status_handler status_handler(404) do "a" end route do |r| response[RodaResponseHeaders::CONTENT_TYPE] = 'text/pdf' response['foo'] = 'bar' nil end end header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/html' header('foo').must_be_nil end it "keeps specific existing headers if :keep_headers option is used with an array value" do app(:bare) do plugin :status_handler status_handler(404, :keep_headers=>['foo']) do "a" end route do |r| response[RodaResponseHeaders::CONTENT_TYPE] = 'text/pdf' response['foo'] = 'bar' nil end end header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '1' header(RodaResponseHeaders::CONTENT_TYPE).must_equal 'text/html' header('foo').must_equal 'bar' end it "raises for invalid :keep_headers option" do app(:bare) do plugin :status_handler proc{status_handler(404, :keep_headers=>true){}}.must_raise Roda::RodaError end end it "does not modify behavior if status_handler is not called" do app(:status_handler) do |r| r.on "a" do "found" end end body.must_equal '' body("/a").must_equal 'found' end it "does not modify behavior if body is not an array" do app(:bare) do plugin :status_handler status_handler(404) do "not found" end o = Object.new def o.each(&_); end route do |r| r.halt [404, {}, o] end end body.must_equal '' end it "does not modify behavior if body is not an empty array" do app(:bare) do plugin :status_handler status_handler(404) do "not found" end route do |r| response.status = 404 response.write 'a' end end body.must_equal 'a' end it "does not allow further status handlers to be added after freezing" do app(:bare) do plugin :status_handler status_handler(404) do "not found" end route{} end app.freeze body.must_equal 'not found' status.must_equal 404 proc{app.status_handler(404) { "blah" }}.must_raise body.must_equal 'not found' end end jeremyevans-roda-4f30bb3/spec/plugin/streaming_spec.rb000066400000000000000000000136411516720775400231750ustar00rootroot00000000000000require_relative "../spec_helper" describe "streaming plugin" do it "adds stream method for streaming responses" do app(:streaming) do |r| stream do |out| %w'a b c'.each do |v| (out << v).must_equal out out.write(v).must_equal 1 end end end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') b.to_a.must_equal %w'a a b b c c' end it "works with IO.copy_stream" do ri = proc{|v| rack_input(v)} app(:streaming) do |r| stream do |out| %w'a b c'.each{|v| IO.copy_stream(ri[v], out) } end end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') # dup as copy_stream reuses the buffer b.map(&:dup).must_equal %w'a b c' end it "should handle errors when streaming, and run callbacks" do a = [] app(:streaming) do |r| stream(:callback=>proc{a << 'e'}) do |out| %w'a b'.each{|v| out << v} raise Roda::RodaError, 'foo' out << 'c' end end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') proc{b.each{|v| a << v}}.must_raise(Roda::RodaError) a.must_equal %w'a b e' end it "should handle :loop option to loop" do a = [] app(:streaming) do |r| b = %w'a b c' stream(:loop=>true, :callback=>proc{a << 'e'}) do |out| out << b.shift raise Roda::RodaError, 'foo' if b.length == 1 end end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') proc{b.each{|v| a << v}}.must_raise(Roda::RodaError) a.must_equal %w'a b e' end it "uses handle_stream_error for handling errors when streaming" do a = [] app(:streaming) do |r| b = %w'a b c' stream(:loop=>true, :callback=>proc{a << 'e'}) do |out| out << b.shift raise Roda::RodaError, 'foo' if b.length == 1 end end app.send(:define_method, :handle_stream_error) do |error, out| out << '1' raise error end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') proc{b.each{|v| a << v}}.must_raise(Roda::RodaError) a.must_equal %w'a b 1 e' end it "should allow closing the stream when handling an error" do a = [] app(:streaming) do |r| b = %w'a b c' stream(:loop=>true, :callback=>proc{a << 'e'}) do |out| out << b.shift raise Roda::RodaError, 'foo' if b.length == 1 end end app.send(:define_method, :handle_stream_error) do |error, out| out.close end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') b.each{|v| a << v} a.must_equal %w'a b e' end it "should allow ignoring errors when streaming" do a = [] b2 = %w'a b c' app(:streaming) do |r| stream(:loop=>true, :callback=>proc{a << 'e'}) do |out| out << b2.shift raise Roda::RodaError end end app.send(:define_method, :handle_stream_error) do |error, out| out << '1' out.close if b2.empty? end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') b.each{|v| a << v} a.must_equal %w'a 1 b 1 c 1 e' end describe "with :async" do it "should stream in a thread" do main_thread = Thread.current minitest = self app(:streaming) do |r| stream(:async=>true) do |out| minitest.refute_equal Thread.current, main_thread %w'a b c'.each do |v| out << v end end end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') b.to_a.must_equal %w'a b c' end it "should propagate exceptions" do app(:streaming) do |r| stream(:async=>true) do |out| Thread.current.report_on_exception = false if Thread.current.respond_to?(:report_on_exception=) %w'a b'.each{|v| out << v} raise Roda::RodaError, 'foo' out << 'c' end end s, h, b = req s.must_equal 200 h.must_equal(RodaResponseHeaders::CONTENT_TYPE=>'text/html') a = [] proc{b.each{|v| a << v}}.must_raise(Roda::RodaError) a.must_equal %w'a b' end it "should terminate the thread on close" do q = Queue.new app(:streaming) do |r| stream(:async=>true) do |out| %w'a b c d e f g h i j'.each{|v| out << v} q.deq out << 'k' end end *, b = req e = b.enum_for(:each) 10.times{e.next} b.close q.enq 'x' proc{e.next}.must_raise(StopIteration) end it "should still run callbacks on close" do callback = false app(:streaming) do |r| stream(:async=>true, :callback=>proc{callback = true}) do |out| %w'a b c'.each{|v| out << v} end end *, b = req b.close callback.must_equal true end it "should apply backpressure by default" do q = Queue.new a = [] app(:streaming) do |r| stream(:async=>true) do |out| %w'a b c d e f g h i j'.each do |v| out << v a << v end q.enq 'x' out << 'k' a << 'k' end end req q.deq a.must_equal %w'a b c d e f g h i j' end it "should handle :queue option to override queue" do q = Queue.new a = [] app(:streaming) do |r| stream(:async=>true, queue: SizedQueue.new(5)) do |out| %w'a b c d e'.each do |v| out << v a << v end q.enq 'x' out << 'f' a << 'f' end end req q.deq a.must_equal %w'a b c d e' end end if RUBY_VERSION >= '2.3' end unless ENV['LINT'] jeremyevans-roda-4f30bb3/spec/plugin/strip_path_prefix_spec.rb000066400000000000000000000017011516720775400247300ustar00rootroot00000000000000require_relative "../spec_helper" describe "strip_path_prefix plugin" do it "strips path prefix when expanding paths" do app(:bare){} abs_dir = app.expand_path('spec') app.plugin :strip_path_prefix app.expand_path('spec').must_equal 'spec' File.expand_path(app.expand_path('spec'), Dir.pwd).must_equal abs_dir app.expand_path('/foo').must_equal '/foo' app.expand_path('bar', '/foo').must_equal '/foo/bar' app.opts[:root] = '/foo' app.expand_path('bar').must_equal '/foo/bar' app.plugin :strip_path_prefix, '/foo' app.expand_path('bar').must_equal 'bar' app.opts[:root] = '/foo/bar' app.expand_path('baz').must_equal 'bar/baz' app(:bare){} app.opts[:root] = '/foo' app.expand_path('bar').must_equal '/foo/bar' app.plugin :strip_path_prefix, '/foo/' app.expand_path('bar').must_equal 'bar' app.opts[:root] = '/foo/bar' app.expand_path('baz').must_equal 'bar/baz' end end jeremyevans-roda-4f30bb3/spec/plugin/symbol_matchers_spec.rb000066400000000000000000000101371516720775400243740ustar00rootroot00000000000000require_relative "../spec_helper" describe "symbol_matchers plugin" do it "allows symbol specific regexps for symbol matchers" do app(:bare) do plugin :symbol_matchers symbol_matcher(:d2, :d) symbol_matcher(:f, /(f+)/) symbol_matcher(:f2, :f) do |fs| fs*2 end symbol_matcher(:f3, :f) symbol_matcher(:c, /(c+)/) do |cs| [cs, cs.length] unless cs.length == 5 end symbol_matcher(:c2, :c) do |cs, len| len end symbol_matcher(:c3, :c) plugin :class_matchers symbol_matcher(:int, Integer) do |i| i*2 end symbol_matcher(:i, Integer) symbol_matcher(:str, String) do |s| s*2 end symbol_matcher(:s, String) route do |r| r.on "X" do r.is "d2", :d2 do |x| "d2-#{x}" end r.is "f", :f do |x| "f-#{x}" end r.is "f2", :f2 do |x| "f2-#{x}" end r.is "f3", :f3 do |x| "f3-#{x}" end r.is "c", :c do |x, len| "c-#{x}-#{len}" end r.is "c2", :c2 do |x| "c2-#{x}" end r.is "c3", :c3 do |x, len| "c3-#{x}-#{len}" end r.is "int", :int do |x| "int-#{x}" end r.is "i", :i do |x| "i-#{x}" end r.is "str", :str do |x| "str-#{x}" end r.is "s", :s do |x| "s-#{x}" end end r.is :d do |d| "d#{d}" end r.is "thing2", :thing do |d| "thing2#{d}" end r.is :f do |f| "f#{f}" end r.is :c do |cs, nc| "#{cs}#{nc}" end r.is 'x', :c2 do |len| len.inspect end r.is 'y', :int do |i| i.inspect end r.is 'q', :rest do |rest| "rest#{rest}" end r.is :w do |w| "w#{w}" end r.is :d, :w, :f, :c do |d, w, f, cs, nc| "dwfc#{d}#{w}#{f}#{cs}#{nc}" end end end status.must_equal 404 body("/1").must_equal 'd1' body("/11232135").must_equal 'd11232135' body("/a").must_equal 'wa' body("/1az0").must_equal 'w1az0' body("/f").must_equal 'ff' body("/ffffffffffffffff").must_equal 'fffffffffffffffff' body("/c").must_equal 'c1' body("/cccc").must_equal 'cccc4' body("/ccccc").must_equal 'wccccc' body("/x/c").must_equal '1' body("/x/cccc").must_equal '4' body("/y/3").must_equal '6' status("/-").must_equal 404 body("/1/1a/f/cc").must_equal 'dwfc11afcc2' body("/12/1azy/fffff/ccc").must_equal 'dwfc121azyfffffccc3' status("/1/f/a").must_equal 404 body("/q/a/b/c/d//f/g").must_equal 'resta/b/c/d//f/g' body('/q/').must_equal 'rest' body('/thing2/q').must_equal 'thing2q' body('/X/d2/1').must_equal 'd2-1' body('/X/f/fff').must_equal 'f-fff' body('/X/f2/ff').must_equal 'f2-ffff' body('/X/f3/fff').must_equal 'f3-fff' body('/X/c/ccc').must_equal 'c-ccc-3' body('/X/c/ccccc').must_equal '' body('/X/c2/ccc').must_equal 'c2-3' body('/X/c2/ccccc').must_equal '' body('/X/c3/ccc').must_equal 'c3-ccc-3' body('/X/c3/ccccc').must_equal '' body('/X/int/3').must_equal 'int-6' body('/X/i/3').must_equal 'i-3' body('/X/str/f').must_equal 'str-ff' body('/X/s/f').must_equal 's-f' end it "raises errors for unsupported calls to class matcher" do app(:symbol_matchers){|r| } proc{app.symbol_matcher(Hash, /a/)}.must_raise Roda::RodaError proc{app.symbol_matcher(:sym, :foo)}.must_raise Roda::RodaError proc{app.symbol_matcher(:sym, Integer)}.must_raise Roda::RodaError app.plugin :class_matchers proc{app.symbol_matcher(:sym, Hash)}.must_raise Roda::RodaError proc{app.symbol_matcher(:sym, Object.new)}.must_raise Roda::RodaError end it "freezes :symbol_matchers option when freezing app" do app(:symbol_matchers){|r| } app.freeze app.opts[:symbol_matchers].frozen?.must_equal true end end jeremyevans-roda-4f30bb3/spec/plugin/symbol_status_spec.rb000066400000000000000000000006371516720775400241150ustar00rootroot00000000000000require_relative "../spec_helper" describe "symbol_status plugin" do it "accepts a symbol" do app(:symbol_status) do |r| r.on do response.status = :unauthorized nil end end status.must_equal 401 end it "accepts a fixnum" do app(:symbol_status) do |r| r.on do response.status = 204 nil end end status.must_equal 204 end end jeremyevans-roda-4f30bb3/spec/plugin/symbol_views_spec.rb000066400000000000000000000010131516720775400237140ustar00rootroot00000000000000require_relative "../spec_helper" describe "symbol_views plugin" do before do app(:bare) do plugin :symbol_views def view(s) "v#{s}" end route do |r| r.root do :sym end r.is "string" do 'string' end end end end it "should call view with the symbol" do body.must_equal "vsym" end it "should not affect other return types" do body("/string").must_equal 'string' body("/foo").must_equal '' end end jeremyevans-roda-4f30bb3/spec/plugin/timestamp_public_spec.rb000066400000000000000000000063551516720775400245510ustar00rootroot00000000000000require_relative "../spec_helper" describe "timestamp_public plugin" do it "adds r.timestamp_public for serving static files from timestamp_public folder" do app(:bare) do plugin :timestamp_public, :root=>'spec/views' route do |r| r.timestamp_public end end status("/about/_test.erb\0").must_equal 404 status("/about/_test.erb").must_equal 404 status("/static/a/about/_test.erb").must_equal 404 status("/static/1/about/_test.erb\0").must_equal 404 body("/static/1/about/_test.erb").must_equal File.read('spec/views/about/_test.erb') end it "adds r.timestamp_public for serving static files from timestamp_public folder" do app(:bare) do plugin :timestamp_public, :root=>'spec/views', :prefix=>'foo' route do |r| r.timestamp_public end end body("/foo/1/about/_test.erb").must_equal File.read('spec/views/about/_test.erb') end it "adds r.timestamp_public for serving static files from timestamp_public folder" do app(:bare) do plugin :timestamp_public, :root=>'spec/plugin' route do |r| r.timestamp_public timestamp_path('../views/about/_test.erb') end end mtime = File.mtime('spec/views/about/_test.erb') body.must_equal "/static/#{sprintf("%i%06i", mtime.to_i, mtime.usec)}/../views/about/_test.erb" status("/static/1/../views/about/_test.erb").must_equal 404 end it "respects the application's :root option" do app(:bare) do opts[:root] = File.expand_path('../../', __FILE__) plugin :timestamp_public, :root=>'views' route do |r| r.timestamp_public end end body('/static/1/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') end it "handles serving gzip files in gzip mode if client supports gzip" do app(:bare) do plugin :timestamp_public, :root=>'spec/views', :gzip=>true route do |r| r.timestamp_public end end body('/static/1/about/_test.erb').must_equal File.read('spec/views/about/_test.erb') header(RodaResponseHeaders::CONTENT_ENCODING, '/about/_test.erb').must_be_nil body('/static/1/about.erb').must_equal File.read('spec/views/about.erb') header(RodaResponseHeaders::CONTENT_ENCODING, '/about.erb').must_be_nil body('/static/1/about/_test.erb', 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip').must_equal File.binread('spec/views/about/_test.erb.gz') h = req('/static/1/about/_test.erb', 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip')[1] h[RodaResponseHeaders::CONTENT_ENCODING].must_equal 'gzip' h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/plain' body('/static/1/about/_test.css', 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip').must_equal File.binread('spec/views/about/_test.css.gz') h = req('/static/1/about/_test.css', 'HTTP_ACCEPT_ENCODING'=>'deflate, gzip')[1] h[RodaResponseHeaders::CONTENT_ENCODING].must_equal 'gzip' h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/css' end it "returns 404 for non-GET requests" do app(:bare) do plugin :timestamp_public, :root=>'spec/views', :prefix=>'foo' route do |r| r.timestamp_public end end status("/foo/1/about/_test.erb", "REQUEST_METHOD"=>"POST").must_equal 404 end end jeremyevans-roda-4f30bb3/spec/plugin/type_routing_spec.rb000066400000000000000000000236421516720775400237360ustar00rootroot00000000000000# frozen_string_literal: true require_relative "../spec_helper" describe "type_routing plugin" do before do app(:type_routing) do |r| r.is 'a' do r.html{ "HTML: #{r.requested_type}" } r.json{ "JSON: #{r.requested_type}" } r.xml{ "XML: #{r.requested_type}" } "No match" end end end it "uses the file extension in the path" do body('/a').must_equal 'HTML: html' header(RodaResponseHeaders::CONTENT_TYPE, '/a').must_equal 'text/html' body('/a.html').must_equal 'HTML: html' header(RodaResponseHeaders::CONTENT_TYPE, '/a.html').must_equal 'text/html' body('/a.json').must_equal 'JSON: json' header(RodaResponseHeaders::CONTENT_TYPE, '/a.json').must_equal 'application/json' body('/a.xml').must_equal 'XML: xml' header(RodaResponseHeaders::CONTENT_TYPE, '/a.xml').must_equal 'application/xml' status('/a.yadda').must_equal 404 end it "uses the Accept header value" do body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML: html' header(RodaResponseHeaders::CONTENT_TYPE, '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'text/html' body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON: json' header(RodaResponseHeaders::CONTENT_TYPE, '/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'application/json' body('/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'XML: xml' header(RodaResponseHeaders::CONTENT_TYPE, '/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'application/xml' body('/a', 'HTTP_ACCEPT' => 'some/thing').must_equal 'HTML: html' header(RodaResponseHeaders::CONTENT_TYPE, '/a', 'HTTP_ACCEPT' => 'some/thing').must_equal 'text/html' end it "sets Vary header when using Accept header value" do body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML: html' header(RodaResponseHeaders::VARY, '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'Accept' app(:type_routing) do |r| response[RodaResponseHeaders::VARY] = 'User-Agent' r.is 'a' do r.html{ "HTML: #{r.requested_type}" } r.json{ "JSON: #{r.requested_type}" } "No match" end end body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON: json' header(RodaResponseHeaders::VARY, '/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'User-Agent, Accept' end it "favors the file extension over the Accept header" do body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON: json' body('/a.xml', 'HTTP_ACCEPT' => 'application/json').must_equal 'XML: xml' body('/a.html', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML: html' end it "works correctly in sub apps" do sup_app = app @app = Class.new(sup_app) app.route do |r| r.run(sup_app) end body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML: html' body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON: json' body('/a.xml', 'HTTP_ACCEPT' => 'application/json').must_equal 'XML: xml' body('/a.html', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML: html' end it "works correctly in sub apps when sub app also handles extensions on empty paths" do sup_app = app @app = Class.new(sup_app) sup_app.route do |r| r.is do r.get do r.html { 'a' } r.json { '{b:1}' } end end r.on 'test' do r.get do r.html { 'c' } r.json { '{d:2}' } end end end app.route do |r| r.on "subpath" do r.run(sup_app) end end unless_lint do body('/subpath').must_equal 'a' body('/subpath.html').must_equal 'a' body('/subpath.json').must_equal '{b:1}' end body('/subpath/test').must_equal 'c' body('/subpath/test.html').must_equal 'c' body('/subpath/test.json').must_equal '{d:2}' end it "uses the default if neither file extension nor Accept header are given" do body('/a').must_equal 'HTML: html' header(RodaResponseHeaders::CONTENT_TYPE, '/a').must_equal 'text/html' end end describe "type_routing plugin" do it "does not use the file extension if its disabled" do app(:bare) do plugin :type_routing, :use_extension => false route do |r| r.is 'a' do r.html{ "HTML" } r.json{ "JSON" } end end end status('/a.json').must_equal 404 status('/a.html').must_equal 404 body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML' body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON' end it "does not use the Accept header if its disabled" do app(:bare) do plugin :type_routing, :use_header => false route do |r| r.is 'a' do r.html{ "HTML" } r.json{ "JSON" } end end end body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML' body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'HTML' body('/a.html', 'HTTP_ACCEPT' => 'application/json').must_equal 'HTML' body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON' end it "only eats known file extensions" do app(:bare) do plugin :type_routing route do |r| r.is 'a' do r.html{ "HTML" } r.json{ "JSON" } r.xml{ "XML" } raise "Mismatch!" end r.is 'a.jpg' do "Okay" end end end body('/a.html').must_equal 'HTML' body('/a.json').must_equal 'JSON' body('/a.xml').must_equal 'XML' body('/a.jpg').must_equal 'Okay' end it "uses custom data types" do app(:bare) do plugin :type_routing, :types => { :yaml => 'application/x-yaml' } route do |r| r.is 'a' do r.html{ "HTML" } r.yaml{ "YAML" } raise "Mismatch!" end end end body('/a.html').must_equal 'HTML' body('/a.yaml').must_equal 'YAML' header(RodaResponseHeaders::CONTENT_TYPE, '/a.yaml').must_equal 'application/x-yaml' end it "handles response-specific type information when using custom types" do app(:bare) do plugin :type_routing, :exclude=>:html, :default_type=>:json, :types => { :html => 'text/html; charset=utf-8' } route do |r| r.is 'a' do r.json{ "JSON" } r.html{ "HTML" } raise "Mismatch!" end end end body('/a').must_equal 'JSON' body('/a.html').must_equal 'HTML' header(RodaResponseHeaders::CONTENT_TYPE, '/a.html').must_equal 'text/html; charset=utf-8' header(RodaResponseHeaders::CONTENT_TYPE, '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'text/html; charset=utf-8' end it "Handle nil content type when using custom types" do app(:bare) do plugin :type_routing, :exclude=>:html, :default_type=>:json, :types => { :html => nil} route do |r| r.is 'a' do r.html{ "HTML" } r.json{ "JSON" } raise "Mismatch!" end end end body('/a').must_equal 'JSON' body('/a.html').must_equal 'HTML' header(RodaResponseHeaders::CONTENT_TYPE, '/a.html').must_equal 'text/html' header(RodaResponseHeaders::CONTENT_TYPE, '/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'application/json' end it "uses custom default type" do app(:bare) do plugin :type_routing, :default_type => :json route do |r| r.is 'a' do r.html{ "HTML" } r.json{ "JSON" } raise "Mismatch!" end end end body('/a').must_equal 'JSON' body('/a.html').must_equal 'HTML' body('/a.json').must_equal 'JSON' end it "supports nil default type" do app(:bare) do plugin :type_routing, :default_type => nil route do |r| r.is 'a' do r.html{ "HTML" } r.json{ "JSON" } "None" end end end body('/a').must_equal 'None' body('/a.html').must_equal 'HTML' body('/a.json').must_equal 'JSON' end it "excludes given types" do app(:bare) do plugin :type_routing, :exclude => [ :xml ] route do |r| r.is 'a' do r.html{ "HTML" } r.json{ "JSON" } r.xml{ raise "Mismatch!" } raise "Mismatch" end end end body('/a.html').must_equal 'HTML' body('/a.json').must_equal 'JSON' status('/a.xml').must_equal 404 body('/a', 'HTTP_ACCEPT' => 'text/xml').must_equal 'HTML' body('/a', 'HTTP_ACCEPT' => 'application/json').must_equal 'JSON' body('/a', 'HTTP_ACCEPT' => 'text/xml').must_equal 'HTML' body('/a', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML' end it "handles loading the plugin multiple times correctly" do app(:bare) do plugin :type_routing, :default_type => :json plugin :type_routing route do |r| r.is 'a' do r.html{ "HTML" } r.json{ "JSON" } raise "Mismatch!" end end end body('/a').must_equal 'JSON' body('/a.html').must_equal 'HTML' body('/a.json').must_equal 'JSON' end it "removes the handled part from r.remaining_path" do app(:bare) do plugin :type_routing route do |r| r.is 'a' do r.html{ r.remaining_path } end end end body('/a.html').must_equal '' end it "overrides r.real_remaining_path correctly" do app(:bare) do plugin :type_routing route do |r| r.is 'a' do r.html{ r.real_remaining_path } end end end body('/a.html').must_equal '.html' end it "takes the longest file extension first, when ambiguous" do app(:bare) do plugin :type_routing, :types => { :gz => 'application/octet-stream', :'tar.gz' => 'application/octet-stream', } route do |r| r.is 'a' do r.on_type(:gz) { 'GZ' } r.on_type(:'tar.gz') { 'TAR.GZ' } "NO" end end end body('/a').must_equal "NO" body('/a.gz').must_equal 'GZ' body('/a.tar.gz').must_equal 'TAR.GZ' end end jeremyevans-roda-4f30bb3/spec/plugin/typecast_params_sized_integers_spec.rb000066400000000000000000000160461516720775400275030ustar00rootroot00000000000000require_relative "../spec_helper" require 'tempfile' describe "typecast_params_sized_integers plugin" do def tp(arg) @tp.call(arg) end before(:all) do res = nil app(:typecast_params_sized_integers) do |r| res = typecast_params '' end @tp = lambda do |i| req('QUERY_STRING'=>"i=#{i}&a[]=#{i}", 'rack.input'=>rack_input) res end @tp_error = Roda::RodaPlugins::TypecastParams::Error end { 8 => ['', '-129', '128'], 16 => ['', '-32769', '32768'], 32 => ['', '-2147483649', '2147483648'], 64 => ['', '-9223372036854775809', '9223372036854775808'], }.each do |i, vals| [:"int#{i}", :"Integer#{i}"].each do |meth| vals.each do |v| it "##{meth} should return nil for #{v.inspect}" do tp(v).send(meth, 'i').must_be_nil end it "#array!(:#{meth}) should raise for [#{v.inspect}]" do proc{tp(v).array!(meth, 'a')}.must_raise @tp_error end end end [:"int#{i}!", :"Integer#{i}!"].each do |meth| vals.each do |v| it "##{meth} should raise for #{v.inspect}" do proc{tp(v).send(meth, 'i')}.must_raise @tp_error end end end end { 8 => ['0', '-128', '127'], 16 => ['0', '-32768', '32767'], 32 => ['0', '-2147483648', '2147483647'], 64 => ['0', '-9223372036854775808', '9223372036854775807'], }.each do |i, vals| [:"int#{i}", :"Integer#{i}", :"int#{i}!", :"Integer#{i}!"].each do |meth| vals.each do |v| it "##{meth} should return #{v} for #{v.inspect}" do tp(v).send(meth, 'i').must_equal(v.to_i) end it "#array(:#{meth}) should return [#{v}] for [#{v.inspect}]" do tp(v).array(meth, 'a').must_equal [v.to_i] end unless meth.to_s.end_with?('!') end end end { 8 => ['', '0', '128'], 16 => ['', '0', '32768'], 32 => ['', '0', '2147483648'], 64 => ['', '0', '9223372036854775808'], }.each do |i, vals| [:"pos_int#{i}"].each do |meth| vals.each do |v| it "##{meth} should return nil for #{v.inspect}" do tp(v).send(meth, 'i').must_be_nil end it "#array!(:#{meth}) should raise for [#{v.inspect}]" do proc{tp(v).array!(meth, 'a')}.must_raise @tp_error end end end [:"pos_int#{i}!"].each do |meth| vals.each do |v| it "##{meth} should raise for #{v.inspect}" do proc{tp(v).send(meth, 'i')}.must_raise @tp_error end end end end { 8 => ['1', '127'], 16 => ['1', '32767'], 32 => ['1', '2147483647'], 64 => ['1', '9223372036854775807'], }.each do |i, vals| [:"pos_int#{i}", :"pos_int#{i}!"].each do |meth| vals.each do |v| it "##{meth} should return #{v} for #{v.inspect}" do tp(v).send(meth, 'i').must_equal(v.to_i) end it "#array(:#{meth}) should return [#{v}] for [#{v.inspect}]" do tp(v).array(meth, 'a').must_equal [v.to_i] end unless meth.to_s.end_with?('!') end end end { 8 => ['', '-1', '256'], 16 => ['', '-1', '65536'], 32 => ['', '-1', '4294967296'], 64 => ['', '-1', '18446744073709551616'], }.each do |i, vals| [:"uint#{i}", :"Integeru#{i}"].each do |meth| vals.each do |v| it "##{meth} should return nil for #{v.inspect}" do tp(v).send(meth, 'i').must_be_nil end it "#array!(:#{meth}) should raise for [#{v.inspect}]" do proc{tp(v).array!(meth, 'a')}.must_raise @tp_error end end end [:"uint#{i}!", :"Integeru#{i}!"].each do |meth| vals.each do |v| it "##{meth} should raise for #{v.inspect}" do proc{tp(v).send(meth, 'i')}.must_raise @tp_error end end end end { 8 => ['0', '255'], 16 => ['0', '65535'], 32 => ['0', '4294967295'], 64 => ['0', '18446744073709551615'], }.each do |i, vals| [:"uint#{i}", :"Integeru#{i}", :"uint#{i}!", :"Integeru#{i}!"].each do |meth| vals.each do |v| it "##{meth} should return #{v} for #{v.inspect}" do tp(v).send(meth, 'i').must_equal(v.to_i) end it "#array(:#{meth}) should return [#{v}] for [#{v.inspect}]" do tp(v).array(meth, 'a').must_equal [v.to_i] end unless meth.to_s.end_with?('!') end end end { 8 => ['', '0', '256'], 16 => ['', '0', '65536'], 32 => ['', '0', '4294967296'], 64 => ['', '0', '18446744073709551616'], }.each do |i, vals| [:"pos_uint#{i}"].each do |meth| vals.each do |v| it "##{meth} should return nil for #{v.inspect}" do tp(v).send(meth, 'i').must_be_nil end it "#array!(:#{meth}) should raise for [#{v.inspect}]" do proc{tp(v).array!(meth, 'a')}.must_raise @tp_error end end end [:"pos_uint#{i}!"].each do |meth| vals.each do |v| it "##{meth} should raise for #{v.inspect}" do proc{tp(v).send(meth, 'i')}.must_raise @tp_error end end end end { 8 => ['1', '255'], 16 => ['1', '65535'], 32 => ['1', '4294967295'], 64 => ['1', '18446744073709551615'], }.each do |i, vals| [:"pos_uint#{i}", :"pos_uint#{i}!"].each do |meth| vals.each do |v| it "##{meth} should return #{v} for #{v.inspect}" do tp(v).send(meth, 'i').must_equal(v.to_i) end it "#array(:#{meth}) should return [#{v}] for [#{v.inspect}]" do tp(v).array(meth, 'a').must_equal [v.to_i] end unless meth.to_s.end_with?('!') end end end end describe "typecast_params_sized_integers plugin" do it "should support a :sizes option for only creating methods for the given sizes" do app(:bare) do plugin :typecast_params_sized_integers, :sizes=>[8] route do |_| "#{typecast_params.respond_to?(:int8)}-#{typecast_params.respond_to?(:int16)}-#{typecast_params.respond_to?(:int32)}" end end body('rack.input'=>rack_input).must_equal 'true-false-false' app.plugin :typecast_params_sized_integers, :sizes=>[8, 16] body('rack.input'=>rack_input).must_equal 'true-true-false' end it "should support a :default_size option for making the unsuffixed methods use the given size" do app(:bare) do plugin :typecast_params_sized_integers, :default_size=>8 route do |r| tp = typecast_params r.is String do |type| %w'ts min no z o max tl umax utl'.map do |param| tp.send(type, param).inspect end.join(" ") end end end h = {'QUERY_STRING'=>'ts=-129&min=-128&no=-1&z=0&o=1&max=127&umax=255&tl=128&umax=255&utl=256', 'rack.input'=>rack_input} body('/int', h).must_equal 'nil -128 -1 0 1 127 nil nil nil' body('/uint', h).must_equal 'nil nil nil 0 1 127 128 255 nil' body('/pos_int', h).must_equal 'nil nil nil nil 1 127 nil nil nil' body('/pos_uint', h).must_equal 'nil nil nil nil 1 127 128 255 nil' body('/Integer', h).must_equal 'nil -128 -1 0 1 127 nil nil nil' body('/Integeru', h).must_equal 'nil nil nil 0 1 127 128 255 nil' end end jeremyevans-roda-4f30bb3/spec/plugin/typecast_params_spec.rb000066400000000000000000001714421516720775400244070ustar00rootroot00000000000000require_relative "../spec_helper" require 'tempfile' describe "typecast_params plugin" do def tp(arg='a=1&b[]=2&b[]=3&c[d]=4&c[e]=5&f=&g[]=&h[i]=') @tp.call(arg) end def error yield rescue @tp_error => e e end before do res = nil app(:typecast_params) do |r| if r.env['rack.params'] r.define_singleton_method(:params){r.env['rack.params']} end res = typecast_params nil end @tp = lambda do |params| if params.is_a?(Hash) req('rack.params'=>params) else req('QUERY_STRING'=>params, 'rack.input'=>rack_input) end res end @tp_error = Roda::RodaPlugins::TypecastParams::Error end it ".new should raise error if params is not a hash" do lambda{Roda::RodaPlugins::TypecastParams::Params.new('a')}.must_raise @tp_error end it ".new should raise for non String/Array args passed to conversion method" do lambda{tp.any({})}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError lambda{tp.any(Object.new)}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError lambda{tp.any(:a)}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError end it "#present should return whether the key is in the obj if given String" do tp.present?('a').must_equal true tp.present?('b').must_equal true tp.present?('c').must_equal true tp.present?('d').must_equal false end it "#present should return whether all keys are in the obj if given an Array" do tp.present?(%w'a b c').must_equal true tp.present?(%w'a b c d').must_equal false end it "#present should raise if given an unexpected object" do lambda{tp.present?(:a)}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError lambda{tp.present?([:a])}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError lambda{tp.present?([['a']])}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError end it "conversion methods should only support one level deep array of keys" do lambda{tp.any([['a']])}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError end it "conversion methods should check for null bytes in strins" do lambda{tp('a=1%00&b=1').any('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').str('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').nonempty_str('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').bool('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').int('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').pos_int('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').Integer('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').float('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').Float('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=2020-10-20%00&b=1').date('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=2020-10-20%00&b=1').time('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=2020-10-20%00&b=1').datetime('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').any!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').str!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').nonempty_str!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').bool!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').int!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').pos_int!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').Integer!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').float!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1%00&b=1').Float!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=2020-10-20%00&b=1').date!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=2020-10-20%00&b=1').time!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=2020-10-20%00&b=1').datetime!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:any, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:str, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:nonempty_str, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:bool, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:int, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:pos_int, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:Integer, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:float, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1%00&b=1').array(:Float, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=2020-10-20%00&b=1').array(:date, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=2020-10-20%00&b=1').array(:time, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=2020-10-20%00&b=1').array(:datetime, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a=1%00&b=1').any('b').must_equal '1' tp('a=1%00&b=1').any!('b').must_equal '1' tp('a[]=1%00&b[]=1').array(:any, 'b').must_equal ['1'] end it "conversion methods should respect :date_parse_input_handler" do app.plugin :typecast_params, :date_parse_input_handler=>proc{|string| string[0, 128]} do max_input_bytesize(:date, 10000) max_input_bytesize(:time, 10000) max_input_bytesize(:datetime, 10000) end tp('a=2021-10-22' + ' '*100).date('a').must_equal Date.new(2021, 10, 22) tp('a=2021-10-22 10:20:30' + ' '*100).datetime('a').must_equal DateTime.new(2021, 10, 22, 10, 20, 30) tp('a=2021-10-22 10:20:30' + ' '*100).time('a').must_equal Time.local(2021, 10, 22, 10, 20, 30) tp('a=2021-10-22' + ' '*1000).date('a').must_equal Date.new(2021, 10, 22) tp('a=2021-10-22 10:20:30' + ' '*1000).datetime('a').must_equal DateTime.new(2021, 10, 22, 10, 20, 30) tp('a=2021-10-22 10:20:30' + ' '*1000).time('a').must_equal Time.local(2021, 10, 22, 10, 20, 30) app.plugin :typecast_params, :date_parse_input_handler=>proc{|string| raise Roda::RodaPlugins::TypecastParams::Error if string.bytesize > 128; string} tp('a=2021-10-22' + ' '*100).date('a').must_equal Date.new(2021, 10, 22) tp('a=2021-10-22 10:20:30' + ' '*100).datetime('a').must_equal DateTime.new(2021, 10, 22, 10, 20, 30) tp('a=2021-10-22 10:20:30' + ' '*100).time('a').must_equal Time.local(2021, 10, 22, 10, 20, 30) lambda{tp('a=2021-10-22' + ' '*1000).date('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=2021-10-22 10:20:30' + ' '*1000).datetime('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=2021-10-22 10:20:30' + ' '*1000).time('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error end it "conversion methods should allow null bytes in strings when :allow_null_bytes plugin option is used" do app.plugin :typecast_params, :allow_null_bytes=>true tp('a=1%00&b=1').any('a').must_equal "1\x00" tp('a=1%00&b=1').str('a').must_equal "1\x00" tp('a=1%00&b=1').nonempty_str('a').must_equal "1\x00" lambda{tp('a=1%00&b=1').bool('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a=1%00&b=1').int('a').must_equal 1 tp('a=1%00&b=1').pos_int('a').must_equal 1 lambda{tp('a=1%00&b=1').Integer('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a=1.1%00&b=1').float('a').must_equal 1.1 lambda{tp('a=1%00&b=1').Float('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a=2020-10-20%00&b=1').date('a').must_equal Date.new(2020, 10, 20) tp('a=2020-10-20%00&b=1').time('a').must_equal Time.local(2020, 10, 20) tp('a=2020-10-20%00&b=1').datetime('a').must_equal DateTime.new(2020, 10, 20) tp('a=1%00&b=1').any!('a').must_equal "1\x00" tp('a=1%00&b=1').str!('a').must_equal "1\x00" tp('a=1%00&b=1').nonempty_str!('a').must_equal "1\x00" lambda{tp('a=1%00&b=1').bool!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a=1%00&b=1').int!('a').must_equal 1 tp('a=1%00&b=1').pos_int!('a').must_equal 1 lambda{tp('a=1%00&b=1').Integer!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a=1.1%00&b=1').float!('a').must_equal 1.1 lambda{tp('a=1%00&b=1').Float!('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a=2020-10-20%00&b=1').date!('a').must_equal Date.new(2020, 10, 20) tp('a=2020-10-20%00&b=1').time!('a').must_equal Time.local(2020, 10, 20) tp('a=2020-10-20%00&b=1').datetime!('a').must_equal DateTime.new(2020, 10, 20) tp('a[]=1%00&b=1').array(:any, 'a').must_equal ["1\x00"] tp('a[]=1%00&b=1').array(:str, 'a').must_equal ["1\x00"] tp('a[]=1%00&b=1').array(:nonempty_str, 'a').must_equal ["1\x00"] lambda{tp('a[]=1%00&b=1').array(:bool, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a[]=1%00&b=1').array(:int, 'a').must_equal [1] tp('a[]=1%00&b=1').array(:pos_int, 'a').must_equal [1] lambda{tp('a[]=1%00&b=1').array(:Integer, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a[]=1.1%00&b=1').array(:float, 'a').must_equal [1.1] lambda{tp('a[]=1%00&b=1').array(:Float, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::Error tp('a[]=2020-10-20%00&b=1').array(:date, 'a').must_equal [Date.new(2020, 10, 20)] tp('a[]=2020-10-20%00&b=1').array(:time, 'a').must_equal [Time.local(2020, 10, 20)] tp('a[]=2020-10-20%00&b=1').array(:datetime, 'a').must_equal [DateTime.new(2020, 10, 20)] tp('a=1%00&b=1').any('b').must_equal '1' tp('a=1%00&b=1').any!('b').must_equal '1' tp('a[]=1%00&b[]=1').array(:any, 'b').must_equal ['1'] end it "conversion methods should allow to typecast input at max input bytesize by default" do int = '1'*100 tp('a='+int).int('a').must_equal int.to_i tp('a='+int).pos_int('a').must_equal int.to_i tp('a='+int).Integer('a').must_equal int.to_i float = '1.'+'1'*998 tp('a='+float).float('a').must_equal float.to_f tp('a='+float).Float('a').must_equal float.to_f date = '2021-10-22' + ' '*118 tp('a='+date).date('a').must_equal Date.new(2021, 10, 22) tp('a='+date).time('a').must_equal Time.local(2021, 10, 22) tp('a='+date).datetime('a').must_equal DateTime.new(2021, 10, 22) end it "conversion methods should not attempt to typecast input over max input bytesize by default" do lambda{tp('a='+'1'*101).int('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a='+'1'*101).pos_int('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a='+'1'*101).Integer('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1.'+'1'*999).float('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a=1.'+'1'*999).Float('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error date = '2021-10-22' + ' '*119 lambda{tp('a='+date).date('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a='+date).time('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a='+date).datetime('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error end it "conversion methods should attempt to typecast input over max input bytesize if :skip_bytesize_checking plugin option is used" do app.plugin :typecast_params, :skip_bytesize_checking=>true, :date_parse_input_handler=>proc{|string| string[0, 128]} int = '1'*101 tp('a='+int).int('a').must_equal int.to_i tp('a='+int).pos_int('a').must_equal int.to_i tp('a='+int).Integer('a').must_equal int.to_i float = '1.'+'1'*999 tp('a='+float).float('a').must_equal float.to_f tp('a='+float).Float('a').must_equal float.to_f date = '2021-10-22' + ' '*119 tp('a='+date).date('a').must_equal Date.new(2021, 10, 22) tp('a='+date).time('a').must_equal Time.local(2021, 10, 22) tp('a='+date).datetime('a').must_equal DateTime.new(2021, 10, 22) end it "should allow overriding max input bytesize using max_input_bytesize" do app.plugin :typecast_params do max_input_bytesize :int, 1 end tp('a=1').int('a').must_equal 1 tp('a[]=1').array(:int, 'a').must_equal [1] tp('a[]=1&a[]=2').array(:int, 'a').must_equal [1, 2] lambda{tp('a=11').int('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=11').int('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error lambda{tp('a[]=1&a[]=22').int('a')}.must_raise Roda::RodaPlugins::TypecastParams::Error end it "should allow overriding invalid value message using max_input_bytesize" do app.plugin :typecast_params do invalid_value_message :pos_int, "value must be greater than 0 for" end tp('a=1').pos_int!('a').must_equal 1 lambda{tp('a=').pos_int!('a')}.must_raise(Roda::RodaPlugins::TypecastParams::Error). message.must_equal "value must be greater than 0 for a" end it "should allow disabling max input bytesize for specific type by passing nil to max_input_bytesize" do app.plugin :typecast_params do max_input_bytesize :int, nil end int = '1'*1000 tp('a='+int).int('a').must_equal int.to_i end it "#any should not do any conversion" do tp.any('a').must_equal '1' tp.any('b').must_equal ["2", "3"] tp.any('c').must_equal('d'=>'4', 'e'=>'5') tp.any('d').must_be_nil tp.any(%w'g h').must_equal([[''], {'i'=>''}]) tp.any!('a').must_equal '1' tp.any!(%w'a').must_equal %w'1' lambda{tp.any!('d')}.must_raise @tp_error lambda{tp.any!(%w'd j')}.must_raise @tp_error lambda{tp.array(:any, 'a')}.must_raise @tp_error tp.array(:any, 'b').must_equal ["2", "3"] lambda{tp.array(:any, 'c')}.must_raise @tp_error tp.array(:any, 'd').must_be_nil tp.array(:any, %w'g').must_equal([['']]) lambda{tp.array(:any, 'h')}.must_raise @tp_error tp.array!(:any, 'b').must_equal ["2", "3"] lambda{tp.array!(:any, 'd')}.must_raise @tp_error tp.array!(:any, %w'g').must_equal([['']]) end it "#str should require strings" do tp.str('a').must_equal '1' lambda{tp.str('b')}.must_raise @tp_error lambda{tp.str('c')}.must_raise @tp_error lambda{tp.str(%w'b c')}.must_raise @tp_error tp.str('d').must_be_nil tp.str('f').must_equal '' lambda{tp.str('g')}.must_raise @tp_error lambda{tp.str('h')}.must_raise @tp_error tp.str!('a').must_equal '1' lambda{tp.str!('d')}.must_raise @tp_error tp.str!('f').must_equal '' lambda{tp.array(:str, 'a')}.must_raise @tp_error tp.array(:str, 'b').must_equal ["2", "3"] lambda{tp.array(:str, 'c')}.must_raise @tp_error tp.array(:str, 'd').must_be_nil tp.array(:str, 'g').must_equal [""] lambda{tp.array(:str, 'h')}.must_raise @tp_error tp.array!(:str, 'b').must_equal ["2", "3"] lambda{tp.array!(:str, 'd')}.must_raise @tp_error tp.array!(:str, 'g').must_equal [""] end it "#nonempty_str should require nonempty strings" do tp.nonempty_str('a').must_equal '1' tp('a=%201').nonempty_str('a').must_equal ' 1' tp('a=1%20').nonempty_str('a').must_equal '1 ' tp('a=%201%20').nonempty_str('a').must_equal ' 1 ' tp('a=%20').nonempty_str('a').must_be_nil lambda{tp.nonempty_str('b')}.must_raise @tp_error lambda{tp.nonempty_str('c')}.must_raise @tp_error tp.nonempty_str('d').must_be_nil tp.nonempty_str('f').must_be_nil lambda{tp.nonempty_str('g')}.must_raise @tp_error lambda{tp.nonempty_str('h')}.must_raise @tp_error tp.nonempty_str!('a').must_equal '1' lambda{tp.nonempty_str!('d')}.must_raise @tp_error lambda{tp.nonempty_str!('f')}.must_raise @tp_error lambda{tp.array(:nonempty_str, 'a')}.must_raise @tp_error tp.array(:nonempty_str, 'b').must_equal ["2", "3"] lambda{tp.array(:nonempty_str, 'c')}.must_raise @tp_error tp.array(:nonempty_str, 'd').must_be_nil tp.array(:nonempty_str, 'g').must_equal [nil] lambda{tp.array(:nonempty_str, 'h')}.must_raise @tp_error tp.array!(:nonempty_str, 'b').must_equal ["2", "3"] lambda{tp.array!(:nonempty_str, 'd')}.must_raise @tp_error lambda{tp.array!(:nonempty_str, 'g')}.must_raise @tp_error end it "#bool should convert to boolean" do tp('a=0').bool('a').must_equal false tp('a=f').bool('a').must_equal false tp('a=false').bool('a').must_equal false tp('a=FALSE').bool('a').must_equal false tp('a=F').bool('a').must_equal false tp('a=n').bool('a').must_equal false tp('a=no').bool('a').must_equal false tp('a=N').bool('a').must_equal false tp('a=NO').bool('a').must_equal false tp('a=off').bool('a').must_equal false tp('a=OFF').bool('a').must_equal false tp('a=1').bool('a').must_equal true tp('a=t').bool('a').must_equal true tp('a=true').bool('a').must_equal true tp('a=TRUE').bool('a').must_equal true tp('a=T').bool('a').must_equal true tp('a=y').bool('a').must_equal true tp('a=yes').bool('a').must_equal true tp('a=Y').bool('a').must_equal true tp('a=YES').bool('a').must_equal true tp('a=on').bool('a').must_equal true tp('a=ON').bool('a').must_equal true tp.bool('a').must_equal true lambda{tp.bool('b')}.must_raise @tp_error lambda{tp.bool('c')}.must_raise @tp_error tp.bool('d').must_be_nil tp.bool('f').must_be_nil tp.bool!('a').must_equal true lambda{tp.bool!('d')}.must_raise @tp_error lambda{tp.bool!('f')}.must_raise @tp_error lambda{tp.array(:bool, 'a')}.must_raise @tp_error tp('b[]=1&b[]=0').array(:bool, 'b').must_equal [true, false] lambda{tp('b[]=1&b[]=a').array(:bool, 'b')}.must_raise @tp_error lambda{tp.array(:bool, 'c')}.must_raise @tp_error tp.array(:bool, 'd').must_be_nil tp.array(:bool, 'g').must_equal [nil] lambda{tp.array(:bool, 'h')}.must_raise @tp_error tp('b[]=1&b[]=0').array!(:bool, 'b').must_equal [true, false] lambda{tp.array!(:bool, 'd')}.must_raise @tp_error lambda{tp.array!(:bool, 'g')}.must_raise @tp_error end it "#int should convert to integer" do tp('a=-1').int('a').must_equal(-1) tp('a=0').int('a').must_equal 0 tp('a=a').int('a').must_equal 0 tp.int('a').must_equal 1 tp.int('a').must_be_kind_of Integer lambda{tp.int('b')}.must_raise @tp_error lambda{tp.int('c')}.must_raise @tp_error tp.int('d').must_be_nil tp.int('f').must_be_nil lambda{tp.int('g')}.must_raise @tp_error lambda{tp.int('h')}.must_raise @tp_error tp.int!('a').must_equal 1 lambda{tp.int!('d')}.must_raise @tp_error lambda{tp.int!('f')}.must_raise @tp_error lambda{tp.array(:int, 'a')}.must_raise @tp_error tp.array(:int, 'b').must_equal [2, 3] lambda{tp.array(:int, 'c')}.must_raise @tp_error tp.array(:int, 'd').must_be_nil tp.array(:int, 'g').must_equal [nil] lambda{tp.array(:int, 'h')}.must_raise @tp_error tp.array!(:int, 'b').must_equal [2, 3] lambda{tp.array!(:int, 'd')}.must_raise @tp_error lambda{tp.array!(:int, 'g')}.must_raise @tp_error end it "#pos_int should convert to positive integer" do tp('a=-1').pos_int('a').must_be_nil tp('a=0').pos_int('a').must_be_nil tp('a=a').pos_int('a').must_be_nil tp.pos_int('a').must_equal 1 tp.pos_int('a').must_be_kind_of Integer lambda{tp.pos_int('b')}.must_raise @tp_error lambda{tp.pos_int('c')}.must_raise @tp_error tp.pos_int('d').must_be_nil tp.pos_int('f').must_be_nil lambda{tp.pos_int('g')}.must_raise @tp_error lambda{tp.pos_int('h')}.must_raise @tp_error lambda{tp('a=-1').pos_int!('a')}.must_raise @tp_error lambda{tp('a=0').pos_int!('a')}.must_raise @tp_error lambda{tp('a=a').pos_int!('a')}.must_raise @tp_error tp.pos_int!('a').must_equal 1 lambda{tp.pos_int!('d')}.must_raise @tp_error lambda{tp.pos_int!('f')}.must_raise @tp_error lambda{tp.array(:pos_int, 'a')}.must_raise @tp_error tp.array(:pos_int, 'b').must_equal [2, 3] lambda{tp.array(:pos_int, 'c')}.must_raise @tp_error tp.array(:pos_int, 'd').must_be_nil tp.array(:pos_int, 'g').must_equal [nil] lambda{tp.array(:pos_int, 'h')}.must_raise @tp_error tp.array!(:pos_int, 'b').must_equal [2, 3] lambda{tp.array!(:pos_int, 'd')}.must_raise @tp_error lambda{tp.array!(:pos_int, 'g')}.must_raise @tp_error end it "#Integer should convert to integer strictly" do tp('a=-1').Integer('a').must_equal(-1) tp('a=0').Integer('a').must_equal 0 lambda{tp('a=a').Integer('a')}.must_raise @tp_error tp.Integer('a').must_equal 1 tp.Integer('a').must_be_kind_of Integer lambda{tp.Integer('b')}.must_raise @tp_error lambda{tp.Integer('c')}.must_raise @tp_error tp.Integer('d').must_be_nil tp.Integer('f').must_be_nil lambda{tp.Integer('g')}.must_raise @tp_error lambda{tp.Integer('h')}.must_raise @tp_error tp.Integer!('a').must_equal 1 lambda{tp.Integer!('d')}.must_raise @tp_error lambda{tp.Integer!('f')}.must_raise @tp_error lambda{tp.array(:Integer, 'a')}.must_raise @tp_error tp.array(:Integer, 'b').must_equal [2, 3] lambda{tp.array(:Integer, 'c')}.must_raise @tp_error tp.array(:Integer, 'd').must_be_nil tp.array(:Integer, 'g').must_equal [nil] lambda{tp.array(:Integer, 'h')}.must_raise @tp_error tp.array!(:Integer, 'b').must_equal [2, 3] lambda{tp.array!(:Integer, 'd')}.must_raise @tp_error lambda{tp.array!(:Integer, 'g')}.must_raise @tp_error a = 1 @app.plugin :hooks @app.before do request.define_singleton_method(:params){{'a'=>a}} end tp.Integer('a').must_equal 1 a = 1.0 tp.Integer('a').must_equal 1 a = 1.1 lambda{tp.Integer('a')}.must_raise @tp_error end it "#float should convert to float" do tp('a=-1').float('a').must_equal(-1) tp('a=0').float('a').must_equal 0 tp('a=a').float('a').must_equal 0 tp.float('a').must_equal 1 tp.float('a').must_be_kind_of Float lambda{tp.float('b')}.must_raise @tp_error lambda{tp.float('c')}.must_raise @tp_error tp.float('d').must_be_nil tp.float('f').must_be_nil lambda{tp.float('g')}.must_raise @tp_error lambda{tp.float('h')}.must_raise @tp_error tp.float!('a').must_equal 1 lambda{tp.float!('d')}.must_raise @tp_error lambda{tp.float!('f')}.must_raise @tp_error lambda{tp.array(:float, 'a')}.must_raise @tp_error tp.array(:float, 'b').must_equal [2, 3] lambda{tp.array(:float, 'c')}.must_raise @tp_error tp.array(:float, 'd').must_be_nil tp.array(:float, 'g').must_equal [nil] lambda{tp.array(:float, 'h')}.must_raise @tp_error tp.array!(:float, 'b').must_equal [2, 3] lambda{tp.array!(:float, 'd')}.must_raise @tp_error lambda{tp.array!(:float, 'g')}.must_raise @tp_error end it "#Float should convert to float strictly" do tp('a=-1').Float('a').must_equal(-1) tp('a=0').Float('a').must_equal 0 lambda{tp('a=a').Float('a')}.must_raise @tp_error tp.Float('a').must_equal 1 tp.Float('a').must_be_kind_of Float lambda{tp.Float('b')}.must_raise @tp_error lambda{tp.Float('c')}.must_raise @tp_error tp.Float('d').must_be_nil tp.Float('f').must_be_nil lambda{tp.Float('g')}.must_raise @tp_error lambda{tp.Float('h')}.must_raise @tp_error tp.Float!('a').must_equal 1 lambda{tp.Float!('d')}.must_raise @tp_error lambda{tp.Float!('f')}.must_raise @tp_error lambda{tp.array(:Float, 'a')}.must_raise @tp_error tp.array(:Float, 'b').must_equal [2, 3] lambda{tp.array(:Float, 'c')}.must_raise @tp_error tp.array(:Float, 'd').must_be_nil tp.array(:Float, 'g').must_equal [nil] lambda{tp.array(:Float, 'h')}.must_raise @tp_error tp.array!(:Float, 'b').must_equal [2, 3] lambda{tp.array!(:Float, 'd')}.must_raise @tp_error lambda{tp.array!(:Float, 'g')}.must_raise @tp_error end it "#Hash should require hashes" do lambda{tp.Hash('a')}.must_raise @tp_error lambda{tp.Hash('b')}.must_raise @tp_error tp.Hash('c').must_equal('d'=>'4', 'e'=>'5') tp.Hash('d').must_be_nil lambda{tp.Hash('f')}.must_raise @tp_error lambda{tp.Hash('g')}.must_raise @tp_error tp.Hash('h').must_equal('i'=>'') tp.Hash!('c').must_equal('d'=>'4', 'e'=>'5') lambda{tp.Hash!('d')}.must_raise @tp_error tp.Hash!('h').must_equal('i'=>'') lambda{tp.array(:Hash, 'c')}.must_raise @tp_error lambda{tp('a[][b]=2&a[]=3').array(:Hash, 'a')}.must_raise @tp_error tp('a[][b]=2&a[][b]=3').array(:Hash, 'a').must_equal [{'b'=>'2'}, {'b'=>'3'}] tp.array(:Hash, 'd').must_be_nil tp('a[][b]=2&a[][b]=3').array!(:Hash, 'a').must_equal [{'b'=>'2'}, {'b'=>'3'}] lambda{tp.array!(:Hash, 'd')}.must_raise @tp_error end it "#Date should parse strings into Date instances" do tp('a=').date('a').must_be_nil tp('a=2017-10-11').date('a').must_equal Date.new(2017, 10, 11) tp('a=17/10/11').date('a').must_equal Date.new(2017, 10, 11) lambda{tp.date('b')}.must_raise @tp_error lambda{tp('a=a').date('a')}.must_raise @tp_error lambda{tp('a=').date!('a')}.must_raise @tp_error tp('a=2017-10-11').date!('a').must_equal Date.new(2017, 10, 11) tp('a[]=2017-10-11&a[]=2017-10-12').array(:date, 'a').must_equal [Date.new(2017, 10, 11), Date.new(2017, 10, 12)] tp('a[]=2017-10-11&a[]=2017-10-12').array(:date, 'b').must_be_nil tp('a[]=2017-10-11&a[]=2017-10-12').array!(:date, 'a').must_equal [Date.new(2017, 10, 11), Date.new(2017, 10, 12)] lambda{tp('a[]=2017-10-11&a[]=a').array!(:date, 'a')}.must_raise @tp_error lambda{tp('a[]=2017-10-11&a[]=2017-10-12').array!(:date, 'b')}.must_raise @tp_error end it "#Time should parse strings into Time instances" do tp('a=').time('a').must_be_nil tp('a=2017-10-11%2012:13:14').time('a').must_equal Time.local(2017, 10, 11, 12, 13, 14) tp('a=17/10/11%2012:13:14').time('a').must_equal Time.local(2017, 10, 11, 12, 13, 14) lambda{tp.time('b')}.must_raise @tp_error lambda{tp('a=a').time('a')}.must_raise @tp_error lambda{tp('a=').time!('a')}.must_raise @tp_error tp('a=2017-10-11%2012:13:14').time!('a').must_equal Time.new(2017, 10, 11, 12, 13, 14) tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array(:time, 'a').must_equal [Time.local(2017, 10, 11, 12, 13, 14), Time.local(2017, 10, 12, 12, 13, 14)] tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array(:time, 'b').must_be_nil tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array!(:time, 'a').must_equal [Time.local(2017, 10, 11, 12, 13, 14), Time.local(2017, 10, 12, 12, 13, 14)] lambda{tp('a[]=2017-10-11%2012:13:14&a[]=a').array!(:time, 'a')}.must_raise @tp_error lambda{tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array!(:time, 'b')}.must_raise @tp_error end it "#DateTime should parse strings into DateTime instances" do tp('a=').datetime('a').must_be_nil tp('a=2017-10-11%2012:13:14').datetime('a').must_equal DateTime.new(2017, 10, 11, 12, 13, 14) tp('a=17/10/11%2012:13:14').datetime('a').must_equal DateTime.new(2017, 10, 11, 12, 13, 14) lambda{tp.datetime('b')}.must_raise @tp_error lambda{tp('a=a').datetime('a')}.must_raise @tp_error lambda{tp('a=').datetime!('a')}.must_raise @tp_error tp('a=2017-10-11%2012:13:14').datetime!('a').must_equal DateTime.new(2017, 10, 11, 12, 13, 14) tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array(:datetime, 'a').must_equal [DateTime.new(2017, 10, 11, 12, 13, 14), DateTime.new(2017, 10, 12, 12, 13, 14)] tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array(:datetime, 'b').must_be_nil tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array!(:datetime, 'a').must_equal [DateTime.new(2017, 10, 11, 12, 13, 14), DateTime.new(2017, 10, 12, 12, 13, 14)] lambda{tp('a[]=2017-10-11%2012:13:14&a[]=a').array!(:datetime, 'a')}.must_raise @tp_error lambda{tp('a[]=2017-10-11%2012:13:14&a[]=2017-10-12%2012:13:14').array!(:datetime, 'b')}.must_raise @tp_error end it "#array should handle defaults" do tp = tp('b[]=1&c[]=') tp.array(:int, 'b', [2]).must_equal [1] tp.array(:int, 'c', [2]).must_equal [nil] tp.array(:int, 'd', []).must_equal [] tp.array(:int, 'e', [1]).must_equal [1] tp('b[]=1&c[]=').array(:int, %w'b c', [2]).must_equal [[1], [nil]] tp('b[]=1&c[]=').array(:int, %w'b d', [2]).must_equal [[1], [2]] end it "#array! should handle defaults" do tp = tp('b[]=1&c[]=') tp.array!(:int, 'b', [2]).must_equal [1] lambda{tp.array!(:int, 'c', [2])}.must_raise @tp_error tp.array!(:int, 'd', []).must_equal [] tp.array!(:int, 'e', [1]).must_equal [1] lambda{tp('b[]=1&c[]=').array!(:int, %w'b c', [2])}.must_raise @tp_error tp('b[]=1&c[]=').array!(:int, %w'b d', [2]).must_equal [[1], [2]] end it "#array should handle key arrays" do tp('b[]=1&c[]=2').array(:int, %w'b c').must_equal [[1], [2]] tp('b[]=1&c[]=').array(:int, %w'b c').must_equal [[1], [nil]] end it "#array! should handle key arrays" do tp('b[]=1&c[]=2').array!(:int, %w'b c').must_equal [[1], [2]] lambda{tp('b[]=1&c[]=').array!(:int, %w'b c')}.must_raise @tp_error end it "#array should raise ProgrammerError for invalid types" do proc{tp('b[]=1').array(:int2, 'b')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError end it "#[] should access nested values" do tp['c'].must_be_kind_of tp.class tp['c'].int('d').must_equal 4 tp['c'].int('e').must_equal 5 tp['c'].int(%w'd e').must_equal [4, 5] end it "#[] should handle deeply nested structures" do tp('a[b][c][d][e]=1')['a']['b']['c']['d'].int('e').must_equal 1 tp('a[][b][][e]=1')['a'][0]['b'][0].int('e').must_equal 1 end it "#[] should raise error for non-Array/Hash parameters" do lambda{tp['a']}.must_raise @tp_error end it "#[] should raise error for accessing hash with integer value (thinking it is an array)" do lambda{tp[1]}.must_raise @tp_error end it "#[] should raise error for accessing array with non-integer value non-Array/Hash parameters" do lambda{tp['b']['a']}.must_raise @tp_error end it "#convert! should return a hash of converted parameters" do tp = tp() tp.convert! do |ptp| ptp.int!('a') ptp.array!(:int, 'b') ptp['c'].convert! do |stp| stp.int!(%w'd e') end end.must_equal("a"=>1, "b"=>[2, 3], "c"=>{"d"=>4, "e"=>5}) end it "#convert! hash should only include changes made inside block" do tp = tp() tp.convert! do |ptp| ptp.int!('a') ptp.array!(:int, 'b') end.must_equal("a"=>1, "b"=>[2, 3]) tp.convert! do |ptp| ptp['c'].convert! do |stp| stp.int!(%w'd e') end end.must_equal("c"=>{"d"=>4, "e"=>5}) end it "#convert! should handle deeply nested structures" do tp = tp('a[b][c][d][e]=1') tp.convert! do |tp0| tp0['a'].convert! do |tp1| tp1['b'].convert! do |tp2| tp2['c'].convert! do |tp3| tp3['d'].convert! do |tp4| tp4.int('e') end end end end end.must_equal('a'=>{'b'=>{'c'=>{'d'=>{'e'=>1}}}}) tp = tp('a[][b][][e]=1') tp.convert! do |tp0| tp0['a'].convert! do |tp1| tp1[0].convert! do |tp2| tp2['b'].convert! do |tp3| tp3[0].convert! do |tp4| tp4.int('e') end end end end end.must_equal('a'=>[{'b'=>[{'e'=>1}]}]) end it "#convert! should not raise errors for missing keys if :raise option is false" do tp = tp('a[b]=1') tp.convert! do |tp0| tp0.convert!('a') do |tp1| tp1.convert!('c', :raise=>false) do |tp2| tp2.convert!('d') do |tp2| end end end end.must_equal('a'=>{}) tp.convert!('b', :raise=>false){}.must_equal({}) tp.convert!('b', :raise=>false) do |tp0| tp1.convert!('c'){} end.must_equal({}) end it "#convert! should not raise errors for explicit nil values if :raise option is false" do tp = tp("id"=>1) tp.convert!("price", :raise=>false) do |tp0| tp0.convert!('d') do |tp2| end end.must_equal({}) tp = tp("id"=>1, "price"=>nil) tp.convert!("price", :raise=>false) do |tp0| tp0.convert!('d') do |tp2| end end.must_equal({}) end it "#convert! should handle #[] without #convert! at each level" do tp = tp('a[b][c][d][e]=1') tp.convert! do |tp0| tp0['a'].convert! do |tp1| tp1['b']['c']['d'].convert! do |tp4| tp4.int('e') end end end.must_equal('a'=>{'b'=>{'c'=>{'d'=>{'e'=>1}}}}) end it "#convert! should handle #[] without #convert! below" do tp = tp('a[b][c][d][e]=1') tp.convert! do |tp0| tp0['a']['b']['c']['d'].int('e') end.must_equal('a'=>{'b'=>{'c'=>{'d'=>{'e'=>1}}}}) end it "#convert! should handle multiple calls to #[] and #convert! below" do tp = tp('a[b]=2&a[c]=3&a[d]=4&a[e]=5&a[f]=6') tp.convert! do |tp0| tp0['a'].int('b') tp0['a'].convert! do |tp1| tp1.int('c') end tp0['a'].int('d') tp0['a'].convert! do |tp1| tp1.int('e') end tp0['a'].int('f') end.must_equal('a'=>{'b'=>2, 'c'=>3, 'd'=>4, 'e'=>5, 'f'=>6}) end it "#convert! should handle defaults" do tp.convert! do |tp0| tp0.int('d', 12) end.must_equal('d'=>12) tp.convert! do |tp0| tp0.int(%w'a d', 12) end.must_equal('a'=>1, 'd'=>12) tp.convert! do |tp0| tp0.array(:int, 'g', []) end.must_equal('g'=>[nil]) tp.convert! do |tp0| tp0.array(:int, 'j', []) end.must_equal('j'=>[]) tp('a[]=1&g[]=').convert! do |tp0| tp0.array(:int, %w'a d g', [2]) end.must_equal('a'=>[1], 'd'=>[2], 'g'=>[nil]) end it "#convert! should handle multiple convert! calls inside" do tp = tp('a[b]=1&c[d]=2') tp.convert! do |tp0| tp0.convert!('a'){|d| d.int('b')} tp0.convert!('c'){|d| d.int('d')} end.must_equal('a'=>{'b'=>1}, 'c'=>{'d'=>2}) tp.convert!(:symbolize=>true) do |tp0| tp0.convert!('a'){|d| d.int('b')} tp0.convert!('c'){|d| d.int('d')} end.must_equal(:a=>{:b=>1}, :c=>{:d=>2}) end it "#convert! should include missing values as nil" do tp = tp() tp.convert! do |ptp| ptp.int('x') ptp.array(:int, 'y') ptp['c'].convert! do |stp| stp.int(%w'x z') end end.must_equal("x"=>nil, "y"=>nil, "c"=>{"x"=>nil, "z"=>nil}) end it "#convert! with :missing=>:skip should not include missing values" do tp = tp() tp.convert!(:skip_missing=>true) do |ptp| ptp.int('x') ptp.array(:int, 'y') ptp['c'].convert! do |stp| stp.int(%w'x z') end end.must_equal("c"=>{}) tp.convert! do |ptp| ptp.int('x') ptp.array(:int, 'y') ptp['c'].convert!(:skip_missing=>true) do |stp| stp.int(%w'x z') end end.must_equal("x"=>nil, "y"=>nil, "c"=>{}) tp.convert!(:skip_missing=>true) do |ptp| ptp.int('x') ptp.array(:int, 'y') ptp['c'].convert!(:skip_missing=>false) do |stp| stp.int(%w'x z') end end.must_equal("c"=>{"x"=>nil, "z"=>nil}) end it "#convert_each! should convert each entry in an array" do tp = tp('a[][b]=1&a[][c]=2&a[][b]=3&a[][c]=4') tp['a'].convert_each! do |tp0| tp0.int(%w'b c') end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}] end it "#convert_each! without :keys option should convert each named entry in a hash when keys are '0'..'N'" do tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4') tp['a'].convert_each! do |tp0| tp0.int(%w'b c') end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}] end it "#convert_each! with :keys option should convert each named entry in a hash when keys are '0'..'N'" do tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4') tp['a'].convert_each!(:keys=>%w'0 1') do |tp0| tp0.int(%w'b c') end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}] end it "#convert_each! with :keys option should convert each named entry in a hash" do tp = tp('a[d][b]=1&a[d][c]=2&a[e][b]=3&a[e][c]=4') tp['a'].convert_each!(:keys=>%w'd e') do |tp0| tp0.int(%w'b c') end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}] end it "#convert_each! with :keys option should store entries when called inside convert" do tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4').convert! do |tp| tp['a'].convert_each!(:keys=>%w'0 1') do |tp0| tp0.int(%w'b c') end end.must_equal("a"=>{"0"=>{'b'=>1, 'c'=>2}, "1"=>{'b'=>3, 'c'=>4}}) end it "#convert_each! :keys option should accept a Proc" do tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4').convert! do |tp| tp['a'].convert_each!(:keys=>proc{|obj| obj.keys}) do |tp0| tp0.int(%w'b c') end end.must_equal("a"=>{"0"=>{'b'=>1, 'c'=>2}, "1"=>{'b'=>3, 'c'=>4}}) end it "#convert_each! should raise if :keys option is given and not an Array/Proc/Method" do tp = tp('a[0][b]=1&a[0][c]=2&a[2][b]=3&a[2][c]=4') lambda{tp['a'].convert_each!(:keys=>Object.new){}}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError end it "#convert_each! should raise if obj is a hash without '0' keys" do lambda{tp.convert_each!{}}.must_raise @tp_error end it "#convert_each! should raise if obj is not a hash with '0' but not '0'..'N' keys" do tp = tp('a[0][b]=1&a[0][c]=2&a[2][b]=3&a[2][c]=4') lambda{tp['b'].convert_each!{}}.must_raise @tp_error end it "#convert_each! should raise if obj is a scalar" do tp = tp('a[d][b]=1&a[d][c]=2&a[e][b]=3&a[e][c]=4') lambda{tp['d']['b'].convert_each!{}}.must_raise @tp_error end it "#convert_each! should raise if obj is a array of non-hashes" do lambda{tp['b'].convert_each!{}}.must_raise @tp_error end it "#convert_each! should not include duplicate errors if there is an internal rescue" do tp = tp('a=') begin tp.convert! do |tp0| tp0['a'].convert_each!{} rescue nil; tp0['a'].convert_each!{} end rescue @tp_error => e end e.all_errors.length.must_equal 1 end it "#convert_each! should not raise errors for missing keys if :raise option is false" do tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4') tp['a'].convert_each!(:keys=>%w'0 2', :raise=>false) do |tp0| tp0.int(%w'b c') end.must_equal [{'b'=>1, 'c'=>2}, nil] end it "#convert! with :symbolize option should return a hash of converted parameters" do tp = tp() tp.convert!(:symbolize=>true) do |ptp| ptp.int!('a') ptp.array!(:int, 'b') ptp['c'].convert! do |stp| stp.int!(%w'd e') end end.must_equal(:a=>1, :b=>[2, 3], :c=>{:d=>4, :e=>5}) end it "#convert! with :symbolize option should not persist" do tp = tp() tp.convert!(:symbolize=>true) do |ptp| ptp.int!('a') ptp.array!(:int, 'b') ptp['c'].convert! do |stp| stp.int!(%w'd e') end end.must_equal(:a=>1, :b=>[2, 3], :c=>{:d=>4, :e=>5}) tp.convert! do |ptp| ptp.int!('a') ptp.array!(:int, 'b') ptp['c'].convert! do |stp| stp.int!(%w'd e') end end.must_equal("a"=>1, "b"=>[2, 3], "c"=>{"d"=>4, "e"=>5}) end it "#convert! with :symbolize option hash should only include changes made inside block" do tp = tp() tp.convert!(:symbolize=>true) do |ptp| ptp.int!('a') ptp.array!(:int, 'b') end.must_equal(:a=>1, :b=>[2, 3]) tp.convert!(:symbolize=>true) do |ptp| ptp['c'].convert! do |stp| stp.int!(%w'd e') end end.must_equal(:c=>{:d=>4, :e=>5}) end it "#convert! with :symbolize option should handle deeply nested structures" do tp = tp('a[b][c][d][e]=1') tp.convert!(:symbolize=>true) do |tp0| tp0['a'].convert! do |tp1| tp1['b'].convert! do |tp2| tp2['c'].convert! do |tp3| tp3['d'].convert! do |tp4| tp4.int('e') end end end end end.must_equal(:a=>{:b=>{:c=>{:d=>{:e=>1}}}}) tp = tp('a[][b][][e]=1') tp.convert!(:symbolize=>true) do |tp0| tp0['a'].convert! do |tp1| tp1[0].convert! do |tp2| tp2['b'].convert! do |tp3| tp3[0].convert! do |tp4| tp4.int('e') end end end end end.must_equal(:a=>[{:b=>[{:e=>1}]}]) end it "#convert! with :symbolize option should handle #[] without #convert! at each level" do tp = tp('a[b][c][d][e]=1') tp.convert!(:symbolize=>true) do |tp0| tp0['a'].convert! do |tp1| tp1['b']['c']['d'].convert! do |tp4| tp4.int('e') end end end.must_equal(:a=>{:b=>{:c=>{:d=>{:e=>1}}}}) end it "#convert! with :symbolize option should handle #[] without #convert! below" do tp = tp('a[b][c][d][e]=1') tp.convert!(:symbolize=>true) do |tp0| tp0['a']['b']['c']['d'].int('e') end.must_equal(:a=>{:b=>{:c=>{:d=>{:e=>1}}}}) end it "#convert! with :symbolize option should handle multiple calls to #[] and #convert! below" do tp = tp('a[b]=2&a[c]=3&a[d]=4&a[e]=5&a[f]=6') tp.convert!(:symbolize=>true) do |tp0| tp0['a'].int('b') tp0['a'].convert! do |tp1| tp1.int('c') end tp0['a'].int('d') tp0['a'].convert! do |tp1| tp1.int('e') end tp0['a'].int('f') end.must_equal(:a=>{:b=>2, :c=>3, :d=>4, :e=>5, :f=>6}) end it "#convert! with :symbolize option should handle defaults" do tp.convert!(:symbolize=>true) do |tp0| tp0.int('d', 12) end.must_equal(:d=>12) tp.convert!(:symbolize=>true) do |tp0| tp0.int(%w'a d', 12) end.must_equal(:a=>1, :d=>12) tp.convert!(:symbolize=>true) do |tp0| tp0.array(:int, 'g', []) end.must_equal(:g=>[nil]) tp.convert!(:symbolize=>true) do |tp0| tp0.array(:int, 'j', []) end.must_equal(:j=>[]) tp('a[]=1&g[]=').convert!(:symbolize=>true) do |tp0| tp0.array(:int, %w'a d g', [2]) end.must_equal(:a=>[1], :d=>[2], :g=>[nil]) end it "#convert_each! with :symbolize option should convert each entry in an array" do tp = tp('a[][b]=1&a[][c]=2&a[][b]=3&a[][c]=4') tp['a'].convert_each!(:symbolize=>true) do |tp0| tp0.int(%w'b c') end.must_equal [{:b=>1, :c=>2}, {:b=>3, :c=>4}] end it "#convert_each! with :symbolize and :keys options should convert each named entry in a hash" do tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4') tp['a'].convert_each!(:keys=>%w'0 1', :symbolize=>true) do |tp0| tp0.int(%w'b c') end.must_equal [{:b=>1, :c=>2}, {:b=>3, :c=>4}] end it "#convert_each! with :symbolize and :keys options should store entries when called inside convert" do tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4').convert!(:symbolize=>true) do |tp| tp['a'].convert_each!(:keys=>%w'0 1') do |tp0| tp0.int(%w'b c') end end.must_equal(:a=>{:'0'=>{:b=>1, :c=>2}, :'1'=>{:b=>3, :c=>4}}) end it "#convert_each! with :symbolize option should not persist" do tp = tp('a[][b]=1&a[][c]=2&a[][b]=3&a[][c]=4') tp['a'].convert_each!(:symbolize=>true) do |tp0| tp0.int(%w'b c') end.must_equal [{:b=>1, :c=>2}, {:b=>3, :c=>4}] tp = tp('a[][b]=1&a[][c]=2&a[][b]=3&a[][c]=4') tp['a'].convert_each! do |tp0| tp0.int(%w'b c') end.must_equal [{'b'=>1, 'c'=>2}, {'b'=>3, 'c'=>4}] end it "#convert! with :symbolize options specified at different levels should work" do tp = tp('a[b][c][d][e]=1') tp.convert!(:symbolize=>true) do |tp0| tp0['a'].convert!(:symbolize=>false) do |tp1| tp1['b'].convert!(:symbolize=>true) do |tp2| tp2['c'].convert!(:symbolize=>false) do |tp3| tp3['d'].convert!(:symbolize=>true) do |tp4| tp4.int('e') end end end end end.must_equal(:a=>{'b'=>{:c=>{'d'=>{:e=>1}}}}) tp = tp('a[][b][][e]=1') tp.convert!(:symbolize=>true) do |tp0| tp0['a'].convert! do |tp1| tp1[0].convert!(:symbolize=>false) do |tp2| tp2['b'].convert! do |tp3| tp3[0].convert!(:symbolize=>true) do |tp4| tp4.int('e') end end end end end.must_equal(:a=>[{'b'=>[{:e=>1}]}]) end it "#convert! should add Error if raised and not also the last capture" do tp = tp() begin tp.convert! do |ptp| raise @tp_error.create('a', 'b', ArgumentError.new('c')) end rescue @tp_error => e end e.must_be_instance_of @tp_error e.all_errors.length.must_equal 1 e.all_errors.first.message.must_equal 'ArgumentError: c' end it "#dig should return nested values or nil if there is no value" do tp = tp('a[b][c][d][e]=1&b=2') tp.dig(:int, 'a', 'b', 'c', 'd', 'e').must_equal 1 tp.dig(:int, 'b').must_equal 2 tp.dig(:int, 'a', 0, 'c', 'd', 'e').must_be_nil tp.dig(:int, 'a', 'd', 'c', 'd', 'e').must_be_nil tp.dig(:int, 'a', 'b', 'c', 'd', 'f').must_be_nil tp.dig(:int, 'c').must_be_nil tp.dig(:int, 'c', 'd').must_be_nil tp = tp('a[][c][][e]=1') tp.dig(:int, 'a', 0, 'c', 0, 'e').must_equal 1 tp.dig(:int, 'a', 1, 'c', 0, 'e').must_be_nil tp.dig(:int, 'a', 'b', 'c', 0, 'e').must_be_nil tp.dig(:int, 'a', 0, 'c', 0, 'f').must_be_nil end it "#dig should raise when accessing past the end of the expected structure" do tp = tp('a[b][c][d][e]=1&b=2') lambda{tp.dig(:int, 'a', 'b', 'c', 'd', 'e', 'f')}.must_raise @tp_error lambda{tp.dig(:int, 'b', 'c')}.must_raise @tp_error tp = tp('a[][c][][e]=1') lambda{tp.dig(:int, 'a', 0, 'c', 0, 'e', 'f')}.must_raise @tp_error end it "#dig and #dig! should handle array keys" do tp('a[b][c][d][e]=1&a[b][c][d][f]=2').dig(:int, 'a', 'b', 'c', 'd', %w'e f').must_equal [1, 2] tp('a[b][c][d][e]=1&a[b][c][d][f]=').dig(:int, 'a', 'b', 'c', 'd', %w'e f').must_equal [1, nil] tp('a[b][c][d][e]=1&a[b][c][d][f]=2').dig!(:int, 'a', 'b', 'c', 'd', %w'e f').must_equal [1, 2] lambda{tp('a[b][c][d][e]=1&a[b][c][d][f]=').dig!(:int, 'a', 'b', 'c', 'd', %w'e f')}.must_raise @tp_error end it "#dig and #dig! should be able to handle arrays using an array for the type" do tp('a[b][c][d][]=1&a[b][c][d][]=2').dig(:array, :int, 'a', 'b', 'c', 'd').must_equal [1, 2] tp('a[b][c][d][]=1&a[b][c][d][]=').dig(:array, :int, 'a', 'b', 'c', 'd').must_equal [1, nil] tp('a[b][c][d][]=1&a[b][c][d][]=2').dig!(:array!, :int, 'a', 'b', 'c', 'd').must_equal [1, 2] lambda{tp('a[b][c][d][]=1&a[b][c][d][]=').dig!(:array!, :int, 'a', 'b', 'c', 'd')}.must_raise @tp_error end it "#dig should raise for unsupported types" do lambda{tp.dig(:foo, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError end it "#dig should raise for array without subtype" do lambda{tp.dig(:array, 'foo', 'a')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError end it "#dig should raise for unsupported nest values" do lambda{tp.dig(:int, :foo, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError lambda{tp.dig(:array, :int, :foo, 'a')}.must_raise Roda::RodaPlugins::TypecastParams::ProgrammerError end it "#dig! should return nested values or raise Error if thers is no value" do tp = tp('a[b][c][d][e]=1&b=2') tp.dig!(:int, 'a', 'b', 'c', 'd', 'e').must_equal 1 tp.dig!(:int, 'b').must_equal 2 lambda{tp.dig!(:int, 'a', 'd', 'c', 'd', 'e')}.must_raise @tp_error lambda{tp.dig!(:int, 'a', 'b', 'c', 'd', 'f')}.must_raise @tp_error lambda{tp.dig!(:int, 'a', 0, 'c', 'd', 'f')}.must_raise @tp_error lambda{tp.dig!(:int, 'c')}.must_raise @tp_error lambda{tp.dig!(:int, 'b', 'c')}.must_raise @tp_error lambda{tp.dig!(:int, 'c', 'd')}.must_raise @tp_error error{tp.dig!(:int, 'a', 'd', 'c', 'd', 'e')}.keys.must_equal %w'a d' error{tp.dig!(:int, 'a', 'b', 'c', 'e', 'e')}.keys.must_equal %w'a b c e' tp = tp('a[][c][][e]=1') tp.dig!(:int, 'a', 0, 'c', 0, 'e').must_equal 1 lambda{tp.dig!(:int, 'a', 1, 'c', 0, 'e')}.must_raise @tp_error lambda{tp.dig!(:int, 'a', 'b', 'c', 0, 'e')}.must_raise @tp_error lambda{tp.dig!(:int, 'a', 0, 'c', 0, 'f')}.must_raise @tp_error end it "#convert! should work with dig" do tp('a[b][c][d][e]=1').convert! do |tp| tp.dig(:int, 'a', 'b', 'c', 'd', 'e') end.must_equal('a'=>{'b'=>{'c'=>{'d'=>{'e'=>1}}}}) end it "#convert! with :symbolize option should work with dig" do tp('a[b][c][d][e]=1').convert!(:symbolize=>true) do |tp| tp.dig(:int, 'a', 'b', 'c', 'd', 'e') end.must_equal(:a=>{:b=>{:c=>{:d=>{:e=>1}}}}) end it "#fetch should be the same as #[] if the key is present" do tp.fetch('c').int('d').must_equal 4 end it "#fetch should return nil if the key is not present and no block is given" do tp.fetch('d').must_be_nil end it "#fetch should call the block if the key is not present and a block is given" do tp.fetch('d'){1}.must_equal 1 end it "Error#keys should be a path to the error" do error{tp.int!('b')}.keys.must_equal ['b'] error{tp.int!(%w'b f')}.keys.must_equal ['b'] error{tp['c'].int!('f')}.keys.must_equal ['c', 'f'] error{tp('a[b][c][d][e]=1')['a']['b']['c']['d'].date('e')}.keys.must_equal %w'a b c d e' end it "Error#param_name should be the name of the parameter" do error{tp.int!('b')}.param_name.must_equal 'b' error{tp.int!(%w'b f')}.param_name.must_equal 'b' error{tp['c'].int!('f')}.param_name.must_equal 'c[f]' error{tp('a[b][c][d][e]=1')['a']['b']['c']['d'].date('e')}.param_name.must_equal 'a[b][c][d][e]' error{tp('a[][c][][e]=1')['a'][0]['c'][0].date('e')}.param_name.must_equal 'a[][c][][e]' error{tp('a[][c][][e]=1').dig(:date, 'a', 0, 'c', 0, 'e')}.param_name.must_equal 'a[][c][][e]' end it "Error#param_names and #reason should be correct for errors" do e = error{tp.int!('b')} e.param_names.must_equal ['b'] e.reason.must_equal :int e = error{tp.int!(%w'b f')} e.param_names.must_equal ['b'] e.reason.must_equal :int e = error{tp['c'].int!('f')} e.param_names.must_equal ['c[f]'] e.reason.must_equal :missing e = error{tp('a[b][c][d][e]=1')['a']['b']['c']['d'].date('e')} e.param_names.must_equal ['a[b][c][d][e]'] e.reason.must_equal :date e = error{tp('a[][c][][e]=1')['a'][0]['c'][0].date('e')} e.param_names.must_equal ['a[][c][][e]'] e.reason.must_equal :date e = error{tp('a[][c][][e]=1').dig(:date, 'a', 0, 'c', 0, 'e')} e.param_names.must_equal ['a[][c][][e]'] e.reason.must_equal :date e = error{tp('a[][c][][e]=1').dig!(:date, 'a', 1, 'c', 0, 'e')} e.param_names.must_equal ['a[]'] e.reason.must_equal :missing e = error{tp('a[][c][][e]=1').dig!(:date, 'a', 'b', 'c', 0, 'e')} e.param_names.must_equal ['a[b]'] e.reason.must_equal :invalid_type end it "Error#param_names and #all_errors should handle array submission" do tp = tp('a[][b]=0') e = error do tp.convert!('a') do |tp0| tp0.int(%w'a b c') tp0.array(:int, %w'a b c') end end e.param_names.must_equal %w'a' e.all_errors.map(&:reason).must_equal [:invalid_type] end it "Error#param_names and #all_errors should include all errors raised in convert! blocks" do tp = tp('a[][b][][e]=0') e = error do tp.convert! do |tp0| tp0['a'].convert! do |tp1| tp1[0].convert! do |tp2| tp2['b'].convert! do |tp3| tp3[0].convert! do |tp4| tp4.pos_int!('e') end end end end tp0.dig!(:pos_int, 'a', 0, 'b', 0, %w'f g') tp0.dig!(:pos_int, 'a', 0, 'b') tp0.int!('c') tp0.array!(:int, %w'd e') tp0['b'] end end e.param_names.must_equal %w'a[][b][][e] a[][b][][f] a[][b][][g] a[][b] c d e b' e.all_errors.map(&:reason).must_equal [:invalid_value, :missing, :missing, :pos_int, :missing, :missing, :missing, :missing] end it "Error#param_names and #all_errors should handle #[] failures by skipping the rest of the block" do tp = tp('a[][b][][e]=0') e = error do tp.convert! do |tp0| tp0['b'] tp0.int!('c') end end e.param_names.must_equal %w'b' e.all_errors.map(&:reason).must_equal [:missing] e = error do tp.convert! do |tp0| tp0['a'][0].convert! do |tp1| tp1['c'] tp1.int!('d') end tp0.int!('c') end end e.param_names.must_equal %w'a[][c] c' e.all_errors.map(&:reason).must_equal [:missing, :missing] end it "Error#param_names and #all_errorsshould handle array! with array of keys where one of the keys is not present" do e = error do tp('e[]=0').convert! do |tp0| tp0.array!(:pos_int, %w'd e') end end e.param_names.must_equal %w'd e' e.all_errors.map(&:reason).must_equal [:missing, :invalid_type] end it "Error#param_names and #all_errors should handle keys given to convert" do tp = tp('e[][b][][e]=0') e = error do tp.convert! do |tp0| tp0.convert!(['a', 0, 'b', 0]) do |tp1| tp1.pos_int!('e') end tp0.convert!('f') do |tp1| tp1.dig!(:pos_int, 0, 'b', 0, %w'f g') end tp0.dig!(:pos_int, 'e', 0, 'b') tp0.int!('c') tp0.array!(:int, 'd') end end e.param_names.must_equal %w'a f e[][b] c d' e.all_errors.map(&:reason).must_equal [:missing, :missing, :pos_int, :missing, :missing] end it "Error#param_names and #all_errors should include all errors raised in convert_each! blocks" do e = error do tp('a[][b]=0&a[][b]=1')['a'].convert_each! do |tp0| tp0.dig!(:pos_int, 'b', 0, 'e') tp0.dig!(:int, 'b', 0, %w'f g') tp0.int!(%w'd e') tp0.pos_int!('b') tp0['c'] end end e.param_names.must_equal %w'a[][b] a[][b] a[][d] a[][e] a[][b] a[][c] a[][b] a[][b] a[][d] a[][e] a[][c]' e.all_errors.map(&:reason).must_equal [:invalid_type, :invalid_type, :missing, :missing, :invalid_value, :missing, :invalid_type, :invalid_type, :missing, :missing, :missing] end it "Error#param_names and #all_errors should include all errors for invalid keys used in convert_each!" do tp = tp('a[0][b]=1&a[0][c]=2&a[1][b]=3&a[1][c]=4') e = error do tp['a'].convert_each!(:keys=>%w'0 2 3') do |tp0| tp0.int(%w'b c') end end e.param_names.must_equal %w'a[2] a[3]' e.all_errors.map(&:reason).must_equal [:missing, :missing] end it "Error.create should handle existing errors with a backtrace" do e = ArgumentError.new('a') e.set_backtrace(nil) e = @tp_error.create('a', 'b', e) e.must_be_instance_of @tp_error e.message.must_equal "ArgumentError: a" e.keys.must_equal 'a' e.reason.must_equal 'b' e.backtrace.must_be_nil end it "should raise error invalid params format" do proc{Roda::RodaPlugins::TypecastParams::Params.new([]).int('a')}.must_raise @tp_error end end describe "typecast_params plugin" do before do app(:typecast_params) do |r| r.post "q" do response.status = typecast_query_params.pos_int("a") '' end r.post "b" do response.status = typecast_body_params.pos_int("a") '' end end end it "typecast_query_params should access params in request query string" do status('/q', 'QUERY_STRING'=>"a=242", 'rack.input'=>StringIO.new('a=243'.encode("BINARY")), "REQUEST_METHOD"=>"POST").must_equal 242 end it "typecast_body_params should access params in request body" do status('/b', 'QUERY_STRING'=>"a=242", 'rack.input'=>StringIO.new('a=243'.encode("BINARY")), "REQUEST_METHOD"=>"POST").must_equal 243 end end describe "typecast_params plugin with customized params" do def tp(arg='a=1&b[]=2&b[]=3&c[d]=4&c[e]=5&f=&g[]=&h[i]=') @tp.call(arg) end before do res = nil app(:bare) do plugin :typecast_params do handle_type(:opp_int) do |v| -v.to_i end end plugin :typecast_params do handle_type(:double) do |v| v*2 end end route do |r| res = typecast_params nil end end @tp = lambda do |params| req('QUERY_STRING'=>params, 'rack.input'=>rack_input) res end @tp_error = Roda::RodaPlugins::TypecastParams::Error end it "should not allow typecast params changes after freezing the app" do app.freeze lambda{app::TypecastParams.handle_type(:foo){|v| v}}.must_raise RuntimeError end it "should pass through non-ArgumentError exceptions raised by conversion blocks" do app::TypecastParams.handle_type(:foo){|v| raise} lambda{tp.foo('a')}.must_raise RuntimeError end it "should respect custom typecasting methods" do tp.opp_int('a').must_equal(-1) tp.opp_int!('a').must_equal(-1) tp.opp_int('d').must_be_nil lambda{tp.opp_int!('d')}.must_raise @tp_error tp.array(:opp_int, 'b').must_equal [-2, -3] tp.array!(:opp_int, 'b').must_equal [-2, -3] tp.double('a').must_equal '11' tp.double!('a').must_equal '11' tp.double('d').must_be_nil lambda{tp.double!('d')}.must_raise @tp_error tp.array(:double, 'b').must_equal ['22', '33'] tp.array!(:double, 'b').must_equal ['22', '33'] end it "should respect custom typecasting methods when subclassing" do @app = Class.new(@app) @app.plugin :typecast_params do handle_type :triple do |v| v * 3 end end tp.opp_int('a').must_equal(-1) tp.opp_int!('a').must_equal(-1) tp.opp_int('d').must_be_nil lambda{tp.opp_int!('d')}.must_raise @tp_error tp.array(:opp_int, 'b').must_equal [-2, -3] tp.array!(:opp_int, 'b').must_equal [-2, -3] tp.double('a').must_equal '11' tp.double!('a').must_equal '11' tp.double('d').must_be_nil lambda{tp.double!('d')}.must_raise @tp_error tp.array(:double, 'b').must_equal ['22', '33'] tp.array!(:double, 'b').must_equal ['22', '33'] tp.triple('a').must_equal '111' tp.triple!('a').must_equal '111' tp.triple('d').must_be_nil lambda{tp.triple!('d')}.must_raise @tp_error tp.array(:triple, 'b').must_equal ['222', '333'] tp.array!(:triple, 'b').must_equal ['222', '333'] end end describe "typecast_params plugin with files" do def tp @tp.call end before do tempfile = @tempfile = Tempfile.new(['roda_typecast_params_spec', '.txt']) tempfile.write('tp_spec') tempfile.rewind res = nil app(:typecast_params) do |r| res = typecast_params nil end app::RodaRequest.send(:define_method, :params) do {'testfile'=>{:tempfile=>tempfile}, 'testfile2'=>{:tempfile=>tempfile}, 'testfile_array'=>[{:tempfile=>tempfile}, {:tempfile=>tempfile}], 'a'=>{'b'=>'c', 'tempfile'=>'f'}, 'c'=>['']} end @tp = lambda do req res end @tp_error = Roda::RodaPlugins::TypecastParams::Error end it "#file should require an uploaded file" do tp.file('testfile').must_equal(:tempfile=>@tempfile) tp.file(%w'testfile testfile2').must_equal [{:tempfile=>@tempfile}, {:tempfile=>@tempfile}] lambda{tp.file('a')}.must_raise @tp_error tp.file('b').must_be_nil lambda{tp.file('c')}.must_raise @tp_error tp.file!('testfile').must_equal(:tempfile=>@tempfile) tp.file!(%w'testfile testfile2').must_equal [{:tempfile=>@tempfile}, {:tempfile=>@tempfile}] lambda{tp.file!('a')}.must_raise @tp_error lambda{tp.file!('b')}.must_raise @tp_error lambda{tp.file!('c')}.must_raise @tp_error tp.array(:file, 'testfile_array').must_equal [{:tempfile=>@tempfile}, {:tempfile=>@tempfile}] lambda{tp.array(:file, 'testfile')}.must_raise @tp_error lambda{tp.array(:file, 'a')}.must_raise @tp_error tp.array(:file, 'b').must_be_nil lambda{tp.array(:file, 'c')}.must_raise @tp_error tp.array!(:file, 'testfile_array').must_equal [{:tempfile=>@tempfile}, {:tempfile=>@tempfile}] lambda{tp.array!(:file, 'testfile')}.must_raise @tp_error lambda{tp.array!(:file, 'a')}.must_raise @tp_error lambda{tp.array!(:file, 'b')}.must_raise @tp_error lambda{tp.array!(:file, 'c')}.must_raise @tp_error end end describe "typecast_params plugin with strip: :all option" do def tp(arg='a=+1+') @tp.call(arg) end before do res = nil app(:bare) do plugin :typecast_params, strip: :all route do |r| res = typecast_params nil end end @tp = lambda do |params| req('QUERY_STRING'=>params, 'rack.input'=>rack_input) res end @tp_error = Roda::RodaPlugins::TypecastParams::Error end it "#should strip String values" do tp.str('a').must_equal '1' tp.nonempty_str('a').must_equal '1' tp.int('a').must_equal 1 tp.pos_int('a').must_equal 1 tp.Integer('a').must_equal 1 tp.float('a').must_equal 1.0 tp.Float('a').must_equal 1.0 end it "#should not attempt to strip non-String values" do @tp.call("a[]=1").any('a').must_equal ["1"] end end jeremyevans-roda-4f30bb3/spec/plugin/unescape_path_spec.rb000066400000000000000000000010661516720775400240210ustar00rootroot00000000000000require_relative "../spec_helper" describe "unescape_path_path plugin" do it "decodes URL-encoded routing path" do app(:unescape_path) do |r| r.on 'b' do r.get(/(.)/) do |a| "#{a}-b" end end r.get :name do |name| name end "#{r.matched_path}|#{r.remaining_path}" end body('/foo/%61').must_equal '|/foo/a' body('/a').must_equal 'a' body('/%61').must_equal 'a' unless_lint do body('%2f%61').must_equal 'a' body('%2f%62%2f%61').must_equal 'a-b' end end end jeremyevans-roda-4f30bb3/spec/plugin/view_options_spec.rb000066400000000000000000000144251516720775400237320ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping view_options plugin test" else describe "view_options plugin view subdirs" do before do app(:bare) do plugin :render, :views=>"." plugin :view_options route do |r| r.on "default" do render("spec/views/comp_test") end append_view_subdir 'spec' r.on "home" do set_view_subdir 'spec/views' view("home", :locals=>{:name => "Agent Smith", :title => "Home"}, :layout_opts=>{:locals=>{:title=>"Home"}}) end r.on "about" do append_view_subdir 'views' r.on 'test' do append_view_subdir 'about' r.is('view'){view("comp_test")} r.is{render("comp_test")} end render("about", :locals=>{:title => "About Roda"}) end r.on "path" do render('spec/views/about', :locals=>{:title => "Path"}, :layout_opts=>{:locals=>{:title=>"Home"}}) end r.on 'test' do set_view_subdir 'spec/views' r.is('view'){view("comp_test")} r.is{render("comp_test")} end end end end it "should use set subdir if template name does not contain a slash" do body("/home").strip.must_equal "Roda: Home\n

Home

\n

Hello Agent Smith

" end it "should not use set subdir if template name contains a slash" do body("/about").strip.must_equal "

About Roda

" end it "should not change behavior when subdir is not set" do body("/path").strip.must_equal "

Path

" end it "should not affect behavior if methods not called during routing" do 3.times do body("/default").strip.must_equal "ct" end end it "should handle template compilation correctly" do @app.plugin :render, :layout=>'./spec/views/comp_layout' 3.times do body("/test").strip.must_equal "ct" body("/about/test").strip.must_equal "about-ct" body("/test/view").strip.must_equal "act\nb" body("/about/test/view").strip.must_equal "aabout-ct\nb" end if Roda::RodaPlugins::Render::COMPILED_METHOD_SUPPORT method_cache = @app.opts[:render][:template_method_cache] method_cache[['spec/views', 'comp_test']].must_be_kind_of(Array) method_cache[['spec/views/about', 'comp_test']].must_be_kind_of(Array) method_cache[:_roda_layout].must_be_kind_of(Array) end end end describe "view_options plugin" do it "should not use :views view option for layout" do app(:bare) do plugin :render, :views=>'spec/views', :allowed_paths=>['spec/views'] plugin :view_options route do set_view_options :views=>'spec/views/about' set_layout_options :template=>'layout-alternative' view('_test', :locals=>{:title=>'About Roda'}, :layout_opts=>{:locals=>{:title=>'Home'}}) end end body.strip.must_equal "Alternative Layout: Home\n

Subdir: About Roda

" end it "should skip template compilation when only :locals key is given when using view options" do app(:bare) do plugin :render, :views=>'spec/views', :allowed_paths=>['spec/views'] plugin :view_options route do set_view_options :views=>'spec/views/about' render('_test', :locals=>{:title=>'About Roda'}) end end 3.times do body.strip.must_equal "

Subdir: About Roda

" end end it "should allow overriding :layout plugin option with set_layout_options :template" do app(:bare) do plugin :render, :views=>'spec/views', :allowed_paths=>['spec/views'] plugin :view_options route do set_view_options :views=>'spec/views/about' set_layout_options :template=>'layout-alternative' view('_test', :locals=>{:title=>'About Roda'}, :layout_opts=>{:locals=>{:title=>'Home'}}) end end body.strip.must_equal "Alternative Layout: Home\n

Subdir: About Roda

" end it "should allow overriding :layout_opts :template plugin option with set_layout_options :template" do app(:bare) do plugin :render, :views=>'spec/views', :allowed_paths=>['spec/views'], :layout_opts=>{:template=>'layout'} plugin :view_options route do set_view_options :views=>'spec/views/about', :layout=>'layout-alternative' set_layout_options :template=>'layout-alternative' view('_test', :locals=>{:title=>'About Roda'}, :layout_opts=>{:locals=>{:title=>'Home'}}) end end body.strip.must_equal "Alternative Layout: Home\n

Subdir: About Roda

" end it "should allow overriding :layout plugin option with set_view_options :layout" do app(:bare) do plugin :render, :views=>'spec/views', :allowed_paths=>['spec/views'], :layout=>'layout' plugin :view_options route do set_view_options :views=>'spec/views/about', :layout=>'layout-alternative' view('_test', :locals=>{:title=>'About Roda'}, :layout_opts=>{:locals=>{:title=>'Home'}}) end end body.strip.must_equal "Alternative Layout: Home\n

Subdir: About Roda

" end it "should set view and layout options to use" do app(:bare) do plugin :render, :allowed_paths=>['spec/views'] plugin :view_options plugin :render_locals, :render=>{:title=>'About Roda'}, :layout=>{:title=>'Home'} route do set_view_options :views=>'spec/views' set_layout_options :views=>'spec/views', :template=>'layout-alternative' view('about') end end body.strip.must_equal "Alternative Layout: Home\n

About Roda

" end it "should merge multiple calls to set view and layout options" do app(:bare) do plugin :render, :allowed_paths=>['spec/views'] plugin :view_options plugin :render_locals, :render=>{:title=>'Home', :b=>'B'}, :layout=>{:title=>'About Roda', :a=>'A'} route do set_layout_options :views=>'spec/views', :template=>'multiple-layout', :engine=>'str' set_view_options :views=>'spec/views', :engine=>'str' set_layout_options :engine=>'erb' set_view_options :engine=>'erb' view('multiple') end end body.strip.must_equal "About Roda:A::Home:B" end end end jeremyevans-roda-4f30bb3/spec/plugin/view_subdir_leading_slash_spec.rb000066400000000000000000000015301516720775400263750ustar00rootroot00000000000000require_relative "../spec_helper" begin require 'tilt/erb' rescue LoadError warn "tilt not installed, skipping view_subdir_leading_slash plugin test" else describe "view_options plugin view subdirs" do before do app(:bare) do plugin :render, :views=>"spec" plugin :view_subdir_leading_slash route do |r| set_view_subdir "views" r.get("a"){render("comp_test")} r.get("b"){render("./comp_test")} render("/views/comp_test") end end end it "should use view subdir if template does not contain /" do body("/a").strip.must_equal "ct" end it "should use view subdir if template contains slash but does not start with /" do body("/b").strip.must_equal "ct" end it "should not use view subdir if template starts with /" do body.strip.must_equal "ct" end end end jeremyevans-roda-4f30bb3/spec/plugin_spec.rb000066400000000000000000000056461516720775400212120ustar00rootroot00000000000000require_relative "spec_helper" require 'tmpdir' describe "plugins" do it "should be able to override class, instance, response, and request methods, and execute configure method" do c = Module.new do self::ClassMethods = Module.new do def fix(str) opts[:prefix] + str.strip end end self::InstanceMethods = Module.new do def fix(str) super("a" + str) end end self::RequestMethods = Module.new do def hello(&block) on self.class.hello, &block end end self::RequestClassMethods = Module.new do def hello(&block) 'hello' end end self::ResponseMethods = Module.new do def foobar self.class.foobar end end self::ResponseClassMethods = Module.new do def foobar "Default " end end def self.load_dependencies(mod, prefix) mod.send(:include, Module.new do def fix(str) self.class.fix(str) end end) end def self.configure(mod, prefix) mod.opts[:prefix] = prefix end end app(:bare) do plugin(c, "Foo ").must_be_nil route do |r| r.hello do fix(response.foobar) end end end body('/hello').must_equal 'Foo aDefault' end it "should support registering plugins and loading them by symbol" do Roda::RodaPlugins.register_plugin(:foo, Module.new{module self::InstanceMethods; def a; '1' end end}) app(:foo) do a end body.must_equal '1' end it "should warn if attempting to load a plugin with arguments or a block" do Roda::RodaPlugins.register_plugin(:foo, Module.new) proc{app.plugin :foo, 1}.must_output(nil, /does not accept arguments or a block/) @app = nil proc{app.plugin(:foo){}}.must_output(nil, /does not accept arguments or a block/) end it "should raise error if attempting to load an invalid plugin" do proc{app(:banana)}.must_raise LoadError Dir.mktmpdir do |dir| begin $:.unshift(dir) Dir.mkdir(File.join(dir, 'roda')) Dir.mkdir(File.join(dir, 'roda', 'plugins')) File.write(File.join(dir, 'roda', 'plugins', 'banana.rb'), '') proc{app(:banana)}.must_raise Roda::RodaError c = Class.new(Roda) proc{c.plugin('banana')}.must_raise Roda::RodaError ensure $:.delete(dir) end end end it "should work with keyword arguments" do mod = Module.new do eval <<-END def self.load_dependencies(app, bar: 1) app.send(:define_method, :foo){bar.to_s} end def self.configure(app, bar: 1) app.send(:define_method, :bar){bar.to_s} end END end app(:bare) do plugin mod, bar: 2 route do foo + bar end end body.must_equal '22' end if RUBY_VERSION >= '2' end jeremyevans-roda-4f30bb3/spec/redirect_spec.rb000066400000000000000000000016411516720775400215040ustar00rootroot00000000000000require_relative "spec_helper" describe "redirects" do it "should be immediately processed" do app do |r| r.root do r.redirect "/hello" "Foo" end r.is "about" do r.redirect "/hello", 301 "Foo" end r.is 'foo' do r.get do r.redirect end r.post do r.redirect end end end status.must_equal 302 header(RodaResponseHeaders::LOCATION).must_equal '/hello' body.must_equal '' status("/about").must_equal 301 header(RodaResponseHeaders::LOCATION, "/about").must_equal '/hello' body("/about").must_equal '' status("/foo", 'REQUEST_METHOD'=>'POST').must_equal 302 header(RodaResponseHeaders::LOCATION, "/foo", 'REQUEST_METHOD'=>'POST').must_equal '/foo' body("/foo", 'REQUEST_METHOD'=>'POST').must_equal '' proc{req('/foo')}.must_raise(Roda::RodaError) end end jeremyevans-roda-4f30bb3/spec/request_spec.rb000066400000000000000000000035521516720775400213760ustar00rootroot00000000000000require_relative "spec_helper" describe "request.path, .remaining_path, and .matched_path" do it "should return the script name and path_info as a string" do app do |r| r.on "foo" do "#{r.path}:#{r.matched_path}:#{r.remaining_path}" end end body("/foo/bar").must_equal "/foo/bar:/foo:/bar" end end describe "request.real_remaining_path" do it "should be an alias of remaining_path" do app do |r| r.on "foo" do "#{r.remaining_path}:#{r.real_remaining_path}" end end body("/foo/bar").must_equal "/bar:/bar" end end describe "request.halt" do it "should return rack response as argument given it as argument" do app do |r| r.halt [200, {}, ['foo']] end body.must_equal "foo" end it "should use current response if no argument is given" do app do |r| response.write('foo') r.halt end body.must_equal "foo" end end describe "request.scope" do it "should return roda instance" do app(:bare) do attr_accessor :b route do |r| self.b = 'a' request.scope.b end end body.must_equal "a" end end describe "request.inspect" do it "should return information about request" do app(:bare) do def self.inspect 'Foo' end route do |r| request.inspect end end body('/a/b').must_equal "#" body('REQUEST_METHOD'=>'POST').must_equal "#" end end describe "TERM.inspect" do it "should return TERM" do app do |r| r.class::TERM.inspect end body.must_equal "TERM" end end describe "roda_class" do it "should return the related roda subclass" do app do |r| self.class.opts[:a] = 'a' r.class.roda_class.opts[:a] + r.roda_class.opts[:a] end body.must_equal "aa" end end jeremyevans-roda-4f30bb3/spec/response_spec.rb000066400000000000000000000127311516720775400215430ustar00rootroot00000000000000require_relative "spec_helper" describe "response #[] and #[]=" do it "should get/set headers" do app do |r| response['foo'] = 'bar' response['foo'] + response.headers['foo'] end header('foo').must_equal "bar" body.must_equal 'barbar' end end describe "response #headers and #body" do it "should return headers and body" do app do |r| response.headers['foo'] = 'bar' response.write response.body.is_a?(Array) end header('foo').must_equal "bar" body.must_equal 'true' end it "uses plain hash for response headers" do app do |r| response.headers['UP'] = 'U' response.headers['down'] = 'd' end req[1].must_be_instance_of Hash header('up').must_be_nil header('UP').must_equal 'U' header('down').must_equal 'd' header('DOWN').must_be_nil end if Rack.release < '3' it "uses Rack::Headers for response headers" do app do |r| response.headers['UP'] = 'U' response.headers['down'] = 'd' end req[1].must_be_instance_of Rack::Headers header('up').must_equal 'U' header('UP').must_equal 'U' header('down').must_equal 'd' header('DOWN').must_equal 'd' end if Rack.release >= '3' && !ENV['PLAIN_HASH_RESPONSE_HEADERS'] end describe "response #write" do it "should add to body" do app do |r| response.write 'a' response.write 'b' end body.must_equal 'ab' end end describe "response #finish" do it "should set status to 404 if body has not been written to" do s, h, b = nil app do |r| s, h, b = response.finish '' end body.must_equal '' s.must_equal 404 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/html' b.join.length.must_equal 0 end it "should set status to 200 if body has been written to" do s, h, b = nil app do |r| response.write 'a' s, h, b = response.finish '' end body.must_equal 'a' s.must_equal 200 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/html' b.join.length.must_equal 1 end it "should set Content-Length header" do app do |r| response.write 'a' response[RodaResponseHeaders::CONTENT_LENGTH].must_be_nil throw :halt, response.finish end header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '1' end [204, 304, 100].each do |status| it "should not set Content-Type or Content-Length header on a #{status} response" do app do |r| response.status = status throw :halt, response.finish end header(RodaResponseHeaders::CONTENT_TYPE).must_be_nil header(RodaResponseHeaders::CONTENT_LENGTH).must_be_nil end end it "should not set Content-Type header on a 205 response, but should set a Content-Length header" do app do |r| response.status = 205 throw :halt, response.finish end header(RodaResponseHeaders::CONTENT_TYPE).must_be_nil if Rack.release < '2.0.2' header(RodaResponseHeaders::CONTENT_LENGTH).must_be_nil else header(RodaResponseHeaders::CONTENT_LENGTH).must_equal '0' end end it "should not overwrite existing status" do s, h, b = nil app do |r| response.status = 500 s, h, b = response.finish '' end body.must_equal '' s.must_equal 500 h[RodaResponseHeaders::CONTENT_TYPE].must_equal 'text/html' b.join.length.must_equal 0 end end describe "response #finish_with_body" do it "should use given body" do app do |r| throw :halt, response.finish_with_body(['123']) end body.must_equal '123' end it "should set status to 200 if status has not been set" do app do |r| throw :halt, response.finish_with_body([]) end status.must_equal 200 end it "should not set Content-Length header" do app do |r| response.write 'a' response[RodaResponseHeaders::CONTENT_LENGTH].must_be_nil throw :halt, response.finish_with_body(['123']) end header(RodaResponseHeaders::CONTENT_LENGTH).must_be_nil end it "should not overwrite existing status" do app do |r| response.status = 500 throw :halt, response.finish_with_body(['123']) end status.must_equal 500 end end describe "response #redirect" do it "should set location and status" do app do |r| r.on 'a' do response.redirect '/foo', 303 end r.on do response.redirect '/bar' end end status('/a').must_equal 303 status.must_equal 302 header(RodaResponseHeaders::LOCATION, '/a').must_equal '/foo' header(RodaResponseHeaders::LOCATION).must_equal '/bar' end end describe "response #empty?" do it "should return whether the body is empty" do app do |r| r.on 'a' do response['foo'] = response.empty?.to_s end r.on do response.write 'a' response['foo'] = response.empty?.to_s end end header('foo', '/a').must_equal 'true' header('foo').must_equal 'false' end end describe "response #inspect" do it "should return information about response" do app(:bare) do def self.inspect 'Foo' end route do |r| response.status = 200 response.inspect end end body.must_equal '#' end end describe "roda_class" do it "should return the related roda subclass" do app do |r| self.class.opts[:a] = 'a' response.class.roda_class.opts[:a] + response.roda_class.opts[:a] end body.must_equal "aa" end end jeremyevans-roda-4f30bb3/spec/route_spec.rb000066400000000000000000000014701516720775400210410ustar00rootroot00000000000000require_relative "spec_helper" describe "Roda.route" do it "should set the route block" do pr = proc{'123'} app.route(&pr) app.route_block.must_equal pr body.must_equal '123' end it "should work if called in subclass and parent class later frozen" do a = app @app = Class.new(a) @app.route{|r| "OK"} body.must_equal "OK" a.freeze body.must_equal "OK" app.freeze body.must_equal "OK" end deprecated "should support #call being overridden" do app.class_eval do def call; super end end app.route{'123'} body.must_equal '123' end deprecated "should support #_call" do pr = proc{env['PATH_INFO']} app{_call(&pr)} body.must_equal '/' end deprecated "should be callable without a block" do app.route.must_be_nil end end jeremyevans-roda-4f30bb3/spec/session_middleware_spec.rb000066400000000000000000000074071516720775400235710ustar00rootroot00000000000000require_relative "spec_helper" if RUBY_VERSION >= '2' require 'roda/session_middleware' describe "RodaSessionMiddleware" do include CookieJar it "operates like a session middleware" do sess = nil env = nil app(:bare) do use RodaSessionMiddleware, :secret=>'1'*64 route do |r| r.get('s', String, String){|k, v| v.force_encoding('UTF-8'); session[k.to_sym] = v} r.get('g', String){|k| session[k.to_sym].to_s} r.get('c'){|k| session.clear; ''} r.get('sh'){|k| env = r.env; sess = session; ''} '' end end _, h, b = req('/') h[RodaResponseHeaders::SET_COOKIE].must_be_nil b.must_equal [''] _, h, b = req('/s/foo/bar') h[RodaResponseHeaders::SET_COOKIE].must_match(/\Aroda\.session=(.*); path=\/; HttpOnly(; SameSite=Lax)?\z/mi) b.must_equal ['bar'] body('/s/foo/bar').must_equal 'bar' body('/g/foo').must_equal 'bar' body('/s/foo/baz').must_equal 'baz' body('/g/foo').must_equal 'baz' body("/s/foo/\u1234".b).must_equal "\u1234" body("/g/foo").must_equal "\u1234" body("/c").must_equal "" body("/g/foo").must_equal "" body('/s/foo/bar') body("/sh").must_equal "" sess.must_be_kind_of RodaSessionMiddleware::SessionHash sess.req.must_be_kind_of Roda::RodaRequest sess.data.must_be_nil sess.options[:secret].must_equal('1'*64) sess.inspect.must_include "not yet loaded" sess.loaded?.must_equal false a = [] sess.each{|k, v| a << k << v} a.must_equal %w'foo bar' sess.data.must_equal("foo"=>"bar") sess.inspect.must_match(/\A\{"foo" ?=> ?"bar"\}\z/) sess.loaded?.must_equal true sess[:foo].must_equal "bar" sess['foo'].must_equal "bar" sess.fetch(:foo).must_equal "bar" sess.fetch('foo').must_equal "bar" proc{sess.fetch('foo2')}.must_raise KeyError sess.fetch(:foo, "baz").must_equal "bar" sess.fetch('foo', "baz").must_equal "bar" sess.fetch('foo2', "baz").must_equal "baz" sess.has_key?(:foo).must_equal true sess.has_key?("foo").must_equal true sess.has_key?("bar").must_equal false sess.key?("foo").must_equal true sess.key?("bar").must_equal false sess.include?("foo").must_equal true sess.include?("bar").must_equal false sess[:foo2] = "bar2" sess['foo2'].must_equal "bar2" sess.store('foo3', "bar3").must_equal "bar3" sess['foo3'].must_equal "bar3" env['roda.session.created_at'] = true env['roda.session.updated_at'] = true sess.clear.must_equal({}) sess.data.must_equal({}) env['roda.session.created_at'].must_be_nil env['roda.session.updated_at'].must_be_nil sess['a'] = 'b' env['roda.session.created_at'] = true env['roda.session.updated_at'] = true sess.destroy.must_equal({}) sess.data.must_equal({}) env['roda.session.created_at'].must_be_nil env['roda.session.updated_at'].must_be_nil sess[:foo] = "bar" sess.to_hash.must_equal("foo"=>"bar") sess.to_hash.wont_be_same_as(sess.data) sess.update("foo2"=>"bar2", :foo=>"bar3").must_equal("foo"=>"bar3", "foo2"=>"bar2") sess.data.must_equal("foo"=>"bar3", "foo2"=>"bar2") sess.merge!("foo2"=>"bar4").must_equal("foo"=>"bar3", "foo2"=>"bar4") sess.replace("foo2"=>"bar5", :foo3=>"bar").must_equal("foo3"=>"bar", "foo2"=>"bar5") sess.data.must_equal("foo3"=>"bar", "foo2"=>"bar5") sess.delete(:foo3).must_equal("bar") sess.data.must_equal("foo2"=>"bar5") sess.delete("foo2").must_equal("bar5") sess.data.must_equal({}) sess.exists?.must_equal true env.delete('roda.session.serialized') sess.exists?.must_equal false sess.empty?.must_equal true sess[:foo] = "bar" sess.empty?.must_equal false sess.keys.must_equal ["foo"] sess.values.must_equal ["bar"] end end end jeremyevans-roda-4f30bb3/spec/session_spec.rb000066400000000000000000000016641516720775400213730ustar00rootroot00000000000000require_relative "spec_helper" describe "session handling" do include CookieJar it "should give a warning if session variable is not available" do app do |r| begin session rescue Exception => e e.message end end body.must_match("You're missing a session handler, try using the sessions plugin.") end it "should return session if session middleware is used" do require 'roda/session_middleware' app(:bare) do if RUBY_VERSION >= '2.0' require 'roda/session_middleware' use RodaSessionMiddleware, :secret=>'1'*64 else use Rack::Session::Cookie, :secret=>'1'*64 end route do |r| r.on do (session[1] ||= 'a'.dup) << 'b' session[1] end end end _, _, b = req b.join.must_equal 'ab' _, _, b = req b.join.must_equal 'abb' _, _, b = req b.join.must_equal 'abbb' end end jeremyevans-roda-4f30bb3/spec/spec_helper.rb000066400000000000000000000130541516720775400211630ustar00rootroot00000000000000$:.unshift(File.expand_path("../lib", File.dirname(__FILE__))) if RUBY_VERSION >= '3' begin require 'warning' rescue LoadError else Warning.ignore(%r{gems/(mail|minjs)-\d}) Warning.dedup if Warning.respond_to?(:dedup) end end if rack_gem_version = ENV.delete('COVERAGE') gem 'rack', rack_gem_version require 'simplecov' SimpleCov.start do enable_coverage :branch command_name "rack #{rack_gem_version}" add_filter{|f| f.filename.match(%r{\A#{Regexp.escape(File.dirname(__FILE__))}/})} add_group('Missing'){|src| src.covered_percent < 100} add_group('Covered'){|src| src.covered_percent == 100} end end require 'rack/lint' if ENV['LINT'] require_relative "../lib/roda" require "stringio" ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins gem 'minitest' require "minitest/global_expectations/autorun" require 'minitest/hooks/default' if ENV['CHECK_METHOD_VISIBILITY'] require 'visibility_checker' VISIBILITY_CHANGES = [] Minitest.after_run do if VISIBILITY_CHANGES.empty? puts "No visibility changes" else puts "Visibility changes:" VISIBILITY_CHANGES.uniq!{|v,| v} puts(*VISIBILITY_CHANGES.map do |v, caller| "#{caller}: #{v.new_visibility} method #{v.overridden_by}##{v.method} overrides #{v.original_visibility} method in #{v.defined_in}" end.sort) end end end if ENV['PLAIN_HASH_RESPONSE_HEADERS'] Roda.plugin :plain_hash_response_headers end RodaResponseHeaders = Roda::RodaResponseHeaders $RODA_WARN = true def (Roda::RodaPlugins).warn(s) return unless $RODA_WARN $stderr.puts s puts caller.grep(/_spec\.rb:\d+:/) end if ENV['RODA_RACK_SESSION_COOKIE'] != '1' require_relative '../lib/roda/session_middleware' DEFAULT_SESSION_MIDDLEWARE_ARGS = [RodaSessionMiddleware, :secret=>'1'*64] DEFAULT_SESSION_ARGS = [:plugin, :sessions, :secret=>'1'*64] else DEFAULT_SESSION_MIDDLEWARE_ARGS = [Rack::Session::Cookie, :secret=>'1'] DEFAULT_SESSION_ARGS = [:use, Rack::Session::Cookie, :secret=>'1'] end if defined?(Rack::Headers) class Rack::Headers def must_equal(hash) case hash when Hash must_be(:==, Rack::Headers[hash]) else super end end end class Array def must_equal(array) case array when Array if array.length == 3 && array[1].is_a?(Hash) must_be(:==, [array[0], Rack::Headers[array[1]], array[2]]) else super end else super end end end end require 'uri' if ENV['LINT'] module CookieJar def req(path='/', env={}) if path.is_a?(Hash) env = path else env['PATH_INFO'] = path.dup end env['HTTP_COOKIE'] = @cookie if @cookie a = super(env) if (set = a[1][RodaResponseHeaders::SET_COOKIE]).is_a?(String) # This currently doesn't handle setting multiple cookies in the same response. # Support for that isn't yet needed in the specs. @cookie = set.sub(/(; path=\/)?(; secure)?; HttpOnly/, '') end a end end class Minitest::Spec def self.deprecated(a, &block) it("#{a} (deprecated)") do begin $RODA_WARN = false instance_exec(&block) ensure $RODA_WARN = true end end end def rack_input(str='') StringIO.new(str.dup.force_encoding('BINARY')) end def app(type=nil, &block) case type when :new @app = _app{route(&block) if block} when :bare @app = _app(&block) when Symbol @app = _app do plugin type route(&block) end else if block @app = _app{route(&block)} else @app ||= _app{} end end if ENV['CHECK_METHOD_VISIBILITY'] caller = caller_locations(1, 1)[0] [@app, @app::RodaRequest, @app::RodaResponse].each do |c| VISIBILITY_CHANGES.concat(VisibilityChecker.visibility_changes(c).map{|v| [v, "#{caller.path}:#{caller.lineno}"]}) end end @app end def req(path='/', env={}) if path.is_a?(Hash) env = path else env['PATH_INFO'] = path.dup end _req(@app, env) end def req_env(env) env = {"REQUEST_METHOD" => "GET", "PATH_INFO" => "/", "SCRIPT_NAME" => ""}.merge!(env) if ENV['LINT'] env['SERVER_NAME'] ||= 'example.com' env['SERVER_PROTOCOL'] ||= env['HTTP_VERSION'] || 'HTTP/1.0' env['HTTP_VERSION'] ||= env['SERVER_PROTOCOL'] env['QUERY_STRING'] ||= '' env['rack.input'] ||= rack_input env['rack.errors'] ||= StringIO.new env['rack.url_scheme'] ||= 'http' env['rack.version'] = [1, 5] if Rack.release < '2.3' env['SERVER_PORT'] ||= '80' env['rack.multiprocess'] = env['rack.multithread'] = env['rack.run_once'] = false end end env end def _req(app, env) a = @app.call(req_env(env)) if ENV['LINT'] orig = a[2] a[2] = a[2].to_enum(:each).to_a orig.close if orig.respond_to?(:close) a[2].define_singleton_method(:to_path){orig.to_path} if orig.respond_to?(:to_path) end a end def unless_lint yield unless ENV['LINT'] end def status(path='/', env={}) req(path, env)[0] end def header(name, path='/', env={}) req(path, env)[1][name] end def body(path='/', env={}) s = String.new b = req(path, env)[2] b.each{|x| s << x} b.close if b.respond_to?(:close) s end def _app(&block) c = Class.new(Roda) c.use Rack::Lint if ENV['LINT'] c.class_eval(&block) c end def with_rack_env(env) ENV['RACK_ENV'] = env yield ensure ENV.delete('RACK_ENV') end end jeremyevans-roda-4f30bb3/spec/version_spec.rb000066400000000000000000000007361516720775400213740ustar00rootroot00000000000000require_relative "spec_helper" describe "Roda version constants" do it "RodaVersion should be a string in x.y.z integer format" do Roda::RodaVersion.must_match(/\A\d+\.\d+\.\d+\z/) end it "Roda*Version and RodaVersionNumber should be integers" do Roda::RodaMajorVersion.must_be_kind_of(Integer) Roda::RodaMinorVersion.must_be_kind_of(Integer) Roda::RodaPatchVersion.must_be_kind_of(Integer) Roda::RodaVersionNumber.must_be_kind_of(Integer) end end jeremyevans-roda-4f30bb3/spec/views/000077500000000000000000000000001516720775400174775ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/_each.foo.str000066400000000000000000000001261516720775400220510ustar00rootroot00000000000000y-#{each if local_variables.include?(:each)}-#{foo if local_variables.include?(:foo)} jeremyevans-roda-4f30bb3/spec/views/_each.str000066400000000000000000000001261516720775400212670ustar00rootroot00000000000000x-#{each if local_variables.include?(:each)}-#{foo if local_variables.include?(:foo)} jeremyevans-roda-4f30bb3/spec/views/_test.erb000066400000000000000000000000261516720775400213050ustar00rootroot00000000000000

<%= title %>

jeremyevans-roda-4f30bb3/spec/views/a.erb000066400000000000000000000000021516720775400204010ustar00rootroot00000000000000a jeremyevans-roda-4f30bb3/spec/views/a.rdoc000066400000000000000000000000121516720775400205610ustar00rootroot00000000000000# a # * b jeremyevans-roda-4f30bb3/spec/views/a1.str000066400000000000000000000000121516720775400205230ustar00rootroot00000000000000a#{1}-str jeremyevans-roda-4f30bb3/spec/views/a2.html000066400000000000000000000000101516720775400206560ustar00rootroot00000000000000a2-html jeremyevans-roda-4f30bb3/spec/views/a3.erb000066400000000000000000000000161516720775400204710ustar00rootroot00000000000000a<%= 3 %>-erb jeremyevans-roda-4f30bb3/spec/views/about.erb000066400000000000000000000000261516720775400213010ustar00rootroot00000000000000

<%= title %>

jeremyevans-roda-4f30bb3/spec/views/about.str000066400000000000000000000000221516720775400213350ustar00rootroot00000000000000

#{title}

jeremyevans-roda-4f30bb3/spec/views/about/000077500000000000000000000000001516720775400206115ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/about/_test.css.gz000066400000000000000000000000611516720775400230550ustar00rootroot000000000000000 .MJ,RQU(,IUPJpܪjeremyevans-roda-4f30bb3/spec/views/about/_test.erb000066400000000000000000000000361516720775400224200ustar00rootroot00000000000000

Subdir: <%= title %>

jeremyevans-roda-4f30bb3/spec/views/about/_test.erb.gz000066400000000000000000000000611516720775400230350ustar00rootroot000000000000000 .MJ,RQU(,IUPJpܪjeremyevans-roda-4f30bb3/spec/views/about/_test2.css000066400000000000000000000000411516720775400225160ustar00rootroot00000000000000body { background-color: cyan; } jeremyevans-roda-4f30bb3/spec/views/about/_test2.css.br000066400000000000000000000000351516720775400231230ustar00rootroot00000000000000 P'Jҵ3<1Nn)8ojeremyevans-roda-4f30bb3/spec/views/about/_test2.css.gz000066400000000000000000000001001516720775400231310ustar00rootroot00000000000000^_test2.cssKOTVHJLN//KM/RHL̳V!jeremyevans-roda-4f30bb3/spec/views/about/_test2.css.zst000066400000000000000000000000561516720775400233430ustar00rootroot00000000000000(/$! body { background-color: cyan; } jeremyevans-roda-4f30bb3/spec/views/about/comp_test.erb000066400000000000000000000000111516720775400232700ustar00rootroot00000000000000about-ct jeremyevans-roda-4f30bb3/spec/views/about/nested/000077500000000000000000000000001516720775400220735ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/about/nested/comp_test.erb000066400000000000000000000000201516720775400245520ustar00rootroot00000000000000about-nested-ct jeremyevans-roda-4f30bb3/spec/views/about/only.erb000066400000000000000000000000001516720775400222520ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/about/only_about.erb000066400000000000000000000000001516720775400234440ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/additional/000077500000000000000000000000001516720775400216075ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/additional/only.erb000066400000000000000000000000001516720775400232500ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/additional/only_add.erb000066400000000000000000000000001516720775400240600ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/b.erb000066400000000000000000000000021516720775400204020ustar00rootroot00000000000000b jeremyevans-roda-4f30bb3/spec/views/c.erb000066400000000000000000000000021516720775400204030ustar00rootroot00000000000000c jeremyevans-roda-4f30bb3/spec/views/comp_layout.erb000066400000000000000000000000171516720775400225220ustar00rootroot00000000000000a<%= yield %>b jeremyevans-roda-4f30bb3/spec/views/comp_test.erb000066400000000000000000000000031516720775400221570ustar00rootroot00000000000000ct jeremyevans-roda-4f30bb3/spec/views/content-yield.erb000066400000000000000000000000341516720775400227440ustar00rootroot00000000000000This is the actual content. jeremyevans-roda-4f30bb3/spec/views/each.foo.str000066400000000000000000000001261516720775400217120ustar00rootroot00000000000000r-#{each if local_variables.include?(:each)}-#{foo if local_variables.include?(:foo)} jeremyevans-roda-4f30bb3/spec/views/each.str000066400000000000000000000001261516720775400211300ustar00rootroot00000000000000r-#{each if local_variables.include?(:each)}-#{foo if local_variables.include?(:foo)} jeremyevans-roda-4f30bb3/spec/views/fixed/000077500000000000000000000000001516720775400205765ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/spec/views/fixed/comp_each_test.erb000066400000000000000000000000431516720775400242420ustar00rootroot00000000000000<%# locals: (comp_each_test:) %>ct jeremyevans-roda-4f30bb3/spec/views/fixed/comp_test.erb000066400000000000000000000000241516720775400232610ustar00rootroot00000000000000<%# locals: () %>ct jeremyevans-roda-4f30bb3/spec/views/fixed/layout.erb000066400000000000000000000001071516720775400226030ustar00rootroot00000000000000<%# locals: (title:) %> Roda: <%= title %> <%= yield %> jeremyevans-roda-4f30bb3/spec/views/fixed/local_test.erb000066400000000000000000000000651516720775400234220ustar00rootroot00000000000000<%# locals: (title:, local_test: nil) %><%= title %> jeremyevans-roda-4f30bb3/spec/views/fixed/opt_local_test.erb000066400000000000000000000000761516720775400243060ustar00rootroot00000000000000<%# locals: (title: 'ct', opt_local_test: nil) %><%= title %> jeremyevans-roda-4f30bb3/spec/views/home.erb000066400000000000000000000000471516720775400211220ustar00rootroot00000000000000

Home

Hello <%= name %>

jeremyevans-roda-4f30bb3/spec/views/home.str000066400000000000000000000000431516720775400211560ustar00rootroot00000000000000

Home

Hello #{name}

jeremyevans-roda-4f30bb3/spec/views/iv.erb000066400000000000000000000000121516720775400206000ustar00rootroot00000000000000<%= @a %> jeremyevans-roda-4f30bb3/spec/views/layout-alternative.erb000066400000000000000000000000751516720775400240240ustar00rootroot00000000000000Alternative Layout: <%= title %> <%= yield %> jeremyevans-roda-4f30bb3/spec/views/layout-yield.erb000066400000000000000000000000331516720775400226060ustar00rootroot00000000000000Header <%= yield %> Footer jeremyevans-roda-4f30bb3/spec/views/layout-yield2.erb000066400000000000000000000000351516720775400226720ustar00rootroot00000000000000Header2 <%= yield %> Footer2 jeremyevans-roda-4f30bb3/spec/views/layout.erb000066400000000000000000000000571516720775400215100ustar00rootroot00000000000000Roda: <%= title %> <%= yield %> jeremyevans-roda-4f30bb3/spec/views/layout.str000066400000000000000000000000531516720775400215440ustar00rootroot00000000000000Roda: #{ title } #{ yield } jeremyevans-roda-4f30bb3/spec/views/multiple-layout.erb000066400000000000000000000000441516720775400233350ustar00rootroot00000000000000<%= title %>:<%= a %>::<%= yield %> jeremyevans-roda-4f30bb3/spec/views/multiple.erb000066400000000000000000000000261516720775400220220ustar00rootroot00000000000000<%= title %>:<%= b %> jeremyevans-roda-4f30bb3/www/000077500000000000000000000000001516720775400162345ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/config.ru000066400000000000000000000003471516720775400200550ustar00rootroot00000000000000use Rack::Static, :urls=>%w'/index.html /why.html /documentation.html /development.html /compare-to-sinatra.html /css /rdoc /images /js', :root=>'public' run proc{[302, {'Content-Type'=>'text/html', 'Location'=>'index.html'}, []]} jeremyevans-roda-4f30bb3/www/layout.erb000066400000000000000000000022411516720775400202420ustar00rootroot00000000000000 <%= "#{current_page.capitalize} | " unless current_page == 'index' %>Roda: Routing Tree Web Toolkit <% unless current_page == 'index' %> <% end %>
<%= content %>
jeremyevans-roda-4f30bb3/www/make_www.rb000077500000000000000000000016041516720775400204060ustar00rootroot00000000000000#!/usr/bin/env ruby require 'erb' require_relative '../lib/roda/version' descriptions = { index: "Roda is a lightweight and productive framework for building web applications in Ruby.", documentation: "Documentation and Tutorials for Roda, the Routing Tree Web Toolkit for Ruby", development: "Contributing to Roda, the Routing Tree Web Toolkit for Ruby", compare_to_sinatra: "A brief breakdown of how Roda stacks up against Sinatra for web development.", } Dir.chdir(File.dirname(__FILE__)) erb = ERB.new(File.read('layout.erb')) Dir['pages/*.erb'].each do |page| public_loc = "#{page.gsub(/\Apages\//, 'public/').sub('.erb', '.html')}" content = content = ERB.new(File.read(page)).result(binding) current_page = File.basename(page.sub('.erb', '')) description = description = descriptions[current_page.to_sym] File.open(public_loc, 'wb'){|f| f.write(erb.result(binding))} end jeremyevans-roda-4f30bb3/www/pages/000077500000000000000000000000001516720775400173335ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/pages/compare_to_sinatra.erb000066400000000000000000000132361516720775400237030ustar00rootroot00000000000000

Comparison to Sinatra

Roda aims to take the ease of development and understanding that Sinatra brings, and enable it to scale to support the development of large web applications. Both Roda and Sinatra share the following basic concepts:

  • Routes are directly expressed, using a block based DSL.
  • The return value of the block is used as the response.
  • There is no separation between the view context and the route handling context.

Some differences are listed below.

Routing Tree

The primary difference between Roda and Sinatra is that Roda uses a routing tree, while Sinatra uses a list of routes. At any point in the routing tree, Roda allows you to operate on the current request. If your URLs reflect your application architecture, this allows you to have DRYer code. Let's examine code examples for a very simple app, implemented in both Roda and Sinatra:

Roda:

require 'roda'

class App < Roda
  plugin :render
  plugin :all_verbs

  route do |r|
    r.root do
      view :index
    end

    r.is 'artist', Integer do |artist_id|
      @artist = Artist[artist_id]
      check_access(@artist)

      r.get do
        view :artist
      end

      r.post do
        @artist.update(r.params['artist'])
        r.redirect
      end

      r.delete do
        @artist.destroy
        r.redirect '/'
      end
    end
  end
end

Sinatra:

require 'sinatra/base'

class App < Sinatra::Base
  get '/' do
    erb :index
  end

  get '/artist/:id' do
    @artist = Artist[params[:id].to_i]
    check_access(@artist)
    erb :artist
  end

  post '/artist/:id' do
    @artist = Artist[params[:id].to_i]
    check_access(@artist)
    @artist.update(params[:artist])
    redirect(request.path_info)
  end

  delete '/artist/:id' do
    @artist = Artist[params[:id].to_i]
    check_access(@artist)
    @artist.destroy
    redirect '/' 
  end
end

While the Roda code is slightly longer, it should be apparent that it is actually simpler. Instead of setting the @artist variable and checking that access is allowed in all three of the sinatra routes, the variables are set as soon as that branch of the tree is taken, and can be used in all routes under that branch. This is why Roda is called a routing tree web toolkit. This is a simplified example, but if you see this type of duplication in a lot of the Sinatra code you write, your app could probably be made simpler by converting to Roda.

Faster

In addition to being more maintainable, Roda's approach is also faster in general. Sinatra routes requests by testing each of the stored routes against the current request. With Roda, once one branch of the tree matches, only routes inside that branch are considered, not any routes after that branch. Roda also has support for a single route that dispatches to multiple branches via the multi_route and multi_run plugins. For large applications, routing in Sinatra is roughly O(N), where N is the number of routes, while routing in Roda can be close to O(log N), depending on how you structure your routing tree. For small applications, because Roda has lower per-request overhead, Roda is about 4 times faster than Sinatra.

Plugin System

Another difference between Roda and Sinatra is that Roda has a very small core, with a plugin system modeled on Sequel's. All parts of Roda (class/instance/request/response) can be overridden by plugins and call super to get the default behavior. This includes when plugins are applied to the Roda class itself (affecting all subclasses). The reason Roda is referred to as a toolkit is that it offers a lot of different tools (in the form of plugins), and you can choose which tools you use to build your application. Some applications (e.g. form-based websites) are best built using some tools, and other applications (e.g. APIs) are best built using different tools. Roda provides a large variety of tools that work together to make it easy to build most web applications.

Many features that are built into Sinatra are shipped as plugins in Roda. This way they can easily be used if needed, but if you don't use them you don't pay the cost for loading them. Near the top of the Roda example, you can see plugin :render, which adds support for template rendering, and plugin :all_verbs, which adds routing methods for all HTTP request methods.

Less Pollution

Roda is very concerned about pollution. In this case, pollution of the scope in which the route block operates. Roda purposely limits the instance variables, methods, and constants available in the route block scope, so that it is unlikely you will run into conflicts. If you've ever tried to use an instance variable named @request inside a Sinatra::Base subclass, you'll appreciate that Roda attempts to avoid polluting the scope.

Immutable

In production use, Roda applications are designed to be frozen (using the standard freeze method), which freezes all internal datastructures (except thead-safe caches used at runtime). Using this reduces the possibility of thread-safety issues when using Roda, by making attempts to modify global settings during runtime raise an error.

jeremyevans-roda-4f30bb3/www/pages/development.erb000066400000000000000000000025241516720775400223520ustar00rootroot00000000000000

Development

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.

Reporting Bugs

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.

Source Code

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

Submitting Patches

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.

License

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

jeremyevans-roda-4f30bb3/www/pages/documentation.erb000066400000000000000000001076011516720775400227030ustar00rootroot00000000000000

Documentation for Roda (v<%= Roda::RodaVersion %>)

README (Introduction to Roda, start here if new)

Online Book

Federico Iachetti has generously put his Mastering Roda book under the Commons Attribution 4.0 International License, and posted a publicly accessible online version. The book has been updated and now is kept up to date with changes to Roda. If you would like to improve the book, please submit a merge request.

Guides

RDoc (frames)

Here are direct links to the most important pages:

Plugins

Plugins are a very important part of Roda, since by design Roda has a very small core.

Plugins that Ship with Roda

  • Routing:
    • autoload_hash_branches: Allows autoloading file for a hash branch when there is a request for that branch.
    • autoload_named_routes: Allows autoloading file for a named route when there is a request for that route.
    • all_verbs: Adds request routing methods for all http verbs.
    • backtracking_array: Allows array matchers to backtrack if later matchers do not match.
    • break: Supports break inside routing blocks for skipping the current matching route block as if it didn't match.
    • class_level_routing: Adds class level routing methods, for a DSL similar to sinatra.
    • error_handler: Adds ability to automatically handle errors raised by the application.
    • hash_branches: Supports O(1) dispatching to multiple branches at all levels in the routing tree.
    • hash_paths: Supports O(1) dispatching to multiple paths at all levels in the routing tree.
    • hash_routes: Provides a DSL to configure the hash_branches and hash_paths plugins.
    • head: Treat HEAD requests like GET requests with an empty response body.
    • hmac_paths: Prevent path enumeration and support access control using HMACs in paths.
    • hooks: Adds before/after hook methods.
    • host_routing: Adds support for routing based on the host header.
    • match_hook: Adds a hook method which is called when a path segment is matched.
    • match_hook_args: Similar to match_hook plugin, but supports passing matchers and block args to hooks.
    • multi_route: Allows dispatching to multiple named route blocks in a single call.
    • multi_run: Adds the ability to dispatch to multiple rack applications based on the request path prefix.
    • named_routes: Allows for multiple named route blocks that can be dispatched to inside the main route block.
    • not_allowed: Adds support for automatically returning 405 Method Not Allowed responses.
    • not_found: Adds not_found method for handling responses not otherwise handled by a route.
    • pass: Adds pass method for skipping the current matching route block as if it didn't match.
    • path_rewriter: Adds support for rewriting paths before routing.
    • route_block_args: Controls which arguments are passed to the route block.
    • run_append_slash: Makes r.run use "/" instead of "" for app's PATH_INFO
    • run_handler: Allows for modifying rack response arrays when using r.run, and continuing routing for 404 responses.
    • run_require_slash: Skip dispatching to another application if PATH_INFO for dispatch would violate Rack SPEC
    • static_routing: Adds class level static routing methods, for maximum performance when handling static routes (routes without placeholders).
    • status_handler: Adds status_handler method for handling responses without bodies for a given status code.
    • type_routing: Route based on path extensions and Accept headers.
    • unescape_path: Decodes URL-encoded PATH_INFO before routing.
  • Rendering/View:
    • additional_render_engines: Allows for considering multiple render engines, using path for first template that exists.
    • additional_view_directories: Allows for checking for templates in multiple view directories, using path for first template that exists.
    • assets: Adds support for rendering CSS/JS javascript assets on the fly in development, or compiling them into a single compressed file in production.
    • assets_preloading: Adds support for generating browser-hinting preload link tags and headers.
    • branch_locals: Adds ability to specify defaults for template locals on a per-branch basis.
    • capture_erb: Allows capturing the output of ERB template blocks, instead of injecting them into the template output.
    • chunked: Adds support for streaming template responses using Transfer-Encoding: chunked.
    • content_for: Allows storage of content in one template and retrieval of that content in a different template.
    • custom_block_results: Adds support for arbitrary objects as block results.
    • each_part: Adds each_part method for simplifying render_each with :locals.
    • erb_h: Adds faster (if slightly less safe method) h method for html escaping, based on erb/escape.
    • exception_page: Shows page with debugging information for exceptions, designed for use in error handler in development mode.
    • h: Adds h method for html escaping.
    • hash_branch_view_subdir: Automatically appends a view subdirectory for each successful hash branch taken.
    • hash_public: Adds support for serving files in the public directory, with paths that change based on file content.
    • inject_erb: Allows injecting arbitrary content directly into ERB template output.
    • json: Allows match blocks to return arrays and hashes, using a json representation as the response body.
    • link_to: Simplifies creation of HTML links.
    • multi_public: Adds support for serving all files in multiple public directories.
    • multi_view: Allows for easily setting up routing for rendering multiple views.
    • named_templates: Adds the ability to create inline templates by name, instead of storing them in the file system.
    • padrino_render: Makes render method that work similarly to Padrino's rendering, using a layout by default.
    • part: Adds part method for simpler rendering of templates with locals.
    • partials: Adds partial method for rendering partials (templates prefixed with an underscore).
    • precompile_templates: Adds support for precompiling templates, saving memory when using a forking webserver.
    • public: Adds support for serving all files in the public directory.
    • recheck_precompiled_assets: Allows checking for the precompiled assets metadata file for updates.
    • render: Adds render method for rendering templates, using tilt.
    • render_each: Render a template for each value in an enumerable.
    • render_coverage: Sets compiled_path for Tilt templates, allowing coverage of compiled templates before Ruby 3.2 (requires tilt 2.1+).
    • render_locals: Adds ability to specify defaults for template locals.
    • static: Adds support for serving static files using Rack::Static.
    • streaming: Adds ability to stream responses.
    • symbol_views: Allows match blocks to return template name symbols, uses the template view as the response body.
    • timestamp_public: Adds support for serving files in the public directory, with paths that change based on file modification time.
    • view_options: Allows for setting view options on a per-request basis.
    • view_subdir_leading_slash: Use view subdirectory for all template names that do not start with a /.
  • Request/Response:
    • assume_ssl: Makes request ssl? method always return true, for use with SSL-terminating reverse proxies that do not set appropriate headers.
    • bearer_token: Adds r.bearer_token method for retrieving bearer token from HTTP Authorization header.
    • caching: Adds request and response methods related to http caching.
    • content_security_policy: Allows setting an appropriate Content-Security-Policy header for the application/branch/action.
    • cookie_flags: Adds checks for certain cookie flags, to update, warn, or error if they are not set correctly.
    • cookies: Adds response methods for handling cookies.
    • default_headers: Allows modifying the default headers for responses.
    • default_status: Allows overriding the default status for responses.
    • delegate: Adds class methods for creating instance methods that delegate to the request, response, or class.
    • delete_empty_headers: Automatically delete response headers with empty values.
    • disallow_file_uploads: Disallow multipart file uploads.
    • drop_body: Automatically drops response body and Content-Type/Content-Length headers for response statuses indicating no body.
    • halt: Augments request halt method for support for setting response status and/or response body.
    • hsts: Sets Strict-Transport-Security response header.
    • invalid_request_body: Allows for custom handling of invalid request bodies.
    • module_include: Adds request_module and response_module class methods for adding modules/methods to request/response classes.
    • permissions_policy: Allows setting an appropriate Permissions-Policy header for the application/branch/action.
    • plain_hash_response_headers: Uses plain hashes for response headers on Rack 3, for much better performance.
    • r: Adds r method for accessing the request, useful when r local variable is not in scope.
    • redirect_http_to_https: Adds request method to redirect HTTP requests to the same location using HTTPS.
    • redirect_path: Allows r.redirect to automatically work with objects registered with the path plugin.
    • request_aref: Adds configurable handling for [] and []= request methods.
    • request_headers: Adds a headers method to the request object, for easier access to request headers.
    • response_attachment: More easily set content-disposition and content-type headers for attachments.
    • response_content_type: More easily set content-type header for responses.
    • response_request: Gives response object access to request object.
    • send_file: Adds send_file method for returning file content as a response.
    • sinatra_helpers: Port of Sinatra::Helpers methods not covered by other plugins.
    • status_303: Uses 303 as the default redirect status for non-GET requests by HTTP 1.1 clients.
    • symbol_status: Allows the use of symbols as status codes, converting them to the appropriate integer.
    • typecast_params: Allows for easily converting parameter values to explicit types.
    • typecast_params_sized_integers: Allows for easily converting parameter values to integers for specific integer sizes (8-bit, 16-bit, 32-bit, and 64-bit).
  • Matchers:
    • class_matchers: Adds support for handling matchers for arbitrary classes, with support for type conversion.
    • custom_matchers: Adds support for arbitrary objects as matchers.
    • empty_root: Makes root matcher match empty string in addition to single slash.
    • hash_matcher: Adds hash_matcher class method for easily defining hash matchers.
    • header_matchers: Adds matchers using information from the request headers.
    • Integer_matcher_max: Sets a maximum integer value that will be matched by the default Integer matcher.
    • map_matcher: Adds support for :map hash matcher for matching next route segment by hash key, yielding hash value.
    • match_affix: Adds support for overriding default prefix/suffix used in match patterns.
    • multibyte_string_matcher: Makes string matcher handle multibyte characters.
    • param_matchers: Adds matchers using information from the request params.
    • params_capturing: Stores matcher captures in the request params.
    • path_matchers: Adds matchers using information from the request path.
    • placeholder_string_matchers: Supports placeholders in strings for backwards compatibility.
    • optimized_segment_matchers: Adds performance optimized matchers for single String class argument.
    • optimized_string_matchers: Adds performance optimized matchers for single string arguments.
    • slash_path_empty: Considers a path of "/" as an empty path when doing a terminal match.
    • symbol_matchers: Adds support for symbol-specific matching regexps.
  • Mail:
    • error_email: Adds ability to easily email a notification when an error is raised by the application, using net/smtp.
    • error_mail: Adds ability to easily email a notification when an error is raised by the application, using mail.
    • mail_processor: Adds support for processing emails using the routing tree.
    • mailer: Adds support for sending emails using the routing tree.
  • Middleware:
    • direct_call: Makes Roda.call skip the middleware stack, allowing more optimization when dispatching routes.
    • middleware: Allows the Roda app to be used as middleware by another app.
    • middleware_stack: Allows removing middleware and inserting middleware before the end of the stack.
  • CSRF Protection:
    • csrf: Older CSRF plugin for backwards compatibility using rack_csrf.
    • route_csrf: Recommended CSRF plugin with request-specific tokens and control over where CSRF tokens are checked during routing.
    • sec_fetch_site_csrf: Simpler CSRF plugin using the Sec-Fetch-Site header used in modern browsers.
  • Other:
    • common_logger: Adds support for logging in common log format.
    • conditional_sessions: Allows for using the session plugin for a subset of requests.
    • environments: Adds support for handling different execution environments (development/test/production).
    • early_hints: Adds support for using 103 Early Hints responses when using a compatible server.
    • filter_common_logger: Adds support for skipping the logging of certain requests when using the common_logger plugin.
    • flash: Adds flash handling.
    • heartbeat: Adds support for heartbeats.
    • host_authorization: Allows configuring an authorized host or an array of authorized hosts.
    • indifferent_params: Adds params method for indifferent parameters.
    • ip_from_header: Gets request IP address from specified header if present.
    • json_parser: Parses request bodies in JSON format.
    • path: Adds support for named paths.
    • relative_path: Adds support for turning absolute paths into paths relative to current request.
    • sessions: Implements support for encrypted sessions.
    • shared_vars: Stores and retrives variables shared between multiple Roda apps.
    • strip_path_prefix: Strips prefixes off internal absolute paths, making them relative paths.

External Resources

Guides

Plugins

These projects ship external plugins for Roda:

  • autoforme: Adds autoforme method for automatic creation of administrative front-end for Sequel models.
  • forme: Adds form method for simple creation of html forms inside erb templates.
  • rodauth: Authentication and account management framework.
  • roda-action: Resolves actions stored in roda-container.
  • roda-auth: Adds authentication support for Roda.
  • roda-basic-auth: Adds support for HTTP basic authentication.
  • roda-component: Adds realtime components using faye and opal.
  • roda-container: Turns application into an inversion of control (IoC) container.
  • roda-enhanced_logger: A powerful logger.
  • roda-flow: Changes routing methods to delegate to containers.
  • roda-i18n: Adds easy internationalization and localization support.
  • roda-mailer_ext: Teach the Roda mailer plugin a few neat tricks.
  • roda-mailer_preview: Preview your emails generated by the Roda mailer plugin.
  • roda-message_bus: MessageBus Integration for Roda.
  • roda-parse-request: Automatically parse JSON and URL-encoded requests.
  • roda-rails: Integration for using Roda as Rack middleware in a Rails app.
  • roda-route_list: Parses route metadata from comments in an app file, allowing introspection of routes.
  • roda-rest_api: Adds support for easily creating RESTful APIs.
  • roda-sprockets: Use Sprockets to build and serve JS and CSS.
  • roda-symbolized_params: Adds params method for symbolized params.
  • roda-unpoly: Easily integrate Unpoly into your Roda application.
  • roda-will_paginate: will_paginate integration for Roda.
  • rom-roda: Adds integration with Ruby Object Mapper.

Libraries

These external projects are related to Roda:

Change Log

Release Notes

    <% %w'3 2 1'.each do |i| %> <% lines = [] Dir["../doc/release_notes/#{i}.*.txt"].map{|f| File.basename(f)}.each do |f| (lines[f.split('.')[1].to_i/10] ||= []) << f end lines.reverse.each do |fs| %>
  • <%= fs.sort_by{|f| f.split('.').map{|x| x.to_i}}.reverse.map do |f| "#{f.sub(/\.txt$/, '').sub(/(..)\.0$/, '\\1')}" end.join(' | ') %>
  • <% end %> <% end %>

License

Resources

Interviews

Books

Presentations

Applications Using Roda

Here are some open source applications that use Roda:

jeremyevans-roda-4f30bb3/www/pages/index.erb000066400000000000000000000107411516720775400211370ustar00rootroot00000000000000
        
# cat config.ru
require "roda"

class App < Roda
  route do |r|
    # GET / request
    r.root do
      r.redirect "/hello"
    end

    # /hello branch
    r.on "hello" do
      # Set variable for all routes in /hello branch
      @greeting = 'Hello'

      # GET /hello/world request
      r.get "world" do
        "#{@greeting} world!"
      end

      # /hello request
      r.is do
        # GET /hello request
        r.get do
          "#{@greeting}!"
        end

        # POST /hello request
        r.post do
          puts "Someone said #{@greeting}!"
          r.redirect
        end
      end
    end
  end
end

run App.freeze.app
        
      

A Modular, Scalable Ruby Framework

    <% (<
  • <%= name %>

    <%= info %>

  • <% end %>

Stable for 10+ years, and constantly improving!

RubyConf 2024 - 10 Years of Roda

Watch Roda's lead developer discuss the history of Roda, and the improvements made in Roda's first 10 years.

Comparison to Popular Ruby Web Frameworks

Compared to other popular Ruby web frameworks, Roda has the highest performance (note log10 scale in request per second graph) and uses the least memory.

Data: Requests/Second | Initial Memory Usage

Compare to Sinatra

Roda aims to take the ease of development and understanding that Sinatra brings, and enable it to scale to support the development of large web applications.

Continue Reading
jeremyevans-roda-4f30bb3/www/public/000077500000000000000000000000001516720775400175125ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/public/css/000077500000000000000000000000001516720775400203025ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/public/css/roda.css000066400000000000000000002714741516720775400217600ustar00rootroot00000000000000/* Reset */ html,body {margin:0; padding:0;} html { box-sizing: border-box; } *, *:before, *:after { box-sizing: inherit; } /* Dead Simple Responsive CSS Grid */ .col {float: left; padding-left: 40px;} .row { zoom: 1; } .row:before, .row:after { content: ""; display: table; } .row:after { clear: both; } .col:first-child {padding-left: 0;} .\32 0 {width: 20%} .\32 5 {width: 25%} .\33 0 {width: 30%} .\33 3 {width: 33.333333333%} .\34 0 {width: 40%} .\35 0 {width: 50%} .\36 0 {width: 60%} .\36 6 {width: 66.666666667%} .\37 0 {width: 70%} .\37 5 {width: 75%} .\38 0 {width: 80%} @media all and (max-width: 799px) {.\32 0,.\32 5,.\33 0,.\33 3,.\34 0,.\35 0,.\36 0,.\36 6,.\37 0,.\37 5,.\38 0 {width: 100%; } .col {padding-left:0;}} /* base64 Fonts */ @font-face { font-family: 'Book'; font-weight: normal; font-style: normal; src: url(data:application/font-woff2;charset=utf-8;base64,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) format('woff2'); } @font-face { font-family: 'Bold'; font-weight: normal; font-style: normal; src: url(data:application/font-woff2;charset=utf-8;base64,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) format('woff2'); } @font-face { font-family: 'Thin'; font-weight: normal; font-style: normal; src: url(data:application/font-woff2;charset=utf-8;base64,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) format('woff2'); } @font-face { font-family: 'Logo'; font-weight: normal; font-style: normal; src: url(data:application/font-woff2;charset=utf-8;base64,d09GMgABAAAAADD4ABIAAAAAmVAAADCSAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP0ZGVE0cGh4bmQgcIAZgAIRCCBYJgnMRCAqB6yiB0xwLgkAAATYCJAOEfAQgBYxyB4xADHIbyooZRDu7xDyWmd6s0ofjlv+NVLBjL+FxoClp29nICDYOCEF2IPv/////1KRjiEJwAVTt2reu+/5fCNyVu46QrNlq9jbmtqlan00cDILkRLscyqXKcwL/Ak9FOdSuJjilVeSXnDKHsIGpSFiXygpIzXwFCuJkmQ7CuUIuIWWqgYnd1JGbb7zeamqvyrWjbyFu7OkqwUQOm/llutH9MJ9LUVMTfRw4oeOTxdqSBn5kP4XKdyGiJEhDlvt9+GssIdUwVEgaGmSxG4c4Iu2B/4rFGxJHFGBp77JI5YZmTgtL3QcMnAmJvTwbpKFDGsKQpY7NHAHT4qX3vw1xzPOtezbk7sgdsUDg3pp0PWShLv+GDXj5U5NXkkx6gmqt/7K6e+ATKiL54lsgcER8gGwvTrhziJLR7VVOq2dGbOAQyq4tZXWggEy56G4d0pUWvA7ooiTOApBSkddb5QN24hwB/wD4BYADtM1ihTpAMUAqjtYGUZA0SG2wULAwN6PByoVL7UWVysJF6fahy3bxfvr7sAH+QRIea/o9woAIYyQ6+jCFuBqj6a/0dUv7vx9u9z1aS2cid1u+/y3YAIEq2wBfvojCJLbRDE8EB3qy9n0IgnCVGarowprec37zcPxZ+Tq/jAsuay1G2E5eQj5CxpWUNpEm5XK5x6D99ZPipLM2HfjFug/Q0LrvQytgvJLFdJfAj/ZgLTyr8YN8852+nKuAW4U0B5QGyJsHdP8U/77vWBKoe1NA6Tt3accZ0x9BOyThJ0yc6lyu2q8A/usbvP8B+sefbbsQC2Lr7waeG1c6c1fS067OrjKpUpThDoDvbs0Xo/xWF2K1UvvPbYiBgrltRRWWXhDEcFhaE1U4CF9OPsBmAK4ufPEXm+1sP3BqHRUXBvpGZ3jCv7C3YL/ceGFhUpATPlkQr+DEJzNkdhBwYzvmISX0mOcb2pZTFPzX7502Z/ApQiK//N596HeWZpcus+lPJl1GyRj7BsWg0lSEROKMDQ88l9O7xqQv2IsOrAFWJpNucpt7JximRRB5AUaWZtiLwJr/tVqdObWIiIRmEikNQvojt/N3/vqZ2z8VVKLa7mLmHW80HtVfF22iyUKiJSrvQquEBA98qlqtPkEOuZdhONT3trrQFJvLWhoOBRPAkLLNjTQ3VhdCEOkA0bqcun1XdVe0nUN7KREzTus8mMAT6XjCP5F/eg8PmhS04gWt8ICWLNmg3zaCgdgIZC//p1VJajPlESLjWMpMfivqm68TSRmM6UdRcp7CMbN/vS7DZe/eu4nWoNAGPCFUtj+0q0PRfGcHCyczfv1LgAB8/Px4EABvPbJLBXz64Kh+IKAnMBhIEOJKCAUkIIDumiyFZs99/gaHAnQEEHu2JQE4IIBAwTiY0G0l1L6OAla71x8jxmx0xVDsjnuxO97Fv8k3aUmRWtLmNJo+ZRsyMefzs/ymEMuSckpJL0WlpUxXTuXtKqgqVJurO9WjmlXfW4/Wl5vlzWXYcdM3aV41Ms3vLadNt3pOX52/3cZVdzCtsyPjo7gX99qy2D3ON7Vb2/0CaNduk4DPi3vkY2pBYl+yKJVLWxiMYZYnOEtlRzlUoaJALhMqJDLjFEtvGYsxKxDD7UNVXDWhquqa2sP9+pxYEGNJB8lAoSsy0AChY5BVjmKKsttxVKxTSgms0/bb8gxlu0geZ4hsfxhY2VCVE5g7wkeUU+qqxFDJI+kJVsaFj5WfolFHJKceSdSE2EXLQ2KTnDLSqCqPPbAhGEHPxK57UhzpFs2SGTNhebC7LsOMeBPJIVMd2w6ypeglA8KluyBMFoxaa53pkjmpKzQPbagTtlk7lQAmdpEnM8LC3Xvgvi/dpbGGmp+6ZNsC7MEOpTJ5n0GyveYtLTIX6zISXR2StEHNhCGR1QTgwbaEQm411IgMjxm2Yn6vRXDx7LJCepAyIYbUHFCXjXHALqgZ2cOnhAFRPu9ZFOoK76tO1MbqQvU+Gnra2tTOj5USdUo8lX0XJA+kp/UFNJNVkfdtcWyt2lZ1IOfCMeUpV0xC7gM+yO/9NM+fEclVPefUpkRF0p6lClpRL7c1aRRfRBgiILkbEFfIf+q5xNo24TgwFgLsM6QVCvYZmp3wZ/dDIWXVFvlOGCkQfuUOeH4XfjXdN6hj4hHV/5fc4H/KWIG2tmgbyvsCHfjqQspcYV5Vt9kKk0vkeFPPBbUtUX2l51Nl6UO9PRo1X4EzgZBpmNECZYqijQBQnrrkTEDsggQU66KFDC2du24gly5kyWmshhnWksOUhrSyHUnuSTcFM48Y7j8R6pt0tR2NRe1ay2XJS5SWTKxL6roUQ8IUy3xBvY9/xvxsCP2lLdRTHaitqY81pBpb2t9LKewnRqno6cvYtnguWXPpgVakhM/5jgSOBCuqc7UEPtUEa9eWD8TEpJx7g6LqQlNrkVka8NJZhk2WHKOyn8FRsU4p5o8sChffv6Tl0hCeyVpFleELchkKsaqbrVaZbojlklCWE0qoiwF0GUSWnOw2HEppI3qDwE1l5sN1dZmGSIyQUOjarFbFY4ktSQV504AlncmS01idcczmJ48c0dFYJdEZHOVdFVzTMNINNstQBLmziJ2TGIWuCoCHBPSRgVw6kyWnsRpmWIzDlJ5TRFWwJl7M+Lk4QolqzddGxL5YSgIKXQygkyUnu45DyREcZgS9lzyeqMpgb+/LY3bXGVQnKkKX1mD0xm8I0Rbc1d7FMp8/zxgAqaQz5QYsZMOC0XMa/mZEejCPEJhJRKwCVmrg0hGFxEgiZMero0hWAI3S+sRkZEgG6DoYVjhBlqmTZUpKXt5J9tmnzxGzNI5jlik5w5DzXeBUZfMMFYtWuMSlVrnSlVbD6WsUiNvjDQH1gvn80g3yi6WWSKinNG+aYibcmlRJvHJj/rbNcgKh5i0Rx1q1i32zq2qSmAaxazspF1HA5YOHpGoVwfL4XVeeVVqGmXr6jDkV2cSL7nGEKKdREyyKkkKkIf0oEwgL70KMMtYCRgXr0pKGSZRV/9rPRxh+nEDCBFmzRILEYBJMRlFgXbAehIqXiIWkutxrl17ikUEbgOkwAyULy8FGEWN4Zixw6XKvvSaJhw1xYEW0UoLVHHKzMp/5FdO1zyJhXIxxSRzg3iMI8SUCw+eiUtL6TbBcqGxBxaW/QSY3aAo9QkkvehYN0q1JByw6uRPrKQEm+jToWt5gjdALD/6SFJqWUTn/rgOQv4/fR4AAxGvJCoyAgTzOhtxdTkagq98fKkYAs2lolDnXudX9HvKopzzjOS95zRve8b7PfOlbP/rNX753LT0dUc2iCU3b7Jr35qfpV2LLFbawjbvaDe70oEc8rkHQ36fgEXj0AO833koULyIsJCjgvz++fXn36sWTezeuXTh78t+/zQ8/dP8988f8INgl/Ph2iDlEzQ2RnIR20pYqQC5V3fwrJfh/SvBPdb16DAhEEpkCUGl0BpPF9vTy9vH18w/gcAN5QcF8QYhQJJZIZaFh4RFyhVKl1kRGRcfExsUnaHWJSckpqfq0dIM0cPY8l4EruIpr1wE3Yf/WbTsfWuzd/cS8Cw6wHdhzHKe3AB7eDO66u7aUBfDY49cNHL1vB85wDhdwCbfc0bn1NrjzdjBQ4kFnXYcOSGOUNeBhwJ/EP4R8MvQ/qyEzWMc3eHwIBnmire/NjPLBYG9QzQVDPG1nEt0+i9VEOkIXvGnLNn09v2UqekIVVbyrOUrZ6vj9EzcXXZ6e+s8Tvac0HwxFoUlB7V226Qet+UN1m/MfQntQzrGTeBpzYbNf4KaEd4628tPvCfE0dCeqkzJJUury+92TLsnKmVon4gjg+QC1zpupZ11ZSdXli6r9Ojo1h2eerg8yQFuzMmRRSXTLUOHkdlggvr8KbmCUpX6nbDMY3uM8bm1GoD/JoW0AQR19AJ4CxFTgJ9D7BzDgXQAGgtn/ZlCqDz1CI1BB+EJKoOyYxGJyJQurjfsQ5R1WCncLknDBqCuZ0iPyAE7SzVAlCdvLgjFL1CN5y4FOSkCAFdwSAeK9QZjNcqwAgSg+32XXtcsawrVDrDAGiFv8UVEUqT2kKZoTgrcqNURZ6ilPL7qKU0yMBh0DZAVEe7vntemhbwBvtzXA5BCJQpoQzRIgRGh3Nxi7ZpDF0EbaZS1qEasFmAZWRay4LAccI8ZIuGxgI6mMHuEeKQWfUw1eBq7KrpL6gQBtCB3kWciMUWLSBFw3ZDFEoiyhTt6Mmqk0qi+KA8D4X0YfB4ujj4LoEG2mQc5lG5HgQwsQZ856NHuA/0eWC9nQ1mCWJO03A5ZA5mPbfz7e1nVLAbOOZIEPTgKXPb+lQp7OceJMHrg/YkWklafM5bZxHbOXY08vP7UTzRPhZnueck1FaTmV0LExc0FYvBScVdJGgDwvZOr/TC95zv6JMJH7KwLCtHkaAdJsvErDq6uzK4xeKZqtWW2bjJYgAmxYhFKxyeD0rlrNzS49wEdlQUY8RB4CPf/DfCyRjBOJQM+LkYKK01oqBQVlJ1oxICifQN5IwH3kzjedT9F6PAbg0BxuvyIMPxkHheHltssh9lyWL9DZltafaY4ey2PPpzAEdUEQ8pwZZ+M3/boAvTtNkmw816gvNpwNR/NZZlH6R0BlWwAFPJRG6SWHCkNvdEthJ3a8adyFBMbm2x8G3M3Vk9jdam46JstvWolM64iZLkrYHaVP5ZQEunHE6DGUr0bNl/EVjHwgwadx2+n0GtEmn2suLaJLtAUgP/MfwslIDWpcPh2bCAvCS5cwJhRCnz4w+Kwv43WXhczZi7LxuU97sgT1sYICChsKw0hDcxrQtC1U+idZKh9l2Iw3nXfpJNP7z1ObQUcPx/9ZOmbKp2Mw6VlgxiCoaEbcH8WfJtlWSnBaGFEDGIi2hXLuWPn/5h51WcqGXPDOZvazOeUfrDPB4uWOk7frhJvaVJbGnpGGsw1ybvCcDyZuut9mz9SfEYgn1uEDpZ7wtVxUCcjoZPMYovptE+xrYrAfAUO/IqOEzR963OYz+8rjPvJCn1QfBc3Tp3o38U7v0j27houiNnciavkUcHNJBIWqXGQHa3miDlVcwH5bQIl0kqns7YH8Ht/AZZnAGXDqT5P161mqyYi1SP7F/uzssF+f583IWZPhHouxPfTIo9VaIuKrhxnPHlAmqMg/1EsV2hqVXv8romudqpZDOeCA+9h5n+LmrfBOy1GB16FCiYY5ru9ljjz9K3KVxXCpfBFYLCg/erfmtudUF/IYxphzjNSPtdSP0hHHAQEPUKx7rMByHnPRQqv3La8beDAq4I4ilNPeUwTlx6+WeDiN87hmu3jvvsnOGvgOAhnapnGGlHqNxsWRv5gAg+4B0Qyd2wxhkkM2KAXpAm4tcSqA0JMFzgyAxGubNIiTHO9rG8/rDk6QtPnbDy9IIhFlARBHGk3DGbX6EISfKJNxEhM9SKkRfyPcyCWJ1uslSddfeERiYDH0XOeda7v81FW4ZmsT328Ebwyb+Bkw3s81nW/KZGScr2IAWeMTaLRlY43nkoNNJEwuTDlah+KsQs7lrG08tIZBY1u0Js48SIsPIOAZujIFUp147Fv2ySUlWO3kkhVsW344gtT0kOdr761bX1tExT2vdh/cNrqlaCGOpm5eJFWXqAKnl1D1XM59R/Rpcpb5KJrsklHV7nyi5qLNTbtmQwcZ2CY6Wg1oXVu8RsLOqtNmx4qtRcYzE7DMDiGM4rxOpU08ewWoyWJZYRhXZ1JasYmnv9OvHL9ZbTDSxCANCp0LmRIp58Cgki05YJdN6CqJnRvhYgIeUU/CWeY+79AClyTXyjHlvC3F6BVtDR1slNjq+Yr7/LkMhqifAcLZxc/fDOyTeiuY2xpvqwDJOm1bZ8K9g5h7c6Ys1mlZ7T2ouHi4YrScLlJzpP6MnbAamSl0RrbkKmv289iyYzV/QHcJfxnKWJKSWB0VXrYQCzvxSpj7yM4X64gYC+olTUE2qbL0LowPK66AbnxWrC7rnWJ2W9ml7Y5DuJGP0KTihhCLuFRMsCGY7+urNR9VnRyEmVR32VyEZcHEXS/13xKj1kviVusBg1YRfDrT7Gn1GmFMdfPuQvOFMFAJvmHjbIG6swdtejrdFuOZRyRWu61aKgpDyWPOpf85TA50OjZnayOObMKyscXI+h4J3yMfGSef6jo3bkH8MNYs2RBZUjPdEr/LfCET2hg0mIa/bsDzw/D4W2DxtUsa9I0IlclolkPbuluj23xS5xLfnWgB68C6YsJv1q+a/WvoZQzHNtAbTGFc+BOZYuZN2uoYVwLNCWH+oPgPh2pcdyHx+AM4/H68MtyBFV1MOfha9uy1EWbR6nEbnVagDUWmKLQxz6Qi1a5wqPujvcmkSAMyTaEoQ82sK/gW7JYrbSEcthfQoACi6VIEMq40o0IMiUjlY77QWne8y8XCCTTsnxZctnL9bxmZphfiBH//EI2S+CUiMA+KoGyNcMBXtlfi8OVtFXh8RVs5HhdWxt1/x2Y6Hf5ycPbzvovuwFcS6SsAfCcSvy9xIAGu/xEW0rJJthJRjw1lU0iq9lFvcz6dho/4IxnH5+cz59KNCbbfOm10SXeu/9GdThCcWvZTRuemrgteBYVM6rg6AhFczJf6ezPmvI/u+lECJ6g+12HIx2m0HSRa0bMfmeBr2ZBr0Ikui7RmrK2tZtwi9dP0RvZ/Hhzsn+2N0msaLJKa0ZYiOQYjE1DY/MGB/o+9muyxrMjsMHuz3N6SEy4s9i/mhCjwYAPNBtTsk+q88o33pgxT9/46i660umPbbRf77ZbdRK++eem8n9aboTM23pj/m0dcgVgK4Vvspr7jSSLkt1NJlIDo9EhuS0Z6Z0BMZAc308BrUWkC4+aGDk50NEz39YyKo35aQCwjJwdwyClCiQ7wD9ABQgkphcMhJ4ulWuCGJJpu2hBINFuxXt00cB5vBMDPQflqeKE06YcfNXL2+q3ixyX2O0qjrj2nsKQeomGCFz5aFJxOEPkrYQN3RPVxfRBVGDQuQ3uyZPkeuVQmxPpiI49f3nYFEi7384YjcTACsIWXB2Fmie4MKGH+YiNBHEyI9vLFhW7ReNQg/zPPn7UMFwHDr2LDN5k3KYoVat5+R8LWXbYlLdlOvbL4ilewl6XwsBg+Z6Rm9Cl5F7aLqM/7I2Gc/y8CIFrmm+p2/vYTDas5Rb/CvzNJQDZKZCkUboieLBISEnxWlpVmIEd9e2tW/RcmSA02UdaEyES+HkegXjicJ/SIh69M+MfVogqI+G1JWNrT/uL8gv9ai/5rLS7+s3K9r4rtQ5bPxSO8veMRc2S5D9tXtd5GKAJPUpQkOJUYFJRKlASTojwFooQxmseoAxWFpjmOeXiMOdLQKKrDKCKHPu7jY6XTrT4+4wuT39AqtkimBArSBo73nX9KUPXh5b4yYYiaG3/hMoAdwOG7sNguPG5Aiu16PaGumtag8PFn4g8o5heZYzEDBGazKvQBOd5j+Y/bHrcW+jY+NGwwrG+A+4ZAvV365Of7jOX4WAt9XR+RJwaAkVsMMlIaG43lemoQLW4ZH4Gl55AB51ioTv+TIasYlCy/H0OyXjTp+6cgjSwRERMDOOhkniQeCMQLmq932Jq7qYEGXISMkadMkwzXrjwSUlh0VGyulO7OkmIa+Sv1KSKsf1QOJ/1keTynzajv8omL6/Q0JgRYojjOSq/FuOX7oGc8KRFzsR4xKR0ttl0eSlWshphbnY0TKDh+Cwa1W9DdKp9Ayyea+N2b4/LmpgPWjVtf8rORCl+G3Isni45wPRkSyWNgpb8YkbJM1RW1j9gpzw7m8tohjVrX7LqElQ4kWvNl2xcYK5VDTpKJDeQQkgjc9/EgI3dK9T3ZKUjTyc02BCfXFZpjWMdJ5MTDPjGPUnKHPu1VQNjcFLJUREnMXjdutY/u8M/UBzYo1dz6jNROv5ju4tUBOtJC+6i/HzlBHKIl+q0eTC5R7/Us8ryqLrlpFfs7F9HuauYV1sJ4V1aumLDcinbyESYN0IQQkw8J62CTnPjv7MJMhGfjH+yu9WY3rZt53Bys5ZtLIFazlaKYwxjLTpFRs/BwB88szf5cMZQh1hG4IkQqf7KODGZxWIOxKmlxL0OrM1vNCeBitdl6s4Rua3vOo/XMB/12G2pcLnHRC+3/q6FLvHWXr5hPnj1LmDBfOj4RFeCTfTTTO+RkGgKbK470iZSUPJVA7kijSkhyMSmZyyUmh4lN5LAZ610V66TfHAbzq2wXK8al95ED0r3s0JAivq1a4ZUwYJBY4RaThBR+lCSxTEvzVSDVB8cvYqygSC7l5l5azXfUhB82Tq47pkqZ7C8l5SJmwCUbxQvUEs7iImg46rYwBnHuIGoURtwR4m0kjDJW/FtBEVa05utJ9B/VnrvlyG9Ein8gPQ7z4l77eGTJrNqifu/TOT4USStWKfJIEkkBSaVgFEcOmQezkhKjohJ0WUOygvGZ59PPW0OvM0KvE6JA048ePmI+ut7ePD5+lT56rcXa+eS6fOb0/Jn5zQvm/5k958GnwJv+K/o/MvSUu+3eVcc5xzlrbffABQWq7HoACC2pagAAf95sTp32ae/ThDpW+wzkt6FEyOshdHKO5ZMlwfhHoL0hlg1Hs4Sxotj8tUc5TSAZiH3mEvdxIcmA+LEnmlauVBeQQzv7e7evs+yN2mu50xOAUE5do1AmJhGqwFaQt0gR2gOtiashg97hXf99jrrLDrAG5czIZNseFg7WU1yExrW/vnm8Isp+Yy4L5P1upuN7KjcXQmZrZwuhNZt6RizDgQjV5ASFcm0KoQwICxAn3pWTczU1Ta87jm+gwKPhlAb8CV1aamqK123eVoTh78ty+ZV5idjGu+VVvmomeXcyAgPR6fX6VK9HvG2IaNAJufy4fTRiK++xV4phpzsxxuGr/rDNMlC91hl6n9yDoubhpNelnNXn9006u774/R1JLbLzv80ahsKhda4s8FMxvipX3H6Smd5m9I5Q0tMDOSS9WK736a/MKJX0cKuZGL2yL+cuhRGB8sZIqHS0VKRCeaOTwzvziPr5CHf24pnz0yVtmYTOfSA6ovtPxHfPLM8u+RQQmkYOCvPIFGUqbHYrspQ2u6dKlmXlXnjId9mqzb+lzkpAXwJF2h0wwS7trmZL83atZe31rclbkyqT65Npf3Wj84KASPQlauFvBhSHl4j9ESchkWv74QoCqkzCtN2W8nC/M0YTZPuTy9Ph3uHDaSSuaa+CDN4Yzi6DzH/4OEvpiNllOKZ/1uNCXLpDitn+4fkUUob81eAh0h9JylZzZ8ZR3RaYaeQ1K5W8FgZepL5Otmg0K/sxj88yadQmNp8PIEAq4Ae/vrx87TyVmAxMBIOJCc9QYjxJYpc17uoCDZL7xM/vLhfpmQxXr361AEcllvL5fDFfvFMZbApWHt+BvAGH30CyTapcGAUlsdHQFED8bVFiCrAnbA0ASj1FwqdgZAnuGDQQFovFxoUDaIy7VjYpm2DI0CSpn48KJIQhG9lE5xBHJxHMOXhh8MhDXAIOAXtIvnFrkEQcunOTTL55Z4hIGrx1g/QX8SsAfCQS/gaAr/dSEFob4FzXVq+zKRBd87P5f6NOtr40vrm01RkZAIH8c6i3j35DkFzVUTmX7ScjALkBcGDnT20lwJWtAnUVr+vgoXGRWHR0CRMH8nKFlpc1Z0ErTm585xogBYDsP3iKVbeXkAyPC7mI1pXqLPVKdVTJgTATOUxMLOfzkYtLSJH6O4vXL6syDIB2z4gpKOKSW1s8KX7bdzhwL3D7SMTdOOxpImnfvQTcsy+3ZsHQvMxn+2HhmPdqvpiPuTinO18eLup8sHqVrrOxjuvobPjeMMo+8b5Ls04Nm0oqlj7912eaPgJLzvPgQtzZsu3PeUuFeGd7bZadqvUUdOd+nc6by4XNSiFzEoDba0B0sx605uffk0i+3mw1iwq41/zYZy9Ta66KZIviviI5IzO4mB45cK8iabhZQVkcrCA/jmbN6dNA+b2XuGzThayc7JzbOaYLvBNMI8s0StOO42ohZtS17vHvWaiMUdiYHWqIfO3Mwhqc0f6H1WSGZU7IY+NiXxkRzgiE7a8ozrwbCFv4ebIg4M51kJRlD1dg1nOQDEX/Wru//mQQHPRtGwq17RsIDrf/elbz1V7qjpx0c5tEfn/4TV2Mov5oCnCkeBs9Xf4+Nb03JiPDsNhxCQuedPGV96nbOpK4T3LG1/crhStPYCwK4YpTG/F1d3vUPXjL3S51F/7m0MqRDt2RMVo/HE1UZaLqTuuq3J6Zfq+CNnx9OhIPrDHTVuHxpjej88EEurVSh0+o1yQhCi0kvUbN8/MTolFCv9f6mWMVub/iJPhY1h77cnXr9QdNO/IKdhRsz8O/icy7wYIIbN7taGIW46GACWU/+GCWJpKk0L5/xhi5Fwv/SYHxhaWkNGGYjizSei6DgUA++jitwF2WZBRbSHKu1mVWfYySO+JwXgn1jK4Ne/z3n9NSMMsHEVDtylUXlzumbRtlbTBpm11jsKNjdfZh0+EngknxZCT/Ob/kp9//YrUhsPjh8RuOV3P8+r/Wr+86sOQvFRcOFt2NY8ssutf6LQXmDgwAxgEWhiVg2VgF1oZtxnZjp7FrGPhZIz9jbzG7Z+xrcHSeYcPEbeB+Lo05hvqQf3amycC+z1oyuyGfyKNqZQIURykWuxgn0ebWX431Men1W7X/PAdLX91zt+ZlwzwW9Uo6yMN8JI6ZN0883sKwFemYuZrjgbh/zVxzlqkzh8zd7h3OYxpnYzvmcT6WcTvuxsP4XkxREe7FdfBmGKdcIKa6ZcqNBcGa3Lhx48ZxHMdxHBccx0Xw+5SpryVFl8uJ3dVF342vvZV7ce9Hrz9PUb4NUGltBTavfXD1i6/5mX0j3bnusV5V/4qSBP/ptVaPFD4p2D1rA/v+Ix6g6IRHLlGuap367XPOzRvgxxCAAACAAKYCJEmSJCkkTRfjsG3btm37W4SPPPk1U1+vAmPxN5mZ4VA9uctneTIzMzNPnjx5CrNij0upp4nM137ZNOlLqM5H23m6QIX8kiRJfvnll1+qA72MsHqm19OEYMM3F2ZWBABAEAQRQC1YLI3vjv2PgAAACIIgSJjVy6z1PAbzBuKCkLyPw5RZ6vNYj+14NpqfirrHqFLiKUQ+ohuujcliOcrMIhJ7Qjx3z5VM3chWC6aFKmOMMcYYY4wD43twYai1qO8bHUUg7jOH2m6jwCP+hda28pfXmce7Rv56Kx778+Ccc84555xzXvik/W/TBUc4yN/B7VApWfC9D9vXPWvYy52+4BlbyJisSLv1vR4epFGEfvWA4gAFQgghhBBCCKGCHLzODF6+dX9c8D/Hnulbz3rXjU+DUkoppZRSSilNwu712USD0+Hpeud4Ysf5jBU0F0lzpbXN6ATN5nDpTtjHtsWq2U12ma/e8llLpGj0IP2i1Rv7zZMEIYQQQgghhJBCHDfvK5culw4OZPK4/BGCndywePzYVtbQZvWbWN4PIfa8uEWEEEIIIYQQQogixPVlkspuSbjVIT0AAAAAAAACoIBl9LqjAp0xzdoHE2BaYpmd6ElW3exsEfr4PD4dUcq3vSsfrx7drE/8aqqvTGu/9stX50aNhKNCixl1ZpimaZqmaZqmaZpmlFNOuymcSimllFJKKRVKqchwS1aRLKPq6dpmMcX/R8I3JthHLk5+KPT/2bEpyFuAtXGrv15Z+YrpwIIxxhhjjDHGWGH68iWGo5Ry0W8m/VRKKaWUUkopQ8oi9/TsWC3k4g7srJasG6OLUkiSJEmSlDKT7fu3VFnHDCDmaHfDxQvhSAAAANCaEMzYE1jm9QtYqGKFJaOseJszei3Uv3D3XNaf6fUPJpuExIayEiIR+RSiGrUXUbnpHeXyU+fr9Xq9c87p9Xp9ONdeUr1/46hP2N2xgkBBPM/zREQ8z/Nj9a5L/9whnDZoTPk8n/GrBrx/rKLLvPzJ/Lm2W8SKOPPs19DaVEs2yh7y89lIi3nFXixhDcYYY4wxxhgHxni6fBKEEEIIIYQQQshUDwAAAAAAgABokZ56wuWrpplHDBoHjbMbg+ygAf1CkfHd7a0j2Ra0tefasEtgj+IVmbqMzbpg6rgPEO2VQpuplE5RkgaFhISEpJRSSikt9FqyX1+LXD2ewxhJ7zWVqhPyYmnJq2BlCrFhw4YNAAAAQADALYJcgqWzg5/rM+dDkIXmogQBCcKFCxcuhBBCCCEk7SFNkmUclEjOMTCvgaysZa9tH4r4JjqSpWEl+/he2iAexMe9fm7Raoqa5bFxK6XfLLmo764Nj9OKp1K+1hQH6SyxsSzLcs45y7JscF443hwJtF7+S1SE2WaG2Q3wAoiejusHAcSwuDcISLCKa92ckEVIj0o7JppuRvHTOyo3JQdgVWLKXj0KuEec9ru2FB9jLsCNsjrvxTR3qEY8Twi7x9TLzE5ZqrAyebiNpSzhPEpOC496YyuVyjez3YZ0bgiaCfx2gAkx9a+D3bp5dyxWKFssEsMKWUmlLy7HwlQohBBSSimEECoqE1r01z52xNXM+hj9P64wHys5aZ0UXQG6CaPVarXGGKPVarXFtC1bipJeKyR74p2Hnud5noiI53k+iApJnG/9eYGBG7pKK6oAz+B62ubvwYcp1f6+8sfn8wZHmmQT39vBQK+Jgaq0uigcXOY58mXH4O9Gh6vnzZMg3eAKg8Fg8N57g8FgCO+LtwnXXdm3iBKCN2pDTG3LRp+P2ZyavUqqOgZTCrFVOfftucFTjUKLM1lJgTjRdsQbU9+2x9JCrX68M5ESRBtBSvbquTYZ6PNxpDPAQ/wxBFTYmNnzthXErrzxcQpIGJ8Rob+NwkI9tyJ8zQ9nm5VE+SN8KtvWe0z1XuprIPdq8exPmwr6oeSr485Tex4zERWDxUAtC+pu0kgypmuzjBRpkVm2rc62v4z9th5D70vS2TY+diqsttPp7MaABxx1KFzXK8dRrde0iJkhb0Fok0onxVa2wSjHk77y/2q7j6UPbk+8Dfp+f2MwPHEwboyyZ0Fef4A47hSSlNDvcVWbW3lSBa1v5r0aX10oo4UQ0NHR0QVBEARBEM6GIl9AvlxdfWL0Z9zvHavdBoiuNvZlw+bbaLdsa8+B/R6u0K/3g+pCZVkFtl2n7J+rOqkXGzoQ3MgLrWTJpgcz88Or2sUOV2WN/UoQ/M4S/KlttH+gtUw1Xa+wp7glTGFt0yypAgOIR8JLhnRdLa/WTj0N0D0JsC4J8qg8g4yNFVrNoObzrNX6fWx33C2fv6Yg0TbHUzcf/tDho+MjIGQIoAkUQcekSoc+e511l/5DmiZqj08HFmszLaF8MjMcAAAAAfvFxIoGLC/HFtTWPZ31mJf4LO38880r1m4K1536STo88wV8vpLeCoRTIUEr9GHVXF5J3Pw6q9dnsnySLC5t8x407bOy2K7KRDKFaE8JZ38TFabVGNrFuDYEr/GY/wT/yWR/B2y6RQq0I0JRCEEQBEEQBEEQBKEIgnO+7yiTMP9DnXv/T7FKYdgSzbRX9dm7VXiJ5EDcfZfWfeF5UfayNHSpu9r6tHpzX46+9JDWPz85k9fTdBBCCCGEEEIoEEL3QbdNwzCZTCaTyWQymUymMJlMaZT84J/zOF632a8cJUOnYlVUTMtVOr4kW9aO7RWTYlOTc1LRrT85DB6H1bjTybn2zSUtq4Dlz+Npc++tjdDdJ9FTkwmCIAiCIAiCIIQgFMHnbz50F9oarLHVCMMwDMMwDMMwDMM4ZPST/Z0ncnmtyXfaNQz2mP0KAgAAAAAAAAoUSIOk0ylNKaWUUkoppUHpUpewMx/Q/B/fskOpuZ9eCIdqZfERD5ZYEHLmRxnKyzWNgTRN04iINE3TBdWykEXSG4WC49832xy4nMGFw7ptNyvAXv3AhSRDchzHSSklx3GcPO0z+KFRaNzbl063MPeRZtNb7SMJlaD+Ecp5c4kfKuNQfYSZAvHVdhQdQWbTcM+ztfe6a5Y+iJLWZwEslJGQQZuD/6qUZxiOOa+KlCu/6+dZfolBPsxzsylk+ND0qrfRNn4N5w4bXYSAEEIhhIAQwmH0ViI6G5vRxbWoGVUpR8XhPCpn6lFXaNaO5oL0/PNcYhwchofq6rRRYhdT0YlXxZp/MktbdEGyx+d19J/YXvz5DvlSXa0eKsR2L8pIoTZHh2J0o8gvn84PiiPW1ya7jfcnx0OucfSTu5oexscz7+Aohw/RN97mutJZtOOVpmkaEZGmaToQ8T5QlSN+r1tjApN5glv/ltYeu2nf3fCZrF9oBrfbrnqsJ9cuwi4WoMbwXl3xXALf6RLBdsh/TXolQJo+aCMx80U6n5X03U6b7TGWw0qnbZ+Pqq4dLGtm8QnH5eE7VSPWI7tOV09AHVqtVqu11lqtVquLJvsnPDPsdDqdzlprdTqdLqwt9qxkEGsBt9C1iLjdqsN3VdM+RhcwmwVHTCJZalX59Bxnq71NBbGq2EpaqWSDbLR74u7pbkbZv9+GDctjjigGQt9VfY2IW8BMyjAMwxhjDMMwwRj7+Rje6++g7mC/WzJ9RKlOpHhZYRRFUQAAFEVRAQD1Oy8TpfWIDTEWD3cd7aX9E5ehQkJCQlJKKaWUBqW0bs4FFo9BeiG1rJEJPrKMCzhQcYXTylGpXa89zrg+3X/gNPGOTC2GltwHP929C6TpdB07qlWRpSLZLoha758TSqLnBsx9u/DvB3i3+/uLD913b8U5eTwSNybjWJlV6Go/XYf6r6Dt7yDeSJ2ORD54AAAA4Hme53me59P6uKdx+bKf8CnKntEv2f7FPaVRTwi+60z9fVEaNb0FHoGOPRRloNNQLbHEEksQQgghhAIhtJtkHvV8+/oRJQHTtBqO2NbQjl85g9is4IbWTaFQPH14b8gHPvAFn4nNcB5UNZqQe0TXywUS4/ZQRT4CFsSQf5aGEtvomTrK8nhi/P/dglSNmtZhkVcKZyDjKRpzF1pvUgW7jGtMdwqLX+t+D13XpdQPT3XFMGeZF+Db4E+5hE+5vUdMgrcEwlnXPdzZTRI1fm1KP0+dXA/N+ZA943uaISDAzS800hb+P8TlL/61evs6Og6a8qU0HAwQ4B/cbke5DijDICcNn2qYPJaAfH7Nt7P3K/Phrcvf+PFwRUdZO73ZXE3nakVednfZzNY6vH81rcPh3XTyXbAhqGXtk2+E2ajxhliNWt9m4/1gS6Mm1qu4PUq5mM7vYa6ORTH62qw5QCr9Zv35WXdVyt0n4oT8CmKiVH9bA+J05Ih2v0H53jvjalqdsa5pvXM8Yg2b6pJEnnTW0SRKln54mb7qSf+I3TXn7iZtpd4qnH3LFav3Yte8/ew+dGt4DmaCbhpd+ecHzXTcXLOfy1sdaYzZMdv7ThC2Cxp2WvWuuaPWZ6dYsC3VuzvA1vTmNunoDjgc9paZjW5AceXWtNz2fqXtK4T/xU4ky5sVvRa+dFW4Tnx2eBQyv9+Ca0+C6l2awU8qfI7Jz8mZzb2mNZxrM7cuh2e2f2sdKczrFaifMsDdmTckDodM84Wza9Sdd5k5pENpFSHgKTk0QAB0Bwk6CMCw2AEdBt9CiwXB638JG8Nk0dij8MYbKnS8r+aOPzS0GrSa0v/ptmzpTn/OegiWop4i5Rl9CeWXtx9SgQyVbsD9dpAGn8G80RuhD0BpiIvRhHgToybewSrsf5x0eib8chPBwtVyEYvFTSoVfuHyTsBDCWDK/6T++IQitYTNZMlQh6qLZiamZkhRoxMpYNN56EaXCBJvotFEC70PkfNIJvCGHBlpr3hvwlhakc7oKoYPmhHsquOp2uWOrumsx8L6pifBfKHpDVsbND6RDBeN9I4JQeOJUXrIb2bAaufN+Gb0FWnTo960Gm8dVu9d2A3ySwYp3wIdWElnJu6SwGaNOAATRjSxYiFUnZRwyzXYhlSItmzh4tmvu6EVVoEJe5mAXtgmEoHpGtXxguGSJy7Pv/fhajKJmYuNc100xJUP4XIQwInsMp3DoExCZhj5osytLgoZesiUBc5dhZxurZpHiaHxjJcevBcDxJ9x7RvnZYdOWDSKxiFjc/uhTC1SMqIK+XDizOwIBT7cJKk6augXKtjimHVRiRUTN8xyrs4wp0nwUmBeAc9+JTfc0iFm4F/wFAgtiJKsqJpumJbtuJ65XIGUKje82sLSylqw0fig1dnq7QxGewdHJ2cXV7fu3Hvw6MmzF6/evPto8slnX3wlJiElEypMuAhyCkoqahqRokSLEStOvARaOomSJEuRSi9NOgOjDJmyZMthkitPvgKFiqywUrESpcqUq1CpSrUatcws6tRrgGAExXCCpGiG5XhBlGRF1ewOp8vt8frq9m2nTJavE8aFDUiOVK42Xn5jMS5sQHKkcrXx8rsX48IGJEcqVxsvfyI0AAAAAAAAACAiIiIiIiIiEhEREREREdGnVplxAUiOVF5+72Jc2EhSaePlt01mOiYo1Wcy02+Cltq82Unl6jG7PVsWtiOVNnl1D2dgxNyUV2GnQe/T8zf8yvG3K4/QjzMqFFfLMe2d7c8BfrAvVnO34Ig7pX/8Bb0K8WWB0gA=) format('woff2'); } body { font: 1.2em Book; color:#231f20;} h1 { font: 2.5em Bold; } h2 { font: 2.7em Thin; margin-bottom: 10px; } h3 { font-size: 1.5em; } h4 { font-size: 1.2; } body.index {font-family: Thin; } h1,h2,h3,h4 {padding-top: 20px;} h1,h2,h3,h4 {margin-top: 0;} strong {font-family: Bold;} h2 small {font-size: 0.5em;} p {font-size: 1.2em;} h2.section {background:#96C9C5; color: #008079; font: 1.3em Logo; text-align: center; padding: 12px 0 8px;} .center {text-align: center;} ul.plain {list-style-type: none; margin-left: 0; padding-left: 0;} a {color: #ef4348;} a:hover {color: #000;} /* Layout */ .contain {max-width: 1000px; margin: 0 auto; padding: 0 20px; position: relative;} nav { background-color: #231f20; color: #4d4d4d; } nav a { color: #f3d90d; display: inline-block; font: 1em Logo; text-decoration: none; margin-left: 10px; } nav a:first-child {margin-left: 0;} nav a:hover {color: #fff; text-decoration: underline;} nav.main {text-align: right;} nav.main a {padding: 20px 20px 15px 20px; text-align: right;} nav.main a.active {background: #00a79d; color: #fff; } nav.main a.index {float: left; padding-top: 10px;} nav.main a.index img {width: 150px; height: 40px;} section div.contain {padding-bottom: 20px;} iframe {width: 100%; min-height: 200px;} /* Background Colors */ .bg-aqua { background-color: #00a79d; } .bg-aquadark { background-color: #008079; } .bg-cream { background-color: #fffac9; } .bg-gray { background-color: #4d4d4d; } .bg-graydark { background-color: #231f20; } .bg-graylight { background-color: #666; } .bg-green { background-color: #8dc63f; } .bg-red { background-color: #ef4348; } .bg-white { background-color: #fff; } .bg-yellow { background-color: #f3d90d; } /* Text Colors */ .aqua { color: #00a79d; } .aquadark { color: #008079; } .cream { color: #fffac9; } .gray { color: #666; } .graydark { color: #231f20; } .graylight { color: #666; } .green { color: #8dc63f; } .red { color: #ef4348; } .white { color: #fff; } .yellow { color: #f3d90d; } /* Icons */ svg.feature {fill: transparent; stroke-width: 2; stroke: #00a79d; width:42px; height: 42px;} svg.like {width: 60px; height: 60px;} svg.like * {fill:none;stroke:#00a79d; stroke-width:1.5px; stroke-linejoin:round;} /* Buttons */ .btn {display: inline-block;text-align: center; padding: 5px 12px; text-decoration: none;} .continue {color: #f3d90d; border: 2px solid #00a79d;} .continue:hover {color: #fff; border-color: #f3d90d; background: #231f20;} /* Home Page */ a#logo {width: 290px; display: block; margin-top: 40px;} #intro-top {display: flex; align-items: center; gap: 20px; padding: 0;} #intro nav {background-color: transparent;} #intro .contain { background-image: url('../images/circuits.svg'); background-position: right bottom; background-repeat: no-repeat; background-size: 57%; padding-top: 10px; padding-bottom: 10px; } #intro small { font: 1.33em Thin; display: block; padding-bottom: 20px;} #intro h2 { font: 1.5em Bold; margin: 0; } #intro p { color: #fff; font-family: Thin; letter-spacing: 0.4px; margin-top: 4px;} #intro pre {display: block; border-radius: 20px} #features ul {max-width: 925px; margin: 0 auto;} #features li { display: inline-block; width: 50%; float: left; padding-left: 50px; position: relative; } #features svg {position: absolute; left: 0px;} #features strong { font: 1.4em Bold; } #features p {margin-top: 0;} #compare h2 { font: 1.2em Bold;} .compare p {font-family: Thin; margin-top: 0;} #ecosystem h2{ padding-bottom: 0; margin-bottom: 0; } #ecosystem strong {font-size: 1.4em; padding: 20px 0; display: inline-block; position: relative;} #ecosystem svg.like {position: absolute; left: -70px; top: 22px;} #ecosystem a {color: #f3d90d;} #ecosystem a:hover {color: #231f20;} .score {display: inline-block; width: 15%; text-align: center; font-family: Book;} .score .bar {height: 160px; position: relative;} .score .fill {width: 100%; position: absolute; bottom:0;} .score.roda .fill {background: #f3d90d;} .score.roda {color: #f3d90d;} .requests .value small {display: none;} .requests .roda .value small {display: inline; color: #f3d90d;} .compare {padding-left: 220px;} .compare.sinatra {background: url(../images/sinatra.png) no-repeat left center;} #intro-top h2 { padding-top: 0; margin-top: 15px;} #intro-top p { padding-bottom: 0; margin-bottom: 15px;} #intro-top pre.language-ruby { max-width: 550px;} /* Home Page Mobile*/ @media all and (max-width: 907px) {nav a {font-size: 0.9em;}} @media all and (max-width: 827px) {nav a {font-size: 0.8em;}} @media all and (max-width: 899px) { nav a {font-size: 1em;} #intro #logo {margin: 0 auto; max-width: 300px;} #intro nav, #intro small {width: 100%; text-align: center;} #intro #logo {margin-top: 20px;} #intro-top {flex-direction: column;} #intro h2,p {text-align: center;} #intro p {max-width: 600px; margin: 0 auto; padding-left: 20px; padding-right: 20px;} #intro pre {margin-top: 40px;} #features strong {font-size: 1.1em;} #features p {text-align: left;} #features ul {display: flex; flex-direction: column; justify-content: center; align-content: center; max-width: 500px; margin: auto;} #features li { width: auto; display: inline;} .score {width: 100%; position: relative; padding-top: 7px; } .score .bar {height: 32px; } .score .value {position: absolute; top:35px; left: 0; right: 0; color: #000;} .requests .value small {color: #000;} .score.roda {color: #000;} .score.roda .name {color: #f3d90d;} .requests .roda .value, .requests .roda .value small {color: #000;} } @media all and (max-width: 684px) { #features li {width:100%;} } @media all and (max-width: 554px) { #intro-top code.language-ruby { font-size: 12px; line-height: 16px } } /* Documentation */ nav.main { width: 100%; } nav.main .item {display: inline-block;} body.documentation li strong { display: block; margin: 20px 0; } /* PrismJS 1.15.0 (Syntax Highlighting) https://prismjs.com/download.html#themes=prism-okaidia&languages=clike+ruby */ code[class*="language-"], pre[class*="language-"] { color: #f8f8f2; background: none; text-align: left; font-family: Consolas, Monaco, monospace; font-size: 16px; line-height: 20px; white-space: pre; word-spacing: normal; word-break: normal; word-wrap: normal; -moz-tab-size: 4; -o-tab-size: 4; tab-size: 4; -webkit-hyphens: none; -moz-hyphens: none; -ms-hyphens: none; hyphens: none; } pre[class*="language-"] { padding: 1em; margin: .2em 0; overflow: auto; border-radius: 0.3em; } :not(pre) > code[class*="language-"], pre[class*="language-"] {background: #000;} body.index :not(pre) > code[class*="language-"], body.index pre[class*="language-"] {background: rgba(0,0,0,0.7);} :not(pre) > code[class*="language-"] {padding: .1em;border-radius: .3em;white-space: normal;} .token.comment,.token.prolog,.token.doctype,.token.cdata {color: #4d4d4d;} .token.punctuation {color: #f8f8f2;} .namespace {opacity: .7;} .token.property, .token.tag,.token.constant, .token.symbol, .token.deleted {color: #f92672;} .token.boolean, .token.number {color: #ae81ff;} .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted {color: #49b69a;} .token.interpolation {color: #f3d90d; } .token.operator, .token.entity, .token.url, .language-css .token.string, .style .token.string, .token.variable { color: #f8f8f2; } .token.atrule, .token.attr-value, .token.function, .token.class-name { color: #f3d90d; } .token.keyword { color: #ef4348; } .token.regex, .token.important { color: #fd971f; } .token.important, .token.bold { font-weight: bold; } .token.italic { font-style: italic; } .token.entity { cursor: help; } div.benchmark-images { display: flex; flex-wrap: wrap; justify-content: center; } div.benchmark-images img { max-width: min(95%, 600px); margin: 10px; } p.benchmark-info { max-width: 1000px; margin: auto;} p.benchmark-data { margin-top: 0; margin-bottom: 0; } section#performance { padding-bottom: 10px; } jeremyevans-roda-4f30bb3/www/public/data/000077500000000000000000000000001516720775400204235ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/public/data/popular/000077500000000000000000000000001516720775400221055ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/public/data/popular/memory.csv000066400000000000000000000002231516720775400241270ustar00rootroot00000000000000app,10,100,1000,10000 roda,15804,15984,19120,52480 rails,56592,56968,68492,218664 sinatra,28104,30648,42912,144420 hanami,17780,19344,27796,125640 jeremyevans-roda-4f30bb3/www/public/data/popular/memory.txt000066400000000000000000000002231516720775400241530ustar00rootroot00000000000000app,10,100,1000,10000 roda,15856,16424,20768,62988 sinatra,27436,27776,39472,142276 rails,53052,53788,58748,131560 hanami,22064,25656,50476,290016 jeremyevans-roda-4f30bb3/www/public/data/popular/rps.csv000066400000000000000000000005251516720775400234300ustar00rootroot00000000000000app,10,100,1000,10000 roda,122027.68715373332,86288.32904777762,65592.25855302342,48549.61274945825 rails,2556.892018813879,2366.9204968394015,2157.915496067917,1994.9102168021407 sinatra,8484.607014811645,4172.018468692109,718.5694845099421,62.90742966597215 hanami,13735.02321225245,12353.031271976473,11008.820045679831,7726.833878773071 jeremyevans-roda-4f30bb3/www/public/data/popular/rps.txt000066400000000000000000000005201516720775400234470ustar00rootroot00000000000000app,10,100,1000,10000 roda,81897.63532611361,51926.95155830795,38141.35070547055,28543.534354564392 sinatra,5058.07626641554,2902.774036185821,592.6469838957286,59.28003578359572 rails,970.9965532736129,943.9953878088751,907.8607165541686,764.2877488894684 hanami,7403.360610369675,6742.304891849171,5900.963791819346,4808.113698816461 jeremyevans-roda-4f30bb3/www/public/images/000077500000000000000000000000001516720775400207575ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/public/images/circuits.svg000066400000000000000000000145201516720775400233270ustar00rootroot00000000000000Asset 1jeremyevans-roda-4f30bb3/www/public/images/popular/000077500000000000000000000000001516720775400224415ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/public/images/popular/memory.png000066400000000000000000007204251516720775400244710ustar00rootroot00000000000000PNG  IHDR8C cHRMz&u0`:pQ<bKGD XIDATxwt&iB M@Mz7@)bEbCDQPPi T7Qz&#Wu6sg^&IR\$;pޓޝ4烀crX]djK}ٷڷ']&{p|pWcR^|+5^z燚V!ٴfٚ߉*rMe_mZ8y^7t둎&_d i~ 7}8÷? &wlA|!HW#gu/f曹%ڡe ].mOb&m8+HV:j dxjφI58w{CO)X!6d[^3#[c^9K5:IW滾3ͥ J_.'Ll?K T! oM6|uKOlՓaV3+\}v ۹,{q|pOL)P! 3!\rwR} b1Θn9-~iq~5\ Sq_RR: .o+9>פ!e~>oi$yt8>'>,ΒYåj|Zc0tͅ-U洙fM۪$wk$. J_1-l )T )r1f~)?!05iʅ_l!|}j]v:֗ߧRG{/C OMhi94`A f O諬wMw4}V֡[Ks_yPr:gZjhkRr/\ Szq_R.uCv54X͛D[lYLٯI[O*1Uë|o}=dZfEoOc0+@,?HA  3~vVtϴӗ%wkPO*"Q|}n?pɥ b>+~ŴԶNes!I3R>g< -x`y`d& ڶzm7F&uaRљ45q][{ZN:1\I,k38r 0`lAQnWG!+R.5HV%^LkSFݒp)L K_1- z$1%dدIVj孕-}R:]su~Rj4<\mR6A:_|-˷rM̔SIF?.DFIrK>9o g*%Ly 0"@of@($ .5MH߸&VAIRn? WҋRR:gN-@屹y#&]>y.BWiX04ٰ~$]\IRI6hjKz@S?hmmjk8p• PE{lnt:vl<7ݚ{W:["Iʷ9_nͿpyj/)K$8*D>D]x#&]8%?ٯA ',?ry)Z)0~ܝMF 3q{~R/2]Nv9Dnk=֩.)>)WimPN,3]nFLa.,&$k],?TpσYo+h&@*X|y3P~.Z۲crҧOݟwۭƴ-iF$wU\ +u]u !pW3 5H˦cOgKpI=<@sl`[B`[B|I֦OK~j~hC(eeDEݚ;䲥ܭ_ܯH:kԶNp,3]nHOxd'CKL'䐿>$nĝ/2)+ |~˒Uצgژ`mp) J_1-:!)-vSsR:H p:gFCL2|~rw\XWnLhOtf;L)YR%5|VIE*_8XxyK?/ $ -Wzζ8gCrh䑖-TY0MZW+YQ2kB7.78Y:L3= v&t fe9>xyKUvLn)O_sI$);n<\ruultpaKl޸9ֱ<ÖVZpȦO_߷VY_l\p\p\dff8ց C3 Mqqfc4E(xpW/*\_!J*T:K5(p Iyђ{9dkJZk^?J(iWG_#m%UWV6^9>XŹT^Ov`>/{ƚ{k(U~_XI{2_-)xt΅H;6.-lo-.?MKhg UVD!REkrw+w?C~ R~_|5>\gG'mټu`iE!+淟_g`?)EqɪC=PRK;%oc8|ܟ떄28с_Қ574/Z[bz~rߣynQΟO_mx~/K.TW 4ygڝ'm:iGߟ]I_1-raH/TR? 6}Y,C~?ǃ ^gdȅ:_h+yK?=Em6^phc)S:ܽ=/|#a߬:kwn,\p>n<=sW–-*8C^zemoc~*mƒ⇖^򑴤K޽y1"bt+b /='=wx*b\\r7 ;j9s3j/ֽXR8ʺr?qI"h/'E$}j_5+Ey,6]ڵx̝n6Bu֯$9W;;]Zkr{=F4MTRK$%s$ޫԼOaB$qI!s+lK&Lzgrzih!Lݞ߬hը~`LI!B6,1ߎf֋!0Q} 6Pr2=kh!Ҋ<+Vt߿} f>luzk7\`i攙glꏩ?~5s̮g&?yv깝A"ıHo ^a}=1c<1s|뵔*kV 1WOj[',`c˫֏#C[ܽ +5׏6}GۃR5vuɬWg=1s4aO&vs+=JIoU_3.l8tSd T$^y\ SzjW-(H"5 IN;]살-?oQ֏:u}-H'UVeU6pEIUdE7GKRGvozhE$ _'kd(=߿=}ݟ:HNu90`r*NkIy;ho/ޚVYglI(IUdQR?S?I}؈I:HZYmi9Q.C?:Oz:Mt{vvqګ:k\H740LZ M^k2qF/^??|v%7Q$uT$("bKҋc_|&҆ g7VOQ2|m\I%/¿E)d}Ȧ ^=:[t>5Oopi8A5iL1YJjV\o%5[2R:t3mk9uCxA2'}qi <±}HYd͐AID~ %E~(r^.z o>$[gW^z*5tX }% X|I:+].쩧ZPu??s$|AO! Pe I#wrl:``k!~"wrR-3j'-踠?.VQiʾ)"S+OG&}/5X$YWdJR["ņdž&g YYIseLE?ռ^3@U^dwUUҗeto쾃>-]}jkɑ!_&l7KUjѕҘ+_e^X"9EKY3t{2iƲ[ktk:NJ7}q_ןWf뉜9ȼinM_'a3gw<%HcfY*RH%KrFՎcfRmVmIW8kuQK9_J/KշV&}qReLY:$k{iccҋ_R;飢~H+9Hk T$9z.܊Hf9|l~ŞmO?Nsya |X{nnJ8>p {},4oOsסÆ?jp\x $T^9J3X6#TtUϊ$dj0Pgz`?+6Y*]}>hG;OR 3 6icHnv[lի1^wIq8G;x'=\Brv8-x?aikIE/-Sd,]•A6 ~ҤٓNN)U¹ KmI $4pi&hp遖(qA7_7jr 09H%?]!i޳ͭ)e,?$|%F>Y|rgTK[wm[J{K .xt -MW"[{}Eҹڞ] ϧ}Hn4Puվ$gSAuQuT%{ݐ=ڴl=F=\?" +8OFQqTMv1c2VI\jt4?n7KMTM 3d]!jC=\Ԃ^OL_VyQw3_,33R[}nim`~LFn}j>ie)වkJ; lrRM6 z||/G4%}[7c$!aǞn?zWH7q)@} }|e c@/2I Sߕ4q)8j8ttW}g&= ƭ0o{G)$RE {lsKJY f)J&T:Zmi~~Ivjɫ%V5Xq9OR`^`znސM҄N\$R,(7S9{*U^adҪ\_il֕wɅHޝcRپeǖ0p)L|>x>K?aȂFR7Ũ}1\ 'kJQ+ SA/WSz4[_|G84S 9'i +KbKŔ5X|Ҽl ܭ{=vգEM/[=mjzC:*?,G:+[0P~|PzHac6A C~iW^zn23:{tIj$rHRӮM41W|Zj҂ f`E^ \sU\= Yl*c_LxiHZM_1->,ܦ." ;7,{y%Iz}Hrt7Gmie-%=rƍJ+\sax6jMv?z[CK!$]E@o?p76O*yC(@r{7-߯q mxÏ>0ˌS C8itzjbl3)Eqɥ G)ܣE|43IY28K5I!ʜ)[f4jŨW:xڻ;NI B]#EFGDVޫ^wK~.YpZmoYZ}eՊpç!%O;N9өyKoQCrB8Wx3j>d})[ͥ%喼8Qֹr7<߳Mco>)yV#\뚥KOIGj1p• ]z~sݥ>뱉> >AڽKZtcm,2-֮s~-vvv)GŜ~4ܦszH5Q몴[sǍ1!]B0\٣l]s_t*KKB j}km"ROOϜ|Wr%I}M4)$[/ɦ2uBm/Г3֕۴mZð -tKO(eZӤfԝIzʤj7lOo}}_ho*$ziҍI'O}O~3Q7#֟֟i]nSB7=cL4;cn4`|txȑV094>`nE܌ҴS?@E飳lԬKMoJC&g-%R }.ITHp•^4I;&.rt՗ ||Hgoo(-~rQK-PL^| ;hI**EW?_|YZF8.MtO7zK,Rr4glWmlɘ\O9>{Tp#eN:) 5!B~0Wf[[~ܨxz9-U[1P6]L*<_acu;\`HsJg&Zk\.͞39*ۄR:)!㑷QysHm쾑%*<҄ [6Vs .|G+VZŤE<_x_Fflח#ح`Z_=.ڦMU~WJ+>^^{bI9!97:;KF:R|Rhhݥrߗ;[.zR*UU%o>R]%v?t•W,8NN̫w].vYigk s&?&<9R@ۀ}œ'˫YFo["5 j(X MgaEil~GSPRn:"rs|R60[ h[vS]\IV8Q!~y]6RJ{IWLKmo2[=g$PgOC.TlR^s{xWb8߿궲nJeVu*,ҙ޲4H pLg0&?Yb 4͞i4x̠-,ء& nj@i+Q#S)io9Q+A5n餹}}n H\lpI)tse )c,%UsFRGJ!Bօ7Μr>jaZ&z“VΓTp-Ν!k$5SeMfΟ1zf!jO^oy6,~p/p` %n#Ӏdz\ڨꚪ 餗;iߤHqq M!K$F:]*}c{  PL/rNHo_S~;$+iA/2]Ny_[_O)ת\'sƘsjԐ(ĕ+Jݣ`ail~G]HO; ҹڞ{YrsOt ^oY,aM70u?;Ni(O=&ܪkWLKmr!zGtsYj\ë q!qӢjHFΎ/m t}S#:w%3:R ?JMiҭ~܏Vi880^+X7HW*^){%d_3qk}_[jCIPY-޲?Ҩ lܙ/2X_s3y= t&ҷMiEݕ%W~!ퟱ}ҍ ]/+K?$TPR*XlKW,?K'f~LӝOOm }bz|W"/n{yWޯv"En4[SŽ={E=T ;>wҤNǛleV}`Im*ܶ黟DR:=-IJ"M%I%~Y_$=s]+M3-lJ7Kjhi {-,8'`V*I5RFJ+utf?+tvFmڂzk3RwJVCժjv-J~1~Y`⹉L|Ns\\D*Ŀf6Xn_ƒn~294З~,}\iaL90rK/<_~TB֚Pˮ.ѝJ8η;\$NN2?5o9 iE%W~)zkŮU>4w\rLJ3OycgsM~ݢu\CqzI[a]oD/F sH(Rq{ WH oN@Cl\^ \g+^z+igI+P¥ ~5#?yL@;*lp›{> WWR`K /չAZT4? m`z,Eb:h0$\s{EjɯrK\wugIR?>IR9I#?tdtDu$L\+! 5!iciܸkz,3{L T3ig{ݡfM')`zƭBURЍ ׳'[*NaNhylUտ>z-əѹtWZPp*;OR`=!❿:9W(~/O:zuwzp*C߇pIdlƔj!RB-붘$8L3Covӱ.zr'Ijf]zEwIuX3J*s̓qɏ H;m7svK>s/rJrLrO:a]`.Eg+ 0-wFNU)$|mI:!:*ДCGe9I?5#Pu=_xyKuAAAV|x~=&[L^I>lȉuNza'?T:k"=]#-8^~W]xFK8kᳳIןQz#)¥޶Im(`Q?8mǵkJ)IU[dKM7Jk_[RX/~4n8D7{nzK]{;f9;اZ<]P:9gH*FZ"i]n[-ڢ-P1-җߌJ)GO(mopgX+ןi]nWC'BΫV/P0CKSO}ojIy:NuߩVjVF='.+RHO$Iݧt-a']FI8g\H\I >פ~/n%q{+5.W\M- ɹ¹:dax/2>e*R#y_~c7I'#Nv=]n 5IoG6{ܖ үJL?Q"OF|JשXs +uuu$0dmƐ}ޗ@-ʚ"k>[hgQSzJOYYWʜ/=_;/>hPC?)cˌg4qB2=WIjvK}U޿H~җuZCԴҠI]|F@iC -:X֟i]RBMvE-?> v{[)m-$I.ս3n[ﴗLJA?䫛y̒+%! Ahp:9y`r.KYf훵Λ>y9$=&Χ)UЈ <,,Y2z /`Sҏ 󭟙z\iǹ(`5 זY|y]j8Q߆JSwMu%)3͛8><碴oy.KzEMq.iM¶OtS:P@oI6m!Kqĸb".Zغbx$pۤw+& H7\?>v\sν!-Y<qsIٕULԔ$ܯ 0/^zfꖉ E7d$Ψᘾm/?ٻe//uH~"ؿ7V_,ԲN̼"EKJz.Y$2#Ow|ԴXӊMI}`%dN罱l Fۖ ܮȡK`KkuY5{_)(-ڷ{$~ӹ9/i{K]R5P}d~5[YH:H C̍`Y54\PKTn[~]*9>&NM%h-5wgߏIzD8nQ榤oT*~j\E$k%H%I =f _BJyB}yz^Kz}lдRXX8 o KNT)~\!xtѹ¹ G͇gm7z%W^<sIbvG1|?ty$M;@=ޭM"Җ![:lMLBzsprWN~#}_ qzgHs ^_v}oKL9Hys\p"v&6am>kn<M;=%UnU;+g^g_2qSv)6åuMBA aCK/;તMk"F_=#=cȻGWy0ua_KzZ$ !Οtq^Wo]6l_?)_|%6!m~W{jڤWQ4cURѠdn|&'x:;W;2_j] l.^XkC.uַ֕UƖ:vt#3sTy:x}ʯ2Ꜫ5?rP xWvY8n[g^=t㥏2ZWC&$J<M֗+`O&)PstjWZ{9J嫖/Nʛ;o7CV|͊7{ Itq= Sch Rt9Q`mҁoI_>`R}6k.ߟr)GDߐVEZ:5kϬUb׋xD]1EIuM W_dԶN?^z֡ jRkx팒ZXg3u#rH>sj7OO伻qg$u侒JNB. 8|ߍv]uxuV9LGrrEW]嗸PVIoփ~Axr$iz~<GJ"W~GԲINϛh_L:M-ɣtGO .c}B~+V6oUWX kVOz s>w8Io橽^꥞$hؼAG >c|'wr?mi$w:N%ܯ vٍ˚G>,!|nnX ^z˽Rz[? ף_ď֗ٔh)ZJ߿(}?t676~z@If4I^QvSB$kuQ |tA;%5~a僤#HA P+|20wi6 Tn!O' 9\\@RUտ|L6^喗QKs-υ0Wv[r*/e~.K[_OK^+ʽʽ@" 0}=iEV{Cwڔ$sL$?(JXz[IuL^{?t_nnuer?^M51fdIV.tER7po }ʦB"~yANs)*Y_~GsvYB:9rіNN[K/踟'߇ƺb]3>6-/%6[lsJ*pB$ILoS+I)9<뎍f][nXns޿@c~Z?7'ݳwowzlгqigT/b<w Xcx\VG"EPqk.~ŴԶNp輏2[r}˜Ys IzDpFjJj(adؒKKQCSޖ/ұ=n 8(_X(>Ic͏O{1opxsFbןi]RrRn(Fx>i(\6.E*@=65T|>HRLw^t ; g,Jʟ/OIfa`w37Ok$z Fm\c,߯hV.] n[ORˋ-3xNW^zHoe|8')`aa}|[i89`98)r?Zը2V9Yi{{Ȭ;>aPߴn<O91})ĭ3vB;qƷupeln}zK~6̪U:$ gds+V/P\'K˲](D|د 7z_ 5=nJE鮙kޮ]gw?$ʦW}0w; dM,VlnmԳ( D)+7^˝/ZjV* Y^Zknvh0ON P,q|f%deԶNH}تدK9vdJJ=C}3R@E TX~I^KzCr .?!S%_._Z1 I;bwa ؿ7?SKIY߼y~gu=ǩ7C)IfiD?(ʺqOIC?9W̒3wN~pjh3R]U Dkg't竿QjLd5]d[KMtMQΕ!%5 %+5 9 #x$P/l`l`9|دp|9-M7Yfw|1~чZ/G#wŠ{/bPG%y6N=ݶM"ڋLuAI543wz5u\#F**Jsx 5R:!-ag|?J"H_ïz虢E'{:3t^WRQmIutmIC5TC$9g|RTQ1=N:uxc"qw2O~YG͒: Fןi]n( I5Ɍ}2y|Urȡ@ z>6kPoϧr|*|R1By_TK[oS;OR8:W8KrCfʼn'&.PƂK,Q]\׈_󭏵nUúq8qWW=z$t7sssM+L%y>n~ג~Mo ^-ɭtC4z'?M QB}دp[A֓yҹ6tG5[\y^R\3v֗Z+?hSzFQ@ZI%9E&Umꯓ} /r]<׿oålqkuɄzj]{TjDMrK 7/\0PSpDr7 p V)d&?}-Wn3#'?4X~:~]9>ϗAҐ6Cv~R; J2mu0')-Xt]fI Y9]R[ެY/]}CRe!IoOmb[76{~u=`woJMdJz*M4iRҽo5 ?|ץpeP|8b?:GKꮾDnG//ޯO6%~Cv(RD!kCVJJl޵W\,)}r+K:*+/`boZIY *4j_g}KoRor?}믑$0ѿ‘%URim`~LFϼ}R՟!)jpTiRIeif_?Pjj!I>{#:KJ z%W]ؿD_ 8p|4ȕ$Ԗd*ЖsLIfiD?ҨPl23Bj>g9uޖN{]7PAB`OB`M Λ X2b3K^^LIKjJJ'\ϓT#Xu\CHW^&ͺ9kЬRQ joͤB_uvQ K@M:*?u0 E&tM%czֵ\ W|-F(DN)nJBP__7 g69lvwR'q:C\̒b4+:wtBR1kc`^s&[llm%~{6IbCc]P@ZIoe66XqRVjo]fo=Tbۋ},mɷŽ/nPė'Q%SHȩI5kkԶNpxvc;kϿߖVH}"^1q>wIK=%U^t/ֵ[~_rhc_F"/2Ee3߼6d#]s'iEHrśL_ ޯ 2}0LNlܭe̡` >R4NH/e{їK>$ M#/vr.t.sFJrI ߟP{B;&=go=T1ww+V^c_2j+ 3nMI3^{ Y2OL W+!ft<_$8MPA__7` V:+]'z]7q~~ȾS״oFgWm؇bw&&'oDD-J NРp: fI[[l /UWar(I/c;w66ݚmxߌap]3-f}77,}F7_?FpagRM36%u)%Pdْ9K#?Qtx~?n]=_A"/n>tF6/dS>#㔂YZ$O4.mMiw2X~B5V۝!9 wڢ#Eáz`{M-0X}oʎOyJ>+}IZG僌u[\n|i-|H?'uiW g{.]nqKHY[ͫ;_s.5Z͠O bi^B{?]lmLJP_]4W͞GbK\i2p)vT0[tSsϝ;&k;/#^8(%s==s~Z+mK9zNZ9t+OHTI,,rSUҟOYR'Tm]?r :>Tj+ͮl=\K*$/'*&Q֗*WK]+4-{+=Mz#S/yyTj͖H#/HpRZf˞ܭ +qo̒^yIqq]"!#WRSrrw~e}\_'K.2yR>7O$v FK T!`? M9LNé/ C=Φ<5%BA?.ճ?-5lL?'Ib# L?|Bǣn" Hb#G\%eyAտOۥouYWϯ5-io7TpƒQg h jB``8tla{od3X~:o`_OZD˒i3~?XenQSjպl`惁;xmf\^@I2S`[8Մqw~/\"{:n,woېv=3<3LYf%A+;*nIꞴ+ds3>ZkeR7PНۇVZu(P DwRK5PmxV1 Uga[|3ufYyz'.몏I¼>ڃN9WDE"EE){ߊZm>v"ϳ;-5Ԝ6IF吗¤NoaoG" Gѡmzbݱc >6[ʢ,bB 9 T|(ܽa߄%Elx0']笯u/=FvݴG{>"|{+Kc5N$iZJR涙;g~Unc/}&"!ت~j f̜n8\38T6ǽz.jvN8+Ҏ<;|&'O\BǩHl [8}/gSjL:)ߎ,hW,u'ռVZ i3͜o$C;$pe7 m77RoW  #(q MjrU3d7Bo`_+\z07W7]W2sJבYJQffL-M[>:ة1G^)-;tܲRMT ˷$ûΏ-j~ե|LoG= f}yr)ň_K'<G'/+]ŘȦP29ǏJc7u\o)c 3 m-lR7B'N \{5(Tgq#׎t;jHTMU 4RJ*%}J&E&ܬ?`]R1|!n, טPgf39b4PU 08 z3=g?3|J%ǧ⻊=)h233Iynp<\U+(p}iIyEﯛfVB~+h:7Tb ڪ W'Rm(E=V])k-ճæz̑FQJQV!Q"Fmjn6ؔvtC[}ښokoIUY&/թ_{;wmL^CԮ[kz~i/3%oܽS_*i4.׸bff017 ~g}WYr-$VZjwj /Q/ږ{ wRx_GJfjY(3΀9@0X~Y2-:m|7j rK]=ΦgV[>޷L'daȲH)G5s[% dcPnGK%)9dCu͂7KItļXxٺfoX/mζ9/y%#GYWOByYwg}=#ؽuDJŪ߯arv~cu}=9tMI>7O4.i፹ˮ*`}S /o`kxϟ# f4}fvrr)RRpHhKtI`?20SeVVM#QAԨSZ"-.xΒ+%y%+~>$!r.[eAꩮ斘^bEǤ<5k#i䕑>qKy.4=׌f[ iqTwi}% VNf?3i7.QB%_67ኒ87\m$؜cU4:K.~\=rv9v)6,6]H <7uGq?^:I((oC?S~xe-Ar7ąťsKӊO8miڞߴ8?E%9IhmG՝oH^Qa_icsM>vPZwkm»DUZ?Cgnݿvv8"ۧ9 O_x@s7v5jzҿOi=tc"탤Osz /e0E-]J6Fv鑥y+Î V}n֟m F呋 WQ5P}P}lU0@[ۈ 33wkK|z>cbl?o~rU#]:9+`T]*][țy(إtHM+{J)إaez/ ɨ*SnIъ.oa>Ka(~>Xy;)էo&N0З<,m öҏ?4d|9Os枟Wr )3?GYt䦤Bmq(|c"%m"Nq47k:: ^4xOjiۥU##ɟ.bV6-,CY3֟i[ To-߯9@ElwlwWN4X_|e+l7`q|r+ݥL$d}{|:ikX_O)\y~Uf-\aB }Qcs$dna2*H_4plqbOԗ-|dѳ2ܭL|vbꦅ+\!FS¤! s2 9`ΑoKemDo\U3sͲ5c""b˷XQפz`tڵ[ؐu%^3Qoϯ30`(ԇʡ0׎5z \str{]--ݽtR+W}>+=^:4ן^|m)wS,86lά9_K^qm xIn՟D>.viJZw_wk$U-_QH?]fwQ[ѹpg",ڿ⎪6%HشL;?׿HuדU3/M9>[)tpLdhg2?( vh#n$nT\eMԛz`dNȰaYcf >*dzwJ}X/,R_'&$ K@a 6{WGm$ņŦXаP]Wq?߉]׍fQ-TRV- uGX0Vf(i鲥?-k)USJ(X/Mi7_dT)o7+tQ,fɛ%nVD~`o/ɢ%l[C:p}R m/yK u']/J_j}) YzV>_cL&cX_Qvyͭwfsx3Ǚ >5@wXV>g U!loba{oԷFZ/ TOxv_]4*f_)ryYĄOn!߇, enU\ҩO}zrיּTr|~sM:p]iyFzvf^r/'E!G~+m/x)S+geϖk%y,(0'^o2+\aJ/.?+{vy,x999ɤnHT9t70o+a>qtRu<-e+F6~xczԥuK~FZtc}t듟\%Eb‡FzJ} ˿TTO-jEի:_RIDAT_o N_f6xwһy=1 K fuӡ&_ d|/tX;ڀRn՗W3쐙!B},Kqݴo@Z삏,9-,učsrRnOܹwH7HACP;|.ڟ˝$ RKoH:+~IXt^#"N =[#-=dvҙq~II.{䭚CAd'Tw1=v |,]|۳HWΌ]IUqlВHJ"! H+TCnAARIIiإa;+βy=={ι9=pc7jw+k &wKK=t0Lp 턌dqQ>^kJb:2}Xss̻ iHTj>}?4͎mTS=y |p-S%siYRGaؕ +SSPrQə%gS},ò?y.\wmĵ=mӞuuw(ou,@;s3|{1Vq[lOxh~Xa%|Au_sW3L}[ 7Opz`*^߱duU6BPA=otΫ -xXcԫSO oW*\ݺ, Y1VP 8Wvkaw]_Vc>}aL;~z)5|m,cԛSox3ݰ:kx{b77;TZ}Zny_Kgػ[eQEi_D/쟺nbP_{@R}.O KV9Yd]vu>ha}MZ'td?Uu|-#ayn=Fϟ"""""""" f á?l\w<Pw>uw<X_pnj|u@g:Ɂ9s綟WfwUp;vmmw;]F$XHuϿ8rͼY3χ ^x1? G_sgfniU+vnJ~/_|BcwjC [p?d =mWc)XO7BfJC$R^Z$;gF<7{voXOW_Ԧ6P9g{ ,yYՁc33sHFhS:MG–c[km }gvv⾓ONvݹ,`##>: &npdМ4w~嫙C;| ܏玞_']FxüJ_'ATisЋ@qV:괫`bf?*?#7Lg^?Az?EDDDDDDD+#fLFEDDDDDDDTn,_X`V @/xM5Lw;O_&uyÜ9s=f{+AzC͐j} w}|w8?/{9q(D<{' ]_WTiiz:J(jAVc4/V1TQ-EՑPzK˥2bg)"""""""OW[;<_ɭ؉Lts`W;58rȿ |ǽ~0=?4vj7:{PP~mB+$OҼ&Ss`1x|!׹aˋz&<#8 ,ga^hUUwPNT`6Fqf^8:NhhD^=͡b*Y;Ȳ+K3ct {.@w<,hڣ1N|.}jGD7^ciLjzӍlʵ{}H~݊+yPA]֭ڻ5…N\xvz}Dzw_0 n9ftO(nkie,Za8ZWSW9xMA݃ UW>[7bsM}oTo:o#49挜{Fqm>/~ٺo[,guyn=ݟ?EDDDDDDD)#b 0:<>_ぇ{L>8x F;#Zq( g7j \\sro~Xʎ&_Ɠƃ܏7BoUN.d52|Γ7'^dxi*f^qwsZA+ZM)p}O珣a](96"W,֫DuXa̅Y!uuӦS FnUCp© _.tFFn;.{5pl?\rb麞dtcYpV/4lF,쟓ܯ|PVE`vߨ;=4),dh>v>%ԃWy2f;5p_h|ecv_]w}1׷Av v.9fgAfs XU˰ni[dZf D@nSm>Kt/8B `} ~S콏Cbi[ϟz1i""""""""6pC|[##<?Q/7948q8i>vl}Aoz:Җ[BzL==p_;Tg*/,_%Wâ, XY?#{}Ot:2p%?{sәbn3x'ClV7'(3/G-i$)\KY0\loJ'>yfhbm3 (ň/CPP`eqŅ^#7jcP8y4ϭO"ё ёqZXXYl.Ý]8ai}!sn 走W#SN;v%_i;ēAyn-cˌ|UvOQ8^xcٵ}Yv ٳw˻-Y#^Ч¼N:i~H:d\]p͵xuAbs Z^oO}tJAO}ٵ+o cˏM9. 2pޒ9 ү4>_(l|qH? y M617N.W\w\Yie͔Uln仱 !#>pi;O?aBw=ȋB#'_67A7L`{xЅ:a c{+Fǂ7;3g(s9<(ףePp.w©T|O5? Ծo߇ omxa \]|m{!qD x.˓RmimAo^I*%$: oExXUW^!pLpr?QvXOw߭y^׫F׮Ͱ.M?tVZjo- d, uo>oey+[ `Hkz~㎰o=.xbb8wܥsH#厬}wߝzkֈ;  Z<+H/xVY*2\0z|u}oWwFC`Gܞ:/r!#Áoo߇}קs}[~47 !5;Vg,3R=In>F,Ij$^{M^mǶiG;.^q%-!""WjAE/fWXnG$ ӐƔw~ގ9A/<[iŭ M6WDDDDDDDD$2R;}ʄ8""""""""""""""""" h1UF4:~bWa{w4rފek['iЦ=}n~8_wR28hcW^S6s I$Uv\yO<4d}˻/9<0xӧ8zM`Pfk7{3T!,I 33{/"";ooWQdmS /lχCL!I`eWÏ<36ؐ|&A|X0YnЪN1Jn*| |!{CDDDDDDDDO|iHTgG!pfл86=.;+]+q np ήM-{~9jk#""""""""9#$ή6gIJjJ)oLq>H”#UT`X 5u 6TaTQFX ?Έ&DDDDDDDDDDDDDDDDl`"N-}PAa7CaٵgbO`Ԥ+Dy1PYR1dHH:gJDDDDDDDD6F,Ijn\ Uewu+箬r`vvDDyB!U-VK~vzjٜ]s뱆2˼]6J}{ z_RDDDDDDDD^| qvEDDDDDDDDDDDDDDDD:)axvick?Aɑ%,ϴIDD"LǴ{#w:VޢBz-[ͅw[pvDDDDDDDDDbuԫ-ʿ v/dœN6VDڪnySdm&=o̓Q !O@rsvbK_{uVtzYgJDDDDDDDD> ^x`OON{垨{ˠl\snk85DDMex`xheʗZj>t W\EwwYrJoZ`aE ۠A];2x=m-![roɎ [A]&;DD$~ϳs뜨TrRiH=i${Tt4]bvcE{T٭%""""""""Xo^yXDDDDDDDDDDDDDDD$*' 0y[ing?rCߞ9{ED$2<0<0܃|nPxmӅ>~ WRJM_cO?x,O6w;@|D͓O.5O;;nJ=,[ADDDDDDDD9 ީ~B EDDDDDDDDDDDDDDDlU&OBrg=Y`dk |vm6;n.91827G`=4eG5g3x5÷F,Ij3f:"""""""""""""""""""""""""""""M,""""""""""""""""""""""""""""7``3I] 0b1XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$nP`8ĈAEDDDDDDDDDDDDDDDDDDDDDDDDDDDD#LhH`Čή@h`" F" ,"""""""""""""""""""""""""""".V`8" XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$N0bDRgWCDDDDDDDDDDDDDDDDDDDDDDDDDDDDDXDDDDDDDDDDDDDDDDDDDDDDDDDDDD$0b"""""""""""""""""""""""""""""q3&" XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$N0b1Ij3&EXP`8ˆ" F,XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$n0,,""""""""""""""""""""""""""""W1kH\aĂή")F,XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$n0bdP`83Z,""""""""""""""""""""""""""""'`&!"""""""""""""""""""""""""""""ؤ"""""""""""""""""""""""""""""q"!F," F̘XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$n0bAEDDDDDDDDDDDDDDDDDDDDDDDDDDDD-C3&:""""""""""""""""""""""""""""",""""""""""""""""""""""""""""1cV`AEDDDDDDDDDDDDDDDDDDDDDDDDDDDD#hH`Čή@h`" ,"""""$]K ήMuã8""(ƙngG:uv-DD$ݍ]+w|[p*gFDDW4ή<|isήțÈ"ľ*A% ' I b&g{3jxzN[sv-'ˀu4-"0zyt3 ONPkpm6[l9ڈfP] )nyyjgBDDDDDDDDDaĂIEDDDDDo] !?EDDHț> 'GEDD弳+ ׈D"8>9BA"J&LҒtW3\M|%:vr)Eϊ,!)6xtFnmv,qvm%DbgWBpԏHqHog"a=__SG@"%O|uM[Zqvm%#gWAϊ 0uִ=#Nhu%Auve0&gWFDD4? """""""""M:"""""" >ZO"ƵH[4; Yhצ]WH/_Z]~9cg/G 22K[w_ ?\~/`q_A~(Hו8;#vve0w@׻娟}@ߡħ%hjs$dn`zהtW{~^pENՇvn9V^Z`>80`i__;"" q5 V:ܫmV02bws^}+l@c^-t8U PCZ/ 9FDDDDDD~X0ˊ""""""N"|38.{=]7KfoWM\Ot=paFcƍ_l%!8K"t#lF4`ʊC/o/GyiէV{+* 3`aFx`C:]WHnhaFxY{,]t*6۵.ڏ|x *T)q]4ڟv]ǭ~GD$⢸A>xc_b+"F1"""""""c41 |^Xc=#L/vve UTߧJ ?PeEUꏩJ87ܐsFgR^7F=tA?qLgWBtD>ZDjDnG>|s2%u%d0bfЌ g%Ax9FUdT^i@@'ǖQ&}x wZ\`iݖՆ֏[i{V- :|!,2 ;85v\ 84Ƕ9"qN+!Da&zڐ /|@{=~!?~ø;"k#""""""b?F,o'-p_L:{XR4^,u~0}q@0j|wWrQ|9:d˓|i>(ٺRJ\ƓFd/tyn {ٕR?_ghGKp/U"NwѬǎECAđH N;ae]Q[XݢT@kFooTCwp M7fiʷbCZle]ZE_)kPJ pC^/;f6kKD4ݟ"g-`/o X+"F1"""""""cĂήH7O] !?C苑& 82qDlJ)i?wcF ~V4,pzµ/|胦YIO_Gp9reټrvľ -L5wG$=p 4?%:/oqƶ2dni7r+)c56ݽmϏD}+qW/ޞwvU4m3CB?0蘞cs+?MSnr`q6E̅R5r^=1Nպc `q$2zl h+LI1Pyecqbzț!?6ϥ@D"ݟ"g6X'y/oO_`C'[wE䍧1b֗EDDDDD9!42z>~ Nf4P .~90`:f)Ϝo`Éɷ,+)~lXku ݈[4?%:/ocx Un* @j~g @ u]#`6Ek$`#񑏍S@ 5Ԉd+r.5?Q3 4.|/ބ cMifY`5fw9r1՟+yZkm9~z6~G FN5" ?El[`{6ɖZO1"""""""cĂY_q_|a @A %fLgC:]WHC5,gW%AtG>(Do9˖wUt+ k>k%.mɴ ӜYI"Ì6hKښek[&Wms*QH(Qhy0qmܱZ W\sy>ovۯ彖ZOx;2&:Ώ!i@ Gꥯ#|><o~k^"`\gh\ͣV4)8L O21H|AI ";oJ?qW>>˓_{:|LDDDDDD$ ]_>_|3gW%A 3%fkC:_4;܏\ ,]⻉]-7~EKֻh?K%iMYz_H?c#DDPJTygo/ =80aDŽv㊏>nvL&d_L' 冗;Q64 `rZG$%) R@Ad:Fqk0#n$" 8~VwIT|ʍʼ¿z>e2$! Ixdo hJS9zZ6hyM% 98زRO/>ې.罣@+sZ '{}ܖ6toH9d^plюw_M h 2D3:cVP ZHA RuKx@i-Ӷf5pEf8?pŕW0K|%c eweoP@RxL~-'xܢs)"@̘ =_![)v:ol?qW>>FpxW_^˜8Zm]Ԧ%] _O|@=$K(NqtfhfaK-~g^%TnKG5?JaR?Vt$"xld#%-n,βƒ%кxmw'-ȈEDDDDDolZ!KLiC:/≮F=iᧅN}B -u}9쾉z߁6h$' c@NotVhPnTWիAz"+/mt5++rRvLy"kD իWMo|` K3|Jp~!،?VRizB_ Cb_DLɲ) ]s]lg`iyyb'ή}Igq|bz;:+cǹjveԔ”1 I1Ag~uQGu~bBwp+yn/{}33ӳ$pK/T"U8gΛH=碒9E&Y/Ky.Y^~Ѓf~~ .|f^CWSL1N|\qd5e:?mkfz[kmVk_1m _  ~Du~fϝ{V*(xǢQf1SD"ș }by B>-3Yz4 #9#M3vCeJ3d~e)F,֢/ ;2츿 TPagό5=y_Vj\inŊ++^x\qMca,o$:"""""" #ؙhuaR`4헮dچiRFZkY ~9hG} rD9qg?fj+y e\Y{֫笊+ _Rz] _3_@p8:_p-I+dmG C:qd8?B cl!;u`hd8 ҍ1li5dk>s;D{ɛ6bC߁gIoH1+E>oS:|Y.\W?N/+.P rU=ˆ4'pҮw@{wEW_ڕ:EU8퓡Rnv]޽ `ffR;J{|΋1%QTig0 3ھ_yOԸSA3;PrX[C7n:f !#kֽXtzq{!ŐS$wܭuغHsg8{ʠ[PrDۥ pO=v}85!5?AG:>D ۟[gY{$g+ma cx;:׿[K {|+2ʌ*s;6R,),:BpC7X|j4 g\ФBYC`eieOO };7Hp0zc&@[M8ܧ>11@u3I& ;mذsCu!B~# IVHz~|0eGSoD[oͽ-)iZzQΰ}9aNb yRs+Tqsz"g;/D.4¢ؿy~t[ܭ7aq&T)'s_Af?x$dDlc|A|㔨GXul{}}*%7T7}\`38vx`UUN=^~'@>}wBs?wk Ї^-[(9`ѯL1""""""" lDDDDDDb>,qv%|_f>4};>x|m?/WE[~ZoH^xRI'_خc0̆n|5_~{?Pwbp2ISAaJI<߯? / 2Pt8.f33`9,㳜Q$S#Y`ӢOA5뤃io&9eHopSXeeƟѼ>IpI!}Oϭ n{z8 =2>6;ڴh5<~td'aڰ5c^]W~tvDKX?۾ bz;8rTa%[-sN`75=q`d=GontK8㗂 *w X'dOirgWBpx9٦ާ!>5 UqXӰ,{!,8`BXQxE֞-{3BX~',:Y aw?L>=Ю{_/uh!qk!ލ;[T}CXao~%z7@"҄*6qDx{<9Q7!{|qM,Kj~%Tk8I' c{xhx㆏< 4gp3M[5en`͗Tr$G_e߇i#}2Hz?h`{~+۠3>hx =h_ƶ2_q3Wu}oW?lK]3M?" H(lxw?wpe}k"~#ݍ^/YpE'^~&?X~$+櫺_8L$vv%wk`6yfy?$/4;%-#-sϧeО-ӞHr%Y}dmE¯aϚkãF<st[dΎv:Z?r⨓rnmZMm]v=#ERX\6Dw0mc:߿Ώ?C0۸f2YcE=]tDńٖqx؇$3 Z6zJ wȏ}yx٥1?Yukurz+sϾ8\?}܍ohϽq' 哭m(Nt qjLDDDDDD$1b 6""""""b_ouvMB#?֘ߟٻu=>?޽׻m|b4p#g^ʹ痆dž5qq,4ƂC@#N7uZCpyi7u<2ʴ=SWӕxZ2oqXnK$+oκGܣ*nXe 4pp3a ٮ; dW0PDa"4R ;F +{8f_s !?{sWgĻ/0wp^͙lpnYdZ{}bPaŽ;!,a#<qcnx 1=n)2+·{LTrippp*|ZPZaњEgN;#[p6C>_>} .$AdLo>,MDow /ta\vر(Ώ}n? >,bv XĻ~@.pq<=KoN|U6{Yf/Nb0D}x8pG-r`x/G41 Eʿj8ou[H?tog'|?罃_cͩO8E3mi1v{Cp 쏼 16ήM;]17o4@t}_ 4JZH3Ľm~O 67k=[Vq,vl綜G;9AClCdwo|*IG?/"W6)]@Mh *W5E/;AUu>-XQyEak-[!*& AήJrBu6쑳Mz>w9_l/Qj!K.6dH<)\|T1h?}an9WwtЄɖNƌ;yey/gOqή8~GzQΫn委+3h) ߑycrCȆtc~8#ĈŶC$`B_<1ٕCٕYu|۬w[޿HRLL,g{?_qy:d,L™1%1m:ϟ_z~܅'p#"k~\?-M dXaquchd |yᇪq=~#F卽owpAS.?_.o{k[o%PZOkZԢ&@ G;pllʰֲ:شhS£Z=vzHq-v&;%|  .6`[m;*u㛬iR暙B~*[j4x;$}Ew6ޟx3H:5 ?ZXwXt__Luj ob>=,bٰ8o,o$F,-'x~ή@z>~S/7@o˜ E6O(j*Zӌk#oԶS]nh4|OKGq5wvj ns(iUn~g۟YH2O9 1G.]N<揞.rHq3lǐd-? D%~6Lo鋑abz;:J@Ctl ݊:ꬭʗ0܇iLՙm?nИƹ 4S$Lf7k|}k£<a{K`"g#`ѸgNaܶBЧL r}p~74,9~_t( օ-/kΦݸy;sta\1(e:?g%8"gqv㋏M xJwRvG``B1扁;l7j2DDDDDDD3#fLzq{x+pz>~"x/~}د&Xӳ_~1UC7s)nvq_6=#!@y? kC ëWu#y2~U}̌r~]Ⅶf>;oYi(RH‘|Hab &>$0kMT{5 ?:ynIW|]@07mHp~_'28>XvH5W'P] L|w ,s%ޒ^nsZВ@3g{*L0@xQ 878,b I#gfD9_߽,w [0 lڹM|ᦈ^Ώs[x?|1Ml+?ލ;iDәbPKv~X)rtDdciٺ?Hwパ`9 ߨsgv.ߓGqmLDDDDDD$ Inb8VX@/1aiaҋ^=tRC*P>t-MZ4Zcˏ<>+|~c^MMϛ5#ޞ|D(GD [fK]i}ƕM_ /7olOg@vہ;f+Ma l< w кO\"N7i}gΕ::1^NÝH<'qMٕ]/I lo bZGCW\|W|ރ¼y2BfCIiNjMuFhs Y@sD6AW 0}"0Eu$rBz.H6&dFWd57r𨂣SC'[;/E*ح¦Fm:_[h>D>6 [Ev&\lL6e98S$!u)źQ 4_|!tabMw C ]\d HX_}W`&Cœz>~lH‹Fvh==XY.sYop9reݧoϿDh˹-_#8Rt*Cxg8{}oÏ>jag憷J+5f+'>9 _‰ė=_ X7<" 36#o[-zeoCis^ I=XrsZ|lC7G }&0@SLY4XVb% t՟kIFaxap$Db?q ]f108(t6Ч|oC,I^~W+\}u*$%iﳧ.^SGd$CG nX60eC:0E]~wXDEΏ~(S;#goANF=٨dsLy2 Ǜ/~v(al<޾>s @vq4RKO/ڌ[e$(.+}61=n?Lk3y_"~nWù9W4~ՆW^^vjְ2t>}jdL: Zs[/_^$!t)gW&x9Z`ԃ!~6^ʼ.㒌{Zi#nxhxhOn-qyhn-3=sqG$:^N:?ItDlG1"~@.pqV{r˦qmLDDDDDD$1blDDDDDDb>F` ˌ6~i7ƠkCz_UY+X/0LBDmn/ٳ..80 SC+Z"JN+y_*y{nK,a{9?.n3CwNkNvaq}x߭ymŷwl/Z[Ⱦ‚[Ο[^nUx'e|G}a8x?>GA韥IB<}D]@x{~M/89(?ku,ǧE?I ~fMA޽3rkG#Nwѝwr~aqȇnH4 2(OU涟:^6Kڴv9r/t2}4~ǍFL *<]%1ڎ~Ou:?̀Ӧ~qlLDDDDDD$1bDRgWCDDDDD$ۦ_|c|ܱ!ii1 ͯ_8z'3gP\Z/gܷc7mx|yv.G]vO{yf>(]T/n ^31nc: >_YD~h!{ >x6䓏Lu׶w=h sTR8oݼ7_+{j{te7m"6^ gXaO *'xZ0n ]IvA-7Ϲ/v_6 Z~kg+d 1=~\m '~ `HACK.mBH@-/Fَf6) AX; 4gq+z7px9Yiюŵf)W~,v+j%g]v ͺ7NէB@A ӚviXSM='"Oh>,BlHg]Evau'΍;NaW- j2IG|?$s48xfu2Cav%y`1v}-z}D%۹|/8z>3ieC:ok_ǴM-nѳFBl.u <3^zaňeVMc;бOÝWts.jl΁ngMIXxݲ gY#Y8q{󇡵 .Wߗq}`rg,|}>>`q͡!hN2l>7e_6XseUIᾘx[z@:ҩR?\۳ #4mYw?]e;{BOjQ5" ~S vߓdO8_ns(Kv%5{p;$z8y@x{߾I&C8>hcz;(y &ZW /*~W^sO\a_t;p2ߩdF]NoLZtjyyvkxuMKe.0LAئcXFS? 7|/Nf[[طu]|:9d Ư,NxOKC/F]9gyBӍMS7)W\Z("6ݷs{;<`utƙ(:Df&:ٲ` &k"NoVW_-``ktm|5,mo6565Ώ~qW\?>s"l;>Ǧv.ߓuF6ZGtsHbČY_q|,e pU/2&8^x x;Ƭ]ml>hY& DG=g~V& `{Uj+? L3Mt>AY=.rqҥP/e ?e'=mkxP@6;[߹N8 SgK]&62Ӂ!N>i:yto=l)/CZSfiWj, I*$.‹hJPduۣ@w&D]L4R~i ɜSs]^=ΆN:v}"Nqd{WKe'CVu~Z8` H8=ԏ@_ \m~ukAn r-+h6672C%\mkD]O8hV~kȟ>91G$u`r,/Fx7xnH UJp_YÁN޻8t|$w/)d:C~<%9qMض. b[(ENwぇ5}?$)]\WcDDDDDDDI_qxJ/%fnC:Hn{[s/uzlD dI{i/B5T6y:ѩHpշCeRw0D>l|Ac@F W蜣=j.} ^L+ۧjXy~_GCi^ϙ mke9_ܰKd9ͿdӜfBStϟ=6Im?o3d0:`Dϙϛ3B'1ӿ#<RM|nn_B#&s,#3Qc\08hDlѢ8AKX?l{1?e6P,4-~Hwmt#~$) LmKw]}\[#up|'g'(lö>tgka\q|y]gU?t|҅IAR6da(?`p0<' qPm.:d-Ht1znRMmzbǻqKFet;A5!#.;^M]ۻwnQ;'|Lg<:/=;a6~<%B DaX//>6E `۳w]@>>0F>q<^6˺:*aG><#l]1""""""" %"""""'Xd$bk~aJ)͆G[mqRgI]<ՆWukx׹E㖏<_cX؜}s-١́y6|Ͻ; ~Ʉ0԰R{?cmW;ҴK@o.>%ap1OŜ<-7y.Sn\A8m`!fhC=^1<~SEFUcJ )?O1ߟ.zS'` ziyjUW?/ovbb:?W4_ 7m[1=nTSǡPB}-뾷kAhIF%}docV,^bm`Wozv)1BIDAT!a<5INLӜY|~PvE _ 97伙}u^')t2H#s[Lp߆taY~w{:4 :/^Y0,`ڼl[i:xI|=g]? O,DlgX҄>qWĹi=>,m W=/ci~KDDDDDD$!0b/m|3UI7X&:2q/x/N/ir(GT3J_囙w2$|B!ćwuW{5\w%ljC ?-', Yb{pe\U6: 0 0[p!H,H3;@ݝ\]kUgR~F_;$$=ݟ׌tJ#Jvo {&𫋆_|C`뵓W[L: &7٣M4}=풻Vk{H/'96ɥwͿtwG߻g&~lqÖm68YYoχK45ilwZ5)˲~سɻO{ỻ$>}(xW&>2I䡻mZ/ͲNˏug}?ƽ7qI=w`r3:d6?rɪ嫮Jd-:v{wOMiV2W|Wﻱ߯|p}+yuWxm ~IǍ^: fܪMwj߾';ߚpyzH=}9)Y.swJgt4۠VM&Sv͔C1CƼ?C2l |m N/ۿOW'C$''fq~ϴao_1_pvWNޜsg, -w>t̤&]|s7'}39yz"a3VLҥE>*~_f"-K>Ʌ ~9Hfҹs`])p:,~~Z~(M R=8K(Jscc?9Yb;zڗ`[_im$.x^f>3`@фϞp]iն[ȅ5.nxڻ6|T]˯KvYq۶,:듛^g综Q?3<{-6cܭuGy*SE 5M4O+:\eZ*ICXC13RcqRt:WYaΛ^tߨ楇>4sqύXT@>{NANLe-:@QQQꢃ?tjRU*@PTm1$)Oe*tڡ&Y|5odR]37oLX;yS'z`5oF+sTzE ZVul@-l%V[G*yc'ݸToɌfLjѩ`VjuE>P Ehqb:LIǽ`ZEOM0aɇoO1E9S՝UWH7>%+ٰ7."jӏꩪS yXlSO\*:ZG_Mٶoߢj@Փ+i5{~ l3N3sMMZmWtJ>z" `vhq~ ;tK{ϓ):WpdLQ=4P7Z `V*۫웲 M=`7I_`Fj&]3.%31rET):YJlԿsC_ἍoLc+/ |sg(:-*Um1`PNJ8gw:4Yⴵ>ON6y٢SAPT FN$gEg :MR%W܋TB`2_XJ쟕UO]v];勋$Zt*tY~MO8O):MQm_4ڰ)%.w jT*߫vHXtڨjt |]9rFT"mu[.z\tڧM3z|aS0^^8ǘ$NR;Z=Hp^%Eڭ<,ߥ۷H)]R5XjD]#ԬY0ЈuG$y9Jiv+OU*uNۛ,lK=I+:Q4joClfT i7 o<$'T]'ޗLy%)]aTP B{4\?YɼZ_,:UwOsI25_$f*UEWA7OVG?ܑE;q%٥TExsr[߫v\ b;{'y@tx+̺|uW599s38t-oq^ݶNNO>0Y}/Yu{O~g%ǟu[$yc=/AF{79Y7\Oѩ/5J^h~oStowP P׵f6'W~EK6LVxsyC=#-}wLz|ҽYmo^Ir7yI`ٿ nE&'5>23n܂k$+rNSII-7|3)ߪFNSS7r'߼&cgjE9S͓x`3*I~[ht;ItL>i7h'&_q%|t٧$Q}G{dc/'Ϳ{L_e7\feZjWmƋ7^q 7{'CNɔueI7:jIu{u׭j~FUrv۳y]s`v*?H].t1+St N>Ϧy/[仕&}\O^skɧ߾soأT0g+yOV PהWaYޭ[,23;}G.y])sdY:_߂[-xK^j Vӧ|q|7K.y5W,߯/O{ʮ,[^G,[Y.:U4SO$_y]'a5غюM'ԩWݝN}UҪ_!='[T=)ߴ&SVdN_oɒ\yв6xU2}iLmSuW0@]Q^Q:rZNO]qcOJV[h%j|y2ܳo[q VH~۷oۗ}nۤ=1C{ -zM'XLx{bewyˋI6v m:%2%CW f7VdǓ7]EsQ_.auIy*QPg,t:?WoWN☫'Cr!K'gU6Ns_VUYٕK[o8:Od1ߛxmRJŬXYĹ%eE7=g& ψ|lzĦol<K$Jߒc+כ΂IztTe|?ѿ>>Mn鼵[{5N.K~FO4kQEz]bӶmwv{y_::zw~c$7IT SJ.u9˿ m7aM|t}h6ͧo<}d Cvwz̫8ʊ>I 7_x݅AIU{_ $Xs<އ\r.ɠi}RdC{ YjdK6aSlmfFßvM^oKJ&8MJyJ%ꌦ5k@2a؄'vΤJfNIJn.:0'}s$e_NG}y,ɟύp}<_㕆FNzߣ~IVy; 7g:?食_{mdh/]̴9#󯜬|VId|]t_6i ǎy.y;g2nw [$#?NP&n?I喕[Vyܖ/ OR~Ie.?La ׎Iꕤ=ڸSeÉIu-ISxH.Բkc&f%CdFEP*1Uk]e&eOVvO o>l𓒃;^J21neZ*ɴ&6ʙv*R:VL;mVoqZr;ŢzoQo?N.ױ?J|N:oĖ%K]T\rt ?(:Uk{FE~TB!Ovh|ԭ0>i5iNӴS=os?ML7>lIwhQj$ؙwG}ki+5NR$i^|飖jUt Miߧ8衅^9y~ӥT5nu_XdݗXIHt.:U-:-:E8⚊e;$xiq/{?qL4yo&~4q$>Ꜿ>~ޝܹW&wNɻ p ' RLzgfޞ|pԫ?4=aSvc)y2BS^JEI>\-wֻ b+\vXt^oə^qxr⧝}#I)8~9fݱG"<񢍗o<#)Ue<5MC_7[&5WPܥK֨Hu+:ӯGt׵׿WSPT}{qykL ޥ_~̙>n9lvc%0l^ㅛ,xZ:;4lF{ϿÓ3~%9`8hz+Vo2Dy<:''MH-hFM~:ݠv2nڼ~U${lިWF;Oʬs6شJC/O|ުNUWoIl{%9+Izڦ$NԴ 9bmz.{ug>?LJ'4-S0{'][.T_ܕsH^Lj#16* 0۲mRrbv?$ܼw8Nvb˓I& teReߛᅴ,ό/B=cx;:(Irύ[|X{=V7IUW]QhN_7ټŔd7>%͞fm7WMKkw%22IU:[HSJWBqW[-9o~a~:o6{}.vmqF2f$Y6{$dl3_&=?y@A' l&>ǪVtw=grnN|`}1:~b'ݼkdYtS=Nm}o..xFJ0R/ICun[yRs>9i+/jˏO->l}VgmT2'X9IU*^C<굿?rJ/}@dƋ3v*_*^8-{%>>Io7Ms;nwN͒~[ON땆$-}I,Ż۽7zɱS]oNziWMU'Wrρ';;ˏ6\~UoL<;![O˿n3w8KR-:U͞]~LFU|vEs=N(+JCKJCW\Rg/oJ>z~I{wOZ\|]|ɧ|I'>7ijo̿{8 ː%>EO:7˜>Jݓ|W7+OڢS{ǒox۞Juz$?J=uE巗 9?=4?ř;%;]23`nt=YziU%M7xͦEK޻مMo쮁K`WT_29C4I=O$Y|1k>x?mdó7g./ɑ1'/Xٷ{u"Y9҂{.W,:vݽP5_Lu7&>5$IepsT2mVn7N4+=3{OIoջnɺS}}UX'to}ARm2rWFm >vI}cN?CI6^3zK?v5|?},uAҦ_=z(:UkOO_iTgy7J>yꢃupgo+:Mƽ<4trL8O2ݘFmUt*Sʴ-:@Sޮ} J}r;$]N\uwϭvVɠtx$WS$z3; kUm$f#*:Mf|17nɘ#.oѩ%L+2f>*$薤׉K-E>Ɍӷ參jRN/d?IUO.:U͞;Cl|biSUt`iZiw_cdWuӓ$UEg/W`ҫ$o[I3bҰ$U%~T"mP7ߦ*Y:e+U۟RtܶoKI}#>;IrE2i\t:[J``RvtGGy7Yrzg>:I2=h=dwK^kВWNM&OԘGTl:?kf&Kޭ7Nn'folcwN(o풼OD/< ݕ,f:I,:/X;{%ܘGtOT>}ԥquuAfwl0,YdUWC \Yѩj/wOE2CT7zSG+Oe*uU5*[o;&y(S E9(yz8EF\ _sTʞ-;fI Iﭗy~NUO'-{-L3m$2m5Uzm[uĎISS셡wv;gdċ_5x$U?R xv+Qz 梇:.Y&mId߯h$o7Mn;owHT ګԡI{^]]tTV\MVsE̳kѩj)NE=Q&ޫRW%K{TtŃJ^/W2yǏy7?UoTTm1YGh^ĴuݞZS7ϗ}EY}v{Iu[JVmy}K'+ܿ٦45/0yFW2~w oTt*4LjPĕVɼ/rJf?oǓqI*Na|00TlvPdջN=-#/TB``V*[֪ހĥ;deNU;5œz{M7>ϔGTl=SRۢcsfUdy~I]kV^\wϻ$ܐW>z:!WSJ>9'%N]&=OkZ\8Ճ~s`2'7qd귓80Qo@mPpk.U]]th|ӓZ^,{{IR1v]6.:U^Wn| X4~\RJ:yH?'KS{ IlZ\;DtC2i):\y*~^nˤk5i{=ZT5{k/ueyI{I+OU*t6v> oӋNS'l3vHBߟ]䡜㔊3U*W]582Y貕lrmEE[ǃ$_Mf\1DuWy2:ܩmY6;j$yT5{[=mîbDs,:~J+YܡRvHz޼kߟ,pmBѩjw:<1ydz=<`NT*`Nt[ڮf!4:-f/r| oŭM:@]jy7 Y-=ޢl'_L1wL:dܓ TP2i[t ;ޙ$ ¤MnHX|NUz#;& }Mf1_%fJEZIVzAGuoov柒; ]qJc_K`JJ)~< Y`tS+]_d`7kdS|`lPW*h_ZNj~4٩GU_f/7o݋J=Iߐ$Y>' J(-ճ}_~dOrsu'ʢܧ4O7; PUmmNSWO]yisn/;cdܼOѩ$)ͳp5S Eڗ/?|U7<ѵϱk,:U>>ɏJ>Zz-eTt*+Oe*ҦP74*;%4ޘ]vlNUOFͣ~d5jT~^R9I>W_0Y+?/W|W&&zn;$ӾrTE/rLej&}[YuV\MGpDtSSt .==vgmdpǷt~gT5{OgdoLפzgܓ$`NUюw{ 0;Tmn '+ՠC*:Mf2郓d?F~uST2mFWew_b6}AkO/:U>i<'%,l̸`i'I*S-:>ߪd島<|ޢ'4Ptx]|2j7??Ɉ$T;qnE7e+$^[tUG$ 5[-.):U;^3o&ޔΓ MJTw<{s Mʚ?YÃ_}ƶ߹T5{/_u.:Yy*iSt Spv= St“ ߴ^9ILHRRTǻo\]]tH6;wYifɲor~;fQg$/s߼LFPt*.+Oe*\9ۯg' ]z7.7.xNUwy阾C 쩲f\~߭OKf[CD P7҉ QqPۡKV*K\:P`L*.l۫K(}G䛕>yŒtOrvѩfT2m?+Y^$I3 -~ɤM~$IeHLu_ G67Yt5n~qYSl=ώc޽\_}L+S:jJ`ժd#nIE43S^8iIS^aߍx~PUtBbm1YF^MO}d VT5h׎yddq9I3O*Ie*P:_8̵<̓%+{_^t_ b~*UtuJKW-xfJ{ewٰ~Ի_;G'/p[&ۮJgaܻ$o5xMM)OnBET .+Oe*Ҷs6YEdBIgtT50׵M~a)J2531Iԩ :EIl~=?NƬ;⠯~C_y2q}$if߿? lS7>=Yվ此ò=?|yRjFMl{)ui頞TW۷Oi84=Z}tn$횴[nI^6ksRbx1i4QFO% 6d~N2IVO&>q%ï~􈥓>Ͼ틧?x>xu躏T0cWUܱA:mVWNSGM9mIw^ mPt*OKNy`fѠ#_qW5Y5W%Y;,"bC:Ytd1]9yuҫvj\gJq{d[)Sm[o%{,FCn{{իNUAXyhzOTsR%:1IK].8_v:*iT7Qt߮zeK^=n=(y$Y__`S~ß~]+^kO7I&~;#n+'#n+'#5rɔ7qMk@-5/mtrY0Ytlfѡj6|/ChrMɔU&8Sk*ϩgOH.rޕPW^z%Pۭլ-, YeWZzd?mzk&>kݛ}v8O#zy`݇:fg7ٵI,Iqjѩj>O׏~tPIdD*:U7979n;.<4ߨ᣿<:xNrѷqrҗ}W뽋^ͬWb˥Z}m=znk%e_rٽW2TwTݳU{b ,uHij6nLJVMM&.7ѣNUrexNӈ/~}O&K/'ɘ?'W]޵ÒθKk)zYhӅ[Ir'~d×tٙtLK޷lrmyaɴ#] N>rE>1s\T5{_䳕>繕ꝫUڡLp]g&ɹCϛrM< oSǜ$#E6X~k˓>82it[_y~9'1ߌUt*fR:XuME ï=2}{$=ytrЗ7 y4ussoE sIp֝?GOb|zzo~ħ`|P՟;4JV~=Tz-:U^roV[th._v^vs54*5z(ٸƻnpJɽIM{^}dkiǻ!uWvp+4NAW~,>]ܵ$F {aE{,,;8wE[dhϋNUwx~]$6zWN@QJ]u^o)J_>NTw4s>vhҹyh}?I֫xMhzA2lϛ74 :o7 :mBI{Td|e:1wЯnx@W7~dʛSt*jT2mP_dG:p緓tzWSOp 'Lu*i[t AldWY|O;I6;idWc^Y2GZ,y9!Ѡ ?]tzf6+t9d=oy\PtFܜv]zG2qq#?O gIRYtF갲KYNjLRh-M*sb'm[o䖤a3iO>rɄFvxn_4fd=~xZ_7fo+v>}Ev|,2/Qf6>9iѱ2-Nƾ4$Gsٻ%mܻ/N ʔ~{GV55Yպ&e7^vޯ! kJn8go^!vs>V^෪wNy~,8GmT-}aѩjAK|v[>PRt ~Q'z4:>C{|]cZӓVyOLF:hQ`TQ+zU*[m|&6\:9#6>x|Ѯ?zѫ69{볎9cnx浒.՝3/ow<{ۅ}N8.)߷ufݺ_?xOHvǮY]a_bt YvkEY36qOb;>m2v;Tu_[$+bKn+.]P\ϻtMGdL͸J]w|g^ ?|.778{goC|G럛,.:<tܕ.:swJ&]/r7& F]St}|Y}p ɴ,ͱ'~>y٧z߾Ml9cS&% 4lg G<7⊑%w\U{%Srϋ~v\h>ũɒ7vT}~ӢSլ>ꭒaw|6t.E`NѪ_7*6OVpprf{fyIEΣzxNuM_do}&<5n丆?y2}Nr[V/$ח]ҕf,Ղ7`]9vqݷK^X͂W{;b&]1E? .J}>;w;ܫIZU_ݿbKNf͠o&oXuÓɻM8flS0hT.K68ն/:',qþo1@TTM1]VsWV9"I:?S>>.9~qgd2_#q͓]uLTzϣO|<#[>c޼ݵ[_rѪG?o6|H5ɢy$mf,e%2kcPڢtNMVxwe/EIN+:¿钃=~{&7WE<ʴ-:B /$ ?7~B|CdfO~WNo^#$rW\cr5c/y-/5G : fyv]|'ͳAWNǩxt5ſiI2o_7)OU*ts7xz+tk~I>MEۻw=)9:ǼLu[:IUΎ_pS2 g3vϴծA^E5X:m62|-T5_ɧYIu13L+`++ϲedqE)βwxև]XdƌXTP7*NT\}+Uld$_ɓ&xջ_]Ix ]vhJ76hOh}#MX~ݓ&eҟQFm0ɤMڤo9eEtLe.Tv. &Ż?,:ӯb;>dԑw|Gǡʢ0B7Y<ɒ;/x^6'_]vP𫔧2:{lf4njݓū^iow#5rM^s'3q $o>3+'mڵY7߸F$^^?Uv|YzC,:}Egss+%^L?tSL?"n}{,Yta7Ywʷ OW)/UB9W,QOo2hʛS'f?ɣ,ot{ݗ|3/QRj-3PG4ݸbv=l_>L_p&u{d_/;h$usR'Eg`ඦw44=}MFhzhMF& /nzl')ǍhEڭyOV}p󈃊N[y*S\,e)Kf_/R&߅EetÖ;%K4X=MZZgSl.tm>=WP ޿U%0>O9dTX4P*NpB<{ ML]Tϴ[<ϯ}St֟ hk/J&M?f>8R!XW_TyeWpۢcwzSg2G廔ׂ_p&o'4[}v$m+:U޽C6^"۹2@-v5-J>i^5MKR7u̕**Be)KYMeL]Y#Nt}45ք|r}].0{F jt$d\RtڧM֮Asy/: nJJп;ϮZ6^ Hޥޥ7]#X{KHd?{$u/+9{<{87:{vNOru]c 6y 8cJ!mzůo3Ϗoچl[%VI,i Pȹ.T{/. br3~$]t@}f}?`jlbYWnwnuI{K|"j 1g'(ׇ?Ltd>ǹ2FV_KVi4W P㗖ߥSvYZ]=%~Bsc[)#-E^oY9IJRg z1ےI*]l6+PH8gTCnj!$pftUf-}ҭW8[7OHaU ,'Z5. H* ϷOxMk5@Œ?Ggk"7hUIJ* cұ O^%eWtNZltq9ϹQuի\$mEAG3K]l62y/]Qys(H;p/UҲc$9jtUU_{cIv1:Z(DI4~Zv<mT!cђsד.gY?蓱A\^6(LF04JrGsRw|J:eUOͤii^ȣ'*;%-uEJb=kF^4*hu tJ dwakOKuO\ڱB2R5$y/jg x&vR~{*LliuK%ZUb=#pLA6ʕc`0F! ;={e H$v_'2M5 j}VR+~%U{>FWĈΕNY5@b#/y &7)`LSB?_'Epںʒyj^w%! yumjroH4MIOIboqj1$yif].62uQ#cNiSitӽ݂_R"O)$b]9)xeHe5 ^,imNОIR/]X}6'cr0k(]sRؚ_6*\ktOr Xo0SY5R櫃~kw.F:?{RMIY ynmK5ZwzAr(kmtk6gl%iJ$I_Si0F{'SW}}~t!gG7rߘFP(TG J]DRdGctT/?V:Rጷv]x脩%.{Kw})乮(!FYNbgV>o/y4')dIe.Kr\ѕ v5@b z!rY\T-f + T>!sZ^tyOIFitU~ҵύ8,UiS-^Cj_n5Frh6>:99ٞǼ eJnyK YA?]ž{~(s%.1(LA,*EU1O66 8b ţJUvX[k,Ӹh ~̐2̷LntaauRfQSojpCbt;6;&ȾRD !Q 3zvҹo]l**e)糌UrGY\TEdWׁ["] /Em0ѽoFɗG,.>o{5*@Qg3Etp~1*;8xNȉJKՐr>:kyR'K“Ͳ i)9/fb__*oLœō(atb$;5&9NY3*HZ4 o\*@QgwʹGz~R*ަrUtKJ|rx,7{)|!>iivɯ~]DTdJޭKxÎ7?6t!#W2SN?y=SKX\TE)}|rDTNst}_Im~`doW.N/k'g;o~of;oxL{{ggW$뷬?~>5V7A(ۄڶœkj]C |u_&v78wIyEX0~RعƕJ!(UZ:0kd)쥄T:ouH SЎԈb,.ĸ珿8Kf<>Aܝ J _'<fsླྀKR]D2fZ`etUv^tR׿4@Q')TX!,*_o-,'qޥ~ &%޸=X~]af :hjXpKuT|f>wdNV-^[l'H잳ѳ346͖Tk8C wռm=w%K6nxSRΪY](2l c7\{HQ_t<Vy?d,'Աo<.ع鹕Rbt)z`,.l޲ԤJ^{}DÞI -\$h;M_Y|BJ\5ѳ0r۫TJOϒ-zI'Ow0PT<9K2o%:O0r.9rtfѺ ť2&]3eE<_=w?ZIֵ}XV|??ocfVV3iyIӇO_6{5*j*u oUZ.9t 7f8JYC2b6 PYua J4*&U߇H6޷.gt޵.ž,eFܾht}`ϲd5cq&Gz3|ᜆ돓LLNgx6S{9]n X^JMI=ѳP9p *~tZ_kow4~Ë37Hq{u?P\RUjmU_߷oE>mth,;{rͩbt{2-"e0Zz%_$[[]J|*??oFG2IK_XwE4SgF@A\* [Fɤ7H{xt)۳](l9tj#UXֱQx?,omJW>r=Bf!pպcRkOiJ6IvzX-6?/%?u+{tDۻWL{D|`=VZ,58Aޯv&5z}'?uv'I3>{ i˺Hw0b;KֻeT[U[]gv:,}Q[Ik23 P؇α BvԟW޳)U&czii[&]oqj͑p^9qUZ5XR!5J4x܇B9vsi {v;N|Sّ'Sp#o/}GIK8صԽx]kJ%k|~޴H!-Y^:qǓ5Kh;;TjW3&y,`Wvc51 P8{%8Α'6{'_ |Y" ,uw~5t셱'%1?F=xNfr֒{;!u;_(}``jRJpVF:5𵈍,eXl@jh^s{=}d5~#>4sČ_NY1^xg^cR =XlK+)J3*~kS|}W*htpe|ێ$ґ|]e9TLX'\ZsRݕ!%piZԬz^>å:]{w?67FZ5$=:jtP48`otV2t}uҝe7:]e av nyCä^?mľwHVN~yLUIgK?~JenٸpfwFE)9f`*]ŏHݳ{,#uw?柟7^JuRYѮ/{ޑ~z֠M>Í N~Co|iF *lH%,T\ [';.yKRȴzm*xܗ~YFX)yF)x|>,F`jiTZzձWR5,Yn=ҺϽ/dBкF (Pdհ U , ~¯fB:>c*):CW ]'UjgFWYޡs;C]^6)wJ)U-)(i\v+J:9>oatw#.x3Ґ-}(u55 (l%ܛnyOK)mRҤOSBRK9 ˱ɡs9B& wKK7*Zl5;D!.*=г;7TRUI^}/iJ*vyFXN񒎑?H?4Sy]N7d`LnXj=KVfʊ"ͭ\YEK1sJe7HUzsJktV'Ro$T4 PP8~QǾQ㬀Rlt够IZQA:lVǤ$<)sF>ҜW5ް}O,.xIft P*ߍ(ux>Bj;]VA1N\f~RSٲWiLbљ[Iy',lV$'w\VۃߐJ}QwUEȷ ˞LY,n]WCF}, pz5+awb2lݶ9f-o(*p0`fTKOH~du%}FkK'glr\)g~vt`6ɺS9xTuUg@WT\{֢_N(]3:))+9Jbݍ!]8֔$'N];Hg'kt PBƒkx,ui5KR{yt<=89h??oy#m2}3眗6Tom8Qg<n:KRUKY5ߡ kl⿺8xk26RZ*JQ7;*aJW~b)ﳜIyWo:m {^R)x;}rk]lAg0[=g^ݚI-ZNl>F+m{柟ן41Hҵ->ѳ2rRrTx۽Ocސ礬N-{P vR+*>iHUS_nFcKOո!͎Fϼ^5sR>%})xQ7E]l6򒇊xU5FRkR'CzFu$?a{?Qiୗ/ș^cvIiC36K{"Fw7%_ aad?ҹuq#bK/ntUNjV{HE~d({'H5 Q 8^_;IS$?^tf^'HE%gѥ+A]9tH4蚂'NK$=bt PS&>I3E.T~J=VMjQMgIUl[ZK(l-M缶0LsV_%.W?k$So~UwNa祪=9V1dݮ.|RzjJx&IuCQΟIڅ|Gw.-鸤vFd$[~y2]]]Q%FW\^l$r]<zO|,xgGЀe CwK3cY{t3?痜 0uMG R٩u%?Su͓FWܼW},t:]yV^1!'@v <;$m*첻E)Owxyvڿ9 5./iyU!I-%$y^^dڥF<+1/{L!}i6872cN&,L|];٢i־s/H/\ѳGSTNkltͽUyA~R:x5tm|VzeIUF,k?FYιa_B:?jOwK)G]!ӺUO^ǼKV76;-#4fKcF)dlдF,. slW;--mrƖd>ndoe?_s;}Yv>n<.6Xm> ՟߼̃{>LV'$4#zUذ5X1Zʛ]NO{r䥨WeD2N\^\')߮Kٳ҆ 7 Q6'ʨxۘz%}*<N_xȀ25hydwUE96节+Q=c|*J5:%z?T67f7J N~e{`j|IR+,P?']|RreMR!!%fz51󉳗UU.ft PS1:0? @XvTdDڭruw,Ijnt_ʅw[&\$Oy;m8kqRġVφ "ɼ#VnZU=%ɕ!XSd8GVΐ^) \YU՟NƿybF)ZPz<l؟v&En %:e˿ltUNX)]ueyy%7Nk1)r~aR!=Gg9qGI'ˬ|z;M}HV(q誇s: ]}Gc SHVCfQL~/:J՞o:찘~ڈT+i?Hi)o]ȏiCz4* SjN덮k;߸V:˘I3|}c%$tpyԫ~tfm<}6KH?5@b u<`xP&X)?U+MT_9uv]/Y#,?4vS/|)wU?co5!Q+R+#Iٶ=?5rN^Btaݶޗ7JYU_LhiyפvQfs[=ksä2ʲ3(\L#"o,uf1R=Z:Ekf2gZYgW:\|TR%G]eyG;.:~MloX\s$sdFW~]gjT{h~ҋ*F-0 K.Қa]NЊDX\T8t, ]̸8蚢;;ثxzERڬ|2[Wc)R-.]kNtf枒ˣA9;_6+^1IO~)2аFWYށ]3.\R9rmRRVIC-4v]WiCfIKFW)Qp,.l]wm' -bĐjRb6!]r~בU>>%0of#k!͎/c!maҷC>t阳tksR |Y!ٿU;EԤǤ?6هsC-&7i*>4gt uFQҍ2'(<>F~)^yGAy%J-ϕv*<땗 هY'X c\So^{gWy]n.}X\XM:jbw~7475^3 8o献-ʔ-\ioiҭ|[ t5f->uR.] ko_n[jRz'_=C:>y̹6RN̈FW~0J 6TJͺ~Ex=V:y~R o%ϐBojuתGRp]7[8sTkg,oS+j!k.֓g$YL-)חO]wK=yE] ЁE5fpawϳuiEO+yxjqtH67$wPF P'iGo#ɳ sސAyKOzri-Ohc)fo-*>FW$]z#$)OFW\}mׇ4JdG-U̶4֜R^m|$NEW&Wm*sP<6fHl{]gy=W)nbllp)jVr?xrL;:l_۠'}1> +\>]٤+'Ds9@Af }%,.*׬2Ҵo\dߏ;{l鉹!oB:Z'/xqVJz/]z99>̮3?j"xL`8w}_5&+otthG~,e6!VQ]TVp}TSß[pM5:rbwmt.4Թ59=*/?Y&mԳ41+zp|,mi\mNy7y(GhEv$S{ӧnjdyf=,NoOJ*h3~ys,.*jW#ڏ҄I)s-M=/8'ꇽ^b_zg9=^v" Y§.InSݶ~o{?GեS}YRl{m|WZI;HOfyU1eZ- [Ib3} Mv&,\mnsU)YJFW~qa9Ց2}J݃Bjf` B)}5+wP ?ٟ7(i՚t~)6<)C(gu YTAS]R]Wrqq)<6?tiDW VY齦ѳx n*UvSF/%sYǥG|u|U𱲷aR*08Zh_FWYމ9@xmǕyR9CgGV԰AFWYn~EvloJߧL{\9%+yuGU˥+Kw&9ڭqKU/'YZ=4vsSҎkI޼>B/}f7_it'|Aj7Y*uVHȧ"^ըGឆ}$UTݻʆ{?9 sꥧ?o}ѳffRX]Z-J,[d gI1?ې$̳mkt<J$>1 5cm 7k2_rm[̦U2GK?(yͱbRHϷpFxXQ>R^fy=ΙM$-n\JfKAOvG "4:8KѱjK<{{|/gi윳Ҥ&=YoFV t`YJu>Bukݞ?$)es95v6] |EꖟO2rvyQҾ*(]v[^R^\ÿes;SiLI?55`XfL,oՙ)Wlomn_*LEmaWCi=w9)ٴnj' U))OOe˶w'O;;?$9urz)Yr8߱s1˽.thRz"@:S̉[` S@RC Be!Xr%=nҙһrKe˪dt}z -o4&Ԫ^jp,'[s ݬ~Gb<|ud[UJz/teFW=,+GN_Z3ƮRO^1:~z!8&$ vezΖz.O:uٮht?0 ,UU*;lnXm)p[[Dww-H%[a~$۲UmӥLϬ'w&׸.#{n<;7%*XQF 0Je6)U~:E,) ɯ]EIMQRդt\+YOaU:93}힘I҅uI)k s¿eU-RW\zqw[:IGNֈ}^D3+U`9 X >l")·^{GVr5*gV|a9) ;HzW9FW@gu1hU;Izl*;tىR۾qc]˥mٿwqGӌ9-o|Ϳ)n )yujTЄ 0󁣥j.m_y:|YRWgI;I[n1 ?5JA-Tvn14Q[k9<¿V ӈ.'I-Ǘ`tm]!*<`9>R*"_&ʛ]};n7UJy5Px99y9m#'̗5^(y*Irm1wo<3EJh"i>OY=nJ7͗*ytl$v?]gys{MVύEJ>u2gUP†ot)V줪[E9Hϕ:M{<,oڧg>(ze-g)쌜)lk ,aR.-2DtMX\VZUEFW^D#ʍ\mAG]_V-Uk1oWdҶM`\Rf[ܤl=,m:֖ZҮv}>pѳ+P46*gw7^Uy5oUP8OM<2T6ŌFWYy} s4]N747"ou챞*5VғE,/G|)nxHRyk])xȞ,UUW*u Uj;mPsK O;vm3s+oj)pV2Il{7uWކq}f}g lyG@ѤĔ2j?sV \p; W]GK!o6Zc献2JszSù#I % t}dt)umrjH+1*˻Ρ'[]^%Xxv))#:1ŅXP*"uԹlǤ΍t&x euL7n3Y_o>lsmLh6EqcݘjoI{7{R ȫpU [NHyƬj}rW*2+䷥31͒IW/RRSKJE]V_N!}_j.cUFSҬsJ*NY+P!(^}ZRi=?ϠO{t'NrZM'd2?/G=Hj~UK26FWoHI-_'IW !dr$z"wu9$Yz.y7$]62ŽH{a̴%ӫw^?ǪizK=g+W?V*S֥Ӓe* vÍ:Ҳ.+z)wvPI% #SSHNŦ UrE ޽UFRVkNr9=튷oϑPwƒmeW.#W ꟏?CK||PGoT+[tnݤz^~Bj߷GKJn~xL0]6ՒJHm)̾VV[+wKޒN쬵^ցFW4]ӒՐCmV6Grر|isؕҹIJ73]6TcwkœvLMouSSHNƽQJwʭet~3 u؋PX-^ٹVRrO]Re|-şP%7Wz{;{_;V XpmC}_(ۚ[yײn#v7|+muˎ)]O}v5Pt98$U[%<jՐگZݿʷ=oϓUqoO.^,G7.~^;)u8UUՊU3I~<-VW<듿YRK->Fg1Y}*[g]t%7nuIR%mT_@/$L҃z=Z=d݅Y>nM+eϘշ'ƶU?z:#Rm+8$5srkG)raaݿF5[gk#ť=u`R?z<l;h)9SII'υ,I:ht%<퓋&GIyrnI tKX/S(iQ~RaJ>*U>$Ji9[ŕI/ͳ.uj'3wHv{Iv?`70}k]vI6-C4EFYOn#x֙S}3+zIy5oH:{w P(XU5Ś&Iʻc Ǒv$12r捉NRr9 ZCjjicCo4ȐmvKQ]$EI,iJKg^T5J.=WRб9;U?{󊕼-/IUSz6jpAV_~^ݿ)44T:߆>Я1)-oH b%GIr)M䠖F7'Xﵺ%ZG4+U$4ђ~xękJrNJR$//ȋOxp$}_fK^cy@ u^-+/7bv.}%&L2ϗ!N\_˾]b =Z5II % Ν8g!Wu+~L7wC JZ@ϋeKǴBI]$䅿ϚJ{\ێJ})$\\]Ц\HT46Ku:[FR_ΏCR^Vni_Qu 0EI6n : ++ȇ$RW#krQO..Ҽo=^F}i&k FZxeH//=~B۳i t~SJ@ZIUY)PO[ [?\+9ӛ(sԎSzH7(\?0oixߗ]jE7޼9Rr{}[TЊ蜎㟏Z\ikҤXuKad Ꭳ%۟m7I^Hm-#qҙ%/bşs[vԵW;0$ſo7_$vW0**88qTZ=$7e=)Mό<[K|4_<ڶ*mt%سRt-/2ReORIcmczKUMRHNUʶ'iх6r[Ebܸ hny]R3U$P$oxS&v+j.Y;boVŬ2Yä)~~s4y-.x;Jut+M%2m=/ᄉs'ҙ4>,i$]W$[/dsR/;thgw.KŶ/}\ixa_.^vz/w`o8u4Wҥw9$O1m\O2B%5%ۥ˧CI>c)l6WTR9nKCD^)+dZWT_3rwRZIMcrgH,_Iu 4v~H =!c%IIOc9#igпNzR;|X?~{JK8t5)evߞ?w.zɭm[\n9ium[k )J+/(a`,DNpG2ewz2etX8).=$=8|l/)ףJYy3OHzTXIbu08Qd{<Y~ժ0 \( qkc>>:'ߦ{J^ofζm0IzX,%xSE)U#%yJ.KG7o*eIp m @!fQzK[fK*`tjnUJM]NN$m3:̈́2_t:5 lP|6K}=ҤN}}tWbzRޓD򷗈`4yʃ7ƃ$iolaҎ}p|LM|94;jR\A#3Kٽ/amUhWqDBWrYq-af:fNr{!{(G9Sa_qT{v.;riRɳJXr!(Gz|h/5+W L}RICFiuelҭw{$̯m\/J\گPsDpI@FYξOGKۧ3$P,Oc@lA_dUO>??>¼=NJZ~ժI5m  W*IS/Nq^Kjz?wc+""i<p J&rq.C%{<.=s=o(U޲%Tf.S2t0qIRZ\ʍ+IhG Iū:-ʮk"T2>vR%-5Kx36tb6_^NILk*4\r}<:IO.;Y p}q$^3*Roä[g’VA$BɖEY쐁=?Sw,q4iK_w?5Hu.<^"R[Y9-$;v6k{xh>>%+(Tl{$'=crWν\ z?ݎ ۼ%~oُ]*kqvJn4o'ܤQ\mtU:o4@j~wɻ]nt9[y|R[ο )R)F'ZBWU!.TeYj~yI"IFϙ,yRJtdԁ ?=dn0I;F7Z3&Ha֓Tq +ۀ)jwK|J|M]Ұ~?moRbݥ}88ΡTs35$q:,jeI ޿0Rm$\^qiqwޅƀNlwvO ]dr{3[RtFP]yY;jX*c.O\UPvul@!d#/;arL$}߿050u$筻:Iv U%iZI2y*wdWRN5::t{-ݺs,!w?,G^g%ͧ@z<C/TBXie-=3/]gg֒:Ҹy+mM]FW@:_Ϋ+Hv&闐=c4rFW-oGlm!\5FW ^)sk 6\k/-,ɀAђ_n}^.6^RVjA,<+WFqo{nZoҧ??~4cˤ7,u:{D:O$/8.i^P_L` oٚsNTYmbZt'r=&w9*MR_r%I*ǖN^|t勍Qn+@ZmK8ki\g'׈Αy[?%q? mM$J#}HVluIR]sBsk+Wδ|D d+uJ$43Jbݾ$}mt%m;}FhiޥNsZ^I?^_$ICu]#$I\?:Bб<5Yr$DuޒM>6RΦ9lKچ^F6;3IR>־!Fe?맬4ܼʸ^IKѯ[ %;~R+^8&;ԖTK%;g_sL\ک i$קMK$R޲XW_yxt 弙<z@ܑJSӯL)t%I1Zftl8sɾҵUǾE&*ARTE.S8ZT|(;?FWZ| V7ms)))8kC-d( ~7"zS72`PfRh1!S1'2sMoI(+߱[weGoN*MM6;T):8.>orr;, Zo?Ǐ~<#qa/;]V2آlY= )OfYtU=?ΧņI:ޖt1Pt%z$^Ipx(~|g\ IXxoJW.JǺn(7}98mkEYNZ:v64mUfTʯ ikR=F:Ϲ.Rb)K$澒%uĂG6[GK}Y I}%g/ZLWD[5RHG$9$ɑK&v.;R/Sʽ=/NrvRjԐmETŠ>n_IRz7VäG?֫Ӟ?ݎջ4X(d-q}#:k)BU~Z橻=X#s ?Kjӓxt{Q~dײR2"]ӽv/ڜ޻7U[3M5$ξKۜ Jyl1:ꄾ5?rjB Iq$ dG;qPsPАR&z$0͒&A6l)JZKzOtPѱ26dԢl 6RM@ru{طHdp_hʧRy/}^'[~oLqIvgH+lM\%[)`p2xb%^vKD}e';9?1;Cr#:dQaWB?Ǣ]_IJZ߼ק(<\VHxUJ K{kNREz@Џ$%\7K_>A:hC42:Jw? gRRR&Kυw{B*Gatq>a(-FczYC%;`Re%cyS|LBF̍҂w/\|Gcq'h4mv?ƚM}y$d;.ZgYҢǯ?qw|oK2d$HMkԼ~4۽㟏twao$)dnN ?xIGu{4)ୁ{h1OG-W/O؜*&i{]i7F#r3*B\KXͶy %8^nXV睰o=]J{ė{MpNM$ =ldU؜i/ [[]yY<-$iKrrAf]t^lj/ "Jђk6/KZ2rf=:i]+9J=w{0^W`16oEw0Hj;KCb}}UUeYfMՄRlSv;)'_TuvI}C<JVh`ాߦ/N K@!e/;@7w]REUZ]Vv%u'MORO̲jo@coTן[h+KZ޺uϧNM.{gHOddikwxꛓ&w4JtA]tc{I Y_Wvjt%U]t0fƁSOeno$鐖IòJ5u>o_z| #%*x|(e撔ҬE^ ..#c P$v6@<$-nr_0w{'`n]&uz#JJs,e+;%-m6ٶWK>{A@!s;[I}cIWﻫTakڑWqͽ}y6L*}Q5Mo0I=5B(LLM}MoK% ^\*6'JzSn+7ƾyjtj/eM| xE (tжΛ~$WJ]w$&`ދgHF&YKw%$Sj.} jHk\/8t^nF7ZҊ.--!=Œ{$Qݒ$$GK1M;Pylte`kXjw''.ω6]^!d*Xy Q65Tflt P4}v;?:ouT@m3J>7kuxa EYM?y)@izo~Ү;M+Rc:eH>OG]7*ggltS>ߔyUM)%!-_* iiWCi,/]u!m6FW^yu8bZj#wKw(*˻'u_z1OO.L~+C\yuLU, aف1?-mvۇlt  5iQ_nzꜗJ)ėO^.Hvl/H $$&K7Gqtx¡HC֍xLs|O}R59IFϮ<|Iz{59t[@e'lR0 @QdzjUux^OIURz{E|+Y-.idtjmRLJim+]|VR@1^=B&UM(vҭ_ҤܭџxJZ/_L{VNf0 +]`jZH˻yż}xƶŹ6͖s[](miTrcA>ݤ?<*;~}smF+)CVU6J)j])W-IҠc_8ht-&f΍*ءJI;̞et(L:Uc0 {ˤ^:2,p|o)7/g](:I*ݦfjɦR96j]ey6,sAVXͤvFW!?v1-jP4dj{zp&pjcҶ׺%Js4 DrWRswQ ٚNZzW&MEPal bA@z{Z(~񠯄2>; <;zjSZD̄ QC׏\y9":qP~ujC^ĜЩ[BKW_ןz7~,9β`>= N?n>Ϧ ~ U-{eT)|U7o즻 go%wD$+p-8 Ew}@y(V ͮ<.w yؾpaٗ'M̮ hoe)p6tkc̗8SFI+lP+'xMT6NDDDFJ6TI +tc1{+#{}7tbF͙ x;ݼ_xkmI0ociB->6,w]uc۷/xXT̮ ()[@](ޫp^(8JHsfWe$M+u4\Xaur#y]ECd=qm"]}飔KûwCWs3f׉ȝ(vNkpvN ,> 5^:Ə򜅿Oԗ.<{iw O5y:/"""""""""""""""""""7Qiu/1rn(`w;͎ O~qSc1'5*6z*8 JoIQP{D2߁.;ZΝ9R*#/ć l )><*h2bmGgϤs3JDDDD U>xG2 e_O`ǁ}ZԻS~9mMT/|3DDDDDDDDDDDDDDDDDDDsl \rʼ0hvՍvIXE"n;DDl,;ewS^̷.4N '7 8{[x1Hmv11 4r۱PxI!#ca8mU|HٕgBrg| ]L/n\爈Mf䳕ŐWСm%T3mˬC~%/#L)20G q($?q~򅥐Wa :oсE{ϖ?hݺup n5cBM~K)qyܹ`;  ҿHqWDNb\7VA.%ޏuue3ON#W^>U2]%7PRZ*xhDj\fW:_l5X{9Obp{wU"""|-UÒ\ \\m:>4腣#呏@kjQ>}M{<.KFO?]챻v[;>[V}ol %JjWͮNؓkm^O1JDӖuldιPqiJ2*=8Б=p"dûgrg<̮ t5_;]3ϗfWevw-{ҾWu"""חQigu?jPSՃq.}]O^RR+\J?5Gׯ~ p_vwrW^| ԭʻޅz!uBAkB3+λHj}̮1o.H 6!eBZ]%"wrg5?xWy2o̷?c?޺fWɍx/:/"Ԟz02ߖNF 93JHč: -Ϊ-)U;+Ҕ++D;xI>߫f׉s6#p!h|E]7o&~ͼ߸6/@oXx8UdGdžzpwi{}o\S:!#>o>9?m0'km4LDL@w= v@b badv.: RΏMu_ۙ8CͮrFkCgWoD]y5Oz9}<}ha_֩߁K&r'.9lDaYV:lSOK .X|  j̨f;g5W n-py廯& ] D>"""""""""""""""""""7׈"mSb>(}nˎ}^*gݲy%M=ÓF9A3bK}" $Ѕ$#oc k>g,Lz7o#NX]!ip+1d^sW0yܑv©[O!|X}nEDD$+Iu6 7{'M> D?묯O:8JSJ4yI@>dFG//Aj/""""""""""""""""""gw{!v@< /;S,{rE)Eßn{HWg/ nP${2^jT"""؈47dY鏦/XAox{}"P=7u/hyhǙ]ܒskx)߸EګZaJvr#+x3f7/% ;wU!tJ嵿C^P$wެ2ʭd+qJ1+o]:tNٜ^t\; B>'P}jstGOYYp 0;WnL<V:3r{H[طwD*"""ٙ" Q֕~CIQ4b\^Zn{a^`C1N2-̮JsZ}hО&T1-^obPHYpي .P${p E_*|(d_u7kCKS q\4E?8IGFƱ rD5Do.cv]k,Cك_%([hDjb#p}CJֵ)@)_7yIՆK`_S ~_o`0w5~I8ѥǞ <3wݵm]oD0O3@DDDDDDDDDDDDDDDDDDcX&(I֙]uc'챪+m"+<eu"r :p@)(duPbyO]w%:];e0=.8XƳi<d75P`#Ы|Q!炯^߇w͎ M46afWq]%">OS"͂2:,iި̎<'ol98 Mӳ;]~knK0~"ob*\i[̮uFamsBƣfWd}FRWY^;??nk-O?95blڦk DвEfua/y;̟?׻>|`75h8YϿvlUm l?7fu6~[cTO yV*wmgSwlj:{?#ٲX ʔx5cЧk9$hn2a'sѠU#OI0JL|@hE9@G[t̮|J{n{agKRƃ/_u""""ُkjpV6(lDQÜ8!W\sv܅9ܽs i[=ey{ı1p'gSgeɳ7O ;}y{Yk1Xޱ i&/7}=vBNjmoADDDDDDDDDDDDDDDDDDf &(۠ Ejvivԍ]~찣`O'?N~* _֓ap^3*3'Ho89|Cѳf8O^O]%TJ&q8b;6NDDDD>|H u/~Q|?jٻ6 }woX_yx5۲D>PtyEc1ƏԐ>L/~wy||_[~yPn(]^gvՍ+*ǴGC+lqovLUN5 DWy.o[bͮ|;R&W9Q'^NnwU򟌵7BS9n((B[ fe ̜y<*>\8^Ե*3FRVN}]0c|_e!\_t⠧_8 &-J_?OOMkXq'-J_"<~汞_G_{_;""""""""""""""""""")J5-oMkn,ݛroؾ]߆C'.n3JDn&KmPdd+rAMavUM/ 8?z_Kρo=9?Z*9  Nn/feu&<}c󝷐U""""WW ZPvÛWj_(VBq`x%o-woٴ4f?x_~byU?﹥Tzl4Z;mmG[QUJ,; F ͮ!pP|y(PW@ͮ<"'N/9&Gf]e:OA^YРL=p̒mvUGv=x26_4ܥ}Kͮ/#YjyZُ:.ۿ=U/k@}"v bl𣐸0qD=pùE ^rTŅٳ}t~Jl\O3zg/׏9|—}ǮG_{Cf]'/Amev_yN3|R^ڙDf~>}A7-2*3%g2gvL~0kა8TZ@0NSK:кu\PϜ]ͮ|Ǘ%ǥW=W ^G3:Y|Wr{O u?[7ڿAXryU)9 mpZf l"*|p!n[>ۻk9GKlX8|^U"A4ad! 6]C ž5;.t3p lAep4UV (44#WpObփ*+sdv\[wkHq*lF\\UZjތ=>c3cͮ9%T|҇mcG@U7-d豽ʜ3ˎV7FD_v=cCه:+=˜=̎7䒄p)Izުa7u6Ho~z!9aF܌N߽y2/dopuxi^}Uo2_Ķoڬov8@Ux 򅕬TU7v薀Ev8Qog 2JD"QCgC)k]ka̮<=f\tHzO[ͮ,z+O_[;ͮuFԳvęQwU""""UqKwrX V*IDAT\rU)m!x;E޺z>߮+lU=] XA[Q*PJU'ߺ/""""""""""""""""""w9{x*ޯ5&J̮ӈji[XbFCy_ kv\9.g_CыҦ_"/Hn6^6h>@>2߅EV6~v&_: |O]'""""م7U82y=uk=Rk >ݣdE՗k`<賓Fg_~C6za^t J&#, '՟EZ7nvՍ޷7pxҦRfWafl\x*=uK~>bv]9xٞ±1~\ߘ]%)_"+s uFn2!/$Wzm8Y))d]'""""ٕ U8x6m=!u^s I0`km-6۶V%|rkK@G&yjThݹV (0x/]k=sHz6c2?)PzۛM?~"̮F[zV?)!U@e>kɷ` 6/?8LjTgUr+wAqemyϠܜ<3LAGo+䏧/e\X+""""""""""""""""""wՋkUw-mv͍%O%SR&1J${ ιMmP#PGШfWey ҿ˦<.z$.q`ur]d36n^艜/CzmvU;Y+eWbqKa+r;L3NDDDD䟱D ,+ .{_OxuIowo<\$Ko|zTI,oÂo#=Ffs%6pWUpz>n/ke6nW͉J< Jm6p ,S;h^|$"WgN# 1Է)i@qx!3W79B׻ T)E^~yɏr~th~`I2EKoU0Q"E}Vë;<:]uf0◣> ŽwaCg|o-Σܙrl*[4'G3ҝ)ya[yMi= ͮ&rRcc5!PFѳZ85ev\93mG uO m.ħ5J3C{w7>K,z ~ù73 `{'&K2x~2JDDDDְ;geaz}vmo.pd~GKq9ػrC>6/ SK~jwa^9_-8@$&ۈn!q9ɹ%"87 rk-<]ҟKE,03>,:_x puNY]BӚ _,Q;`rv-}N喝|OB}gs"#Bdj96A٣.>GpzT3DZ>k^/G,r#d,5 t"01;omj'rmYЊz ]yƞ@۳c쁴fv.B/HbiZbձpVom?' hָBw {:vtтNAͭvt^`Y,{l bn% @G6* G;M;{/+uDž$l*?my`g_/:x˺Ǩ y~1Th}sXgdhv]9W߮|8'x&]%ׅ\cB 7c}̗)}F5%evBS}7_:ۃH 8Y}/mݵH 7nV,خ("""""""""""""""""bc+ȳ.tPZϵkD S" S*u!eѥ}ﮊ R\=2vxO\y]k܁M]bչA (!]y'NN[?sęp`M⦴K@u<\{0Rۇ#fgH&_g ,`Xb5GDDDDDDDDDDDDDDDDDD GmPaX˯| l87 G P8{P/@ę|1πcbP;` 9uCK@93lGj%^Uo[ swqR6[,^+^"BF1ֆ"_7;2xLGpHsZ""""" scv[`zDDDDDDDDDDDDDDDDDDFCz;[<晊┓g 殺{TLq}6ɉMɱ=#D7'8`vYx2'/v@B2W>?\Bp[#K1 }Q'<=_iv72L`맆\:Ox+ᶺn]DDDDb#!?nC -zDDDDDDDDDDDDDDDDDD\yڑ]u= Gcw nD`A{" RFޅG*ɝ.vnDS&ž3؜<h, @w53ֲҲjs0,K+ex/fWfðsU!0ڽ}#lDat2g،LF*ߛ!eʥԂ]dNx>EDX@hhAAO[+r,>83}.9acBAiR<@;u)ri54K9Sgs cvYX;c |b_d14}KS{" cEDDDD! Ƶ;?w]zDDDDDDDDDDDDDDDDDD.TJԮu}Yk|.0[ {ۚ&5!bPowb)R8@M*țԧ۞[/}dcHK7'0 yb|:¡WErY̨s miqǔ]0M|pdCrZF`/WH)""""r ؈ }Tcvoł6";_A_[|5w.OYW$ضvcV@j+.Bc]&W^acUox(;ߵV>` 630s© pa0y:Z#M (&{G7 iv][xߩ|0'|.n 9abgٵ"""""ًHC1;|)rrLXpdW3Jߧ +lݰمneO1Mo[ ]-ww|kmqmK,qܖUȵPQK=ͮy8$˹/\6bCEoɖ^$ /m%O"""""̉ط"cg/kvɸ1BDDDDDDDDDDDDDDDDDsN f;;7ZG熤 S#ͮ;KYár+SU2SVX0o1)*,[ýȷ*oE02lk-.,+|2ߩ)d4m=SsT~g9 k"""""wIyΐLavMj=֝Pxl#̮tQqE I-3}9@ .8 UoϛݖR$I ^:piѴǁ ]*\x\/PmH]g5-3mraqRLz)K2 `D3و$\u8wbĩZfW"a#DDDDDDDDDDDDDDDDD$K".,0ƣ`،O,1` hpZڀ~y _>1ubȱy l-G}sS`pv {}K1(1c8J+OR^}W.xdN Dnv* IOOO|@"J tc45^vx h7_;'(X772 }2p)1#D`".(Ċ'xFs xKK: o[Ka+2 (GkuvZc\`3q1-0ljù0x8z6 օ8>t1؞laɑY/?~\\ SF'6F6D@̂CAi[Im BB_3;.ٶم`E ]h6*GYRP՟+#t4("w] .=rkDLx"cʅ =;:c2 FߚyWid{`Ga`й2 ;[%x8t~GBm'GDpa%=dve_d ;Jjީ)2:Υnx!y'u{Xˆ?Cߍ%Gu޳o0.rl,pKJwoödq`;!}yQY! T0YRk־`b^,ض[}?q4[[{^gXw| 2{`P.hؽS@F`9pQH:8&KLJί>9""9s`꺇f3FqOX|ͮ<͓G)΁=t 0.r1&Bͩy:}6/[9w5wkX2&*k]%"""""YQյXDDDDDDDDDDDDDDDDo@KcFg60*[Z>kmm2X.: ۳|`epn'םCva Ρ)`̙#h2~ Ȁo#!dw""r;6o7;-?nv)rDq[ZT2Ob)s`wy,<<8N]}Y콬#Nc\ͮ|&g[:]ɔAwL`1N6Z,""""""""""""""""T!3d`Q2 ,E-,}xؚ,km[RX_pV[7GXK9~+Ǚ(9םI}:`/ N[oL7P-cݸt{ƚ]#' ,,Ҫs sV̫9͎<H񚐜ܣi]%Q s9]~hvdgFٵq?""""""""""""""""L!Dс x0cyג,-A֍`L~j} ,~kn{-v~mI"j9{=ΙSӜfsT |=V4g;V{Zw-! /v}rDDD2㋽s|ofy]#ٕa3@ %orKP<l "̮?f;OrS`IƗ䳞̮Qv_cՇidA),,hdd ,#,z5Ö,M׃uj ;j=yl_;> WQ1&0{*\pe r5BƂmc}EOHPq2Ͽ]#ٍe),}cŧ]1/tܣ9^;+p^K4wbpy{]u货醰ƅIU""""""({>isFYxXo^.c=-;omkwku0[Z {1GV':W=``;lwV[Ug ؏ 9C1,e8?t0ؽ>3`{&Ab[O̮ζY:Tv. I̷ϔC>8Ύ 1~ivU -Q+);fWe1\+{]y45|o`u""""""QQZ,""""""""""""""r'1 R "*q`̵)K=kXZր>c`gZľ1{tǑu \sσ}` H > !Q"P'”6숈_?W\S?N Wݴɫ=<}T+>^82e|zܟgJ)U.U8τպ7j%h8@C2j;M*x`-\8o[0.*6ЩL\;\fWeM[.<&581pH7#kEDDDDf [} FSnxc3PغA*sysa]ywsm/\9V`$HlwޱfYmEQF^k`c,Zz,s,߁%Z,}lS`kXP| `}^Yl+N-2kun _w9;x8  gDCRz#""b.W'U6͕=L5;Rr|{VFhK^<]y{twGgtoE8WĹwYWVāoo{G𞿅 fgǴ|#Z#fz/^<6\~tCͮ"WY,(z@՞͛﬚Q j͕!𽿽uُQxh] w'\jv]rrz0&ށbfW|F踩Տi_e  r/d):RY(߶^{yh ͯg a~"""""""tBޟc U*VV-wݏ S^Ң}VDD0'Ə`|ox`doXrbˏl/aaֻOe-} X]1Ύ``/ x`8<ZQ{~ !8-C{[.OeJ{܅Ҏ&Ai%Ws)](tFR=Y18-<]}FYvdT_{$xp}=Wԛso< W~ wcBlUU,@?%}ya{o\$*S3$0oZ(}6)27LKt1sՃO?ǶݲR|H#x%vAq`l3O]?p`E] W ~EDDDD$0U[Z=M EDDDLQʞf BEnykp"|GyW}VDDDDDDD2IrW>P %wCW~s=w%PDg4;00vYZ[Vgl׬ۺv ,q^혽Xwr8Z~ֹl;pnGb`Iu 8xӠ`|#؃x30؋\ >nI1/'mJRmpuL| noz45<2׵6%trKމsw N7s >{:>oi}|ٟ ".q#YV9%&yCyhruk>_cv4bx"Ŕǘ'G 2k> mzϽ?]sc|㼫`KYG!į.hvfY*~f _w|(>LkDF,5ǙhcQ FeX԰ºк ςqJ{,XWZ95ʾXwtJ8O8 gߠh 8<<+6GBSfsS+!ذJtBL}u'w~mvܮa$BX4P'(ov]9Xk 9 ?{/`lvG!/ا GP-`y˪?&""ٜ'r/y"K+7;SsJДu*xe|V<]ςg{t7=. =]s/A/gW[ !޹?[k!4rDnKF%W..vfW}.^?\pͮۍkKRaC]]62* Hϱu0p{4V29.Zw=P%|jvͳo AbNdvȿG:;7fdm$7*nv i[# deK-Xem`IJCa~cp|d`me73(쭜?g`O 4-y!H 2km>9"""v \N% I M )],qܛ2^L o1/}xJΌ -:<9_os]+ow>7z8G?HDNY oTMf9'sҢj&2JnՁm[jԃ4{Pn2x?gͥEk]gvUx1 jͽ)KUik=4U6JDDDDDe{Lhdſb>̳Og$(T:C xyĜ͎|NotuJ]#7d` ԢσqRF5,'X[lVְ 0[1, vc؆9 G@cUw LPgX$ _lǑ"h0#""b.oCpLېNE;R6;2[~0OɴHFW.t'd7dR󤸟 8{}{ sxۀ?}aY djmx_>.]fW74Y߷*138wPPQȲ: e/Oހ]~P+?ͮ~ޱV Eϕ ~?\ [_w&?p]:z7x{̮:r_)^cH(82T'̮|A{o4{7Ż*ɮv.ި4(Bm>49_؝U1f܆JQ`l,3n4,ei0^Vv!v} m-[qq4xl-xV=L z웜{W%p |5pl t{fsxz˚6#!UON/&O%~joܓ<5'g4O[ õ>vuݕ^<3huwu'< $&{K{·I 7I |*( LjB=Po;Qd@#+7v_.:> ,ǀP <W(kU')aoY+9O82~tXb wYxU/Qr1*r945~qt8rG79?~r vOqw@.`.$vK2:gADD\)sSƃf5p}6$ApON nϰokDzuTsO;\c3Ƃ爻XFUw}O{1y<ʞ=O_ɿ]~V1""""r0 u!rs{N? ]yN%nltQ8l [fWe?8*|cU)9;(2.HC`Ip|+ KO;oU""""r#&&jN#*..(abPd^2QR'GOzƹw/sݖCq3NDr[t1O EDD$ $\Pyw.pv(ğٗ8]#""""""Y[{Vz /mvJ3Ӛ+6lPpXrCpHRϦNrZ$xJ5yvȨPi= w} M"ًQX~56id=Ar+*m#{̮}w#0Qj@QҨUm_VQw׏+Mu0SWFu_[_c&kXjqk ֨7 Z@'ΨƵ-e$ΨF\ e0u~7m[>6Qи>۶PF9rF[Qr\ P6.g4 Q(O0 SWkխQF@*5Ph1*^;"}kبDko1Mh_c)Mew}h1Th1̀FT1Q?l*F (UW긊bqUZŨv}l\_7]ov\K0QVW3Z1T6_@ Ռֿ|kƵ-mͷbũA[056jmjبI;0~?u[/qukkި]@ j_^/Am:%󵍻m \?θ>(y}:`4~ktݸQטN@kǗ2kwcQWƵ-Կ:o\/M}Q|i]F}7i@4=@70=`׏oHk7:6_݁Xwk[#FF hD ׷Fј`ym M ƿmYs/PW獦ƽ׎(K[^ьW F9f3W͹r43]Shεmm}(o\iqu<7Z^¸my}xk׶ƃ@ `/PVƵ-5ZQ<*k6o6oCͷ|0*uS`T2_a~?xD;\>Fe1ձўFe^m|*7`T.xb\W.㱫[qWw6vсJq w8P<1oT56r7OVl8Ǽ<.+_xzBZ7yL}O[jcxl}سiwu""""g8bC ևf=2U;'Y9_jD6\R5kd=+>q(н̗]#""""""""""w*þ_+(unR3pN)ۙ8X\6מ15twwȨ _\;3΂'c xzdOo|~q}|g1G0`#'qfζ^&+̷/gSwN x>2*)0<(Y:/ԨPhUoKaRыr)3n|jq^y߲oz?8䭒u(~3ݗgyfWdMFy{jkd!5jBV=w1""""""""""Mڂ˕:&)\wMǥ Wzzi1)5fTH̴^m媞1z=K].y׺G5Gx F@<>9"""""7 P}ؼhk lvUvm>;vh$_r{6.0OQ5CXDDDQGp@+T6FDDDDDDDDD>OqA3y?K0ϤwIFseZ-rLOo3qwLFVwwo{1x{o%O _x_ߟ e9>+"""""Wtp| џ6x*(8<̮<)]5}$IHO<{ID7]}>3"ra-kecCs`ŕs M4JDDDDZ-C*=k|t^՜̮L"""u )Tflvf׈'u&}*]Lo Ji)).|pI]O˒M[x_p MW̛csW;E| OĻ^ώ , t^!w_#]y.Bp܉t2@ ; fãx zuw?FvfeA)oIu""""OqU*h x9(57c9x95;QV_k`:§q7Z}5"""""""""r+yxvZ+:mW~&{r1pM?K܃2JCiɗq&mxu?ܗ2T2=Iq?Eܱ=5ϧg5_H%#""""" 쵂ۃ߁RmE!'Cy^8csHH^'gUg}ۋxuFD 2*<-!pĄ/fDDDDf) jk,[k2_S޵/ ||_uM__s{qOQrcFLxfWeC-L8y N0\ d oC,A% 1.MxË`Ai.ށfWȭ!Aڔ}^4{3_ g/O/sZ,"""YGşشn̮;;k]BSwBzdJ4e|}.sFd/#*9=g/_|pytM:׿xb(:5u5"&fn4,"""ǎ$\ʣν٠kDDDDDDDD_q{GZ9P:D.7tn(* ں衂V\Ց +Cy)G#O?ek>2Q-t_qW7pWͻ^5G 7~Nl̅jﯸxO [ޕ8 ܍AW?RB\27j/p \ }\[{gt܂ / NDDDDvƈ5o6@5{s%uFd<8 ȱ#t_*wFDDDDDDD$xݪ!ڨpMyF]P)-D_YD)^p"F/zdOч=rK%1U J]͏ ޷ncŢu]  z /xڿ(&oGDDDDDD-.D%5kʗC*]V^tS\@VX1y7YEwA?*s(=PV]sPe[uM7o&d˻9[t5w/C]$նfCWF/1yN34,"""ǐSټ ]}躠cDDDDDDDD(;S!-O~M,.#wwjtE^Ź ѵs 696(6G~M}g0q b'~"""""""WHܔϡnם2rWٳ}S*XNۼ d߰mW~awuǁ%f% .>SCqe&4:~٭a5}wc JDDDDvu`1C*t3oqY<7ɘ< ȱuN[jбmB5"""""17g3^/?`u%\KΧ#SYuJ)?|?X^0 XO5/BK֩BdCy[& V?t>S|eK_2,db*}W_7X=_1%%+J|oK Ko<-`)3Jg )yۥ{X]`)3R.Y-ԟY?x> ,1J_/{rf1drk{a<)9_V+Z_G/ٗ'X՟O*y]$`?Ib6?Vcٔ j~`_rJO5OL ?pX|dO_~s>֖|Z~?0π>|gBȆ**}-]#"""""""G5  RW<4gj{ԺSVz֟u3!oҞAW?|SLe% k 3+ )ׅA~~D ng>nXDDDD^a]b΀SoQ\?x2Sw]ugCyFf24iEDDؓ|yUn^}ꊠk;x'C;&9%?w_J+J;=_ `աs翂C,}%{s~9~v乿yڳƟb J.?}p?n ݬ;8/Yײdu |}_"?|s/.]{ 7^6K{X}uޗ&&d=?eˁL6zr ̊sJL+}l9c +}~=[oe/Y\:_ g/]x)}^n/Y:?v֖?uv,_dczJ>kd;=d @6;_~d/]dM@d-ly~fJ&ہ<;Jw__?adw@vu% :~yyZ b}h(Yx^ ւ{?P@NO%KEe?%_/.%{p_:xK?u%CCG)qs>򇽇K_죀Gt?1;t߭A?X=i^'""ǽf)'vPfdU&x4֓*]#"""""""GsoV+?Kh|[$mHNx3g{y=^=tEP8*kQ8k-tjJg#*]tՑfv5>H76* 9SH~tݕn}UVh kLgA顙ӡ)* 7~Ԃ;SޑW?Xռb ȱg:UpOin]8֜ɹA׈"BK*/̠kԹ_]#"""""""GdYf)T۠SIly->ΘB{muhغaVT?8I挆SNZEkt+-o]xet&{YÊDz(|^\ NDDDD7{uDt.PKUM׵BͭMM :qPqǮe׾.]#rt3{_9U"""r*7 K{yAgSz̓y\torn&#o?~j x+ ѠkDDDDDDD$v$T|By5ͿT | ܆/:~T]Y(N9ЉhʲE_ߛ*mj;mtoUbvߥ 2]#rts(KY*!"""rfێ5PV.J~*mQUGήs sUz?aokN4̧߽]ޕ^CsqYwmALp!4<紤cy9iœ?ܼ ;Z0 .J&\:w;B t4~cO8Z`?Ht=jɐ>qdZU-ɄkCM&BoM¥x?}84{INaje)K(iΝ """r%;}_?2#W' F|tΨr $;/.gKu_'/sˠkDDDDDDDHH:5;Tu^UufUOȠI 7>[;55DeH:nxQy$]w{[3v At7!* 'b%| i#SZAٓO5~߇ ށ?߄ ^i:^x.tO^fkDne/w`9~wsqP?3?YrSeg1"""""""j_upAu2?ڔ5<5]%"""""""C|ߴsPe6UjmI?X7N-dl|_A9s{JkS6K_t~뗯޴),|jлܷ9vY3 r_[M֖*ӝz_8աmZBкɘPbE'G߇K7k`5"G7ӲUqj$f5O]]7mkDDDDDDD5j5n}Rk}n>o>X6"ﲠDDDDDDD䯔QPo?kLZl Dfܸu>٫`7 W|Zײ/A^Kkt׼ w¸޻֞YxoA׉ u"0-n>R?6I`g -Z4|;2tŨ3\c]r/3(L5"G7ӲW/:?`9#jf<޴ݿ:߷fOg>kKÕA׈ߔ\jv[t_mthY# JDDDDDDȰ9M)P撚_%W2CK RUٔXH7VC2'M(,rR_xM|ciOEH kg|PFH^팤ZAᳫ璔}a Mq-AAWƣI)poon, :s/OaGz9AW+(!ķLSHS +veh|F4hP״oH0wHqkDnSV;רs@ϛ\*c!H3#w YOQU"G7;:O2e!픪Յ)]y#QICyj3+ȟ#Y )OTZ-*hxV”Z4}69tR vXY< Íow[i/ADDDDDDJ_%5tHafv.xjcfٗI[ !=_.mO3~(wq?uЦ`W׃XHgAWR^ }ffTUf4_]uDRa9k\z+ 1tAcSↅn5@Zkh2e߽|QU}B7C ?/rtr§˾hw5"jXDDD+Ce!s2B;u,Ͽiqa+'r ڬ~"""""""G98 @IMڸ |hv{^SQ9(n=}(Z߄H0oEN{?[9U^a}Pjr)=Tk^3?t􊉏 \5cŞ~aWj 'TmAW~WfXjnܖ ,gou""""`5%"SoZhUZ?޴chǡAW.],?Y/F"6 UGBǏmeT~ˀi3 }yȹn[F]+""""""""""""""ǫڟu. ?E}M*ZM/A5OʑKaŭ]<80tC_?t1 S6I0qE@ej& :}rÔ[Sq(*9ԡ:6 5c!mI֫P.0Wfk~h1AcFжEХP ط/Ǜ}EXAiSptݑygTޭ0a̗# ]#bZNjQsD"""""""""""""""""""ǃ:LT~֚Ic3a>ܲ ~^|`}}ac`mh5mjbyzmn Ӄ;>yžĝS/"Q|U"""r,ʚ$Mh*A{e[X PMzv14Ng/>Ijh AW&kJ5#K~%eVώ>k2=v#h%ޙP?2=?_'4j/w~;bcqeAWߴhܥEDDDDDDDDDDDDDDDDDDee3K9 zFe>}ܽtձ/~̂o'L+fPqGUGΨ)kvodLU"""PJ!Ԍ1PsPPo[$h~WKݢ <~4x9,k(į+̓Mx??[ >j5Sgab+Eзq 8.ly+vS5"O4ȱifu\5϶}oe;hIVȠ}eZ)^QT7Fr/]u(̀wN\Wf X" L^ NDDDVGkHڝnBe,5U<4Y'5Z to'u_XEEX#-on)_ crBQP /zݻ#q]wN9's> 欍4 Fʹ.]4,""""""""""""""""""r,j|ejmjگJ?'wN7z!rRjѠ}U%\z 5(:IUߒ&6ݵ/6_9"tV)qD^2S/^[~9x9,w&̯h"pn1lnw'qo]pbtS[i׼-S7^&?s|OA׈L˜w9K"""""""""""""""""""ǒiWC ?cV5ǯ~eǰc/2{JAW| ~.\IjL+o=;f=_}ygx]U"""W6ȀrM2T8jZ5:nEx36/ sOYVuXx͋0X7hPNؖep3(\< @nX'gn]_ 3W\3cie594| j[;TqU5Ǐ']سiyIrx:v9)1Z=^M ltN(Y-YA~N8#3f>|(ŭ}t% B;ByLYk *^|FlТR)I~\qP_řx"˟7 v=Tam=g+b+ ykAW}58VYg |8K vwʚko<tȱɴJop5,""""""""""""""""""r, =i-Mݠk]}ư'n|U^:-1/otݱ+a3j[W:~[׫pIU__ oܲ`_.l NDDfiIo$,6!Pl+ j~Z {4R}hsל)qFa fjKhɰ;dg`m-7_w_]>;6w;70?y.p C;.}%iYUb_Tx>uDmUuyQ""""""""""""""""""" yv`&sp0լFfiV3<XkXXm? q5o# ' 7qB&^ʀOI]n\& N/W%Hx슸cG){շLݳ9m%w: ?]w*7ƪo?T{9m[U֎ͼD3 _t1*$ bR YW@A5F9}8]- Ն*=hO :^7+n!߰hXy]687vwr{?@X9&]-uM38aiq5''}iVQ0Yū!A߆Ŵ.kXDDDDDDDDDDDDDDDDDDjӃ*&`]iO1Zhnk"[&[.y4n 8޷˂sQu93Bk9i~;)'t5vf@7C5UUJ A_v M y+C`aqgj.w?>\A~- çxÎ'E2{3Znl|P\k7Zyyę_k|3: o.j9T0_sK^]slpwj8: g; ^v_+BWB[ }5.|-vnՄEg~dB~oD6A3~ߝVeEDDDDDDDDDDDDDDDDD(WL?0u)# VE' cmVX_Z3068s/i; 3BI+lߟs*&=᳓ SNƩ7rXCuoߋq7/{ݠ]fy\Mn/ .|A˂;ؿmN0k܎@֮H{WU"""G1z=i_3Cҟ/mg5iOj~NԨ^srYfl"~h-Xqچxشd{X rWűB""r2" N06`>6g$V]vi V3 %fXsC-i>z qﴛB聄mOx^ .5Im!49Zq#SV@qpn{e2Ҝv9_E> z*ku+JzJ:rƎX?k sލ]%"" u. w *,,j |=m})Iou|AiۂMӅ{`F>>}ƶs'w)ePZ1~;o@""r0niv K5MW*yę50ٟr`^3 ;K@wwvZDDw#-6vjXDDDDDDDDDDDDDDDDD" 3q;Y`X0]kL>X۝*$Zoe;}*燇Z^༙u^P(qw$uzBx}ݡ!|Z]7-J+I j_aW+y]sH??4\}Egᄟ*Lmt᷶ON0ƌ!~vޥŃDDDoǭM}HɃ]dVCUNFSjvCNk9áw÷Ws. AS|.Z|Bt:,:#:֮\58ljw\zؿ'; Ewo =A׋ߘV/ꚦ`i#s:OIjU`=fgf`2֣`_i-V[X=:.^N|&>J :#i@6Jjzɱ\(T")г g 9mvޙ{/կLrR5CLERεIqa@ZӠɈ2%T :qr^nUvw/u'6@w2 ۽|Pt' oknByPsgixzݳa͢"q}%,lݎxk wt:;p+^"""Gi^P"""""""""""""""""A'*yX\&Znfh}hʀ5y\V[L+նN X}oOpĽnǁsf͞ e /%m]a7 g"O~*Aܲԋ@\^ oCo16F5G?ErSvK|/Aɓ|߆o޹63ȝV]%""fӒXJko-TcMFj'9CƅV[ƒ\aP'z9^>TTs.iwZkLIkwfF?'k (%tɴQkW q"4`.gg[ϛ`]nrۘSJ ̛`?଴=<>[#:un༒!?q 7%tBAWrg#}nqURs5#K9~,ѫ F]sšn@)CIkT2#gtkEfEofU""r̰BxʌOΰ.M*TzBݏvh֪~džӚ ZU :^7kܔ[^>VY*۶E`wso7ݠEDD-u^+w|`N10fXC=1Xo`a-ЕV}Zz\+~] B&LrtSŸ%unp~*yN妾B;}""^(z' ^ɡ6pyU~-!XRvAW~kFF߂wNZX.kpRŠDD`jP76iߠQk49hBVl[GWZAfsV7\3Z sVr(%2<؍.Oa` Vh[^?(4t U$㼮/iXDDDDDDDDDDDDDDDLgn90:<U,Akiv+XCO[:4Ъ vp Np^ÚN,~]97Chi=)Ꮢ8B8=)34|J/.м뜙A߆wmUY޿rAEZZ}PpmOWqA~NҾ0=|\ *;ʜۜVXlVn'Cu@=ꌨ.toݶ5C|vdX:Ŷ1`}5D?]蠫EDDhfZ'e\ APx+~K0[own.sC-lnq_Y*iW@>Ihn*IC} | =g4/J;~N:ZV}%""",~#J(xd"?w~ԅHA^#,^_}"w X8&vD7_\> b[#ۼtw@x#bG-=DDbZ0$~Yci[3ƐJS7WM^h9#9kum8gۯkEPZۣ`/a%nd=w ^DDD%u{~`ZB@3Ŝ f(0 L&XcƦ'Xaz;߭` euwmL;NNk Of?Nn—J_4i _. = ͝E~'%q B=8YBk(rGF@e[ 4osDMuCaZV(wHac1pEc=x^ľ=۽y=ѫ n.om瀿ү L.Ăhe}~ e[]kZ7 W O{SUʜ }.lAW~ 㚬`͸}_t8fHm9 *TiT@ w6@/ ̈́Wo Z7n.=?n6&o#fg3x ')jkpDDDgZxb _ʻf' g*jssFKw7,@XEDՁh f lE`7`m?7ufh5B[nd qEʄ1p3B{:~/3©[CS |!ⶦ>B A_?C(ZS" ş獎VH[B.Hs+@t`ص)n'c]< bq&)3ֹv&?Ż͏_E)d})Gۇ-# r&塞=T'sխ7Цn+~ :wҾIe[zv.h91*cn2 hVBee>Bs+?oje-j<9:o=TB;ךE \r¢Ͱ__Ú߈f uξ_񛀿ׯ˴>!;4,"""""""""""GZ%6*ϷiS~>T_qZőU.7x;r K""G^eZRx+昩`ca`o4jMP o,{*{¹i0=B kDLzyq_tuƩ瀆A/EDDϢ ~u^O "EyBb.(-h1n_+}bE'rwĶF^XnfDW/v/Ě/"~;A߆oU>՜g4溜'ޓ_=E5 3'Aiܓpn/>)˅]^Dsp~0C*I^PyHP'Z>Plg1+l!1_c"j9l轵vl3Z>-z,u(9lܻmp,vݰJo3jZ""""Gi}NƄEDDDDDDDDDD(R&>Rٚzi /z9gl:vay>A__(Lq dNy`a׵e;yfX~6*^qOI\g{58fza]ugox=s=̩gqh($kt$2$dďe XIcި 0h0V_{ݨ :ղB{ڙA__- ]=ڝ kjl[}eW1T,(| _';9zWeLVV"""""""""""tgug@jk 7;3ݳ䂍AWZ;s-m:+sku'X}BC`O *]#\۾q9,prg۩\0ҾB$\LPĎNc' M)B1[28t9 2r#Bi9G@djޘh-̻Ž" zFhnA[ n/Sc_ۦX** v")-[ wl?b71{=a~c=,@З"0Yg2~^:規tnɲ[j]s 6'b==VrSn ԾeAW~߹'x aUk[ K~>P&s9=:8\ +nP Xj}}z͜/RBoAV5+]ƶ۶b0o=궁m*zgmyMY&J""""_5|QS?/t޺3&o+yvB -]%"_iNY0,.[`ebl03`s:Xz!"`WY}2n8!1 jگA(9q&~/9AB nZjPk U8ED(/VFâ_@ aDG/5D.*u!ڥp{ n\p(N mMP;xb{/|Bo\Q 6DmM}Рi5A>Mt¬wZY7}7904Qr8jm6ÉwTޗZtܯz r/*~b]'"3Ef!!+sJ'C{k5υ9f閖{Cu3>z9^Ώ/qFaw5¶vrgAEI~##oOeM"""""""""""rUMnwzhcgk]j~r?ߛ9JH.0 փf$V_v5Үauwg!쥡V`~ qmeܾ  |v}NuJ:-t+$Wq~rI:| سCYu?]]E8sBtDn-Ʉ-n?5"D?nh}QIp3bX3P<*6bM7{ wS!ɟ232=sH*^*|:{׊ E~\(Z@HKʒ'.ԺYtpړ AS=sO4^[l\U"">8AIWPId3z={ 4Yqf@כm Rk.O Z7jo O[d XjnG0q[/vl9% Z䥠EDDDeZO~Vҗ' aKփ31=ڟS9)s%(} ?mϝEBS-92h3cxLZclެš^z(*9n1WӖ-v{{u+X'~PؕUk}_ Hx9CYbKLJ$aABy,Po5OlFf _D"Bqy%*wL>Dj g{s!WV-hJ[-KbWB,.;^_;5?b7.U~?y/fЗ""qA/3o2!킠`y抰}.7x wI x lz369>$9?j*]%"_;*@†\(7 -Rm.hpz->FЩ{B2L^^'~_,~&EW)֬tFlkO궱-=]#GLiBM&l1_0=ȜV-5gB;/Υq7w 6 'Pp{3oOj 5Ý' B+!<9Y1WNn7YF 7y*:"&Bo"!$7v+7ƃV=Vũ ^g^&ĦyX=^.mƊoCD#fH\ԼSi&T ݡOUbTsOw Tm/}*;ׯwBU:RnSvL)_-t+l\sDZ[wV?E-Þ8 Df{9G}%/!q~/]WƑf <5o8{-*99O&0W@U@f8At Z7Svk f׆} ;ٔ#OOII'Ze%q9)S×-5Kz^}-͆Ⱥ^9%ojU~V0*:AthaCipBl@X p=^ȍEs!%'~%7Ŀ8Y@\Aߊ_ws>zyՏlSNҳ̇6~;7ܜ8K_&@dgUfI*IMAFU̍wN]a6x+A׉ȱĬ` !˸f6kVü?o5jCF346Ht/3_,+jSV뮀5ljNwCYPؠh_l^DDDD5ջ`n\%ch~ހ _w㨶:@nήkuJ4A9w,.~^UV 7Mm{U g ;oe]!\޺ !q[)쌟f?>B/p>PFNG$:A(h*>zcHI2DDD x{sGWBdF[&wD΃haAD//oE~}r{+rnjGy_BhZg"Nq_&w_ș?;YȾDDDnR<{;\yrà˻闤\t|=W/ϳOg" W0[@=]~Ҳ0ifln?a`#y]'"'N}*BIխ&Pqy:gT[e-&}包v Z7#;; po9:'UîM.ȹ o?" s@ /vzc&hXDDD_&J0CÌnT]js[4-e7yw}8?KL9f0M]> _(\ f`OXS심45d(b} <: Fۯ=;{'t;ZB4H` IChi`|vh!?H qi١;4 ]AߪȟEO)Ƚ[<"]-$zDn/v@tYo c;&weŊ PqJ4mFC_d tw:nxjeP2˃ O'eɍTtͿ#sVL5?y=_ތ3u^~Z;6eLܙAAɃ/3k(d6/. JD&f) /0B9Lz=Xiu4ȪZ^eǁ3n -WMAq7גs>_ti4 7Z]Tٝ [}d-Ά>7ܟ˦EDDDD=uVݾ,nh9һ@Z'% ߷9Onͪ ]-}[σ tMԘpbpnLn_ sR!IrW)9 Y'y`X`9xx89W}('|4"F{CO? j]V}^XK%~= f%ϋ{wD]L"Aߊߋbm`o}ziAW + `mJ E]iq|+Y]{}xuky8ׅnvlBU"r$9r"pS 23\ɏ/pݡ n6Vbť`L*`^45J+i2V' 9L0L25< s ςaj^5̶a$9i&߭fmY80fLLӚ0{E 05f5κddht2`2E`N(,ucu $Ԛbži\kdʃu=TsTnkU.SlS̝ZS#$"""r$Eo.uHP<:hyMk zK~$%DVƁ۳pTB~Tx劺"{8 푵1^OGL lu;.20tn2Y`wY 6ܬ|w쵆bCYo)xF;x8𮋭~Ql_ }\{3("ˏ}+""""Ο禔k F',7|Gذ}sAAWIqkN)3zկu4x v5Hlx:ΨB :^7s\) ]՗m[66 5?*˯x]"""""`tlhrcUɩgFC%!#/- ;qk.^U""$C6@YV+4( T=@-zPp\th!T:Xy7-Vr2OV(|ts[2XiJ+)`.;4X3}\o^Mfr0\fI94Xy3L-sV#Fև+ƙ^f0 V_9ϔ V^p0Ӭ̳`f[~20ҼbJ+]0&J&j}_79ɲ֚`mk0> 4Xg4%U)W*05%vVoL3lL+"J>ʒOn2`u6`zYK>~Da0>Xgo<]s5trIsE`u)&kU+MU7`w: ԶAVYqB !];6ikt(D_.h ͭѫ 7w~᧱9v(~4Vbbt[~] I}w0Ư 6DDDDﱿȠk_jvȾ:G J`R&}i*C ʽhms`M= m?nZ9 ;y$j9lq[{eܧ`u{`Cca{AvfL^='| ɼ"""""OM/uo`9vUaf 鴫jtMp\k6C΃[FLᏃHQTi^:Xفt6=K+{SLcs6 .К+MC?V`К?Vh:g%L<8XiNsӀ V.?pYiskVA`94X2ySJӸt; gN2<csyfÁiV>`^yܼb=˛`W0VZ`VO`5tJd2,=4XYK+O,ddYP2Xi`g9*vf-Xuf#FVL3lOlo]eɾ,0=J{H`u=x`β,J{ u=$ԚZXe0XyTJs+sL0nMпYDDDDDD}{l}&+{Q2D7B; ~u!zb(p'n.GcgBlGjmN+A'xuӽ:V~o %L~fIз!""""˝[iH2kls0gPX) *QQR Rx2/YB7Z@jd ;SЇ| {S)x9ڒh7V4/ ltpȬs`Rt_ SXtI-v`9vK z~xCkF}BהVH,+VfP~hi@M9ZVjP:Xٍ{K?d=Ǽd:gޤ+pJ+' @9pf`bVq;惃f cqL`%O[\yS djs;LZ2Xil3y '悒J벒JK+׀s%H? V>+J3I?+//du|bC߃"~c[+˗~be5L>je`jX:00 >f+?hig_e2llt=4XYuG|b u=ădƔ V)U*ͦX{L0۹> vпDDDDDDDX.P\.Pp^zԇȶm=3.D*xVyn//{ܬp/.)b?{b͢W{m/:=][ x"뀱I|roEDDDDU;&v'Qk_ucza-kGhXp39%:} 뤑,TlVfkSdhR}4 M[+<to,K=,}߷v{lعmv칹{3 )ږ'lrɼ"""""crJs4,"""Ǟ6N-_k^87͐DDDDDDDDXW|Y\(gEȫ=Uq{M]P496bUH Kfwgtޚ@N=OoRR? {-c2=A߆ȑqzW m3ߞ4" M{6$^X7 s!@um.VwߣfM; 4sBB eSSqrޥFEZZ>͆-#v>= ށ˽֢~MZzCXtL-~EDDӼ?nԴLkDDDDDDDDD,wVSP)|w["yMB-DB7A~\tCSpӋ:|p'΂؎*E&xYcMg{;i lILDDDDDGH" f@-0帊`EsXn{mmJ `sNĺ,xbk0 {k!q{]ap}NCb[7v"սY[}֛~~a ldWз!""""ԉ $fzL+n1=،"xs`׶01B̓f8Zl$?gSVօY@|ɌT[H8`wVBMV6$t^!*{>$m[!i`=W:!_/r8~TbmJbQOd*G) ijM**N_S./wjCr]-Ǜ#v͋ :{/v-l`뽱氳3\z,?"#NyEDDDDxbLiC ȱ#=6W=kDDDDDDDD=űP5R f;!rO~ĝ u zJN.!9: ĆwGd7x};Z U7m[?]~yҴ:å30\0h:\!^J'BawyMwh<ϙE I%IHHJY"JAByy8sZDv~չ閷O9d' ^*B`ә`i>p3Ap$FCg>Z&3BW/u9{M~oCEN s1t;C|Cg _?:/AB-7|%X)tr9'!Th^ p@ru[BBU-g33(/UD:WmyyXs8}L#V$)ɄbLqK=涀Z*w ڈr+Cƺk9woP cXm{hׅ=K N'v٩9)}i2ߗuFƁ"""r=T([pw)݈d?~6~~g.# vA {~{x=$MNp`?v;Ϳ&ʖ`?Ӗ#g+, G; 658h R` uxl|`¶\A)l -ԶW-4Ҷ5W-\n[%l=^ ``/l/q0`!vg\b_b oCmI=w˶:cm=>;6_;J)dg_7OLr`}ޮ .}fabY1([IRO: "6X>JWjPCKkdHzrm q916Gc}pHɓ}!\Ԭq 5e!.[4kMgOvF8C0 $$8g"$AXMro[?nt21rcE-e.6w z=U ]_1\(snXzvհ=yO18xב#+nJΞ]EDDDDDfj'ոI^EDDRނC}WnDDDDBe傡~@vva AK[6{~'Qp e_w0i6wc69S6KYJ5[`- vu mK|g˂, *`lu+lA`` 6lm 6wP^6P5k;-4 TV ְ7D}-`w[&m Vg l;; s?ثN+?{0-a@OȎǮYY`xxfر`N4Xl'A^&b`ey!|>^d+owmhr|e3SʇYfx4XEjׁ]D6l;-`Yr8ZGF*ҁLND"gpޤbhƴP:qo̖P\'36(M-:,nl8xp˱ԭ?wqqw?'*<$%Fy$78Cg^ Ӝ/ n%1{ |șk q")ԂKͥhzB|-!HX.3!;S q[YCW;; iAR'N~HT亡 d$T׽{s9ۺ{رXw#Ǚ\Av;!x.(e}ɯlC0Ưm7N𳂕mmBYpu~i`mCl6sm. Q6 d[5'!jK̲ ؊`+m|ǂ]Ft h2gۀcFո yMm'e TYʪ5ؚko0؇ l {69{} \L`ثA]xnhg3eۧʭv$pwQ`+5ZPN]E[~[)k>;k-ڝ VYgdoBG'Y̷#ʱv=L+?d+Tr&{G{Y Vp')``yniyW/qNUϧWR-,|7G+`ǨׅSmrl z۶K(G~0۹`3ugY.5yB95x͵f6/2ft0ifrAK'񵜆}tA|EEs7$LrsA3ǺXm鬅đ;c}I'C 29!Pd!dck_sap%-q)G\ EN2Y敫z_78Ǻy9,޸’"kn΃uC7}{\,pdS'"|dY]F&MOĻWVFIw'f>v}p?S~ ~i(==u=DMvf}b=>o=2ԫ<:k/Ly/ +2F!5ki9kP`_\/r߂c/%qyɶC`yv`_}W׳G4' xocw`?ZO׆N `9y. F`l!Vڢ``7(v?ǖ;Xd+= D{l'cMl9ؤ ۋ K rE#ؒAs l;jZlfn[=^ <e0 `/ ^+`a݂/x ΰCdIV> 6l/`e Yώ;Ć 9 VoSl%>;ֲY Dے\j۫;Y{K9< `})z`fkl`6 V2>SʝNLGJH7o9i+G6yDžSźܷ+ߐ6d3/KQCQ8jǨRDBP<0t829i0=lapf4f v, ΋f;793.V8iiZCOķv9A|3{[븗;! Iݷ<9ܩɥCi BUP;6TK9v (g;1>P[N \swA;PNF\6\8ԣP {{2X7/%3^XwӖ"66oϾu8x/k!m%ZDDDDDDcjw1IoEDDCXe<}NwCc[=J͌?Sb#!Y 6׿?<`ĕpuzZXwrh'Ww:Z"]\ L rYTso&kiR;gmTm}e: TrBbm,G[ rB<0S;u!_sdjpw*""""UN޻m-~#99% &$cC:x"97Aťr%f݊BFXw-皃V\͇6V¦)A wAGx.u3>`6ĺ{?}cMRXDDD^*\ӪtGȳrN熦՗N9祎{lTd;t]H+_y߿S7Gw] ;yc=DDDDDDD=oDr:z\Wvsx?iۈ#{cݍD0R`J^43̕03 y 4ܴ4_B(i@a3pęo{TS9L-^nA^gs3N6@fw3 q1$r8s! CC Hn~䆤BʐI(8BҶS)""""? v'c!e7r{E,i ^BLPnPmd_[%{0;._mWKsWXwO4.a7< fȴ" s2 ӡmvҪjCw9cݼk_kKU-8nΞED8ȫAwH#- CǺkS4yI`9{%m7=a[O~\g@7L:&Xw֯ip_'*I[2ź{9WnuVxj u7lxp$hʤXw#0.p/)昹ci ^17s`"]MmVupb3r6i49!۩F9Mun5]!;ʹ >q7OABe&$NvRnk ƹw:!Pu'^ -pJBrP{d*tlP{8XTxr("BN/́^`3iv7"/~=h'9kE~C;2f a0Nσ[Ȭ AP08 Eă7aKW,k/og;ϦlIIR^;``dO-5o)aj53#K|.'af'ߔ:oVs T|~7ԌEh1X]O~kr9TSĊebȆӰi ~k #Wfc{/05WDDDDDDaj1ɻ ٯm7}Z7ҿ'ov _{*6<}w36.<'nYY%3)= N :ᣁ>W}Xw+""""""B;;`ֺ_n<˦zv6:2=!Lk*iuS '&͌SLrAlVBh9q& kC\N#N{[!cHUH&8BPg$swvBҾӎI7_;!yKhS u rZCPcorq?\)[| jHл<8iAAlc5||n.CA rwp-~k{1n`# A{+]Kv6lsXͿ2m &i|z2OuBy f(*iE_9 7;~ ՚ -rOvɟlcFz`ɬ5żZܖ6lg[p`ᇂziŶ07+YEDDDDDDtTXDDD~ 4jrCpYuwr^>S_)[<ܾްI7 `Gcݥ?\ʼUɥ.~W3v' xaqζ}: 80L]ōxę cfy\a f N8}n^6; ϙe"9lAVqݜ֦įv8A|us/$,r8Ab7ݼ ? icT'ɟV8e J:Bɡ*U5Ӊd*<`kCJNoZ0RGx/!5 C^`E>>K^Fȸ9hc]KyM}2W?9yEl MHkoVkl߶Mڮ]m-wBjG@Pva"o1oX6r$@kQUC2eΊu7хLц}{~{tvQuHi@spFC|3+RDⳡە[ɹpcaVܲ! o+*pȈt8)a>3Ɍu"""""""0߭JEDDܑw`WSS,(e8Wm Rd dsUw}ޒXw;Y'aCu7@<LOSv:jtAZּCp:U^ps^bw 4t1C3Ԭ4g !\l2I7ItJC\5io;7_}ƹ _wP/3YI#H  Iw Ar$tȩ  m!Xs#~""""sH?Ե̅ԫ-KaA.H}T9'kd~027ۧ \0 2 Y&cWAd\Hᠩ C%ז/Wm~VE#xa ؁n>{7BvIUJ^|Ay6$ [jxҧyf!T烣s2dd lPΪׅ Byz9ƺ[9,e}{vX֚8!ۼ?z 1R*WXw-"""""""`jSp} ϼn.0%*g/xT^XwǙ9hrxwG[ƺ9WywNӒou71cQpnsYf㝅{9i C\55!> Oq!3+Kx>$nq8![I!N*$u}d䅡 nyH84lBΐwcDDDDg'W!7/ {'Z+ ~(  }?)(y%AH+dbP!aXo?`m"wA CgoFu𮵟6௵[l;l `o_w92ͮXMs[up}FT/.>5gXwW {'@xVaXw#g# &+ xae1q;nAy;2~4nدȈu"""""""w`j/aXEDD״mFٯKWkb͟gHԇ-+S*fu7"""""rNc DAM.$H(N2q@T ; @r 7I`j$ 4ـ$;";Ж".3E Etk(I0Q!/Гrӗ&?TDA` )Rx8SnjpEQP(S8s%L`ޣ1|dPfMY0hA903L+?Њ `ц `~-,5J`Vr)\F0L{T+fh9D's&t&LkL-0Nm0qt$- =TB9Cm1 Hy|^`)5BQHd Cn ~&췴rEȼ4k?y<;"'FJT4??x6=vp;my; x/ۇlng)اY> z6["4{=?Pt܄4ĺ^ڜX A~Xw#7+ڮ=[a[=ɇ[!%?Ώu"""""""r6374"""r~ohsP=ŖǺ?_f  }APnLuW"""b0@<.Ȗ3|$ E{EI"(E6′Ѡ^%rT'`jD^4א|ddLhP53hOV`# tE{Gz7Ss+3Y;HA0PTFa1SЬ#y`6E)jSOJy6(|`{Д2`>)L({ZQZS|."ش5,2*Yo{[At\i9UTsNb:SL:] Ƌ0Lm0!{FSL~n>8S4S 6L_.S8O#0 &`4ӒhNsiRL"p2s 6\k874ws;id47nKߍex!H[| ) =σj6H 7Mdmm5p_'SQ2xھ _!3Ӯ\ x6 '^xƾhlOP!8d//Cp'`w*?#['c6ERIƀ)MQ\'\N3‛H<$1d0pWf\`v1 &N~0M*42qs)Lapn5y`4)΋ry׹0( Jg(eg^88P4S*f!il W;Gm>8: ,ꂻN}p 9H{;r)׸LsnC݃kn`ZCh6uKBܺ\nks)C]Bݾ\ڡ}U;B#9tt[мpHw㞄]fM\s5E0=H1wٯa6rVT06 ޹ <~2wp'HyEDDDDDD>Rd ٫sW6 wΑ-Ψ809\(-o?ĩz8 1AC<.Pd_Mx 9Is~4z4;5Ypd=5YR΋N+F.h`+%ɝ5Ynd=S@~;Y T0pV`FpO͍ङϸ _p 86sMV`/>+dzL78y ̤8E/8,Ș8djvpꙬ^C&,Np[ff"oj"""""""MAA)弹;:;|yłu:ʻ$8i{DHʛxQ3p}!>} 2c!_2+B`w@&[u :| j]n?o7oe}:#_l3r.I6 Pp-s`0=I< L6O t9,#rk#7,'/f'T4iSLGS6Uy`6M((=yHI0ߙ) * ʀsЌ;3)L"8UTEVbS ~n75T89 l]Vӽu0vEhnM-4P7g s\kZA37m 4-G;p/6 vrF{q4W@t=L'?Kg+rn^p-sjr˄Z ԉ!|yVzB͡M/~\uo?6.tRO7 s,Xwr=P!Wf(rEߦND[#]ee IϚ5~[X;o 9\ '<͏ZDDDDDDDgꘚ56UXDDD>Ou#Ůu7+xnƎvF&D'Q`S\@G 5Yt \Oi7Q|`nQ` qOV`D'g5) <$LQYj)&QY=~w^VPE"eY m=U젃fj:r>t|0'Atטd=j q-u$pud3SLnn0u zS:yʸS3+:Yoh6Yo1{gdvw=8zY{'OM|d$E{4z78LzSdG{e)of~o~4|n :Y/+,Np[.C"""""""""!6wY )'#_!no{0R{9ez0i;Ƞ jɫ}%ooK`89'g_f+1-mHEm D66xlYJ7lc_ ~* 4li ;q dA2bV s.p!x0L'D0#$0f >=fy3I&Bt2EMa0}M5QӔb`^v:PMgs1( !g eq8onug il 8]T|p:M p_u*QOg{uǜ;ƻChnQg[ӝLc]N5MD3pr6B\]cZ6Z*mͥJv2 =+ .WBD:Bh:oB_k 8s-wRsjq=ˆZ!0|57C\Э =Mo~)qC=&æ^ 7m02̜Xw/"""""""3ul*EDDS?>+^W/݈gNWBJoE)~̇vHD 2L+~3h'3˃ 9;2JK( >̎A"qv'xC6WVE>6^.s+my x|—@s%`0^058t5r><8E *srvPnN UÉ3ST&SFGrꂳɹ; Mp!/s!uwh 7 =܅4fZ;=E:pܟ\ BBݸ B\G vDGtг\ It‹CBhW(™\\r`煡.q=C~ !nd oG߆gBw"">O]8rbqILuL(X ߺ'[B@Mq8/C~* X|+`l n|o8#R?Xw-"""""""aꔭye> -<J@\Xw#""""""""""`ٮyؼW&փȃf@pM{NC,rO?붵~c00J6'Br8mOpGWB}Jv1-QqQo w7! w* FcN2`z@{/ q3³M?p3h,""" ?>- ׺ -gjh-Ou2![cPzj0{I Z b=v ;` +{8ef5u{A8ng.0ƒEDDDDDDD^Nݚ75}X`9{jrӧcݍ__ABif 2p'l6Ixw]|uΗk Kq\WTj܃.8"pn` h's{}LP1}Hw`2UNa>3󼹍~&?kK v1 ~"""""*eSk ,9^X3bm~l۹kI cݵӦMG),"""go ͇Uk;/͈Ygض4]n߮7!- ] pfǟ m2ŵ6- NømMp [आr 8+Bc|naoLws;>ۄ`ʻ{(0ay&4#09;78i~3e%la+PQXMGImfa˧{aXxC簱n?>΄u2ہ@ZӵMSXDDDjd ʧu7"""""""""`>sf3kA~B1 s6pOn4øY6ܧڼoK:[[r^4DŽsVl cЍ\ΫlC_`:nσ~bǁ9t]0 /,0L'= lZw JR_YEDDDD?z]w=ޝ?^b ߌYZDDDDDDDDbSsTGyv@QnDDDDDDDDD;6V RJc7K>p67~:{N[ܚqlprkf Y5j{34`ntiVi0e0g0s2Wffs/ $kf>; L1XMމI6,-/;,d}OlLQ{_~'sfv^DDDDDDDD.LjNnJ`9{\ _8y'R|oź9τ;p Zp%L;w^k}pߊ&3$-q֖I;|p w gr18Ól'p]fo32Fpn\^00UE0 XƃC0Ck`\0;Yt7 4Et|yS n5mnmo3a7A_8!u0/ou""""""""r3ue ٣Q"CG ȷ=݈f .!#*+-n7=a=$-ap_\ [vqN[y"pvg|ϵ*`{y{\m7/ρNcpnm`/||e󔹁E@z@9&AߪUuθ{V!V+""""""""55l"""rhVpm|3u7""""""""H[d?QCpik7Bc]gqYB` mNpzifF2'x&|>3YkcG^SUUf%ہZ3`F,s ~s x'09YNv}L]Vzqcb67d@09f:s`5 t3\cv?F7؞ۓw|Z qrW źs7L=y8>Xw#""""""""ϙ:jmVE`9{~a7p6?ocݍP1ڒ f*sL4\`s`^72xy53yh`o<|`y3ʹ(s/ `#+g٬^_ϙ^,s Y=.F8Xf`0O8Y 50X <\20,1-Ya.rػ0+g.`A.+Zs U*ǔʰ`J̝N1a d-&kۢ&>&ٮ$Dz_, eM 0ǢAClѻCIDATޞh`o'[lg .f t6 \mֱ L'm@G@GLf;Gef6;@;3`.15_hYW)2hZw^^0y@S3}@SƱhl^eFf4~ S1@=1S3: PQ*wrjq Lerdzq -*pǁrǁ\ 0e ڦH PeŹ4(mHΣ%@aӜ40iB:P p'.'|"dJ&d  %l(Gđ#&Xoi{{̊$}_"N\W?A:^pGuFDDDDD"mv d^iūyc c!r۾F:D!25 ؛D~ b2DrF6Dy'i%Dxx!2k]'l?ow/%x< W_4{t;); =/,/d6x3˘ :悿߿[]~%_9"SY 7hChp`yX k Xc=l'M A@>6] 쀠` vαCf5G^OSuz񔲰e̮Xw#""""""""ϙ:+kf4k=05=s@H|`[8]L_u>Yeo&=7y`^N{y4+gdΘ72:Yo1Yoatb)ӝE`<5Y K Nj:%`F' 9m2Q5>o7/+Gk3J2a |#~ae{r悿ƿ?|]~e_-xa ],.`Hp8 /!!(ZYn tc#E~l} N;>a Zb}`!hspqJrlmQۖ^"HIu>&YD,]@M\W+ >BlcMzk ԉFDDDDDDDDc쮕Ԭ"""rhԢfPqx(r~-Xwב0*oI 8W E']/&5a\Ռet^VP塞 {d~5Yh`o=>5YjAzF{wG{wrbq4:YPz-YWn ҍ`Еf~wE4d^ȚFd3&]H"{-Q| {^/)Dx8ة+~ ^g?'E+~5~à~~#?|] ,|=2  ;vCceYܠk!4`=l;6@\k7AP4-Դ6Hnb`nhs 8f`S(a6G5Gֵ9zsl;T& S NB&% VvTSy='< y>n|ݯeXԶAXw#""""""""1ukiW`9{TQla ș']Ş)~o_*oqVV֤D?Ek5{Ǯ9 'x#l8v/j&dź]&v,D:z22{G3"{7 #{ދv"DfFM\}[9ͷ!!;ȻU>D{"|^_"yh3B Y`o _ws)v*x^u߂ v&xI|~Qf7Ưl?۹ :0hp/ ۟?/"k6 B-X2X] kA|d JSА.e# xp[ cc+muYN{] `{~aOly86lr(epl=ە`/)`o Y>msȧd_3`w8I""k9$/3|3o{b͟ÝreިurCϘ4{D`9{\)S+y)װgw^bC8Tg٨ձ.v{Őry"ĺ9WEu(d㔺3)M!ymޔXw#"VlЎ̫#1"}Fo@d7 A%;"{o3 "ky ""?ٷ!xy"üj}.r!Dze"zcxm1^W>c69xs|o_?߀Wğtj>fϴ߁w/`Sv?F'?''YA5,`.X2 v'׃xVCyP,*-A#6@\Fl[2`@Pa킗mvNeO?`Gv5`kM,f8m9{ ^m>D*p?ϑY;$>'"GdϪսjл{%u7e0s82فJDDDDDDDD?cը ę75+d7aͬb xPuP1Cs]z_enw۴kXO3Xw#$ ĎHH{Af 02_ eD>F7!2{ . !r[h߆H̻)=DZykr|;  2^7>o~\aFy&S+e:| ~fw`xÃu&soi7<7~??@/?""""r:咛JmJ߻۳tǺSjK}6iGBa0wF᧖%n;T5S:'CR£+bݍș> y`ah\)iK W32K~ G^s4!ћ9-@$ʻ)}jz"m|^>oNp7̻~|o"_Ob*xWvx%y| ^=3o`&x=~=~.~o_j3/G;3,?O'ϰRv ),`1X Y 3IVA01Hke-j`GД dW &6CP&`avc=vde-`Rrn[`#T\=8q}4"H&m#,g=>"""""tS+o }UwmI]nOGĺۤVf+,"""g7zw%>a {QXwɿΓ'A>9G *~)=Qvux2K&؟'UݷzzϿO[{SuFWWz9g֭8U/Yuvpk?c箴J6=oϬ[u*X&|~U:k6~e?8~u?ןy7m5>:|`Mz:{?-|kZϩ}_֧߷W~u:ߩzj?9/߿mg=v纬 >oA6ܿ _7ج:zS_؍6; Ml:u_tϨ;lz'Y9sS\3k8>S=? rZ]l~uSn?>[uV5mv~:>n?Zu3קuZ1,؟g}S^SuASXOe9#X8Y{79/k8co[̞~~NgSj]7~)r1N>8oĿ/J3e $]Y$k/Ow=6<EDDDDDDtzظGa`Û&du7s_5}cݍ۾VfibsMZ}f?5_i3%]%w)Rk .K0X w<2fzXw)""""""]44n lqo Z8n}vMho\2Ao׉x|҃X rRwِoǺsGZ;y h;ƺ+-SZ5;:Muɱ&~d-.X9ĺ^B(~(so#'ibnӦR^>$ƺ[9Wa}~ijY()A88mMΟGK/l~/S:?N\wNv]{p׿G֦ܵtIq?@ߍ]~?>ծm~ """""""""'Z57 4(ܓ>3soy]f(l\+W-ssQB$kuɚn`w/W^>Ex/du""""""rjc0b]c݈oBJḒpGoDN~ ^%=j }X>ϝl،0E3aWDDDDDDtJQoz]؇vdkT,ܦK/>q2_6h/x߆ȟ} (,"""gK\w}z]rߞw7xNad84t_ǺWd{KFCgvEۺܴ#?8Ǻk9=]& @˩ "揓:"vWҕfY0WS=kcݍݘoysEDDskOkO w%ɐ^iO5%ؗ2vI_xmU1c WaO*""""""o]?g+S*1zyñGASuV u7""""""""""""""""weN 9%pwτퟍ?&mu9r ԮB)prnJ/oR~/ۜrHL+0֟55Cry=ox0}0LkcvX\O&=pR(AXwfS; bݍݙ jmo~""""z78\pkBkΣieaYº~f w kŗNeZ78]e}{+6}DDDDDDDDDDDDDDDDlanu "/;yk@77v&ź_r2`h0vZpl{lBߪ\^!ibRQ~/i~ o6[dĺ+9ۘkG+,"""kUpk^7zB/H(yc}sa0qs2n7툈9L}B U _*>/vܓ3d>͸ FƁ/ű~+""""""""""""""""r3um|?PXDDDU/.einBq}禽rz&}f>vص@k_Kފ9P#r7nh#4_|6+EOۈ0wΒޕ0+쪐?v;02.RDDDDDDDDDDDDDDDDU^RXDD䯬[KqF @uךn?%_9}[rPj(G{)u#kaŦV 9͞9/,̫y"쓽ArY å*C iBR{–mǭi쯎u""""""""""""""""""""""""Wcꕮ]EDDJ]R쫢\PAw>ܢg*WcXk0n"""ovj<G_ĕ}RuË^ """"""""""""""""""""""""rn3ZQXDD$oL=%;:gОBw~d6C]]Rӿǘ7^;nq~+"""""""""""""""""""""""""&Ss^- *,"" ne7{ɸ2PbbB~.[ F}a+aš-W?E?*Hn~El+밺]AMk`͗m$""""""""""""""""""""""""rn1z׾E5EDDb _q ]=~ݷ\sP! nCϻ~>=/vG߯e^n;0!oIDDDDDDDDDDDDDDDDDDDDDDDD"6DDDMtZy{@]}¡#dv{+@ц=uO=VO~]ruBH:v{(oKDDDDDDDDDDDDDDDDDDDDDDDD"7s^DuOL~8Y_U7sB+Z,`3eVr=󂈈ȟ"kpX!""rniNZ%_k:ؒmP*=%[waF/X yADDDDDDDDDDDDDDDDDDDDDDDDO"75XDDUp~ ۶hZ[y]4JQlrKvS~k"""""""""""""""""""""""""yȥ """RUKS"oٴmˀ+>l|~!7\?;۝_k^󞂃y^wd ^9$Dnrkȟ#qFfuDz[y(ZT9.7d; <@nLDDDDDDDDDDDDDDDDDDDDDDDD"4XDDϑ8,RML8gɪ'gL.~]r&wyŬ?kB&ɭȟ#GO^9=cR8&7~26dwx&^w{w"4b" R("!D&lB6$G{v=3y3I<󼧻dz e spI;=!oӍD.0cG*2˗N~ß3CB%sKbE3T<`@&dI&meml?=䣬VĹd9UTf 3xGZTU^òi= IaY&:J|-q$U& z=d([үIz3dJQ& @*ubۑ:sߐ GC6=^j[5M2t'_ZO#o%=J$CR78pBQ"GF TUВT;\ƺ~k_ڴVZ7dMkz6L:B 1ҺQ0՘,:3,0|(ɬĆ2Gž ^Rwn[3$_ yH $z:XR> JF௃ )'("5Ts`ǖ\: TN&|{s_!IzA 2*z]RLt0Hz'U;bvyyQܸ+dyOR4v}" x^Q&-KdFK`Rٗ 5N*vqg,-]j9[g"i7 @j1@5^:qĮ?KUb|ܸ:TluFZV,=A>_/سQ$ {V*B?tbҴ\t**E5>)[G{Nu  o?|bR^F͎ʥA2Ck4H0ip" 放 $={HuwmwtW/]VodsR /=گoojɭgV_~gi}TPC 'R =x3K>#nُ~˸wpn~5WIܚ7`щ> @fY&4$~|N^" rܽAR/f ~]ǕEˏ!߷ ]_ =+PZC?/UQmN7쫳LUM|KJN?9m7̸g&:/m-P%<""\"ڧuvPmr1 wP L2)[ZO$eFkKi$LtQfD`.e/`4eF &4HQ&FE`ND FK`>#F(Qfz`:v(L0`2˗}L0`2$)ؗ}0LtQf L_:1,_ `I&_ZOD`>e 2!PlKuy6L*1DRQ?zvZ2m/G9jJK[V:Y>Ln|JƋ҃MF=-EcOvgtpفsGIÛF| d\'g^Nu$sE~gj̩:I={̼RF -k8K'=݋zPBRj j-ֳ.̛+iasW—BWW]+xfv^cO@Zg 5KurF:w~?K=燔e{MP 2~Ƭfl+5bu=Ry/瞘zU."5n֠eiGU*(}qJINo:d~Y0hIm}vR䧓Z䌞 Ou2ȘX/?֓G~L_ZO \]8O,R-7%U]yYrSCWIҧj dAJg/c7?VrtΚ{+tRxfKlWU:Y2sNl]?PJ q =.6S8uK/䌞/#/}^eE~g 5WtJ-۵i'{ ?SEL'WPbR }-sX񚴰⽳H cݏ.9[֣އ__Rx,c:9uKoHK__z[os~HU]d.}{a/ސXN>wPRhТ;,,0gM8cھzV٥}o<涏qǸ9,r@䴤mW( [?bN ͷK;=:(2:9ur$z9Zz]os~HFli= +8Q"\yB{sڮ+mmGXMToG<0 )ڭk*n{m:)qdȒߏ+_R9^sUܸ*yFT'娛#7nzzT:9]#/}^N-#?{h0$VĿ=ӹ=u=~1u367w?E@Ą$hv[؏AԺO*0`hR[乕pܒ_D !d?xލ5U'ڣRo\=K޼  NN䌞rI~;?t"?2D`nO`i}(]˰oHGw$14, ;Y}RKU)Iz`)nn Hy]Q7K_X#`{ō+U*Jߛ_#9s:9}#/}^N-#?{(sVH[PRS.H27Ir\-.@S>h at \jAۭwJ2f']~TǂErs\,c>=?H+:w~"?2LZ4E_q rR>?{W/~u1嵒޳|.~s_$Iww~H)YbrX'ǿ3z~N?@z9$LR1Z g9x7P>ɰC{^Qr$5/+$e|y\}ơ'wv^S?{3|urRz urX'LrXO_fi(|E`8Yc6Origq?,r'%IrgOY%>2JurF2 bL~29b=I~)? :NʭA*p\1LJ'Eh1?TL8㤇ѽl25[5JurF2 bL~29b=I~)? LŊoro[p['I{|%e'@Y1?tH8c^"NWJ3z~aNdɱ^IL!mmZ8[5HOr$i@[ۚqT-IRqqy͆KurF2 cL~27c=I~(? Y8Yc6O2I&I tl?*\Ys:9~)N@:9~@z9~'/凴aY&:LEz`a $'=VyeцUZ!7yKurF2 cL~27c=I~)? L2d*rVqCl}tO^V]1||b|bnurRzurX'̍rXO_fi qn[7 l 6>o!ņUU.w2KurF2 cL~27c=I~)?`*@&bp\%%--q7džU$^ۑ_K+'l8@`^'g ` Sc֓䗙C0Қd:rM9 >)jDщ9tamx\|`G Z=$Ω H䌞d:dnz2Q~HFY&@f,g-0 TO !$mN8u[ۇƩc!CHJo8gR x:9xN&?x$̔҆Q&V7"W 1 P['$i8u]IbWvkx\ȱ!C$Nn>T:9^)N@:9^@&z9^'/3凴aY0Lɪ ;8{X㎒-aq2jt][4Rl|urRz urX'LrXO_fi( s R\,dZI+\zTTjg<oޑ{!M ,C|xjturX'LrXO_fHkf8+8o2HutfC烤j{qz~w>ovrTd1?̏;~ߙ}K*#d(eurF=bL~29b=I~1?.t|U^} =I0|P^$פ[J_gd’mݕr~st m];wKJX'[3z~N?$^֓䗙C2$ SqA9Hut[~S' ^&*Y|qzSiYWux^=\:MxxRQuyK _>%<*pc&g5T~5M6VzP/I|Slu_'g b,u2^zPsyכJ%>߆R=A7lԛaaa4oJ%)6+?sIiͿX)kQգō7+z[Ww"޺z o?|ApI/3@YۧSxRY'g b: .ˬ'ɏ6 57U" 7# I~h|ndAmIMk4*ԟ_ fNg]ݽwz;,Rٜ4 +4:k&+nykfqiN@z:u2@\YO!mj>Y  ịGd-i|nHIA[HM[4~Yͳgfx,MHn5~0}h[o|?*ɖ:9dX'̉e'ɏ`+jN0<ƍ}wpgwֽweItQ& FY&4HQ& 0` 2F tQ&(Qf ~i= Rt`_:(L0` Lz&:h0˗}0L`^e}0,_:(L(| ~i= b``YtQf(Q& Ew0` 2FK`> &:(30$__ZOD`D FK`>Dw F &4H} ̢0`'2D`>e/`0D0`/| ~i= llRw*'~ 7WJ$:Iެ3ua6J%,Wlda$or$M n\u K,mɾҶi X?.h>aF D$i -I҄K^yJ]\E$wAޒL2inZg3, ]v٢şoϻ![<uUK/HDX ON:F:z9~;ʨv\ܾ{oMilQe*Wݲ"g@ڢ sI>=Ur814{Q~I.]ӗ\rAo?aRK[ңƏJ5jcN!~cS'Of9s)e/3rߵMaGjԳ{KoOFӪ r$|D*Y3 ՙej'=Na;$͎JuO#kHn (*9rtF ,'N l4XY!9$s _I&+)#jVGǟ?y쥁_Z(mXۖ ix|aI& G9ZS(Lun^ݿ&q]jAZ?RjE,|i`O սe%g$wҗ->m! &$=#9:;z: ۔[R4s=.UHR4PRs ^5X>L~%˿w.rta6=_l?2/ ƘJTgp$=I$Huw6u3P󡫥5y{t?IS$I'zFҋ11-^Ԩy҃F$}5^4XZ2cx2KSg2i[}n'=j;4tYtE3WHkj6=k_z u}NG?&լ_x0isI) [v)~Q``W3j#c_ޭp+HۖqtҕypO -9I'&\5ٺ`O x<;Xp6G$4NhlyGRQ&ͫuCixV" ,ب`)jE`FXx'\"w+x~ 3HZ,ZE5W7I&-Քu28Oo-}5aFOꬖ˲8Ǎ+y6EH>-}{ (!0Ƞc;9 "2o^8Rp Bײ5!n30)m6'2h"n<@pu>T)y[-/~? DR/W;/mWaYx E۴Z*\gI[}0!m!A,%p:=3y}qc秜>j6CoWJrtkޭwZ|-^|gjS0v]7mxm^ ܖr }V6nF Z1+Il{wШ^%i\Cƃَ. 2⎗lVҽXcƃU>*DTSgɷio9)r[H ?х϶'>^;KOOԲ~N{#R/R~3Jy_{9sϒR#I?"]|'Kikk=6=p%O\*N,q&EgIZTVeIYdesקAQGYJ/ߓf踣N^vqGy%]EQTzJ} +0XYQDň)/4}xD[դ}8w{ȞҕW\˚7ߐjoz;n7fAJZs^ά~$ߘb {VL}}7Ɗ~ +0X2]SV2rXTzOzP@s]-wtvG|]?Ny1WW.KvedokECi_oZga?iҴ"?7皵 +8(7 ."ztҫPˡY l([`CN;NV?1t'iϾܕ8D5:򛧟=2z8Q. 9oVoTᮁn]ICZ kѷyx)8kqr.r֭ď͞xK%|1(Q.s]J2kOw"_ܳZT% [I /muBRu\ʥrͥ+:}몆nK_i4)pij9_ |sm"{=^ek1:I۴U7$I$Iu ]+vq'?eTrZHmK}oXIZO/s^".e}_5қkjM߂Y&1$d%t$IUdtBR6mtGwJSO`}nIVɵRjB:Ԗn߾OSsVIyQ{)IFI9vʵkNeT^TF޵_yAp}7[I_T\8E:?o1N3Kp_ƦJ7`SI}cProS^=~^%IiAr4X2&(q$/.IYUR֢YeTe1BzaIwpG#sJ.#hCOJQj녧\u"dG9h/Hy߿vHyNϾ>Ij ᭧o+ܗPN6_B\b)K\K@/4 V\܊uDϦfJ2\wTLUXRkm\ҍE7Jߚ,}}Tq[=mxzFe0vQ 0 ClAI3?TS&I>K|y?UYS~q uä o NrdVd=p!0:*%_+<)d#.\8א*42iډEGFIn]\y:湐4{ƕ!]??7T?#|8zx*9 KZl!ia)7]Rg!sz X@nɷG>)؉|HejHs^WpQiؗCNsg)y!{W'1r7Kyb;OJlVIԵʓr-~ta؅cCb7AFU]]4G  X->(6Ozw #IYfk%x'_gi#( p\#:Mwv-kRwȕ ذ\垊h(CLoBlZQM9YȡCIfG"_Ĩ ݵ"&ϵZIc~grUr qt;+g'kF~Ӯ'ee==3p+F+{c 0n)(;=>9nP /_FڷkCus?S7\nV=op\w9qqAOxB$c.rTXTN9OHѴ1$R4c'})@7%ߞ5$a._:_S\6~zҰ^C換$}q_|/ە_ T їC]`*y}Hg|m KʉMH."Nbzb>'I}-r~V]t >:̸07]ER+^? 7-|7/ԿϜXSkO⯻v=Gf?z(9thhruu8@t\wr¯7/0GjK3~vf>O^oz;H$׵4f;' e HG厗/}Pc\/y -ekn^}T2_1W7Mr'{ϫ)T|\ɦ'}$x}wJO|y|Ώ;w(+y>Ɡukv\DV;m fXI:aobZ'!@8lw$񸀋_= H2G/u9J7Ҁιۭ:6;~6H;okBAOO:WRI-r\2Zʥ*-WWz{yo&&oܮ'~v]GIT;ɯl?H9%$Id7|nB*6'%_fTm 5>Ix$s̻뿎@ӓ:~h i^rnK,IWig7gg5ZTZ#6GIOh<^-ڣK;L{s~՚-[~?~}A҃z$陿H35!i:,$H4cYEo7JvzQZ'=M>K={I|i;] w~M3.rN]%I"sRIRUzUr=#F~|qW<ɱL,#)O?jfԔS$;%u}q+r / =$4rKJǚ}dL'X IkDz[ZSKK~e]uT̳x“3N_89 E.:@H]:R`sAхCt/w5ze54om"Nq_W3NŃƹϯ[P`Zd\o>߿uZ1w }3d{K [$o{~Od3٤aMޙ. Iō/5t⍤_6}xiimI+/<<#qg}?fz^uSH{UWQE44$jx$,|!4UBTp‘iJC#:IZ+w{xOZmi[v璦.؄9 UWJ]6=~y Ǿ_&KхnY;5}ʹڧFkK8.lZXy_8]KR[N2K ,;E.)oF\=y3p܂^4߷׀'5Z6i>RE:_{{.bw/}ڦߴCaY;/rLP(#n?b_( }fOxs~rW+H,f<$ },77tN SLMD5O_$i|_~+]s|S-M:,tptɬ1&CU QA}LRҙ'%oW I/E^q4W .IV~gOްw̓Ik޵B`͌҆|[oIB~E[h|u;K'iUܸu\SZwU#OF4fJ%rQdw 6s֛lYHy᭎ҍ4g.AVB9 'ģ O!=itIf%}ຮ! ;nK\˹vr4n|'rv J=Ǿ_&[^2)ݜ8tǪ`iD,\'vpqo( @י2K 3Z;>O:Eq ~-ڂW%9C6XM| _Ieu.O벷(Qc%)D Qt dd1@ Փ7j\K| gl,~WsJSnyRjUa,;'v3PjE\t#"H:2S(vQ/D65޸kkz}B-K\-mʲqRgsY#Eu5_25L47[dhuMu{aN;$ mVJV%/7B==>j<)ڢg|=Nqo_ݗzj*i#i/~KY_ ?5?VdW|XEFK*=o_#ҍYrRyKGA|1exOB#JSGM7䥈O\>s' ZGKR~[a|_뛚Kސ/i4sF/OH.)oF\3_R$zw~132*[T<5;$컳}!]xt9=e)]~C2e0nlܠwg|( r|)n\-|6tz[ `ՎZSHXIrXb)/yN{&䙄Xt$`޺R \s_].kk)nGl?zB=])[?ےʤI؁\)|:qx?*ŏoYjj*\xLQ/W/'Tٹa:=T)6NvP QRdGSq+=c,(elj|+e'U)uJ#%EyWvqJkV^i#OyX,8>v=-)wE^ݥ!E)V>hI%{n9\?J|{3JM2{tgw-]$n> c:?esqY$<>YJN9‡E\z|~;a9tVq-$[2Mߐ @Ff}`c7|=orTw C_>k]^}u@s)~a, LV=o-6W>V׽קqI2/yqwo'R';HVF$ {}Iyu}VkiA߽/f%)] .^ڙsL3?{xŽԻRW{J\)~T'xIаŎuBw*+}eNߐ @fp:b;'+ǮyTz@*9TboǍqNR-=wf?3fa;][`eoy*DJS򉙇GvHAl_O"$HHIsz?ȅFhvaa~T/'irINDOtŁWKT\)d.4Pr[O? μqzٹ i-V;B )B-soj~}[eR_?zd@3t^ 8$R(Q.珚95iyJj.Kx*hצ iMn;-*tl8L.VwvNvCVuڳOy)BL2%aaVt60?oy׊8ˆpnZ7&:rUQoFHTGxƽ [擧qCp?vARtH-_꼥ܻs7ϹUREI⎇]6Dz$<.p$,ݲn2V򏿧~Z"SK7BzHK}jGѕ G=~__t¸Kx|3Z)q)飠}:]zل#fHk8_|S>O:|t=/?st7~zGۧ6bY),D8Y|R+٤E |bmxwu8݌1L4* ykXR':P ><b #,{>8(ˍ_{^B%&T@ ^ꠎzARTר1Qgo{uv$m[Ki(dKiAz-ܳrT: (t0qwypeHMߘ{2i;tstdIT%A^/k &yE$ci~׊`[Qiq!H{b\*=? ?&/u?vO+˯g$I58UvjtD_/ǣsA}{K$)'gQI9?O}hh,/yɤt{-u[~^$^j"8 g顳8Ǫ7zt"}:iODF6am|\:x>?}FVg;;o)-x~P7%ܩS+ÒZw_"懷 3|sxIdd(R"-y+p/F\K@Xߔk~5T+RnzRf*80H|N7ZljFV^NҤN/,=g>ߌ2W+4]("?V,O3;5j]6iq4l{o?$mV;8K^#<䓌y*,BG 5lB2ZQ윌p;X̣G;1?;X(f$yQGK$ՉCrNeH[J{A@Պ.w/<S??>|%ݒŲР[AK2t%{GS:=r!8^>U~QJoH_<4]3L/pjgs4;6ӧ~_71rdϵ$:UԈJNYIhSN'mJYTFLWsc"RS>)v=yX"Ml~?SґzG8ևHQt ddr-I&eI,VݪCr7xY]~qtJG3$y'X,qM=Oݵ"#@6pуD:G]~6IXSLr".婜wK莧QTy0)`Q#s)]X'm_nͿ$ƽOEYSIzX_-m? _L<ƱnmdUUMƎbOdI?&;~6e:b:ߗQҖqeǔYTcs_U]YtǾOLw=g29EI>O B/ o&rLc:T_iAﴼTrH}ڽRّ^-}*vu_ov2aRĨQߦudnFp249ꚅ:ʥz5x|Gsf%M?΄6c{4|[Y=?]36̉avq0GWcK;.nX%ϘN{I;?HSCL`/j7]l %]tfQ1a\<IfH}L<.,ms`9s}TuHA$ )~c~L=QۓO$}z%`E`קO\K%g7ʼnCy^SRUKUr,(tu}tX 2$_4qnZw)t>2#iM* T+5'yRн%$;T %<{I)~T6)x`jM'2 ,uH:v胓S>xш,\r`%{jemZ|t|iYRsF7&u_n[/Z̵RПBG=ņɣx~K%</d0$(-(5A}U7_q]꾺i.1A 6h@p?ٮ __Gbpϴ&~=;LR6jQ'.kz侾hY@U)7kYq%id0np+e)=~6c9Z,2 \7O~pj/JC~ :tN6?NtR\jRkLN鵃x$ixtKjVvyUrOV~2&INؿ\X<\/׶RwA<}ZUV;WKw'wS-wtUzi%WJ#4rd|ut!ύ ZH3O5">byRZN$sۤOJM3&}:,T6hyK=URuUיѩS3^iȱa}KoMaRԤq=Fe}-ǔ;7N7a~i]7mtA5S12MulygիW7)sλrom+}mʴpMY _> (ij˞bhk?5@4Uk֣ކۥe*rmS3IR&wo>$Rn 27=~6EY)0οJJRW.].r9fO-=yI'%ǟw:7㒗ڵFKzݤ/wJ~M.ξ뤏VL}21s$E?Zz2`TිLSwk )_E<% /-uwVתPےy)vLZ={ոuҝo[d9ߗGÀ叜 -|E%O$$ĖN_N=68c.Ǥv4rom;)ɤNs#=XJ.XRaoS>.I؀u~9`̣=Ҷ[TW\YIYSfޝCǮKߎONc_g5If4YĽJ9r$ g:%Vl6.oIf}eybk<w>'l۽n8^.SOHSNk^5iٶשׁ#4u 2vT.wygސ^Ɲ.>R9ŧٓT_H9FCn-d2*Q&N쌣Y'5$|魹/Jc4NC~Jc m[JzrUKNewōmqһ»>~;c{EK]w 6=Uc]QldL)oI&H%K"#ݑ:'m.rt|tk;J]ܧM`UI X&/ 2X{#G%Yf%>o<+MwLj^?|tKK~ӗ-) _Q]ucY׼$ϙGA7VoBF< #0,_pqеD b;e?J/4R+Ts*߲NrTT₿6H3]>ipuzIG 4BʡWY.ͽ>'btw-^fa鴗<]( ㆘{Ӻ$¸QFSqOv)j[YgNMES"wv6-nG>';H>ȻC x2M6tK;[nFAM_ޭ<>^~]lZߊ5)[W-s-Ũސ&y/CðG5} /(rZJ?p'Pz{ω=o~z$Yu0 e)Qjga8Ӟ%}~qOwKrKkLI }(4`g{|97S_sIGVqTuJ7^!ܼ*vTw s/n?qe&ߒ&GL{]ɯYG$aC|!L?s4T'؂LrOyQw픴RM%U~rB]]c^ 927tV=z]t̑'~]Tpȡ\WIo$ɥn’'2!Oy;&Ϥv6x۪o$G#_F唌"WKd瓴K& ۻFTTŞjͮM[gZKYfe^}}y-2,tdOzI[B7A:u"^:U~ÞLO5W [UvFJJ#+-K*ZK|z-=Z I;幔|ihMߤk=uc;dU$S慄O\Ke?WQک'Ѳ_/UQPiTbqޑt})gusA/$Ǫѝ .=tCI>8wRݣ'Jnٴc0aG%mUc'~̫ܶVTTZOdِ HO2`Nez$:v(Lt`/`0݈L0ݱD1 CAP`3Be{=H~ ;T:`X2i`ءҹï$IeN+U*`Xi`ء2 CI;N I* ;T&`Xi`ء2ï$p+U* [T& ;T\`Wv*>@\`ء2( N+u*z$N+U`ʤ`*N`XW^2 +T: ;T&Wvi`ء2|g|`J+I+U`ʤ0P|g8 B Cer`ʤ`Jz$u&WvL`J`* HeVLn3$t: Be Cs`_V3$t0 T0Pgr`Dtq~ ; Ber`(v-* `J`* He C98 N{=Hʤ +T:WvI>rugIENDB`jeremyevans-roda-4f30bb3/www/public/images/popular/rps.png000066400000000000000000007155421516720775400237710ustar00rootroot00000000000000PNG  IHDR8C cHRMz&u0`:pQ<bKGD XIDATxuTev.-Hww4"Ң*( KtK*ݝxY93|^s՜ju&BDDDDDDDDDDDDDDDDDDDDDDDDDDDDDΌ A.H!""""""""""""""""""""""""""""""""""""""""""""""""""""""""""NŌ"83VXDDDDDDDDDDDDDDDDDDDDDDDDDDDD9*0c"""""""""""""""""""""""""""""@q] Eqf*s0cbR``Ɗq fbdQ`gaj(s0c!PEDDDDDDDDDDDDDDDDDDDDDDDDDDDD+,""""""""""""""""""""""""""""$XXDDDDDDDDDDDDDDDDDDDDDDDDDDDDY```Ɗ`GCDDDDDDDDDDDDDDDDDDDDDDDDDDDDD :p"8Eq"fXL,""""""""""""""""""""""""""""XDEDDDDDDDDDDDDDDDDDDDDDDDDDDDD+] Eqf*s0cU`ga6Y(s0c5*s0c""""""""""""""""""""""""""""" .@@Eqf,,"""""""""""""""""""""""""""",XXDDDDDDDDDDDDDDDDDDDDDDDDDDDD)83,;"""""""""""""""""""""""""""""-,""""""""""""""""""""""""""""XT`"""""""""""""""""""""""""""""N$"83XDDDDDDDDDDDDDDDDDDDDDDDDDDDD9MV,:,""""""""""""""""""""""""""""Xb+,""""""""""""""""""""""""""""XM,""""""""""""""""""""""""""""$XXDDDDDDDDDDDDDDDDDDDDDDDDDDDD)83,;"""""""""""""""""""""""""""""-,""""""""""""""""""""""""""""X T``Ƣ"""""""""""""""""""""""""""""Œ"83VXDDDDDDDDDDDDDDDDDDDDDDDDDDDD9``8P`b6Y(s0c%Ф"""""""""""""""""""""""""""""N :,""""""""""""""""""""""""""""XM] ,""""""""""""""""""""~-K,ц}> 0(GVDDDDDD`23gqk{΅:OjOk柟߬q6,IC][+EyO L߄ .EDDDDDDΌ@] yt Nwt3P4{9x⹊_ܵs7˕N;:]SH1=Ŭ ?)<7z]0oYDDDDDD$Ln3]g64CJֲNɜ4ӥO3ZNoN>nQh&!K,]|cQPH~_Q&nmƶ&4Ydws3>[ĺۿY-fWdY?DzDzDz9W8W'^Ggtt' ]iZAE6%;*qL LLc^芫Ltѵ0^C ZiEkO5[]@E*QHc3M:>aE~+:W8J8&~ݡ"WHOy_]~,λ8pHϸ Xt+!""""Tr~@7KgQ} i_5r6|^z~ꧫU?\]Mnۿt Z:[hBom83L3 `Nl>BkR~y礘_ L|2c s Xyy]J/> .@DDDDDDĖX$-Wr.MX+C^gü@MW6- 6נ$\pU™@F+fxW+Sr9<@G(~hssnos~~6z_b]o>%;;RmwK#SמI'pt)EDDD}UڥtҿWD…^8h|;8Spd~}5ݤn`Bjh%薫W]4tOsy>"""$_| l@DDDDDDf,X$-WNHZrBʶF&_5]$/d|1k51}lql`w`DЈV;w^]$K,9`7bs-,>.wXG-`y.p^@+Z (MECjSN|sb-.B"=>n絛Wc.8=+][y[To{6@7K'Ƣ-6sڈumn`۾}Mv5$Os9P-o ,*0@(TZRinoZkMwtDDD""""""""qfJIOA[;@l`EPvK[o2IV(mE$g>]R טHסIn@諒K)ӒwU W;Yt|*8tN^wLzd(~۴ica\R*W utDDDM(ֳI0x=x3ϙw`!v|3>c_HN+unaYe7FѭxYfm5²KM6ѥqN@DDDDDD& nᅷ9؊PD;@QMlzjp~ pWop `>|Ke,̒|:(hQyoh8{+Q C.O8 V5Z]gXI0g8t"""/ b"?d!m^oc9Q&̯`ղ1v}p)Żiz릹I!}%a5AS7>U fz!4K'""`!o`1ؓ+] ';>v'"_>(h2&ɠAKWr@m^r-G?_Fхy}nZǯfs,oۦsgu~#,w8rk^ 9($h,ھׅϡ#̂A/H҉kqf?| @-F.N];$EmU_j+_:?hG Vo,|%-Lb'SV[o̼(./#k^.""""""GfIZ"""""""ɓH3 Ji>O] jm_5Ii;͑m󯳋߷kWmCdrtacn k~>FttDDD䵘 hE%O5~F ;0/ <*$$gLc!0׈CZofMmf嵖[YZtYK%"" Gk^.""""""gf,z+yo;? @uy_?o1\.>g<|sq' O[.x#:7蚣 ^}|}$m R OeҎJ'+歛nR}j@ƕ3UTRՂ'i"ֱ׼=+w^cao?vjvގ.ĉMy+{(\!0wuw9rKK zrioUύ7k 8xp3cϬE|T'}yj=`S/砐 7mְӼ"=g&7߄Ֆ !W\s="E 7A,|o|=cy:t | P|e[ԟL428/u qu^!,,Ei%޼\l lMD@<g<4쓫oN9݇);ۗ?UT U>Q)*p"+­ƒv m亓!',`A wC'U;t|kppznIMA{HH<qv.30}?|[oc4v U/]6Cf̸CBso^FCh wvkڞigq0Q40|e{256j8QS']"%qfZH)h goEWsV_k0ƈn=SXnikA0Ѡ-o7?YNq#" qV巖Tk>~0YW{]uҘKC.-S JQvn (X`cb⿥rZbmǴ٦=Tio/_pyOrѵ:.o|!ss~ TUתeWB1h[C/NS[\/?G;vD֟ZvN^exWa-30|;<3[DD.| 3wy@EDDDDDD OBOH}]wއC%n ʒ,ÆpצҔ7mq|WW/vxw%Xky`i-3p0Prn3n8ntyyS=d\>-n%-m|" XQgE#`>(Zbb2z9|^1> K(E1pVi:-lf(]Ws}.|CB>|I\seu>fgFO{^%4Sh~x\qs`ߣ} vv4}~_acgwZxn3U9@dH 'I0|ϓ?O<-ܫypj٩ykk-n>lMk=ݢ>^#~w_3mX9C I!Ȥ{}uמ|PAzn| 0ptߏF{/ 7jqskL1oL`^o#(܎r!`}ဋO'߻W`E-[ b0. XL%7f8Bsf -^zn|rɕ;5rѵw+?͙:gΜWcl5r'ʝ! H,m4y gɇ&~[v}+<4"p7q]Z: {mo!PhccnݻvttO[Ÿp |m%"A5<1뭋˭Ӡ2tJ/9d)%zeOR$9zw^u[MN-;5Թ.f-(C5)2vk0*qfC' um軮'_Yg~8}ѵ~wޘUujՅUAZ:7RI}{W^{˛8LszObͽJ)|³ǩQݫ@)CG.wm7 ӢzLN?;x+q{ٮ?miΜ񓏷}^Ո<ϳo\oYgaEVo?nnmS`j}}x"YQWхOV ηv1t{eۧvgLL2v39p ?~ ꍪ,y^>W 78Xon|nRdv8Hrɑ+5heˠnzFBoW\ g.2uqX87:u\ڀ .?)Zk$$ml߹禝RMf\kF;:qkH'Šu޼7luk88P`BoWZfmY!~oA]'t5GpnZW~i1@Z;7>^֟~[-; ̛sJ8OwRI'2&m40Yvvב`!I&m:=ҳLzm$ە/K,DoG an-ML )UhwqJ8M9]@\mzm>O66gWۗ#7+'wI>@DDDDDD$>X4IZ"""""""'\C>aL C5?9F v ۺl3_p{˲в2Ӝ4-˶m}ޟ {#Wѭ&' &~PGq:/{ 8+ fPh;gm~ԮoRKlXw <#Ê=W<)Sh3Ar݀||ik Yd)%M~#U'oYNFW }~JӼ9_~Bpͭnrt]ͫ} F2c~h R7O1u~d&凶lM41tQ߿z(^з`Y0w13hqǝƵv׸+N1uh>}O-=CI& e;\%_ҙ ˺,3}K_o0`umxH.'}u9KWNCͯ CwW/,.zyTaG5|#dcfۛK߲Բ2hC[G'4W~Ɇ&#z~ A#ZO}b)JQ=tn[OK,[/ov4hgϯԭMxEpf0ac@-5_Pc >6!|]JY7 G-<4)F`Lӱ/\ þE3seN.f[ǻl/{׈Fv ZHo^xm| ZO!I$i=.,PfK2o EYt4pfW]sul|Ȧl̲~sn ^$~XפǧИnh /̺p ^x1EzpvO+5wo5}O 4V[nÈ=y\8>p}z3.\x=sþSpz:́Oj:}i'>EBqݾ&=f /̢eWlzvޙc{1hIxSXgeU' K,U/Xk\us@(sjWWl"%W꜇`׬CYf*lrՆr0wܟ;O>W_A7~Q,u9 *,?%HoGG:jk?|ӆvZލQqNu*9sL$Fz,ݣo+[iqF4]M_Bkzn QnQn!([ m¨QF.*}Oi_UO8]*/߀C-7:չW\6f˺κ:ntfH#KĢTSM Ɖ,,L_~q?z<05'_ȒEs~LK4´zÏ7Kw|rZxnӮ;B{?|MT9k}  ] 'i:Dѕ~{T_y^C%[py C(GŞAƵ ֜^x j= iOnmy`fbw c ,vN>0yml[Yf kO/>sx!GC% \v} )lA05T#{8m+xTQG L䣢jdD/+8|x{N&ٯ `oYl ɐ^9@i; ,ҁv>`鸥ۗŷQܐG3^ymI$%%Ҡ O  GtyI6id|(7{7z^{Rc3|TB:WEy0G*iҨv(~bA̵ y^_z8@ZבsӾΰ:Qo,<2bxRI^ )[$凶'i#6N;M&=y3]zl.gh/3=W֯JM\fjѥ{}uZP)=G췡zjk3!g<{ѥa>Ʋ:5J7?lXm-oNpļ?ƻ}*\\K!? _cx<.\xP U*pt#f}Z˷omo!""""""0cC;/p}zm:VxnW3_30_9hώjӌL/#fC{l\p_G]w.K=6 9ֱ蕢l TlCY~z`[ml y-D~ϵ#qa6Eu15QQ^Q~lKq`wh )II#3ȑ_,N<] V Xz9{h/O… *~Y\n%I_6} aÎWRKM*.8ù/ fbGnbӫL/Ms9q紝Ы1>{ sIO0BaG7ʫnzCYlo!""""""0z+SH}&+9Ϋq&U 0=519adC[I03 ªj eZ)rT/OkK/ƭ\b7&+<]Y]qi/'ޯL{Y ú?*oT\oDI&$*Iw0FDvvW,$duKw&+a`乤} NZ6{}rHY5e6 \:ұ\_Kq_ê ;dTSqOŷp|7(KYAL$q7 _L*Fkb"]/?mبNn܃[8}.UVXJ(>+=ytqH =H uבUZoOg{)}q}%&<[>+om935 yX+$ih!llq <ɋ/RXwq#b4@/+{SIHVY{ei(ê*6waޛ{: ѭlo!""""""0c'iX ^8'rtezoaf5E00Dptl*~u:vkH 7,iQ #AiW/vIFT{R!`qojz ᡆzA+ GDEE_q#b}6߼ώAƵ׼VKf̛q~Fh`oi¸o{]oji&aȐ!ý+/dO=M0f ;%[[X ԓDDDDDDD^ O|O~$J;-!0/ ~^ Ϻpt'I$Ò4? Pų{U/40Y'[-+~c+={k\iSgD:^z"t_}U&0EG#[7%$:בx8TUu|wj'(((?XpI%+c| +V՛s3>39U*SLE2싋:8L3)۰hŸМBر`NN}XOWp <cO5{ոWoRm| n\oEdn>y />N4W7k]k'f} "ą#"]i1/;RL8O+tihՓaefwleg(UQ~5ym" d䇾6sZThq&%υlA.g=ھlIǃXvFϨ'_C.UV0Q>{,^X`łJp@؁pvǂ˓!Xg&:r 5T4Nոx9vqcN=ݽ`I&'xRŇAI@?׺oq„?W}7:l˱m s\;tLK!C I$Ա_fO/~1#?5_V,; HcD ;gqg:5soSbS;M;WzvC ߿W]]vgFxaGF5>~ .xG-"""""""-:"""""""c%fGW2Ϝ7mh;md1߼b|p p_ڑvY{ .O2?DJҔKS3MBauA[s!İ;TZ7W^ۥyCKdy'_XZk|3/~i!;-X\ή>1zq=f&@ 5,S(Kkۯ/ynyjxN^n덽!cAnna޳yE쁓pC\=x|r<[awvp<ٗY^-F\wqŃ0# U[Zv/:?1x\QG :#C *2+ ڜh޺  q촥ՎaMv󑞓z Yae6ܾ߲ W]YQQnQ<"їm!gT΀GUU} 5,`x8 tlsa#k`_zY)SsC{S_aBs Wi/+ۣ`} ɽǮ(GDDDDDDDę 7ՐзM߉}O9Yd= gYs扣kx j.rAsQF O$< 7/i:.0_ui[}u úˆ#hyt?q841X|]vg3$RaƏ3O|R4K>E>1sFǭ[gk]vom”}S]&p,{E -qlaKpE컹 ۥ[R65GO~in|=ƍˬ0!vg{g1?$#ǥ/~5_PlS:۽l\ycpG3)kǭAK>Yېo/}s/]ٴG)ӛߜZz07eI 蚶noXFk8zXz=0ws<=?qcPC*e(YTac%vS6<%Q YN'O}lY2wi~h8aSmK.ac?}]69Xźn>uuŕc.ߵ|~t|sLHA8|;}$C2ҤnXע*O})ş8nz|6;6H6HY;G)3WU?Wlo2@/KD_իWXq^hŤIou:]LL >XaՇmsϺDwϷ[sZ{jV\=t{{5oXaNJϷO7n O|GZ~y`o ۬ti7a}  =IKDDDDDDϟ]sR&떼TSӪa} ˈM/!dMؐ@j%>v9P>h͍6ykO}rSBʸ_l2A*A&3ܱfi:z`G,{bm쳩cPVŚ?o}ϙXV(6m0C7c̻^?ϻ"ݛdl<sH)u;,GXqbr)TiUSCD=/zu c~INtJ hSAau;o7jgg54L٨xxlRarEo ǿ;;o?]CԸ.j9k(OQhVIV\41,Xp302z^+}P7oxdȝ!IЮ⻲lzv7Gu4~H ik俟o _Fŗrxt]GqcuGUB -4Rvr97>/leخa3 DcM ۺKj '*awhOiOYp32ߤ~ \3 ~RqG4ǍWP1,\3W7z hD@DPh[ ds-֮Yk__);Y`hާzǏqu{rY]+_=;tP1l6M6 [ɒ@wdN9_;ڲڲ:GiqF ,h ~J( 8l6QdO?A Ŧh^)em} +!@IX -o'Z*wyq >4xO_:7꼫3fzg%jYq/t)=:yIwJ*Ureʹv(ݚ=upC]x8dCֈ`hyRmA/rdJ)kF#ֺ _=L0s؊^3z^+f}Pz޴I;d0ެ{XCkۧkɑſ,spO2y#nCX,alӘ(~@ "1:9V:拚O'L *QDt׉OROV .㺬|ѥ{}YfI8L?:;Mk餪w?3ѥx[W€V[=n} v5T) m{)Ҷ>l\pru.Ɋ.3俘??ln $}zO" 5Eb657 >P:6ЈF5lt3谪CۧN@zF-k"-?.ݺ9]svYlzk~{F"""""""3,&=IKDDDDDDēvSL>S+8 hp׸/.S/|Y HC*#G",H[n>D$\/>NKȞm_\܂O,gPňvDFA_6v".DL38uԱ@ȹEB&@ŝ߉7o#ӷ7vpwooyŨ'3q)m/o0s񳹯-:zji_o#f(gYXͪ|7,gXI}ה)~.-`䯣ƍZ cX& yïQfA15֌ >uP##XXgYzk 3\5ÈL0a0Gx`&'Q!@%߀8uё2 %`! Y5$ƕ䋟) ɘG^ͯW&ҤJ߉ױ%7.iXԙ5,g?S/O9 ^xut^r/}tsOc&|b63ʩq<~;~T~B9+Pɘ^:jJ>G{ ;v8_zb {=@Le*aKӍ}$2e< )hKζ{ h"oqމ ')ݧTZzыO"""""""3V,FDTyxT2Z X+CO@:yӖLiR`#fT_x^y{biwN; ~L%В4 ңJP*{!<51=$t,8哹T/҆~!HyF{W~:A;nx`o'k^ +$40wÃKئ Ǭ|gnL7+w\v]ZvHJ,Q&OLzYXdS@Oq1GұuAl`cؾW\߼^'OXa،Un]< ''ҀW\{AT=ϭQus/:zTQ >|:,:[me+kĭ75:!ӳ~3 l.p@7G.|~3Ae>(|ҪS>묵_uKxw/1UG~۶wxd`VfX)g^|ɅaD s^v)z `%C\{x?${-DDDDDDDfb89; @̎_njY0F("8",)ޒ\=禜>>}վ:H?ȼ#׸ЎB' q0'ܥ؈so=Ѓ2f\c<±]﷽侮|aJAui`)xH蠖qN=xaD}~ڂqDpm ?"JP6L?烜 1}3\A6 |n5/ imS/Kxbʛ|y+.ķ˗b&-6?`>c .n" >mgHgb/CџK5]GͶ+ISd1_:,:m\cxK>V}[l :<㰜`lnA/[KQ/EY`Y, ?a[nIg>qk3 (pq mmغ{F7\8!3m=i\h6t%b5 houkЭZ{: J+|7C VuֹD7mBDDDDDD=`Ƣ'iR$!,rR8Vqokڶ p޽]b}흶۱O>H%q<>ϥ Sn2։۶`1""n)RH :>;t3v6YeZ;paY>d%됬X:[N{kv?7fJ)C31_}6 f<9-2%_|1 RKH:G/0g&6 7I'U]w92Իgz,;3.Xf38uS &#KџK4:rN^{xm*CĶi e[:Гo]=] V.X%|O6@v&u8ѥ[^n&랷9HU}qpkm}~L|\'s6~y`?ޗ]}ydn-3Єzk޶ }`6yaCuq+SL_[d}th~x٥gk[XIZ"""""""/{D3@12`e:y#n{[8Hk%2v']YPT7/d[ X04)}/7DEǙ"DHx7|!nW| 1{~i^tgžZ .O愙f>n\>2>)D%"qxo("O 'ܳvL9]tr{sMϿicgL׸8^RcZyp SL]8UWUݫW 6p^\45Yc5 …p#H t9J *uxxW~mxIUѱ/ߩ̣5| F^f0S@ͳ :8 H*}t]חz9fÖe[m> ʔQO8s얳Gm8#K̻'ϟwɭ&Blt/6?Xizi޶绹(5Z)\O>OoGo'Fm۷y'iBv9J ~O>Gƥ"ĵrѵ#G䍹ȀOS:MYvWcc)>y%#ڹ nyS7c9nW|}ر L+f\ut=^O^8fLiR!9"@\7f̜p(nݮ0s^hjsd}Yl~IR\vX'Z4܎>>V2!7FұUj טb?_F\߹@{݃}]o^4!gPҔ&@tްɮ; Q鏼{fakA56@o}n#R׳nccx~o+!+އu̓:O=i Lk_Mv(ٱd_@˥-H5k9<zWleJQ#Ei`5kẀJ)Oōoqۈ8H.i%4ƀ'ϤH ۧ*:=px6Oq״o[ݿ\:f6F^3_74_AZf/ g:pqо۶o!""""""0,z+] ;? dB+m?-,o~s"ݓd!]k8vcD}IK>:ur':K\痌+Պ``ޙqxscnrzapcqvx;7 L3z\ux=^㿞Fu>I[*]a7"KL-JP\9;Jeдd!͂Ug年KPw/#ج~Fϗb"V0~^k698L5y3āH"}t!hF<1?k<h0cÐ v߭"=f4{5& أuy^ >b-I'iLK-M4,TkG6qmc&<5WhpgPOɉ%]{.7]H `ATHk%"Q8nq^Gt "A 3+ϵf_=AGiݿi}PCs!hXFD?xeq;s̱)1nBDDDDDD=`ƊEOy/<8 &\wV&ѵp"0 }G54gB}ui5Vj;t_~ߍ7o ww?%;n#"D&z:ov}H ?vnac^+Wo1_6<7fޜ|s )]^FұxjF _F\3P:AiR !띬Y;Vwj<)V}9` ~鴰0‡ wݾ\Yxy֕.oL`7rJW:xoM7"0Y~L(UpM׻^zɆ jBoR袳f%ztY.qq?+@8٣ a_ (:uz?i@dl^|6@(f̘nGWE$H\p`@a׍7.QƤ<߳Jvqt{+2~Vo5ď aBw\yln!#^; ݏy\8m|y~sX ~@1^wĮmo^8^5/8>o۾{ :p!""""""L^ B`|}Z{|FZi姭α:OWmiV_[wܯڡuVS RύIe'ޘ00}3/#;q{fZ!黾p w}%,KGXA6x$iCбyx!:<>}6><3?0?4 #Ɣ#2A52g ӋG1Ќtl1=_o7׸KGۧV4ð*5~ cyEYٶ™˧I ]63۵w^e+I>س9̘:cy:^~7>><pߘbDEFZ^]EcH:%lX޸2z^Q*Rc#qk?];eN>]O۠gz6k];ү|||ӝn@R% 5056\\~!&L60} tG傕T ^:z:8j7}~ux _|;w Ɨ[R 3oh>2s#4l3#+xbH.tl4?_lyˈRcNrS@f523 7ھ4^P$89pz`G7Jܥ(j@X嵲P([a* Xj`^Fv#YgRND҈`e"ãk<<捽m"""""""3,^DDDDDD]]؊=Zք끞@_2`:oM}ۧ۽Kf^Xs'՟zR| 27;St 0hk7^Ō9&S? #O^#yi td; d 7~1! s߆ySN^3e&tiHDžOA wI.,Y-Cx\q'! 0`; /|xe~gwVm*o:ih$#H:=_IY/?6Nr2TM豩Lj@[}N=e'XvTZ=P.wjeCJ%KwC3yf\ zǬLY_q771y`}ZD:6qހ{}pgott%-GlM8xG}^{9><־fb89O; ϔ ̎(}t' ]kH;uT~+K.R!6 Hk@2Eeϸ6Y7naD00 ކxe7];\*OY0efx-ă%Na@1s5:w J!g~ +xI_5/(F1jt(ߡGo`]k ;v˕2gʢFC _|Hw0/v[< ?|%x8I9M4[.;2SKxmƕW\qH=E`5Y]}a?p=ԂOI*e+|د@.EFt钥=Ƅ.&3heOπ~NXeEpb /#bL"M4Wkoh8w3Zx>Hyp;ۛ8{ڿ)v=eGoB=}"aa_v]m_ԹRMn-DDDDDDDzȫx< |b#E G/YJVr/ Y-띠}gƥ=rw=]v˧ ]tq8۹lMKǃwl`s`nco@ m~ wL;Ιhg0".~9  ɩoü#o|3o5k. I$͒d}>7T[og-; H?Јmqi˥7 |Ix&`ueKGTЗGG1aRl*{8l_Nq*tu8ͩ,f3`)7J:q?B ڃj v I*0vx0UkuI?N K5ܖ.{/&7}| RV,V h鄧OZ` GIx &7 'w0*8< HCR~ڔi yǑmw%#;fC K[.ғt/Sx&ߌvvZÿy-DDDDDDDf,X$-W@ ^_>&@4>|֤dk Ϳ,_t$zFWrX:ӱ*ҳȸ` E^FpP qy[:d~ڔp8n!.tlU?K~8F^~f}?;dv gŸ;wL;6];ŸT8f5| 58/@? IQYmR6pz,w~޴o[ݿN^1 濶S^'Nύ9sgS9ſ|om۷y'iH+?\hr 灥ЈFFܷGE+NGk>o> ?Xk@0*A۳Ӹ jt+,.\x˸'9 *_L%үU)Sq`13M(((##&/aGqf鋀_$0ۼwzH1 ׸+~H\Wwf,2%M!㍌3α}98xg6yGӲOK>ők1Q {{W[\R}QT)dz?,珐Nrw}<^{ksx{ve@y2`uy((( :8;4JU>b͊)7K#SN-0 Z} K>{9WصuWm+/y-}O>xpv=/H !!ADPD6`wJT+ѾjBf\~TEᅯB ]ӿA:ڣ4a_//}}o^={#%ER ܝru >b`{[o!"""""">0c}#-ą"`x>WCk\+I&iĀg?VX1mEf_o  lgoO>4GۓovI'שH0OWW^ #Xeng =&û7o-iv*PR>n۬@:qSNT{Q$aԶOqw!; IDATL;V2 ayIsֳ1dEyt88xhAF'm֏?z6_f@E*n9ܖ){nM۪0ɴ_/5 xJ,UTaIʥ?::w -E ݽ8ܭKqHyHRZ$ a79'4;;3wɝ[l O[6\|rZg{܁ݮۿj *v#hcWo@3$lc.\{6wyIS/#l (O䩒Im-,p?28ӦO?avĥW ʉ60VpV2C6lM e7tϗG%}JF/+8s<"tnͥ'][Ol5y࿻ZQo ;4pp|w2<5Sj;8ȸ+g&ȃoؿ~, q-2=}8p#-2xLZgk=u_Yw/6O\^vw MA^oOF9 ^q56RXu QSS#f(f%:K* ege#{{.^T'uOb+ۛm/ę -p'xQGWW?C;wgx|W_yUw]o'4-}o[asFM ~e\{zN;\Q`<'x㈷ZnԂڽ?GxnMfu/O Yfﱿ @?huVGeqAKZir vRt;\zq=M[,ͦO M -t:Prզ܊rg%ne Oָ{/]},[bh8?pwg s/FB\,[N{ћq Caw]iw߃+O\|w\#cY,WiAl0pǂ(+W,~(p|EO SU˕׽nRJ3 /(N;v0F`tX9q84_p~/FZ3` g7\L5tY2"/4Y4bQ/c?*|Ҍ[s HMj,8z ,%Iy<孔`u%W=ꮇ ۞}6mކ1Ǽr>v|mE`ǭs1،?f Zۙ a #?*4/N nfs4 G%w֞A{sk>|=8rv{\gtlxA:a 잳~K$G\IS4_1[h(_6~n=p:ti'-DDDDDDDX""""""""Ytk uaiҼIҦw}[m>l3888{=O?Oߋ2/v\ L:qџ_`N[w:/zѽ2IX`4 ~yo_7_|M85#K/kߎsf9440C/jTQ=w-@yFΓ7"AKWz+O2-ʹqƾPp<GLj?kc6m2o șl?˵STQܔf \2\?ⷳص6kbYhɅvA 7,Yd-ܰcϿӀ[;-UGqY3zR?hI#՟U:e?4?U\x&љA!b]>\ןMo9orcf3N{`*͏eϗGUWTLi9e/Xp5/Nr֖@{Kq1ݣ'p:ɪy-^eUwfO/_?z=,[v0oPKљX~;xmHZ6dۋԏ#nݲO|m6|z3hgnw{/ 0Eedhen͑$h{Cu6ƭ5]u1䶓OI2 8sle?^z%狾Cy9 GZ|xw%L `ZéI<[`}枷9nv~"""""""!V{nc[:,vmn3=ÛxoIemKb-)1ᎇ> 352\h0[o/㫴<%n#!W„ @5e.N _ VZ>|tS\Iujqw-àS ?Ewg= n?0,yYeg]Or8u2;\쵲u*]ȝ=w\"f.sVhԳaFS"|skdjQUyar`vCKuֆ?:t.<^ޖ+S -(o>lzS8YdS}|WXb 9~z ogm߮y QUD+R@pҳKV\а[m K,8Yv v+^Xߜv౺'Kq,@SWy^yK=v䂍6 >c?ި{?oy24?iVY_@ W|?9AQF>uii^~ǪP F'ZO s̭?/\$0d ΔYZdP0FL&bw&˙$gzB'f4C=]Bficn~WoKZ)'Ϛ|M7WװzU!dɦ;6|hKC(Qw 3.Y༰cPrwɻ%x0aǞwdA`.k]6̆K|_pQv{ut:K6- %xR@Ѩg\U̿إqe`خNC7D|^{D{/ivmp]7i˧³>U}ڃE'}t6qD t+ %NxQܟR?#~Ve\e\*R/Ox,uay 7r|ʬ=ayWo.?WyVYw/f%N8v;{JAiXrl,5?И&o$mjM&7tRoϖZ](_,^8&oƋ-9cgoNN8YTg= 6lނAN2t2U(~eK[| NhHH޹kyح3]=*;`ӊL65TTyJpD47y|˃fN]@'guyb8qD!n9⼲VG~3W064ҰH\goXr,`؁N| ϲU}o|WXuL9y$3< ^~ z8-ySnﺽs1/^D65Hiv%K/.GO},٫VU|y咗=]oT_fA#e֛}fA}^yCpi#㒈>~B*ǫC?i~kν&; Ϙ(|`/վV[VF~gL~~Wן}-?H+q;)`s-wyXױu=[?ݓ?!)vܩʔgxrnQE`I+2_ݳhӭtasx +2,MMCX (l{nVJp8pa:u΋/~)nw?m޳os ֎]pmAȕ+W՜](,?˵.WQǭ֛u֖[o!u'~`߇RCuұ7 oD;oNyeoӞMAYl.xsߊ/6 *WqEp5 [ö۶m y͛ǂvaݭu/g忬aʠ)+z 땪->ϮpOν;v6޲ Hq>5+?TlN[9t&#E9b?!ϩ ;5~;4M-Xlղ O~x0[7oWuJ ?nToS/SF(l7ޔ{'`}T `bqv%hoM r8` HQȾ8*u{څm7>ݔ({ozjܩQfػeG+,0?aЙ-Dž>/_%yZ7XqY擻6Fr9k4qw3w|$o⊻7u{&K +Lr%PrXQ-DDDDDDDfv0䜴1b&$?`CP+fL[+Hmo^E3jޯ9VBxm\dޜ]旆]*Ǫ4,~#FzAH}-oopWD -}\p C䭯j3-slW0ATܟQ O<:2쇮Wv&ޚwI;6yܣʹ⎋.-){V.[ˬ[pâ-V:N>P<_񗦰@}i6@e`粞3l>%YqturP8Z;CI&1Bď?U{qy(JŇ::l^vK3{읲7j«qjkn{pĈgu}ܗGFVX_ P거šˣ]إM`UUW ֮]w+-m˕~,uU Ch Hko,2 03Ό5wE[k~<,eiyX]-XQ-DDDDDDDfvx&-8SǪ!7y<7[~-k65a8ñb"/kAΕV=7O޼p3LnҲ{|=!Oqr~m։̿='Eag#[{c%.f i9Ns2>ReKB ~nQQ O'^?2}/knֺPTїEK[yv,˼m e%_|Bb oʲa~U'V6Yo9<0_@975ʟVM_qbVY;fJk.?L25xZ>k`hiBY>"X3ݽ{4O}o)~xko]}yk5k#xމ8K7:[|׮^z hS]ۊ0 |C ; [|¤7i gE,6< '^u`>Y{#-E_4r9T{w흷i<v SL2YvwƍS7cC1eJ]{1pay#>W@1{j b- )T+|bc7o&k4A8Zxu]q`ޯWKp1.jZE!5/Md::uXrgI1(k~Wy5~z_vh㡏7C1ŷ|CkBV7g@+sM5(p6NiWˢ{ z|Sʝv)V]y J*ʱOp7%O ]t%\yroYCɣ%ۗoa$y;(A],{ٷu߬}1о g̫ &~x[=y h]U"~tBG N7k7 syˎU^"D^jqV`3#hnV+\J%-ѹtqTΑh}d͠ufwk]xwu!ԏG}oW|B%S=Uèݣ< sTo\m[˝hr܉P9kU5k>ײcd[[k~C(Y _똫wBysPVٚX/ƻ(?\ vPOe3-6gA4o!""""""5Q`p WkW" <Ă -, Vw!L6xo 2XMҪ@-KZ<p+I#۵o޾PJ}bN&lkշW_֕[~^yCnKz3x_bTQ1O^^ 㶍[5 |][]nuסMw{'ڵ"j\aēhvq򞷠gY2,0>[8B&x1`[e hͯ|gp.vX+R5|VٟzYNˆ!#RL|lo2. ־Ww+4ԧskֿ_fGn^zQEg(IIJ@u.2Pq~ +_4vrT+7g:|W([Wc_m=x ,F̎f,6&|^cY.yz-dur!7UPףn:hڭnty<` 2\  l7hnvPsie/X}:> @ݟ_~aʉ̌cKy&&]WoOk߮u$r-{({}く{<^=cu t#oW}C hoƾ\ؽOLyQaAS ƦM98u%ifzN7ʀiB [i~#~[ۘdO?z-wWZv߽⻷2Mʥc?v2]L(0-DDDDDDD&,""""""8agJDa&Do>]L3 O>t,ܱk8b?vc|DUN솮]ws5E$Y4NVDV# \kw>Bë M ]{67"^K{/,1ԲR`l!`ăQAI'`#F:wq`[l\@_2e*]w5@=zfc'@\{uûǪwav dIPzYLD+Ԡ-X^uhVM-9*(؋z%C6]֏]tC&mɿ˚jvLL[UW9^)SO}Ng;ܟ֮;& _i3h[?2௣B, IJz]JpRchxF`}S5*;imO,x8)ȿ>`  4HP?V{9 &}?ISN'~[ =_}pf} M[e᫇Q t ֮S?`UUV'z*լԧk~ʁmj۰kD1(B8#\W\"dpݯkϫ]ݭw޽vaXb]dq/[ۘbzэbܻ^VG𼅈tة˪kWDDDDDDD$2[c;vm<7 {<˷?gT*]SP3AR[B^|#.ߢ|škR ӎOr~oķKDI&偔MM6~*WVq)8ns>{bd߱vdm70}URK(aDC1:65ѱF6 ]`x3u{ȓ0ּ/^xI]nTZj]!N8b? Eyy]aӎO=N:nGbrΕ-/SFjLzx4qGKkݙfMFo}mWm&hDyMo5M5}!w7qwd葎GɊ+t仓OmqGdJ NW`DE3^9B,g )68$Idh(f:uHp8pa׻ϫBSV_ks8|B&MU7rnȽZ>~{IknujlNۜv9-ZP| O>t2\{R}8\Ȉÿyco~y[|z˱u?gΜB@$Cw7ݸr39xpxđߏ8&?`[r^H0H(pcAjam)Wsъje#[ yv.Ot-TuZݳ\?t#U7?f>*2/g O=n_TZ;d| eɂd'J:ďwj*))9/n3 _.&RK8SL3Fcox,5|ڭyH`吡Q?3 oυx%ѣя?8y+x:ߙ9Ʈ]6m7w}w|k4/ߒ~6OًeEvW8dhwzHv WMe]Wǵ&?p}>w޹꠿8ԩU-oUvg;ηX)njco,m'1D]Xn Z8+`f]oͽnB '֊O-Zk W{&貢XP]U"aF0Yey/qjN>M0`鬽""")O o 6`D4koHT,Z{e*}I>/그:0WňHTd !!""""""""Q$E)pxAGܬ]jM˂d0xD Oe>FECakoD ̂`? L.`,p,藇??W_~UVwu6 /HTdFBߌ/6n;q=8(l`ύ=Vv [l.r/L7K%sxDDDw\`DE{[6 `*˰<'\DDDDDDDDDDD,;0y JN+fXvMumOno;m3o`%\y-%ECH 7<-p`ODD^_xӳO_7#eU\PDDDDDDDDDDD0;LS\5A>?l~;d$Á1X2tձLepg6Y~x??L1{@1Y7$ bWͻ!?(/z㩈n`fޙO7B * 5Z?6Og^MNۜA.\ -jw("b2-4oν;$0ZTR,]`ODDDDDDDDDDDDDD$S`,#mFqO<(qpSb?*8Wܽs3RK/O>d8&_ w;nTt`}'J 1/_q蟻сoOpFEDDDDDDDDDDDDDDvxaEDDDDDDD$4Ի\TQ9wjUӐT"Z?I}%.]:uu ¨7Hh^yu ^Y]v9g;;iwnki~/ڻx!4sg߳>C@j"""ph` A 5N0 """"""""y kY|:Ĉf3Kc]{w쮵)Z d:HeHg&.""""""""trک~!A<;+Vv|ad)Gy֮=iߧa 'u9T`J 7jZ0w?(F|S1`USWwb51Yܚj6ӕ/1# W^Uv=f}',JAhLDDD kK!0DZv/LĴv5DDDDDDDDkuԀysh&?f1ِrOKa!x[ =7׉]{^z zY_xy}΃ۗ$ ..TPB8-a{_ z y2$7`A?y[ɀ""""""""""""""""""""""""""""""b .""""""""""""""""""""""""""""9ᅉ֮@``"DvxaT`/LZ,""""""""""""""""""""""""""""9aDLkWCDDDDDDDDDDDDDDDDDDDDDDDDDDDDD2XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$rP`H/LEˆD v0lR``¨""""""""""""""""""""""""""""""D" XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$RÄQEDDDDDDDDDDDDDDDDDDDDDDDDDDDD"Z,"""""""""""""""""""""""""""")ᅑ֮aR`/L,""""""""""""""""""""""""""""9ahP`H!0D vxLĴv5DDDDDDDDDDDDDDDDDDDDDDDDDDDDDFEBEDDDDDDDDDDDDDDDDDDDDDDDDDDDD" ;LXDDDDDDDDDDDDDDDDDDDDDDDDDDDD$r "Dv/E,0iHdaFbZ""""""""""""""""""""""""""""",""""""""""""""""""""""""""""IEDDDDDDDDDDDDDDDDDDDDDDDDDDDD";0XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$RÄ -DLkWCDDDDDDDDDDDDDDDDDDDDDDDDDDDDD 0QEDDDDDDDDDDDDDDDDDDDDDDDDDDDD"ED2XDDDDDDDDDDDDDDDDDDDDDDDDDDDD$rÄQEDDDDDDDDDDDDDDDDDDDDDDDDDDDD";P`HB EDDDDDDDDDDDDDDDDDDDDDDDDDDDD"; #1] ET0aR`/E„D v0lT`AEDDDDDvm2p|jӰڵv{mkBDD,n֮:_kkBDD>ov;]oWmD뵽\kFDD$js:o6k!?)ڵ:¤""""""O<1fø=ЂTiAw =P-"թR] 'CMCDvΎCV/l|.NؽOf;X6"""Q?8oZH3LY""""""""""Q^XDDDDD \qڕcIX#Yڕ9G61Z2"""Q^ckW@g DX7s :nX~V3WbI:z:EDD[kW@kDDDDDDDDDD>^fE+pR֮7OL2iwoL?Ga}Q+$Dl|pfN>CE;eq8$sL4p0&57>ozg^۱ozp⛿wDDFFׄ(߿~;>n{!^C[vkYէZruy)]8DZ/M}QoV7\8ĝh$r=b \ӫ H3Q@m}oֿyF/Uk 0fBnrdXI1"yahʊE/QZMn6* {T\-ڻ{N\Yџ{Gh( <ÐNX=Z {=܊;/T<.t~GH!mş.P*%n_?ӡig:)+x,${R}~G׍Yo~GD[E5! kg7t;^Ab}E$>FDDDDDD| &L_DDDDD$cd/,O<0ߓ!MO]9иO*G_Ψꭁ3ngZb>w\РO,.0ڕQ?4,P5t.555㿛v)+$&6v]i/νct)whorӶO;K0K.kѣ)Yk~G[S]Of{fW ˳˖2gI8t3]kHfPڕkB1#]Ðg\#A{9wCaX_01;0 """"""V+ZHa}xy^fĈnK˭?Rsl1gO38).\J 3M?\|i6(ڲbneƓ8`OHt9`p$KbʊE/Qb9??LөϜYǿ[_J,#تp x=Tn긥E-;&dhψ 0Cnކ瓟z\뻶p }unwkz~e,H8%a9y''D{,H”C\"2/Lea0x`+"F1"""""""cFbZ"""""" W\xcJ4ڕDLx:dO2M2xȿd,mg{'yQjo^:à:kO6l|MLJ' {4*m,y{CAS"~6J{t kV)@1b/b%:= p? u^ kWX{ j1‹mhbBxڋ/XN::̾;kwr+Cx 5\(N@#P5CߪDq[k콢X^zbM>a4TlZpp 0]w$j= q "O!3<'oO7 COa(O1"""""""cIoV W\ynJ(&:_ =!8@/_ſ]ytб pV9â=7.x= y|$B@?-˻ ?-|lذ ݈}(p 4?% y_g^yЭZ}Txlg=@8kKd<ڕ9-reՒUkU5\t=P:G9_rʯWr*|kحn= b#mF#`Ug20Pׂ͡z{Yof-,HA`İuCcZO1- +_SX^h!"QcDDDDDDD/LzsàaX'DDŽ0s9(ҀEo?/ 4(.Ͼ->Z꭪†lhO,tO 0EPy%dPHA尨uvZ˃gԇ jήT;)\qٕ+w7Ͻ)Pm [b;v:fm7 XmO_`&J!Oyi*50a B8 O>k+""""""""مyPDDDDDD>Kxy0=iO;823TtZ;6 =HmG%\BLXz@{K2<"ʹvUi8Pou@?*~5vڙO=Vxv0ϲ=0 !at &*decMu[݊yt֮Yrbl{0"""""""""""""""+>A$C?„aHQm80c05hѢE>e c;<2WKM3HP3p/^-9xe^8]t3`=Z5{=gxbne KWY1+ZJο!XscW^t{vp– V1 eLpJ.%(P˂!~xwK]]p<ع9<ܓ8p_n]wn<8Vf[ju&:zK-Z^ُ* 5+N+3LN?ydNc׈p`N#;e=Yz_s1s@311˺pKޛ܅)5|dԑz֊kы+,l;1HCp֮Lcs~;oMN$~+mWe`YZy{YY\rŅ.Lqwown(SOvPYEB;]FĚ;/8t;<2}Z~lM67be*Uh]5> Is%Y_7^08BU"Ysż{!oǼ nYfg횝c͓ϣE4 i}!澘c%+&ƒi'[,v-/|¹qe]_*VHPc ALj tWxaw1F`wݎ{^+c;|i3oH`_DDDDD$}Z*4O<1c“gaHσF~7^~HE6Jx`[xZAmz\T1SmKDKl"ZsM.y)HAP{z PaP,wܯ_9R~ -׎߭wA)OP J.}T`2%W!q\"'[SwCj91> 4!=CH;s? %,\3nw(S̫Á%ϿL ~'}t@#,~r:'мnÑil$fEbygK,nt=-'Ը[#s ˩WI4+S#F:9`„0 Z'LBNߩB}m׍k8<A:R Ht౵+szaяz˰1xxk|ץ^7%+'~.83bg>zY_>E;.=]4XFH'Ca,ÏBvm~\ @%nϗ_# X 7hK[hiŔ ZUVyNil8a(lyk;z0?Z£/2pg 9~#O@L "z:N le"Oesckfٸ=4%""""""5ÄQ7""""""V aZ!;^) q 0& FmgrόϠʼ)["sw~ o,6gl5__6 ;\7>4t8q`9+v7˽Q̘RI!$J(We']*a׼m.z=yX>k ]qZ=۽B //g+}vt=&vrHSb -v }ҧK CbJ=+$ Y&l؜ ၢ[Cnuuv}is֝`&W%/A9< 7^-c&X~RJ+GcSu>+I,躵+ g_u 7gпCWᾣ[o\s5Ng?7<[3zk dȔ#cnw|*&\gBbUEm]"+ ~4Dmy9ԡNڕ?<>\r#f%_:\7@&8Fa k~F$痐Biis^h{ 7{ǩ@$z`em #v+<ӰD{OTw X=I OwCb͌ xgߋ:^^=^ ܩygX`&ʅ!C9>jpp߹'p@㚐 3 > W<Ð wE8w8wx}y\t#y/^#-joXѹkHM: Zm뗔0q$ f~nΆ_ L!S ?>]~gh֨խvyK؅yPDDDDDD+oF2xVaĈ}9=hd*vxbǃ'LJ+q/۽1pw~~z;Y77Yp NޓKOAVtx96U+ p9͹e>`A隧a.&{&u{x+Rªn}+={ANw/Kg5XYzǪAHLfkW4ժV@ӏv.M]~u)hY_ԉ&ƌ7bX@[6C_͖X  p0t y0N𿧿ص g^F~_l~?Y߸P8$hi~n&;-f,)3ƅh棯81Nm[h.;zXB$@K[6m.Ckk ׿ ,cx9c܌E3(3g7<Z:Єx>grF~7|.$I0ˋv5hꥪ 1Ѧ5þ9Quܱybv'rY]Ng>}L$XXGZr1av攄Ko/]~04&5^[nQuH4 _D|'sy k!9w_z{v;ؾaewׅ_O|}__<PJUB}y݋{}X^*~ ]<`Ab046Lxe/v,y쮿{AWFe'Θs\߲Jо~m{gk<5RxHgjωDmo]hoR q38=qUq'F $7r)פ܂!LU2.t V$^4>a?'cv}z=8(kS%+v\ 8,`І[,g-G)h"PbsujԎSMz)k$PiU|:݅T\.̇<.W<?GUaXaëan]^\w!5 XCu2o@lv5>萢O _}v c0a,hG;|$}9.d %*p=\=seBa/O䛠~~kn\q %a8{K_ghg<ɇ߭~PbD;6,O= #oƄO]lsȩ˰iku4,hfX굴2]'"7x 8XhN;dLDQ5G9Lu0Q@RL :$l޷i!t}ȑ΂ zL:G+B2 %_TuWF09b?r?\Ae~ۃ? tlk8rjYUW)^% O=4&6JxV˳LЃtHzRnUܒ@oV-2.e0-+K] 3?/? Q0C˜?ƺ _yFw}xl"d0_]{1=;&5]O%0u7꼨3^?ؕfw!ىdϒ0}m39:zmi!,0{SM7j5b>`ٗւKO.='h? {2i/V{[5k9qa jV-ub g_uۍ. C?C e3# ?`Alxi| fitw :әkC`x4>+{C+[Cߪ~nw9wܕw:}Y9d Y73 &.tyVyh2 Z,0š'둳^ä>Mq07SO9ۯtiKkv!oh)W7ɖ\`eF'Ϻ0&ژu~?0T#}S95 ߿vׄ̋1t =Y5~~ {UU7l;Whuh F5QAxAﵟŋ.=D?4|۠Sq_4 -ӮcF;~C*ukއ鲞56,zشsCKLj|C0aB7""""""ϕAŚ< t_#F w( qƕ_Q?*75jh!,p6/ֿSP1Ɔ.5:#Osq~z[do}_+L`G%ȜLPmτ9WV}*%2l-UwO>%ߴKp! {oH(HA |,#8_-ZٟOW^qzm^wO> ғtIZ6^||t^Ƿۮt=-[)nO0CGqӦPYռUj|$!VVy}uza}1ڕ" BMh~A/bcY}K.+Ů. G.뾇JC^<'[sÃK?5z]vҔG%̑bcF3o^ ÄxϬ]o'Qt&< NV;ۜ=ksHGCJXE5^6Bǫsؽ"ސ9gS45SL2x{}Pw)B~c~(sMOlxeeTveU](tGo:K]R҆7RO>GW^G܆gk-zv=#Q!:/ he|p1|A]keO v7z%٠2ף~vPcU jC}/D|=xdw[XQϽXvY3d+\y}4_w[e_n~aIK/0y.3li+ ^/Ermxz`Js|/cN97 ğcDDDDDDD!vxaDDDDDD?<] `c[kW H+YӪзepHObemS>uLJNoW?YNR$-i8ͷ%vI"hz=1Rٻ9ҥNW9moyNE@Fi>)8<1 &O.p^&9Lw:`pڞz_H}#2m7\ô{[8ɇ&u 7 V8>LZMC͵ָ_-߱b:ښ KO vJ c>֮7oU﫧6Ӻt?lՐmEYC%w Xl!$0{aCP(WxD!Wh3Mvy`n9w!,bǡcX8 _~w4x@/t\%k)O֮Xz{f )<4[jI MSBx!%xX0Qи+Ph[ďhaH&|8cl.Z>FDDDDDDbW&EDDDDD^c^+ ֮L$b“7֮ wԐbBj)Bg* UUMPe $ H7QOХK>XF1tc+w°"ǡ1h!Ph},\ba&ShcD A|9nZ*ߴ`Gl3gcY|v/ܟqPB N>j/d~9{q8t3ot C,/؅ >0-tpZ0Ռ;޵aL<+4:>OZ h\2h\W! ^lt5 k*<_M?qW T,~vJ>=AkA)8biW_8_`6nEꟾy`" i)O`^c߅ OC۠w\FF@Xs\8=?7#a,n|q=yjPoWAi,ܝ]vΦΓ~|ՓA=zp2",<°gzy_ﻕp01¢u+#~tSXCޜy E S;ni k>ePvR«vZ8?ՏFbc@ >{X@"_׻%%/kjb}[m T A/no 'ay17@ðՍ;ybD3c G9ݟb-]AMhal3|.0OCy`"[ x8 1o\B!mG3oH`֮7q/{ô0ߑ>;tX{ie,e)t 6F=qljƼ?GGN}Ě}6"H-a˂§mymy},'c~.FW!8` 7G#M x eV7Z|:ݔsSzNwHip3 {9&wt=L5kWBt^g7!]xoo {9O7o^H$ZP}BUkB<M/>|WoxFƃf/4 8H9 o}-k~!x!ʧ|}[5 :RW;6aL!~k_]sm7]?`P}2ײrqVx {)Dٯ}VC)]\r0YԜ? {Jq1M=8XYΟo*\<8x㓚o{gv%H즁NDDDDDDD!~('h@&Z>y^\~K/-t>: @KZrߴvʵ9??F?ybU3.`nTmPUXiQGgͻp/׷}sf~=sNe_^bήh4X~qő63sBWQmmmk^]׷N`/'}_1Evg|=v_aP+Cvپ._zENXY۬o~,5Ο;.J*O}=37}/ȥ.Ouc~/+ߒz;wKov?,C~ZF_Df{;׿p N.9NpW{zed?þU@r+EQ?"2o,oy;מaλ5>:GpA Ɩ}p&""""""r Sneǻ>v \8A|̌1]?^ - ޘǷ[l2jzooo*KTG)(V}^q#9w|x`3̚ s>u]&6ᜳsnm|~*@(>;şnAfݚk\=ǝDwpRheg&=w!}L?3,יOI~/}gyq>;H [YX_ǧ8O{X~Pi89,?Y[{X 3cӓEDDDDD"]=aaY#)͝//8OC:p|7.Z5uI7k<ӻOo8~;<82 >No7ZV`H?,7rug>'sOSƦS6= 4񱕀{QQ}ȍv̼vfzeXew5A9p}u'}o lz˳jUN_^zs'ܿ==?}>XDsb=4 "}_1Evg8:niÏBq,W2}E Y`Ǘ;>ٹd]u:ݶض۪)ϋ@@ !tݍw~ƻϧi][8Ĝמ<:;nwKћow}*Ή=ǝ{Xm.?({=%CQ!"""""r $c)W]mڥF;8=ٵݣE{ a^k.)@mm 2d^y ELx{ ]ҭ^Uns9۝0`n@ZU:pퟪwӿBX(>e܇==7ޘӢ lPsNγ.\tɉޖ[k}jzN^bP׀{?g4wžOvvgtV hha_? Kp{ۿo{ 8&*p:so?9; t?HhGoɆNOe@^O9O>ۀ͵9v_̸v iKaI ,<hO}^| /kFU5Ө;vl U'mעmqn:ݻ{+i]l';Mv[$6 &+=`w$ٸyݶrC]{=emӶ{ow̓ן|||NHqȾ|=,L7s;̬c>λcA{ {>-wco?42}:FDDDDDD⧠'hB&0Keٲ2v4up[5 нj5Wz@kMx<{Ɵ;o{베8x vk2giRzL?ҽ&~t:xUL%o_-7\<բ>C>uW+?'J :5ik^|?o_==&ZM+Ta L;){<@UO$'9)CЪd-'Aױ] iM~1?+X÷W}{ipȳj_\s>;<0o.2oq{z.wQ_ܰw>tU>+^v~T}~8FGf*8 t?Hswfx%4`Ӆ]g_w&jSB5ǥ=p,O/~"Ew@:M`_OY?e`5tp^[M4mi19 `:e^KͽρE&|:n dɺ$+o{^z_^~ФW~|0zzؚޯY.ZSc4<2.+F_}BGK| e{;~K~]tݗ!rv o `K--_Cުyo]|[ń/Λ _ qJq&ڤ:}>~>ˢ5}h }`wΞfv_2{ svgӘ w<1"""""""?y Z"""""")O>%z]_Nr 0tp͋51Tl8VBS@{&lg*9Ζ6{?-l*0jHZ֞r5šeڜ&ޫ<>cKιuۜ7NvҚL2KoxnݶXhYe t7Z e l_ 8g)tV97f4ۯG??W_i\ p1CL/ >g"[loik^\dڳ;xI %T1+}ejB}rg}=A^,L˛&}{XkF_ 4- Zan/Z](uiKg,;aUM,/u'GCSr>'+.?Rǚc=Jk~3=rooՂer}麥Z =?Σ@Oo(E'¯[&ovoms>rS?JHh VsOU zՀ};?B{X~\ \LtQw?&:qpWoWnZJc`7q-.<5'Ճ wqj߽?,e.Gx! Ouy^op~8d3uuxO 5r)Oxq?mg޿~u1%"""""# ;!?xG.]3 OЯS?>IzM =*Q~54o|Ox'uk{˷k.Y(u)G<>E,z C֢mUvma;Wm/k>8q|+̵gXC]B)y_,ҁ=~) Ӟ[_hg6]:]񵻊܁9 Φ^oy;}<&t 0\|[&mL;*|?W+36AÂ;us xG~C㩍W]Fx[O9:Z]EE)K;9~6hueZ]Vs[ωVr5p9!/17oBX~|/xFl_tPёE/Z@E;tjyj?x7lnNy{]Usr9, {Gyx  WVi6ኚw3.EFz3! g5U.Gx!6 t`/Ct`ܧesvvWt^ghNߵ}rxv^M'5I {<1"""""""?- ]O̶>/ا¤?ht/Vo uXt\(SLғڮ{{F^1r+!~{]^="L6AA =Z6_'\+7_<~<X~]Z3~k_EW}._qz{‹ O7?о-I?_ohg}p`M |2PR7퟿/}lm.ۏ&G&,rލP{鷥ߝj5^ju9D*ߎQ󹽯twY~={~qQ9Նv"|`m~ݟ/`3W^_Ο3ݞ$Ǹ?ϗs>T_%.^h2˗Nޱ;1́T1MMxDXQ1`8ٿnۭއvD5f }Э}oA~9}/?> Rk5U8ZSR`Eb_!_[A2ٗp9s~X]]F}`V}@c #}/3w!%""""""r,s&""""""&]rL!)8T9dI# 8_Hܑ% l}b0:ҵJ7+5nꖟՅy UP:+Վ.KC;k1,rPxJ kwm~X:=arI/΀GƔ- 2}fko| jP[`^>y{߹m%(7eT oq0}Ń0ϋMFUÕ[^mp?0foyfsFz0e@U{Y֬HӥpI?%TzZY(euZ|p ['_R})DFfF>"0Oo+8s!dEvg?E;gу:AO~Ch6͆uꔀ*_(qs?nq!俛zշuǶ:ŷ-.ͪ0H?~w× 9_>V<[11c>ƬSwN)upQ߆sPS% QUe!"~M3l_%`}?|`؂`N9vǏ?uX[-?q!@`/ Î}hLt#c7^np`=ܾⶖP9k}hh苰յ=`ne&fN5%GclUXzy=>c`28Q?4/ֻf99j(4""""""/]ܧHTѝOucWK$bjxhӈȁvݩV/3\A> [eȡl -/~y絹NsZ<啴ot>#""|9CRBv֩N!"""""""""rGESCDDDDDI&LucZ.9B>9jHu}h#!o <;:1?3瑕0"""GFDDDDDDDDDd#ѓEDDDDD9'L' xNs UXDw:>&I:$HGEDDF :?~ 'xȡE[RB ~]{#>&R[rL :A5""""""""""OyMu cP&x!8cFED,];SU㨈-t^#"""""""""_$OOVIL2e>9htZDȒ*AqAqTDDD:/~ R@OVI,2ؔBGpu},""r>ȑ%Ngas t8.aDDDz g~'+ŽK{_C?T9v$P BRFDD/|zT"q_CȾye[1}܊!NsƂ;'yEӈ>xŷoHu Q""""""""""Gn֦:1p,""""""""""""""""""""""""""""rx;SOA("""""""""""""""""""""""""""""'XDDDDDDDDDDDDDDDDDDDDDDDDDDDD yf9,)Hf9<ɣ EDDDDDDDDDDDDDDDDDDDDDDDDDDDD\41DDDDDDDDDDDDDDDDDDDDDDDDDDDDD8O3f9<)EDDDDDDDDDDDDDDDDDDDDDDDDDDDD~ """""""""""""""""""""""""""""?)EDDDDDDDDDDDDDDDDDDDDDDDDDDDD`ÂQ41DDDDDDDDDDDDDDDDDDDDDDDDDDDDDt h`Ã<,""""""""""""""""""""""""""""rXS<,""""""""""""""""""""""""""""rxGhcYEDDDDDDDDDDDDDDDDDDDDDDDDDDDD ~(EDDDDDDDDDDDDDDDDDDDDDDDDDDDDXDDDDDDDDDDDDDDDDDDDDDDDDDDDD0Gf9,ɣf9<1 EDDDDDDDDDDDDDDDDDDDDDDDDDDDD \41DDDDDDDDDDDDDDDDDDDDDDDDDDDDD4aOA,""""""""""""""""""""""""""""rXG,""""""""""""""""""""""""""""rx;<GT?)EDDDDDDDDDDDDDDDDDDDDDDDDDDDD~ j`Å<4OA h`C+gF{pW8-޻Ҕp³ +]?l_r2y |{^EyN;hL)toۋ6ADDDFk 7mRH-iQ[/;[Mt^ """"""""""""""""""""""""")ԧžRȁZݩB׏)5թE{|^ZWVKu*;NW >Ul EDD:lFv뿮BTٽOo{nyKۖ&~Lu*B# .I"""& w~ݒ̰TzpX0hERFDDDDDDDDDDDDDDDDDDDDDDDDDB *SȾp" A/_-v_ݲxTs G& .:ȱ)}IŋXDDd_tݠR.7,4yߵX1.%:ȱ)kWQȞ4C?uNs49 aG^Fӈ[$!""T_甫 *SS?V]W_?͏NV`6 䭧MuCخnȤ:ȱQ"!""T%j OFdI8ҏ3vܘ,RA  ^8r0vNu*cf9h\|`Sl)GqQUI wéN:/4pyW=/XR!""""""""""""""""""""""""r()Hޑ<3ȱ %åNYLTw_j^2:K*D ɝ3 ,oY^FSQHQDDPeg^8x`}r-۶3< pfҾ˜~~*~ߦ:f훖n2Қ >%}''2;hi&Cb}r]KA&.$ y9*xh^m&=`>}`ۈmwӼ`=IzNƅTͯpF.7?{ⷺCwozvj<㊜0* cSFv'0cU:yE`^eؙ^ՇwkCw'ˑ3 6Cet/_!rr1/ 'blD*S $ Jbox}־ډ(Hb.Dd)3l$+y_ٲ|+boK4&ٶyρy~;`v25  B )|C OUȑ>*P5^̪]Dw>Mk{S˻V\ݴqN#b%W#ohW v~Ucb'k#Cn,< E/6 \82n-fD.0k!R;1[o Cԍ1Bfטi3moo0!qUL;Oؙ,\fl+6Ed*{r%x<w0j>^;f0۩`]XLb:[uEDDDDDDDDDDDDDDDDDDDDDDTYT,"roA߹xŞ;tg.W齯F|괇y>\T!{dw,_˄;'N8ǻFnn|_xyo{/@xfF3§FF)y67?Bd{: "WG7~~DKů4 Plb+q b$jA<=oބɋH|lDVXQ%[{me&yul=7`z fesy`Gcd&ػy,`)Tj?Q]ṹPd/bu gN6u0>y/|r\k_#߬`#oo3X6}Pp3ڮ˧7VO{7թv٭SBD r*a;z qZWv>Wv k'yWC/Ñ^lFC7t-nC4#H ѹBxք؋eskɉq?OLw!xF$&FE$7!9ʻ |u ɅJLMoWM{>P0_۝| => @<Ճ#""""NJ`ngrG&&B[f3^C6~8e}:;߷Še!""""""""""""""""")|%*9Ҕ\"xW}{ZyZa}~4թFN!""Z>;V\0Wv?;W{ Sv~"+L)R{Śi7}!+XES: >^IĺĻG!cbZlA&6 qSi6/lYz8MޭR -M=5ms`02$u ,2@>LGGDDDts?\҅'s{(}Mp|g*<eǖ>tnSޭg<}e盗:ȱ)<Ћ%NSȑѼF/xuFm< *m4'i.VSBDDDg"eWaGpa/vޗ);?jCyJ9$| }O!|s$ݼ3#7x#!R9:L3Q "[ך*6„ :.fLi]a΄%%5m V/~C3%vط ~[9W2Yaol}.g L[`;bS=*"""Z]N^  UA#GVZW>ʊ` Vt섎8^tiƴjge:?X ;_YqZ~E(|.Tع#<׫; dCgnޝ_:2"y!"| _-Hz/lD4'AZS bbMSEV5O1@|Rb񲉐y&Z/!:]kY޻$$<["2øK&b7x LK ODbuS=8""r/Usun:SKds>­?\py30+̙<<K#;a""/ 5,Ȣh="D~7!jcMU4شJ{6] '< M H\jA|Lq;?ڕ7Y $<ɶ6xn0ۇT2w3L? v-Wt'c;'rD*[, |ZT9:x|oÈاnz iDDDDDDDDDDDDDDDDDnNDJ<`#ͨ>^ꌬ;[~?t|߷d:?wވ3L=ITcA$#:LE {`}sIxw>!6#O Hyӣ7!\) DRl]cv@{& g~n(V aB|ϯNVӋ:gL\Raӈve+z%`ss{xv^>y] TdyFxC "z#_y;V̬HZAd9[Σ0^HuCos^iO':)r\xU,"ry#| iIPntl)tcl/foT]S#fakC_mw~+Vκֵ.[3S <Ӊj 4~/+> ?_}gLgBoy lN gn`4`SLMh*BhU)i60Y isߘEζLDDDDD!}=1~[7&ZKn'Ƹr?!(,㉳o!>%?; $:'W}!>2Y߽m<1ys$Zz/8@NMH.6!swBur}i^qրyX^ ͙n[%[`֙{d;yl+-]bȞ4g/>OuI;@8!9/ AqIGy}־P0iDDDDDDDDDDDDDDDdO~cSBDSj_XDHW"N^,˦s5?q}&MbNw߾ǭ7>yERuILISTIauxŒFTX630-a-$dIFǁ}՜g61 t %ޗ@Jv)UvLvpͮI`Yl_[Di`l/{csn4c[ M-`"XgFsMWZs\^`ɉ 6&+l Veofv؂Tȡ8ف@_-@)RnnFskҳ@qwy@qgNZe;1W;24"ΫiS! vzB큙o70孒~ ɮ^:pn*ߣ_j9>dm$\@""""""r k ঝ[f{ᶏ(vLȾMu}:(ӥ̠ҿ:wCkNCNiX0ۍ]u`:+mSXvQN#/!M@mNa8mmOq5=h[k3)s\p;(p8t-nSbNs87]͏>X-9-w'9 Kw;܀炯[[7}gz7m|ܛ_@rLV{ߨdMK8߲O/;NI\ ~׽kA9EI?|c!i, =͇L]6fBe*|+&Mo$Mwx9l߻`ʛoi Q&ay0n&x n>Mv3S.uƀa74ql{'lq:0n}ss*ٶSj{c:w;?=>$6І!<$3iv*K9X򉑑A9SKpE&Zjr:g4;g^u2 ,n sςΥ,8ù B}OBhc` . Uq߂ ph;Һ;k  Bkiǻ!æօݫ Y>(d͘{ 6fvGAe2'~Bپ-+Oȁb;ّv8jޅh;)s"i͇p(fDi6C$$ R?:d@ yW<`?=F)؈=i`*~a0l6dGYDDDDwFd{^k+xp/iTsp,XT968E,ܲ* ܠG_fݷkV׼S+թR# ev\]]:?_.1TƮӃ[),s&PܺN3m nqCN_]Uv?pƁilo_嬄w(N.sN`. tBN"V <!_pn sޅ/ rhҞ㮇-]2L-cp@FѴ'|@f%ns\^Ž2d< YC绯B/#Ⱥ9odȜ[3s R=8""""""r1L{?E4CxQ …y@idYg+gjɈv4ņL-4% :5nCy,։u6@p?Vij?3^݉n /B|Sg3 ߖhhd3%8!]V^QkɥɇmAH>Cjo-7+eow^ ^)7hn(>y`vj?' Z\|~ |—l]"""r$;aii0w?esWЩOӿ{KN#""""""""rts\S*suF-#rF>bk߄no:t:Ȁ;?i|Y[>*uVCra'X4~م>N#""r8 d߹w.wi n)w}-< n'-}{WęVg-n}xq A@k"ԅ@ȽBw:@*t P3w|ם iBq'H+҃ߖ-/rCiiAs!c{z==uU n4cod8 dݟo6d] =WOfnW@H-3K"w!\<:ԛk " U=lHN"F6ɔȌX@ ]ͅ}$>4rbxAb$ ~oKߞXiކxDQ1$:&/!~!%> Y.ƮDd)P2/ɞ0xeO kfWoxf+|T鄶kztUMu.#< _,q{T9:9E/pU,""w46\xPݷ_^̏b/N}wҙ#sF?7ũN{|apASJDDDh..p=Yʁ嬤6+wg@ulgH<˄ƺpZ=2V_ 5w&Lh:>s2ba]5;^ ;3wVCewxGDDDDDDdxgxl93r+.ŷHOf1E,7; R88"Ʌ賱MΉJ͈54 zQmQDW FuC{ } c Gk;?+YҎ݉Wx̅Ħv9$*'ۍl,kcx'̆ DZy[ IxKk3O\}`vخ`koσ<`د v `TfO+}7`3`R=""rl_ }T9x.ËfÂ[V4"""""""""GȤϕK""ϖ _6=4*ըZ/aOeazx_kUg5)mC%XfFrWOK< _hN7tai]x9~Kuc@rSaT$'L=pz98ׂ;6r8=7ҽ|ξu;Y }: 0ȿ B ?)O:!89Թ'Ns[BgQ+}!tI3FwAZv;w6:] ?q~ft72^L+ 9"K)dN(2O}2Y2w!aFk$z/c փ TȡUI}#]dafCug"EZ/64-n|&ɃȀJS?ĊS!od΁XCb1WClF|i @ ◙!h14'ǙW HlBqvM6S!16 HTMn!1yM@56 뼧lQH'6|ӔukaW$/ Hn7Cdym$7{bjOd{/ *z2e 4`smy%;l-k-3,@*~$зDK^^н;SЧ6}ZcN#""""""""rtp(QWU,"r}#9Sͭ_ϰ3:.[$yJ/0ooKw_ۍ=qME\rsToO%? }gW8mjV8jsZJ}2C٥% V@P0n[taqoYc L:͝Wkg~3I"""""G2&-hko^N7X v $I葻!U_3Nuԩ?E ߂E4"""""""""G6ȶ_`#E7O=ޯ擣w߮yupLZ?i\/=1рf;XvֻfccMV#ԧSN9۝\iΏ>Lmwtվy$};Y{u7?8E p~N2 Lu·?PԹڻw@hfpB H{:Ҷ>q+CiHjnbi ߗ渥 ciUn58/EFzܽ2&..c{*d4pBS/썙}+ :82Ճ#"""""""RDc䏋\nAxg"գ<QѲfDǮ4 T>a rssSb];TqkM]k.ؠ~0!V8Qvؕ'ğ?kClv+3;'* lef@bjN$K&6H>oO9@0-vW|'b2آ\;Utb w߳|*P">}sԠT]3sMv~׸ţRFDDDDDDDD4)~^"5LB ,_m-˜+%~pI-7YG݊+jFkl`LV9¥w.08T<4ps9mnQq^_?o3|j8!P ?v:%!pS,:!"Ot^[Hf3!P)gH6 UҾt3 nYؖv{6d^>m ҳ}7AVn7ȼ?cӐ,#}AЌ!۟i}?яQ(s ?43A8}|᳣{ 5 'x{d@8Dލ@tu9'[zk{\ϼl[RRgê7}4"""""""""G&h¿\`#M-Z\W <Ӆw..,r<ݡ߂gð:lkTĺp]]ho) 7|bel ?$[IDDDDDDDjE(HiM p8;s\ntF|_W:w-#Eվr\{N::esUY5 ݙ/"""""""rOOM"? )zȉl:D;F4% 2.fME::5=hAt`sb?c*k!~w^b%vQhn'Adg$V'F۟!qzr SW^ lo=&Ujzmn)xgM=lVڻ4'Bh_o}.cl)[^j{#=Rȑ)ZΒ[U,"r۽OV[-񎽷72X?u ? =KSjR'f =o|t[SUDDDDDDDD7msjTIwGk8-rNp_vGڃ[n{֭<lߍ yg$/Msƃ/W*,?6Jl@Lǀ3Ʉ@_)37 pW`s&?vCpCq.`% kpBe~ w@ڏ!DH+΂+BiΏ\n0ȸ8m2R15"dŭ祽^i5Byd9EGK͐])7䗈4/AM4o8GFO!23 |~dY Qz& tM2% :&4 1@am b!$o:@YuJt1 >> q7Վ$Hh@bE}3$*%͐'YHNNjs!mBJo `$o6bۀn~3x_Q09^g_oy.cm1=^|0 LPDDDDD;<)pެ3n}63KIkwªўpo~V~S͹lGTocS"EJR/8su *֩ص<_凭 By3blW-"""""""rT tOzC4y6:͡V1/lx}jʦN#GwW#)λӜj:8':a[ѹ לiRyn;͹ܫ<8C`$f:/7jB |m?1ɂ@[5s:!%wBJ{#nt;7 :8 !w k;!mih;ҊtgA5 GH{!vCzN;.~i72L񕂌i!×ͭGBG TB` 9>^o/a^pHмGxBxg:ѯbbAh]H&&Jjs za>b?1- 8t؉3#h;$6Qo8L ;HaCBw孁6o- ^So=/{'p>xV)xUk5{2:)ii<ث &7"ƀ]g pm|<әNhj9T*|v³ia>O ǥhTۿW{[~,HT99E[)W EDVמyn-pNo^^(I xT2Yy恛s`kO\ L>}̖/Oi 8-w `¶d_H<6eC9W]Q44N/̭ ;Ƥ:߮jT"čg>?Y4_{g_uAoc?s j8%!pSp΂8|ܦ8i NN sCh]pB qB·sh; J y(] /껫!}AZWw pӖ9/--!#-[2/L^Ɂ1d9暲?+ c^yE/>cf&kGy"j:ύgb-ߤC]t) b1Sx s.DǺFkZ@la|=~`F;%*I́ĻgH\c7C)@b@җj˂[|;Wsm ^eom |aGky̕f Cfۏ f.7Ax[oωL6?AgAXmHhwbM Meζ@Xb}cL Ώo ~\tnMtOA|L|y z; $H<́Ov9$NJ!1y5kcs I {${?s:xҶ.$1-lS0if $S>&hy0M g?-`OJKfHT7 =}72թe1tTR-T,""""""""""OxvB6U>Luoxm':pZT#{iGIg,pOwp.L;iOKp׺:=ŝKwpor}No qk0|;+{|3}!gu~'g5tvc!0ß@ 㔂@N>9K d;W@f ]; q=;O@`g :oB{!mMh3 N p6bH{=tҗ=]үÍiҐ1/[2rvφK..NTص'MSȟ~< yA"w!V|3›{3 \+:Ǣ "DF3mԔȈX34Ѧ~o V*^<wا aby v$/H tf;?&2!q|b۽'V6{ yqr=2r%Zw f)#`J&y}~o?KK+OL *Í?0lnv'%.S*թ)ozs"bmcl0թDlN\W EDDDDDDDw:uLd Le($FDhN1"E2)%ʔ9Cyks|stT?붻Z+nn7)""[*/?Olםg흛4"""'4 {^EMpG.Wυ]sptcnn3}Ë]ύPEoCCin)ׅ>#D^ mp]AEi~Մȳޅ&5h):@qbGG/NFz!~~ 7bE[{ >57cӼAxIo=$x!56;D /㕃5^MH+ҮJCxpg!""""""E ԅ2߂2 &CK!`h5Oe!ciV ~$H̛fe!kRIYdz .F9m kP29g ֜l8dA0 r2sGsYn5rj+ wMrm _C+YHHHAH&w$[./ .vg#{>XQWK\`[-} V+ a9@-"0|#?|FT%ЋCm4"l>o eVf+'],iu|mg(t!&BKBܚT?d?ss/ 3Czې%3&C琱?_2/}}V 2f?? @cv6d>7.ʾ/h|dC֊Ao.S39aD'swS!InU9Cr{ ]m!\rrCV9H+: v)k=׸^fy ho/Aچ3`6YEk~ϞW`.r " OQDN'ok]0/Wޑ4y <ܼRFɕeG,""""""""""[Jˍ BW6.vyScBZV}񋌢N#""""O*@>BZшB3>ʃ .p\p\>Z{]JGx}\wNzF 2C=Cfdžej =@kx; 7ۜp1W"Eí]yjBdxdk%9Dcђ#DGx!tdb_Fgx}!vC vQw >36b?'[n$:x!;wg??x%mJt Մ!^H뜘5P~5aSλ#!댧!=_Wې~mOљs!coV` d,28~  [ + gC֡% d?]rwCvoʽ=9LQs2mGNnHq'!Q= 3~$Tūz mkW'z [+9xT?dSnZǹ_eDHȸ&3ןf^̅ݙK!㼬F웕oY0O]e!k\l:];Bր)v=d'eO@ !gbx9GsAE }6ylLnC{Kb y29*@RՁ5Up5/d7Ap{ZdW"6FC}4u+Jvld@}Q_y"t,ܢCTM pk[;:+ȲU=JHnR7/ps=|"<¬{{f?N] +Far/Gnj5wEi@%u=Kzqx{\?3B+[:&@o:3 |wh[ɡRn۸>3 [ŸtvD~ u!R&Uh3D^rA~#k 6uؐ w/ľ~=P<ĚF+M7ٱyW0v[cU $ aIHs+~+igye!#ͫ*_@ڍ^v]T?d}$іB˜9x'a02vf=,x։d~u^  sAYu( Yr.zwZ~2gRpd-9gZ?#r(9զBn |y:z[˒#m3OnC{S%!AZ+V?Y] [cH 2 v#˸ACE.=ڷ| <`M=VO$s(?TXcX:jۤԻε ;-2iD\?,I`Trۮ:jDy8ռ5S8\3 =㛠3C gwvwjlƒ\~~ypɊT_$ňƵ( }]%p?.]sJw-x ݓyp?x?J3Ͻ-rbDѕjVNEo!wGc2 =17 WefS <؞p 2d}d@Ƃs\N5ApgL*\9b#&ſ({L`_Рei :MUmbI3:ZQ kJ^a :~ts!^[ =nu!7@xQ8 R0•he걑o=XHDDt!$: bl7 b7Dx@|b!ďfz ~q츷 ~}H}BZ/?$kye!dW5O.!]DUzW6nw}PT?CM$IV5iDNodT۬zѴQ95:Mn fxCo)ğ6BbIno$ΈOr qc|iս2v,ӫi'{Cg۽+!nd&.wKڻmtD.|c!xYSFJ/\9EDDDDDDDDGIDATDDDDDDDDDD?vY7: z'i~k6de&~KuөJ=JHjm!+] Zw:0C 4kނP84 >}B Z>;<á. "_=Hd^Õ7N#rzs%/+kogJ=jT[v /hoTJzq%}FpV\ WԵEj:޽UG9po@( f"Z9~5v!K[ jne@pE!/q% &; #"s\}n >":{ba7bWEk{o@]n"w&{ ^;[ Gbռ5*~8/Үo H>{t[*'.sqX++"j}5<]_"'\ =׻EsAGܳk鍀^nķfAfl-Īx!EhwW7yaH{;^6tmw΄ډ7 9[(ޭcCrߺREDDD~; svAWwqtӈ\[KT*G`{?al=,sf9pGn] rT"""r)N57սB5p鬤>K]!^omwӋ0BӼue jY ܷZjVCb ^Ig0D <΄KO\=nu!Z%R׻bwGvAlVt ,=+bgyAhO7bO!^-*z?C؍n$; Hmٻ}(u|Ѵ ^*a~"""򷬥 1PҥW>4"7WR} FTN{e3CK`k:?$gnj^LpY|xyW n0W[M!&@З| CI«B.zHlw.(DEpU!2<<Ճ! #g{!vgNw+DgD&y} Be*^pc!>:ƛ*-7;V ׻m[qHk_9H?+iׄ*CZ+޹J)}(u|^m;ow?xǼ~n|vk Wywӈ\J4- """""""""""""""""""jk+|nUPb"Sui_zv+؟4""""+NQ" n8g;RwkI'AVJx x7{g 4ٻԽ'x5 cy9n! 7p \*[ß>΃wDKEhȭ;ĦFyC,=K >$zU4HmxX HLur[!; f&ΫiBu!_ }y _nZЍyY- ~JC!yow5x4iDNo=}H*yWy'hpqMï_b-n8(o9?zH^~>/7~_w:_%wADDDDD(BAp%E1n n[.p!kFG\ {_xݼECX] t 4| EC_!|cx; 9>E^DLr@tYd%"U zsw3>f7bDz/BlP{+cozATl%$ns+ aC$CbpN/3qwkT7^p{SEgWwacv:͕WrU9U8tPHe^w>|QñNgŽN{嬇-(5 ~팬 +DB:ъ{nn[suoݓ xx.^{Bc ޾#n />u!/t^>׭p=n+# "5\!\UW"K"۽&-CM&N/I7^hg7 b# ^<7Ŋ c$˅gq/?@ZĠِ=лMI8Jw2q 5N< c{]uV~ W2SFJ(L*_\Aׁ{*v9\]g\ޣ=ẃ}u2S0!9b)4" ,""""""""""""""""""""""""""[pdFZ4wXdTcS"Ou ?ew~'8ȃəpF"ᶩN;tAXE>?ǕJ*EDDDDDDDDDDDDDDDDDDDDDDDDDD~K/fLxcYR>saE5:K"Dczn:0̲VCmwTy?|X{鍁"ȿ&LQh2[DDDDDDDDDDDDDDDDDDDDDDDDDD?W<[5ċDWz:o/;3p4{ """"""""""""""""""""""""" WRoWo!~F7pÄ*kJWRT;k0,㤧:O\&WȿZ%)-<7juw:?C]\ڇeRJDDDDDDDDDDDDDDDDDDDDDDDDD+sf,""/.T=V3R_{՗[>䥩N#"""""""""""""""""""""""""LRK+?-)wFΩpоMe':Կ_ߺQXKu2y;*i žJL}?oy붏߇d`_pKZDDDDDDDDDDDDDDDDD_\V~?5WzY㛊?:oW`[w?4"""""""NPn=r(H}`f-vz͒R}WDDDDDDDDDDDDDDDDDo2ו:X) """/-ӻ ~]xd|?(iDDDDDDDZxHsj? e#7&:{nm+bsYT"""""""""""""""""m]*,""?)<082&܋&:3w߇/_4"""""""LFzgHuϯ<xxgkR9%SJDDDDDDDDDDDDDDDDɕW@*O@4>~úG??iDDDDDDDNo.{w̾n^eOu羵d(k?]ӈ2/P """\7$^w 5?RPӤ2Ymqs\: N:i:E(=VpEJK/IuM|x8pU iDDDDDDD"VBhrfًvTynh+ ?'i{j~lhZDDDDDDDDD+3tJU[wEЬd%nLu}×9>T9\笶߄:RTY7ke5WϺy+yfP"""""""2e?nyVJȲC>sc_lqa#79wEDDDDDDDD͕^J,""2>K©Ns848ovoغ#oc?Zq;lmT>fUvZ5n ;`5!n6G ;v΃`eX=X]t !\ W|g1 ,͚A4`V3 X k q+C{uY%U3X!nAP V!fԶ vvY#JzCp]ν`fk#`s }Yg{gr Uγ`#yN6{xl+|I{ l=~~xl 1`x6,L3{`1^@{61Oδ16 |&2"lL+L6o.̶ns/XW m@xޖVxUlF`YvcN`[m7`]O(Բm^EI1f+T3f眛4y wu\β7-Hu?qo[B_dPٴTo+'HGxې &de,Ku*WWWRu[ 톯[/pGgY@0yY#Z&/5'gC YK~"`6t+kWAS] A>f!(f5Ai;nM<9Vnk` AzA.7hjuv[k@p+m!;=BuZA]g< dπ[6n!L>اK|n*X./g0,#"&J3*}>oֈ)%Xs>k XGḇ[R^%ˁX`-?V ^G~a5[Ʊ_ldmfl/Wd?9`?G8I2d '\ TT}:5=ijw?sTc4|›}W?eKK""""""""" Wfuƪ,""ZA疝Wu*eVm>5׼d_5U.;"iDDDv+mm v bǖ |b i~ac!Ks-~bW- :几~-"_n?X1m,݃V^*jU xvZ5j?Q gVɖc?|.`aC𕅭 -f 5VbE;]+I[Yi l+o *q 1j!(DuVnv#gڹV`\4vWej BK ij{خ|Ï@_3,a 0Ėze~`#؛bk >|/fVvc;oe09 +qvNp,rt$@e|+Tí;nZTIv?p $Y:iDDDDDDDNo_^|x0h%O֩4wo\UmXq˪RFDDDDDDDD͕UJUk|~/>;9g{X+X뚑kR} Р;iDDDDDNS$evmo H-#6ٲ > rl% 3Ap}naDb_YZBy ЖXa.VVҊ>XeZin VAojUc; `Մ`ɎX޴v.Z:Ce[}>\kfv1̳F|c.`ŭ)?Y~VZBՊV@pRt+$u dZY5@PjXWJv8 a-`X}nBkhC.^4 ZSNւ fXiy~`}!xN<6LuyOHWx%ֹ*i~;#wô7ou0iDDDDDDDDD\+-SXD*Q<^5 ?՜ϧC+0V[n#ޱݵ:g{"""""""d)W;vkmo'?LLO؇ ~}l OL6Byb!f_[u[~} IԊ`g@pd%`?SB6Z6[%mVmUIc5Svv>c-A0rBE15`2VXfl`J[ k 1+cWAeX% JU AAnAzE*wVI$O?N4 ro.f-ɋ/-} &:"KּSКTsqe_M`~+w4kUDfϚ1kS}1K^TӍ_[ A[66B`P 6~Zm{v x޶d0$7cmM OZIfI#_Z v_=N ЖXa/vW؏VJ-Xo a"m+vX5 ogC:a\߳tA0)?|kg_[Ě@4'o [+v\ +i 8ie*VѮ bUvuz*Z]nn-k vAS۝`WZS5 fvkv5=d/dT4O*j09`D]d֌X\iyd1|f,f!?--u`/bh+0 y [ |6}c8,c/Uv 2ddK1֋_Eg^)rz:'#2g=4"""""""""l+e,"WS{{kσ<}iu^Kٿzp7{ՋJu ?\kf\ |kѼ%|v9k-!bE5)nm 8b ^lv&] (b5+el*v.7Cp6VY A#^`-͸kv]CA{{>@_kOu {l ث֝A6 6`L/A{ l+ {l-O2 l = q`Gy&el%G`Ex`eV1jx>IkMk=33&>6`5+X`O5P[n^G~F3[ƳmT~a0泃}`l7(rt`%\ $>D/""qV> :+TY~6;z,Â3E춃N%"""""""".=r~ED*Bo gɻpvVC^GU겖F`͡ՓS}u_=qToJgvj>9?*dD^v?;xv}.x%zZI %ܼ%0?ͶŃSKmf_[>"+Auէ胮rb!˶Ze-?Dlp;luݎq.Y}˶ !vj>D4{[3~{E9ϫ,uy~~6l#O.6{1`lNP%&EU,?#V|V޵`g-џc3!p6o:0E@7_2KV-?bVXZm'6ﱖ`觱]glcvrXb{99 `;'A6p\r$J"{(~yuLۣ=Mu[uш >::iDDDDDDDDD\ƥ\Y`^Kپ糽5orrZkYm XslS}uٯ_~ƩN!"""""""""""""ݤ ࠟKgZ ɧOE \{@raZ_gGߔD޷ N-ǂ/lYR6bW%`<[l ĖYQDw XnV*ApjUvqm%SK[v·`erSN-f@0AҸVZ@يⴁకdX9Y%A,op})F*9v3;.'% 47m }p%zI;{{jQdp l_< 6 ] !G^[h`q[Cɞ=v,cM˰L3^f X^ &ӀUf8>;N-_rjک%kb%̷`V-{VYzW[lqW)yK3n`->91` 8 $Y2HL5ƽsj+>(%iR؅sgB1sؔT"""""""""LUTP`?JWT_!?{ EbCw$13J0o؞y[вfNU&Lo9PtU|Q4;iDDDDDDDDDDDDDDDRZW{ C ofHN_^SKnHη׃goAWh;; SKc6Ų DfA1kQ-__߾"7 [1ZُSK~[g ~VN-}Vj?Q ;nC_KY.^O(:~WoJ4"""""""""Llһ+UXDnuSL<xRoY̺`czO{On\ߨ˵04s7󶺫e aMk*} Vu J2Z%8rv˜5KN@ueA+df'9yE8dLvσ~_ d]|7_OCfG.dc VAATs_ZPrg[-l Bn <A%{ ]n)=Xytj෰oq΃`4@{-b}FK"""""""")QED.`a?|qKO:VlJXӵR}sJ\Mu:eMÐ1+lEȈ~O.ɥ"Ȝ< ~O'O/ Ӑ˩BtNm?́~~M+D so^!Bt`COs%H:kfS| 6~x0r!~࿞Wiŭ 4ZoUgs[V^*"""""""" W2ʏ,"gS? &|1Ps{ dh'QBFv;KsmؒܲcaMf_}b~{qTsl䗃g!#;)92M.A!df$*Y/o]s!koO.BFG0 k3~"8U^ AN9 YU"4܂+$o $Y%HZ&C򪠧-XlhB~81;v5O'v&cm,'`1{8 """""""#ʔ<\`?E)F>kZחް-[T`׍+Kik׺sZhٽݰƭ|ퟨ0`˞u7\^%oOuԻ+;B'JgtbNcr.0\n2WіK9jk b:h pݭwಸ]ZpQׇ \q7+G?w3xUnw6 nE 0VĽrruytܣ{<A7׽O4݇<cxo faMt1n{Lp_}o2x Wnڍ[ॻnx>BnSviu{'qJ ^ w}s 7 ]&k;}-x\usQB=\-NWB W­s[\y]E BxUVƻjn' B \=wBy!.rG!]⎃]NBheBkr t]r!8U]GPaE!Tbw \CyWmHF3ޯ&/CF8wRC蔷~ 2[$/KB~g<ړ5'wBv ́ϐo2}AUȝWB ~=ɏB"$Md6[{6ض$b9_s ؖW>.`OWY? -9*Do_.7+Qh4~L3·Krw7Usv jt`6,,ɃS̃mɸkse'Qy ""lUNxw{~_V"h+Mu7\mx*].ˇ\y[Ӷ?bmT_ol&;[.qN:{^J?9n_/p ^"xx@bĉeO\ EQ R@CJQ8TО*:jw5TzS3=yw/e港k}7o[-qo-gxh𶳊wkxn"xYngn^mwWdxfWp3019w'<.rYK\][^gv߃wc9xwn%tOug!4+ւ+zʻ_{Uv!~DWm#WLy\wBK݅Obw B\:]&.tJCwmA(vk\BE]gP!TɻP w+sܭjn DDDDDDDDDDDDDD䷖/2f-D{B5KŐ9!o!d~I\}8թD\٥e\`?!zn{Ò MWm*%6_Cܷru߭x0tΰ1 ZrH1&ߚk.U> jlP6i~??x֑yܡEV4""""r DEW|$"P"Ρ+ 4ŀ& p-@ILΤAVjQ ܝԥ*p?繳7s u@c75<`ӊ!inew3-F fם+Vڂ;*p'u=\ι\>:pݣ\$s \5\m <]Npz½ĽuUw?w{= QpѮ/x1~qaLuςAv30a}>s/%f/ݫ^B}o[;mt?Oo[ƃwĭwtmqm# | ^1O+O1Z sI7B ]ޥ.k| xsBZrXޭ.C.W^W]1 \ B2%Wmw]U ]-Bn7xss^}B߻0~t#sqm.u'!5s:NOzW\庶.; p]BEk]Bu.YM O?HW&?2;$&Cv 5XeVK:uC}Vv)fR}7~{E򗿠PQ;.zU1+}sSPl5|1=l:w&"J\ Ā 8.?p6(ԣE)M1pM)G WRUT .Ey&jRImsܙ|{  ! us嚹zFӂDZǮ U4kn!Rw[Z҃6pmv2MpN<຀CtW4_'MUvO\ sݳ' L/pMPw'x- M*>}S g>$xuq7BolowۚhB*Їs^zV{7-r\.B. !sA(v {׺0J\ B. B5wȻ.WBmܽ4=C6qWB'3!; -""""""""""rJ&dL}2,92:UW>,yY_ 2_Iv·m *Dg +DWW~Я 9 6!)!SYAȍ:Hv+DO ً8U[-o2+;du!` 5B:y26O|e+汝}@.Aȿƥsj?Z)tȮZTM|Jko}lRS9MhX)wZs+'4<~nqÆ?jSmškqwЮ=B5?6 }VTM/|qW] '- ;l,t"""""""|{IӤ޼FCɷT=EqP J€* vu}ݘ(>p91|pNiO$Б8>Hn;JzF|{ԑO'O. 7ԑ)H6(D+D68uOa(B)b>=5QLp_R,p )F&%A&8S p[(Aq%w;I)eQtpA1LiNKP R;re9"8 *9&99T.]DEkr5]*qUTN^WpMTxQ;xQGp,; Ps܋T0x#\ 658 n459yi,w6Km9޷ xKVRrە~c?xGܹ/^<8}E>ܩc!.W"BܩϦ{zEDDDDDDDDDSU·3z֐i 9%nCΕv?M!K!w1lcC !yw!E.T{!9E]@9#Lr5!9Ŧr g_TͣLdeRW $X[Ձ]kYTߕN_*.KgEp 3Ps3sژÅ;?~_6_z-nw"iEDDDDDDRko郦kשN:un~e;mvNuC!Hr@8d@[`= KlpȾa>py2i&;h`;[/ml= l` }kx~{'n끕ݮu[k .ֈ7ԷV*ͷVg00J[mE YϲO-lǀ閴`,öSL=3-XL-0V|2EG`|c؛nc!`#m< ٠N`h?l=j݀k;6-v9؀:= -j`XPJ  b=AYV<8 vGP3ۀn UM`)؍`!X`?l fuS:+w`W `ς69VQeAw&+X)#pxpXà9X}1X_<Ơ6p?Np_/Vo;˿؏*Am?X"տEDDDDDDD424zB/&7,&B1/w}x]k_^X^N' !ѓ*:#V#Eր{#Z'"x#- bu|EJ~pW.nhXHp'¥Cmֆ[z--^P?`WЫowx{Mwk \®$}~3!@QMSoj~a95f46qƮ^Wq7N#)3JED(j\T0gϸ w~S{wnU;c젻;$FZ E@@iE@$i vs_?zyZcy17^<6}̆>;`qGu|5Կ3;xRO᩹~@ zI3Â(3axó5u?nr'#B̵=˿Fu [`_S !B!eu ]C毗-7]CtRu!) nO,\YN 0NplnMVu%+g&pj >1A}Q bWQx!YV5.W5]R偝zءʫv l0Y@}4FUET:eVE rd`}kS7@P `NKUu"u^X'vP!k0GV;%YFkF5M->j<5[f &Z>TcUO`1jjX-AP=F{k6aV{*0Tʁl5J:V!PTu kU2c=g޲A[&Ce[$P[){kVuTg:Z8W$ vV xɺam&x1TK딵hnjj ~0mcP 58PujXs}@`Mz|jځ TkH!~V-Pe^*s׃eA=,&XTVNPEV`*Pa PyŃ~P9)_B!B!%}}5s\\AlF?gs.蝜]m@o }szJI߲s,=[@_f{~bIok ZOzkm#h5lyޠ0.@b~n^i5ghf6vA?aDϘbZEc(CG=Ywh6-#PA^ (0Bu~vtJvtOS7L4ް9 :(i{Ҳ4+WBǝWQV^Hū\u_tqE?/۶Il酱yv_ Dz;|ZOs|Q̱uF85)1kwnĤ*ez3V.UEáS,[J@ 5Msj9BK!B!abj] /:ͣ:Gېf/ZKsS !B{) U@'D \Lqq0PG!v.p@;F Lma &RV-"X" B`.ɗj6h,S Ze2,5V dH>bj0ѼQ#To -T @:@z6AKz@@FMtRLeU Ȭڨ̴E5Qe* d*Z6UCjJ**3CR\JRU[PNr<]) ʬ@^^%UjShTPvu(tu(,$PXyQJV"*N~Qm@QuMmZ::k:%Q(Yj UP:` jc>4v@}b|nh ~76Zuͨ|ֳڨX{`I Uл:*{Z$pQ^f p( | J3'&y@Doϩ4׎}:/ZRVj B{5+S'uJj2f8|hݼ0qF~jy e?vxPҟU=#t(Wc7S١ =y]B+؜Dڤ4l? gArǘqgrX$Iț'LI|y<ϿKl T@. 6k\`u-XZǬ@s ݀+zO]!B!ӢۙWrPﻜ'3F:ͣ=Nm4B!B!2ڃ:118 "pJU2xlL5'/YvвQ8y~Z)62FSg@DӵgAKMJ%u( RM4*=ͨ ~ZpTkKMVkZ'v.4-ZД֓f9yVEi}6ed mA˩  2\am0\~%B!RB' ,|u{{Yn*o 3glExP3+xEk6x3 k=x泎osN>"ݕmVn-VZ ZBVoh=Z V{pu.RB]u[렊PʡzєAwTP|JP&+9u P?'뻊k48QnN 779/i{Ѳȴ%WGBUO lyq뫀k >:(nX{΁yomn cf^W;O 2\N.Pd]ru?=gZy]=%/ og 3J iJ1\v%H~=vg (L3=J _n0 iWPZ}-F#9ZI0b5zUOh]vQϪ[ ad|{Ncn{o}ۍ{zEp2R-,i UFs}8uc\/3mgn1΀+d$iQ6gf20CX[?KB!B~VAHZ 8=E]O}+T.` zLyxN[S(8#<Z#7x18~u济wcu|Vw Zh:b@}˭zVEvW侀v` QTa"TMPT14a-/SAE2M} eW3UFI]HiΓm Sԗ7i{вt0WB _s trҊ_P;PhQ\`JY}[jsEa\~{]gk&6(;{AOkVu[~ȯ?oMQ՝Ɋt@3+4^ΆǬ `\Yf{Q[S063Mpk^J 7jJ6 ^l<6S!WGv}I fyfl l, +k>Tc~ٌ =zpl5eD(aWf3} 8Ϙtc k\0UԌHdkKf-:0 lmH{B!BwZ$/Nא\+7D?N@l`&p 4>9>:'䟐2WDdwz u,t}ZebeX!7/lR0iG4^U׬g:OԿB<\;;=s=Y0~ VNX- }_Wzss&oo_ν7 w,x L-X.YWz6xCuϺb@uR :ýPB!Bݥ]֖F/%0|Z]-F=ϴZ 0`+^sև팞Iz%} xO ,{w} ;z9pDhk3zs>A8뛟5h=J&f pM2A9Q Š] |mhC~!B!_U~8 rN#M20Sly2,9k |F5oy WPm|6չZNZx>R_Sd7AI'] z\&I][6*{@dzOC[ cTP$uW{1> !B!ӤzTӽbgbS).ϝvnBN%B!B=9EnBנ\Ptޜn#9=-Or7_KIhP.Zh@K -];Ӷh.iKAoc_GSm^ ̛J.G`֓`;g>[A17AjԳcqK/7Gk84#3sYǜjlg8ogX`*# \cf;m{a׌>6|AM CB!Bk !fs|^MNUP~^p4:okq}kCB(7@{+o3#?h }׃ 7X*ݪ7VPy@P pPB!dvn}ȬZcyr9'KVw1PB!Bd |Rj?:ͣ3O̓\oqg]!y&h}x|4תVKw3}6tvQ`,jHjk^0jݴ4`L>ԟVlZG0GI3h-^^_ w~|0NBYθ?  FteN16sN?)>ʘA afn;i{ިaFKge6 E!Bkb|gWz4B<޴,2 !ֳ͗>W|P3T 흱t8"g™^׿_C s7&Ϫ?7)Oߛ7=&fg1s}) ;߀q.oJ8aIG|5!?VBj$/zo^_LSZZ`U *SM@{@7Hi!Bt+RQUNjMޟ͒fWUlmsu޵B!Bt(e2# /QLN)wtwV u!x&pzS|w4ӪZG:1] *m3A7 _4m<#^ywR1Y?}V ROZ'vDOM[n5/_{}ءg7u8Lé7g3̆L44fskj47 aGͪfcdƛFhCPB!ğ9'SҜ94B<޴,_eΕ !yZokukZeJsCSy-vL_\S!_W 9bwR߳ <-~Uޝt_|ٓ/%zC7> C}-ZЭDjjV;* $pPB!/Cau?sx)f ѾmpZLCV!B!I\_M ޥB߄:nNe.VξwF!x$pޤmHТӵ>mdhŠ7v1GY0z-%i"`^җk5,^p=^CzZm reF*أu zp4%>8.kgӘjZp}dy¸װka?rx27z6@ CB!.MHN]ö8#ixiYg.#B!~uZ.9ǥ]FOXc1_ u?F~Zr|Q:B<=|g@򔘽5)wS?no3MLIM'7c')pw=|G3`V,/Xu]PW45Peb=B!B!B!O9 V +~1i~Vćҍ{N#B֓m MZ֗V4mWhtK;-΂~_{d0+]h0&jc`חi5,ڃm=6l [_{>[ec++z pXMFFUp6ya|DpM0Wu.No Bɉs:'8>g09`,YnZY P X<> !B!B!Be5_6k)˙j-oKNOLLFE}=ܶu*!B#hiB%Rh}hKLzLxm5K -vZu- ̊ZZ!0K`k/m~=6lR2w}1@p}{[cKKz1p S F>38gucv71A_qƏ:k1ndh6`d}f4e3CXw]NeBLk#cNr5sܹeX!/n,>Vj/Jup걤=Bϋs'W'B/s].&x~<'6;iIɾv+_N{)!/:`>ZkX,Vk4;Vj*ǀx> !B!B!B'_;]_C絅9@jF\Rq~hɡ3‘ĿSB! @Hc*ަ%hmm=Aiт60>ӲkAU,E1FDfA}lz?0)Q//[W;t[iظm5:sQKΛf7c89\aIq\CAX3 v匒|la]lCAo7_ !xđ%{]յz6 Ǫx+ԩ<^F J}nPB!BL|8g4ω8aLH=;I:d]7ߛ&}g] U**RJ0 /ԧ!B!B!BI[P~o{C޹jdCf+ );S O4ԻB!2x@:Q4-@k` zSkim Ӵl)ohU0K띴0Fih O}9Y#(d*n}**U9f~C}B!B!B!d`*?>ވ yy&pSl0H>\B!BJpR!@koiAkd}Fzv-`LѲj'AU|N逸c6BQ}V< w`~GjF//{'E;6anhlS}qN/ C5Yi @p^11Z_e.p64.+gLeleօFg0kP?!.jre%,{j*N#M˚1ܝdX!/}sgő{]C4N u^+hjt#C&tL<;z·(B}M-H{Ƌ*?˂ j{?Y޴{tX[s/J1~UR 9G(gWp lFGAoi:PB'g`kb}c !B!B!B!B!x# ЉB!A351=A 6Y;e+Z8ikyUOGcc  8b KUhTō2pk.1v߸aٍ\l)4,iHQȫ9BPēn`p8n8OxӲ<-BK|b4^W| p1ĐP~<m|աN߶ 5N)@ۡ7ՖB+zF_+z0s:[Xjn092}HX?ahk )ςZ<9qoF~.Gr(}C!ēS2a7/7Ś~1y<#W;(|zUͣmcI; fqƻp-Ze0V`oiUǃ-R/-{{ l+i)z"8H6NqQnFUp3;1Ro5΃+y@f \m@x E&d3_ }-r:s_(2ixiY+f^{ OL1?4 *Wȟ@|.5uX4sS-O} U~hjTvU2CJ--"{Ex &sL v2\u򵒰yԦo߄3jkB}B'oi2lC`cl=Co֚g@ۭ־N+zQƇkze0غ߁Q lM}`o6ۻ!c`O>+GσㅈN<1q\&;WX8p͉ 6æ a!xxG{o%pKy<1鼷3*1%f%<௟\ew{)C W:8}l}ֱ@u+X=lPU_>SYXB}B!B!B!B!B!8qbS}գh=AkMYvWFu|"qOB;2&uBIx{‡ H] 3=_V.}:}`Džгb/{~,b__du5®3zyx9^j0hkk/7n0cg񳇀U*m 9 !"Rap*qq'٣?mۭTE53ԩğVFkOqkh߀IU+ o#^ `n39<w~ pd eo )>_G쯂|-p9\)g;E/\@﫿 a!xxO~ ɻcӹo}\fIχ y}; MEC$_Mwi(IDAT)@:oD.?L`ԳjV| X]B!B!B!B!B@iG #ꕋ-47]cWuElPA>diY6ia1 勔?z Z)\Fl.Y9zji-a0O " !ē#-[xbx#ld VxsUz;8]nUსN2g{ZA܏M4BhY|;U~ZL2)ЬC3o?q, nO[|~Xf]ە`kf{VZk5WoͻDe~.jڞ+G 8۟^e?=?v>iէ/xUx~[o4߭M'v5x-ԩxTh@;c 轌AzuƻNg7Q68^8EwL2læ.=w ['plL86RG}tki}a> !ODp+w\P8+xu~P7% ߾Z \/ ^w#TL6 d@^o{) /^u+* ֻ P^jmOA!~y4r<~ނ| ӳȓ27 vip I:3ޘPB!B!B!Bdܰt!UBN>~=0' u!dlle—~^=ߚuNyXX~[p |aK*vx^Ȯ{vC& /bN.>L2h;ؤ}~+}=ru+_wB'`DGm(}]b !ނ^u_ѿJpIW}S !B6H 9m(;@c=}m O`hhΩi6)l2ϵ ooYGpފ\װȕJ酰)7DPB'{b zH[ɣ/gxjxP7w8o2$7gn)޶(m` n-~U ֛j5L۪4RSB} BjMjC '[6((ԡ~awÞ/j #CJ!B!B!B!xٶu.SV)X:P٭A?t^r)4iCs>s. CF!B!B!B!xdݕ1L\M/Nupo;BF7-,Y'Uwso5ٖgaAV2^o<ر ;?SL=4(`[<9<ўPYO ~*.Һ=*{lz۶FXzi~Eo\1V} 2+ oW}}l+=~r#щzB5f,5m/2XlNכQ>8 1%09:}ZR  [a)VuZ[HQ[!,svg\;ˇB{3 |}%o}'0*k *ld߅0xgٯ@vެ"9#4N9pC:B!B!B!Bdɕ'rQx|\eaNIIN#ēA˺'3yˍ.:`PWn~n<蘱#\QiG.ܝmmmE~nY6ak=ڣ//Xi~?Ul׾gFTrO|S dѲR)O#~RNQ%:V6Dl|E۔nicƏX<zfWF]7πs[msnߠvŁF7-B!ߚZ+,'>0h@dOm5K`/7;sy l7`ZoRm'V{<8G| Ĉ/Eu WNu0s}C!ēFW=T)H~5g =3)5L͉oxķ|w3muj;4PUà pA gijgyѱl;H{+OTMC0wneL4B!B!B!B!ēqԸ:} (;:կYrqU0oH0_WkZֻY,O۱\~~ĸQe޿K_7[Z w7‡w_cB;sv?߿z2+WxB!B<:hK1g3gƶx:%0/טsylMfw ja?1b8ݑ~pN:QGFFpa!xҨ*Ǿn- ovL<$ -o"ໟ4Ŀ|ﻇ@&5;2P5c!pa2Xo?ĪLkJ|^xXv~Ԣ?S?6ک ]tMҎ kf&u }_Շ_!B!ēC1ЀbZ֖i'Ao_#J;zsU086]߱ռHwf {o`}v78:8hN_aG\9+_)J5eB}B!4{Vw ܽcz3'/xf .ϳ񃽅'] ?112o Pqp/ LXs~jj/)ԧՋ 2+W k4B!B!B!B!!s@}ٚ Myÿϊ_\.׆&-p x:i٢3QTj_r ~+͛ҁFW|_^SBf͊?,J@@-<ٷM!bl|jehug͟W!B!:xM֠Y'kAgn/& 0;39عӼT=7ȵߖJlpv8Dg0Ckrpp:4ǑBo[o`bpGƾ) yý+[.o H|Jϛ<|?W'4<fU`mZ ηZh-R?*o}^=oY\ivO/tV9\ؼ|S.::B!B!B!BtglrDD @Rdv4\?9>E=^/ &5pg/\MJ~8&ob3p/)C!+ZYY ONasքcOm]ÿ)GE@-{6k*|3>pzH깩I]~B d*~_!B!8v Nk 6q)ƷzG k`l}wmqێF0u7cuKے^%?8=c8Uyu> G_u4cmC!ēƊ vJ@И<+&nxč~(osW#Y u;ͨ7d 6 u!B!B!B!B!,Zg|g ?(efH׃}!_ӟ~ƶ6v#-~nL߁[xz6ƙocgf_~yk˜rYVyB!B80VL; ?h^ܭ{^?#f3}" d 8{F\r N=C科7@3ѷpt8B!Bky9N]u!B!B!B!B! &&mcGf[?B2?_7oAދy3Y'>~jRk uoq"plݗw׃p1śR("?B!B76GU+A<'SK{ @Pk j"Еwr,Pߟk.3h}vY~+`{~Ԉ3q Qf22ΙuG!8Ǜ7yakF=(pu:1Ecs~B!Vf _5iB!B!B!B!x2"X)O? )QX__Sk p"O<קI`?rcE€.rJJ;},HeB!B}VS@ 9KP/vv =H~2?X_±hAKn>8zi>g0;`hzuo®ق`~>Sܱkx;}p:#bvp͌Jp׳0pJq؞!⯖F MMj>ԩB!B!B!B!{Ӳuɲ#OPFOQ6'sTάͪ HS?myuÔN(o{'s Ò-=k,2M۰rl;ھRU`GwB!B!JN"50H;E맴{/0Nko^<ǃqvfg#L1 Λl\mC.M~xm88.|'"8"pΉ:ƀ38F7BWlk_g9c^vWJ'w؆Ow-UlmsD uz!B!B!B!B!ZY䍖'3isl}]?D揬׭.at趰=!:߯kh-9W$7Db9 2wwzOW6E͐Y~sJf0 ~B!B!R.q6L;ϛ/hq-1.ko^ټ'qvf-fqGfly]wɕ`(<>8msgddQ1DW ?nB{ 1=1 Ո8Hv;Oq<)#?X1U ` !B!B!B!Bpie7 ?醎 f{ݏlu? kawWUV{qGE]?s|9tR}T߾m>7+G70c3SKǬu.vsFuqdpsB!B!S$Ԅ6B۠eoh-7nx޼{A?n= P0?q&m{l[|6Mฑ²;1(Bgtׂ(sD>s.\qkr{m׍|fj4GB!B!_/LD6^K` h9d-Zãm}햡p` aVe}{p},8sxDCptD˨IV9:3:S !^ʉU}θ3_ݫ]k qcnJx%7vohPt uz!B!B!B!Bweۚ5]Wei& e'Uk7{ٗ7o64fz䷡N:gjTf7ւZq*VGwB!B!1D6I3)7JzSu0. }m 0 N[IWm wE6<61)R#9""c"83DqDkyTzl\lC}BBN?*GO}sW)HuF'!8ia~ !B!B!B!BPѲ5A2*Lrr,q;?~Ύ:wU[~PSؿ.?^O u9^+_u"ۧٺX=f'cye*X iҝex]\#I`+xOZ7b=%,*B!BaI4hS4C{k875k0c) Hc,_80vްԶpOkQ83GerWQYU=:3 :_6 a!$慻_p"dE|-Hu!9snt t;N/B!B!B!B!,-ەeNঢ়]A/L ԬsVF9.B걩 Bw)M8l7֥͂&?|ԨS~Q޺0SÔbt|q64s Kg; y2jzx׏ethpor=޽B!BVZ Z6gF6f-1!F8i`.p3#^Ε6l3٢!<}8Rd؈l52# VEr~y6yьadqpȹ1 ˫CB[_$5˙ @Iuz!B!B!B!BZĬ.`xvS7ʒ*9*uNZöOf>]_VnyPB*NH* [D@TtsmM·Zm-~aB>!B!◴$ 0[KZ #6fz=lndDƙٜ%άf*PX[Jwi o<>9"9ӂV;3̕> !Bs½ވ- +LJz9Etts:.͘n|]@gTS !B!'mԇBӏ'4B<]<^wΟSr!Z ޽3o z nB!B!B!B!Bֱy2ʰ" p\7CF!Rx)a({q7BI?^scI1gOTB!B!)k-2́һ_>PH#igMa=Rsu:!~)q]{~6sr޸ ҭR!y0Ew> |z N/B!B!B!B~RBY'7{vKv,μ3li۷-!R`cS !司sHPytn9pps:ƙPB!B!/Q7V{=#BmqwnWW_7kEM!5pqOz"fGdwE fH=E28,3q宗MwΙ1BWR} WoL1ܳw< 6׷B!B!B!B!h_κ,_>~R]{S~aDg}*XoIe=33yCZ!z/=|l"]/ԩ~(\{}MCF!B!x/ _\|S7+B!B!O콲nWAT']~߿=c3X7+M^Imz7B6ȱ B+%x)iz4CۡN#B!B<)1/Cy(SFşŘW&,'|3 SB !t vsX3jyoD+H}0/ABgw ? 9.\^>$&%O8w'R08ԩB!-0^3V;_w9ON6TB!B!xiهg1_~R:ز7b3L~);Xk7bK^x q4΍d0A?B!|V.<)(/ u.8={ou}TB!B!B<]쓳 ? (p3kJoEshi:{u"*9Yי K/V\>p/ĄzB/zr7(xWC&,wXN#B!B<"$T|y>iE{F=EǗ:g! |/A]c>\q$ˎd8ZAҀ1|]/vX[)prDH7. D:-9{^-ȕ//r%o_; |}-⦸э }!qN8x@Peyi !Bdp Y |԰k!3mP~hsQNCF!B!CiY(T su+!܊ڟn )6S8!6p+pi ܯtcйmu9-' ZY,O{Asv/~顕 | \ +asMW\:vt.z`U[QCB=6j.C>\4GOv}dwUmeSG9@p`j"AF* 1JjjTvPiGn`jXuTtV)5U 4^W%iρLPeT-U lަ0GK?U B Tu >jj ,bj ,Qԗ|ZH:_!Ujz7|kZ5U g:}@mT3;&f25آ2 6,`ZF%AT`_'^V3 دWl`pHmVla!pm,u2dp=ΰO#[@]\W8.* \4?79uq.W8 gAp @, sW]M 8nxH")7/n H`$VV!B/nc9Ԩy6REúa139![}C?8Goϻ8:h~n|\b:~qtS?ƪjoRp[c>8hg[aI)_w-{X)/d6=&H;C//Wn܂;ξϿ$$d:$M}F^} ƀ"&B! {5٧Pd\_p+G:B!Bt0֙7m }\ՊU˝i%8 $]+ 8k;p} ;r&4xhfU>`,y\Ziy7hޫyڦg ?ǯwc7kүt. _~sy=zk2X'x E KT|}W:2CRAVKU Pir$tVPJLj*zT6Ѫ *VrV=PV#Py jj*eUkUHW"TuHuUL^U:fuUBP= s :oUV]T@SA@yuU Uɺa UIT#@UUwQ@UuOU]XGjk2PK%j+[3*>@) P jPn-r0h7ZHk-6*QԷک6JvzEeT;Au tTY~PUvWU.uc|$MTg@u:PX@RUPo[T@MiuʪXP**dPTeQMA V+ U5j~.8PuAA]p0Z5S週T@WUCՆ& :"\pPT^YSM ԯ s \PgjjzW5_ Q 򃂃KT`Xuu b-_ ƁɃAXp#~QpV[B? X{pRbpWp)8A r7xPpYpp Npb AA$A?()8B!3YX6U"_5>| .ig.]fa +SK s܁`O3G_{@<4o1Q3E {F+D&EHJQ <=SeW΋N//%}: ܸ!i [s;CrKݝ;6g*`g{5N/B!&MLB8C9`c/%C]CF!B!xdQ+`ڮP'\yjZVPɦeߕ-:"S4R{hj]šU fO`vnǶa$qIÆo>>Ww-xjW*j <(8P3TOU[T5[:0&yju Vjjjh ,VRF֠Q`zXƩV>(8X1 T`탂|fw@mR3`_ `oxPpNÃN sF<<(8P8v9.ڃkf?pn 0pKpk ~PppM 8up?(8H,~@I]}3>}m1[Nq`;z`s}ϯN^w/fB!?qf8&sXǯgpgؕ'o>O;3#1c":B?ui앩jhHu%{ѐEfF}VO N//F__V??0<Į~YH(t{|pώ9Ts|!A`u:B!?s. ף*j C ڗOz4lx~M0ZF5Qo\XM >3༄Ɓz%8|Nj7NZ.עx/3ja`Nw0-pL@i *>S/ꜣxҝ_6u"EP]0*5.- 7A^wMiX#ƒX B!⩦՚J63KjT޷ER|5<ۓ:B!BB #iMn~FdѲV"O2,R'KU)YZ]laPoej -8YXsKꥫҬՉ% nO:C^o>JS!B!BPSdunuauGe]uc]+FnZA%!V<$kx0GoAQPc-e}h9~JJWhP6kCM2rYR9VNPjZ@RKTQPirYP鬯22UVPZ UTv٪ *fG}yV3P5Bjj:huUL^U:MzT5 <9N׏8@Up}2;!jIl]3? S\bSM{-.sv]k锼 <6w`M[[C^!B4B! \>'J =!*H;AzGTT" (WiҋHT; ! <nn߽_ucͳfy R1OEBƕWX4ܲsxScFWdMR)]Vn^_xÛ:|hP(8YŗmÒ\K>]z>xCG߭RUP[*c;:RJ)RJ)Rۡ &* C]b' 8# cٮN7!֓G~ eLT e\ܧ5 ^kQ;%s $%3 Lj`6O ^ |8)v 7{̲3`0|;_ &^bhۅ5+`5 0d2?ڕd7Ձ &avۍ2[`V`v;9n:y͜9gS\?S¾bzov_0m3LYsLy *G(0ULybOSęi@-d I1si,g[0LSi͌L g6ieR@d0{t0@gbB9crs`3ye0y; cy ۼa>ה7IgT66q `SxCo 3 Mj`ij9 3&0t0yɦ)f*"tӍ`f) 6T3>T~>%f 1o+pJFX8L0Y>֛c`#3ffV36N À],3efGX,0Gd'R wy`U Iv79>68318 qs#pxnфq185Q`H H!OW( ܿEGq[<^ioqtRJ)RŔuyO54קut;GQ&>YL`5$\uo ll{o|W](,}m3`+sAEa;nR/-S(RJ)RJ)p&@i`dvsYw\KݢbzWv=z I5c< ^#L^Ev6*@+J)fw;Y0wnE0Lw*0cG}L&#<Oę/0 P;Ln[`RѶ 8eO naϴ34s`R`|";7h/_{VP LYm&. d2M%0Y&`ծ&n|VL.n&9hZa-qB 9kw{Z`>Jv/0%u)M30e]P6E=0=B^2&L_ @S`>7U 25p%u'03`&Ie25-M00&; L)fJa`yfy,)̡'3ZbXj?-`-|gFv*ƚNfL#`3L/`L_0[<>|̷fUL40898e-pf9pg-pb#pcf+`p3nq=.}sß ns x}<CX%H"X $:JE3ڻ![¥* {"6\Fm)RJ)R*mS(}:8:͟oGo< v(r,sr,]@5V4ڤ$>Q .|;e:leO۴K]Ύ;`Iinp]+^D5jt 3)RJ)RJ)R_<Ï@gkRJycNv o7n3߽FԗGAZ7zYtC e`vXwn{ILYfwt:pNRJ¹E0i0wͳBC a棔`O >v(yV`$!v2${-`R(`,3dOivZ0f˞mgm١`|v.0~;@.WE3MI0vY0z,FBSL6~Zn`vK0a 9i }ּn7MO0%5P170L=Le;Œh{"&|Ig7 fƲиe@cnVifhi0MZL;dڛLf?N&9t1q0]Ms̻&}F~ṡ t7E]-PR\oOrcǕo"ۇgqtRJ)Rʱ$EYPqV'w9:՟/VO{K &o::R/ .g\:^Lfz3cqhFMI}Av;ߟA,iYaiʷ /5{,UJj0fS(RJ)RJ)"Z|N<0*X] =Yv񉀕}x?8, +]GWyvgW,`W!0'kWS69ֵYhxHGWJ)R|ij`5ۅ1=_e^c5ed|6ȟljo 1[M- di:oiM1`򘃦>l0fN]1gM70@QL s65w 0}{0T2f$&e&fؓ-m,0uL? `140[qilJ0MhfRk4Z`ZheR`ژ 27^0L&sdGt~V 5wi=-0n& }S\>4`.yz&L/S<>1M4>1@_S$g*d3S0j,`a\|ajwKSxB= 5 LᦑI 3ʼe2Mshmoژڛ\`&&/0t1|ۼL5 `fL)`nʂM*sL/S'/57usSFKKbYnw-Ҍ6f,o?=0kz3| l`:=k ̢/s'v`v À][3rF{YaƃgV1 8u| eX`+f8gJ{0~8ȏ% 1 9.9>60w9<*,-.sx}<&M8w'<>K$a@X-]?^Lzc\fy~dz ~tH`^Y' <(7SJ\6c+RJ)R!ns`b;/[@Ƭ_^idoIoˀh7G2,f]Llf 5z`]vC0e[k6` v{0qL!6PĜSĜ3)f.3"+'L)g T0P0L=iUMěi@-hS$si,'{ ,ظ&^ 6ke|z7ieV0M hc.0Mz{/d2t$9 1ǀ.&Ԝ\-2 0N&?0żm^3ռcyY)6`TO ,fgj̼ fHC[0fifCi 0Mk`%h2cLG0ko~0xXd>u+?262&3| f sL'3,0_b;nԌq>V `Lf-3#GGd9f'ofYa+f5p}0`#p#f pls<-2\0G{\$¸y \"=sE7(¸ <"y# "DH$H!YM?LR".۠jN{4i}rAJ+IF72%WCV/ƍ'M4IVM*7OI 70IRՅt!9 qtRJ)RJ)+-,5g@XMl%$ts] nUqApk @ZNRf긝~yΔOi0xtЭ6v& ptjRJ)RJ)'̤2ko`e"v Yi W%7?إd0kr`2M%0Y v0Yծ dw 4`r<L^L~sĴ vG0ILasYY(f.)a`Jk`J=(k/7!`*03L%=l`({"3aT0L=LmlfkRy@=Yq2߀i`@#fVib<*0͌hf|u`Z?LKfImoƤ3;&hh2C@'ld3't5irs`5E}\)`_>4[`>2EP<z^  1L4>5q<-2HT4)g'`yZde<3zf0#L#ISfin2cL hmoښDڛ`&&0t1L+Ei=SA7SL) 2r`Ѓ \TT. 0,ai,5_Xr3w4mUff5|xL27Sf צf63?|lg f'0 eQ3wf߬b2|W!֚a3 8j6q6 pl3r,X gւh6W9f~suNeβysEC\9׿^ TX>G9މb[;/4J$KŹ`j*h"m<#jg:x,֯OX8~s܍I^)("(zE-8:RJ)RJ)RJ N_%} \9aHȘ`uy~c"j::R⸩& ilo k"do`>78'LORJ)RJ)RJNboÖwF}H"XGR&}Vw-/=hfOAK:NI/ݒdd! {%5 d(dƂrZAH rTY_28!ANJak.i9+ŭE 礤| r^JY@.I;UNV\ZkRZrݪjm)e -im#u w7==/AJCгxGQJ)RJ)z@K]{(į@ ;_,c~=kzz wc::R3JBJ74:{{C580c]=qv)@[J)RJ)RJ)R#E~4uxq-i}pRr.V/΃;w}g::REfDor7׫rioc>sNda8+++xxxH*O|Ix/R% ? @j2 HC@(2b@f|R @Bx@ RA j)I] @^$?i 4 IZQ6@KשEgԥ+P7]$ (% ;H)(K3 RT JVЁ!@U,#@ӅQ@Mޑ@My ux_&uCo/=e6А^G4hJYҜh)_h v`NFV2V/@:Dta2yr xOfȉgg(8|̢,8x|*I8H_Y- ><+8Spl=_pd 0QNJ0d9#ـ8+9A+70M.Ig@fu) ̒Rs~.8sA`/ , JU_ d-Ub%,8pځWydx 𹂃O v<+8!s`d`sd$pY!)A[I(88&e:Ȝ|3RYA Y`)piRZr]ܰ\p Z K 89"ApUJ)RJ!Xg-KI rƩybJ..qZ}LuEN=}ReGWyf\bi HQq{};:W/0B<)re*0 e9:RJ)RJ)RJ)ԋZv,s{r4J$xy{{RJ9Ӌ}ʧ+6yѩR/"^,-T)q@p:2ѩznl5Zq6ݾF)Rθ.7& @R i O:2$94d}Vp, }Zp? r\pP $ӂBӂ8\A OyςR<-8h y 6PzP4@i(C3(' *d8 j=+8ςyVptCf ςe/ Zig끶2@;Qp0炃 ͤ a~.89~Upi_`<F dyVp`y`$_ 2 +$9-ـ) S  ̔k,8R|IYP <-8|#R_Zo`$>+8HyVp`K ^ kE:>+8@6J*C` [$ MRKV:/Cs][2?+8쓐 F|2i58..8 rZ^Zp`ͳeE)mxVp T*U rSj<+8mZue7=g/ O'?\RJ)BHK : w'VCq@6=yrx{|Io>5i+<;w¼TwVO'rs>{j0S|]r+BZRJ)RJ)RJWXQ7l;Fo͒v p(x2h]-|ݚiOF@%!?eM})R/2AkC*BJu4K|J lOWIiTJ)RJW/ \gq} ć4\p >&C2 P Y$dUAQ ӿS@eAI24?T HjHuj (*ui.8 }Zp H MH]ࠛL9@#>y Y4~.8h!_h`mdl3- t|VpY& ӂ ȴgs8 |(s,<_pp%K.',<_p/ ~ IC'Χ92KnHgAZu= u4# f BZXgAu$jg]Mxyۺ h#a )Ķ>b@DzX NVo+E>r'#CNiAd$5 V&Z )V.dq*~l2 $sX ˰F`v+ 7"{BqC=z+o!|_wk yb0/86b 6ΎNRJ)RJ)RJ)KN46l4;s?R?cY=s࿻j.&U9}立G@x\yk2v2OzpRO2t]{5h-_Ԡz̿}!ǃrvY)2n ?,)i|{N-0v:DMq{(RJ)R/VER} ˴58޹{,} ppOOB€</e$ _!M\aS<@FGRdC/Ǭ d&  d\| *@VQٓeܗ2 TIU K%F$^|GdX#"mA~\ M6&A'I-}S2@= WBdnY9ekM9&dI)l9%[R\JY@.JYk%)/߃\zV5k3Mem#udpϪgy :jnVr$ZZW@b 8bIZ@=+Ė(c}lŁXJqXMY ; RX~`5ޕkTARUFsb۳4GԿ!vD@}]̘`oٞ\8jIcGVJ)RJ)RJ)161~6}ԋMg͐+En^+SL[3{(yF%ĵ_wj`:nwIKqOU%x{Mmn>{Θ[).[kA2}9x~ `SNu?~/GQJ)RJ)^L{;ٴ/dJgzN8[mt0aGQJ? ^\9}|c^v(ac1GQJ)RJ)^ln<:{mbstޙv~n;}a+"J)gLFXcY5sʯ{H3]_/g˗O 6:g(U+ 1QIaꋹ#Q9#A{OBLlq aUtU.!s\05f;: gWW<_$=K2'$94d!/D6)dd$%,LAJ*٥5\RG9Dsפ SԖ@q˻@IH)@iFo-P|RI`*Tjʻ2xS4=Pz<1h*ɷ o1@V-K"@gQƱ$ -SȻ2M@p<|,HOY", J j 2H6b edx dt 9&IrJA&YL VrY̐kRM)2Gf[$@zl֧ ;$H>ieJV OB $9,I G$58.e&I5 RrKV9Y\ZkRrC[[@nYd;k/  Fpifr$je]vUXdXDyzlo=C Jqz[ .\,`yxY_X@R+aQV8++HZk /o[aRJ)RJ))2Aމ+Ott^l✵L:w1e^ݙw\lelۻ]M7ْ~[bUUr>.ye.鿿<{ON}iRֽWY(=ɞ--g%G]xTpcziRJ)RJK\Ų@w*Th܊;\T{$0f]PJ?OuS]:->^]!U^=YV> _h=^Ŷ ~9ͼal Q GybOC!efKJ0J,ptz '—oIBLA 2HM#@FrQB^RPʁd0Pyj@NJPC),TH!,רN;uj (&u+PRFPZ (Gs@gm  TjHWƂԦLʇLc @z\,H?YҌh!Xbi#eЎQ c҉ @aޓy_fYCE#Y(WAz7r@ *y ҟ 𹬓xQR/"2D 0Lv'#>(9 r5ƣv|q{!A\' zQ]mI|#-oJ&i/v >)dK2Y_Q\l /hC$5) Ǭ4RH9!YS5 礄䂔@.I9ke`\*:Rr炃6R[vܕ7= :&@4 ӯgRJ)Roz ws鴺(bY.Fލo.N;x<)f5p!#i vئ`MV Mғ )EL$|J! @6P"H.yC)E}~YHP^"oB+ר&m"ԠԢ3P:(N=)) C(MS Rҗ *Ӟ U<-.H:og:_uHc/4YX" Y4dHsJ|jb2Tm&- el:1NvtI0Y]eyir xOfɧ>`.?Ѓo&H/Y" |*+H?Y#Q@~ sY'qQ@IlK,2X~g`7a<GR}2FK:qx9.@&) &i2EIN+`%2CH!`\ m$'A}),0H"* T!Zj|+1RdY AVH4Y%X-H+5bI;7^* d>.C[쒌2dd`lHVvrHrZXAH^ 8&O dI5䬔.Hik9E$5 WTA[լ 7Զw:ԳܗAV#H42J)ReqUƒJWӞy# ɝuF:[LwAsy͜7[0vB$N+Qѣ++׏Y< `W=KQ?Aq5np_a/¸ZO K[-]" SZڽ;΁N9pf)5!W::RJ?ӺwnSv ^ xs{ Kf-i|r' ^=xkdTj[vaDcu7"D3]x|%>$v,M^)ʻmPz =-GqYwo]Ztt( "Wp'> HE _Ғ#HB@v 5#@@yHn@VO B(,gAvQ$<-24 |Ƞ hR(LuKM:E#oywAJH}yxtJDze)AќO@iK*t`PU:p:]d4PCa,H-{ZdLJw&1/=eА2|B }|&KAb +2HV-,߃!0B6dli (c ҉ "e?L 0M tc>rPEX We\!KЛ-|' }Y%~D|&k^@FIȗU,`d`/W|Q1rHcQh Ǭ9)Y@&)2JK.irQ!׬ _ y-8#ܷʂ,RdKe`DHUo$ʪ DH`J=eo5N $[ X-^=V\ ]Q$d[=A~@ $i ۭ S2XAvK&-Y $DFPk4!@I.k"pkM9*eq)h9!e6))bN[Ŭ g,9/ E)'@.[@Jek=pMRrӪim-u wU_ܗ!: n5NeyTJ)Vvw_/cDVpt^lFrwWU +.o|iya1'-N:uYf/-ߵ_{ZΥK y׉aEsaƷ3~qb}RJlF8YC#^WͿanv8_v9D ~'ѻRJ)RJڜO w%4*4x汣SùQ~9:RJ KLw)>nWGwwTޛ! oH3&8N?L+. ճ㩷G'I,6&q;+e0O](z`ʇZށ~4I9yL}J)RJ 3 n<-2\ 7~>^E_Ґ %diA(HFrBn -FAd"@FrQ DJY$B!R*@vR $Z@.J&O y)K# T@*U5h tR[ŨKWRO%i@)PZH/E-PAZP22 T2 FF5m Rw}ԑgE3zCЋ@#D4,H\4h~Zd k 2B6c4[2V/@:D ta @ޑrxG1=fIe>dPsEWAzXn=[[=+$ TVIH?Y-Q@~ed$ Mb_Ed$.n dx`#e#%-89* L$.IDAT  2EIrA.W5ץr*2GnKI`ܓ 侔Y aVE`ѳ"XHY*1֛ K%N+$Q$)*ji j HGufu֋xI7Mc}Ic`X@~4d2)A % kel28 ٭ %9, G$5 5  Yq n0L&Y8KٞT7ql>{CJ@78TJ$vswV7K%+3{WZ@˼SZ|]?D`i9qn93~>]$fg0&%A؃mUJY#%A!@ƯsK':aWn>_}9?{X=~mzGߴRJ)RJ)dt*?\H=ONI~N(zUY?:K?pat}S4qjH$_HԿS+<\6æ>jx\"3bMV'8ARɏ,gӎNRR}g=tm/::_gϗ+֍[*4J)RJ)ԋ!cniTK3νy iz9HHrOWՌ3MY U.0{qv^Ԣɻo п֧e[l)S4 rrQ)RJ)RJ)WAu(t 5: $; g}w/]YѩRJE `ru=3 m!%e;i 0W')q$M^ jOBkQ5jzboH>0))J)~GC[C,u4s'Y3(RJ)R/.fA524~c eVRPBCGQ !d[~Umgې3[_Vuj¥/rTX_tE0W_9dǴ7|pQ)RJ)RJ)9Gnt?! u8YdsU}aopzώ&AㇳnUt(R<$;8mq9\zu T7m *~ pq0>5C?^%u8'"D152?D7I>"Ά'_$އz RNj^)26=x_XjLNǹyɽ3LVtyBiRJ)RJWR /Yy6izg}v/XY nğd6GQ"!ʽU_UٽMESԇY5WLjry9ҭJw5m4=7}RJ)RJ)RQgsd B_qH1woٲyY|aߡd(^)zR˻n\O X1 |:- ~;3d>ٿxG+'⾁#o5Wd=8cH{-!DR2t3?::RJ:2/Fwwkه ;8L0mMGRJ)RJ)^lT9ҥSR$7N2R`fډ)JLȺ=I~Ukt!*5˽?*UxK%9\ʸVwE.ĝ7MWp-TZɧGR/7i=k@jV%O^\`B_ > |}ނ\iBdxf8Rcwaڭ]cGSJ)RJ)pmQ.( #m\vOi7₟Q>:0yCp^)RJ)RJ)R'If9Ε݇;/N/I˳tHͻ-lɯ "8/sz^Ķ#@x╶F'֍2<$~[*璋9x"Iptz븗 -ZMv|NOdzWΚDv}x_ RJ)R ]l xm9:XMW߃t;QizIڬ7r:RZâ wP\+ek+孩MJ.Lh0 BRk*r>=v20$=J)RJ)RJ)R?VWϬ̀[xk{ !@9N V/R=ხՊ k+D1 ~vdV)AWNR,kU.ҲNssӛK"ε )$-hPJ)RJ)iW",WvGwʎc.8:R ٓ5:Oj~U]y~=,&`l(~h8>U+@]JzReu`m˛,liK/-!!\z)AdS}!{Vj@6 5CQ_;'wG)RJ)RJ)RJ& ',:RգpX[}Җl2) [Bre!92::Rϳ]Su?]+x DvI5gJP&xds2IqtzH>JAҿ .]"5d9z{e^r ܈:rW^ҵ7<{w J)RJ)KB:ނpoٕ,'9:R 9'亚|̯_ ;>,V(zePdj\_J\Z:wr.kae9>5J˗օ?=6/l.A➤MIQ2Nuvj_j1ipzjYN8B[J)RJ)RJ)R;ΕpKxd+~]ɜg!? emGWyI_Žv1ϣh,D=6w; SdI`4ttz'igoZ@䣉A|'o%+][ \^)RJ)R',X9 wt_'SzchxwHk+ґ}@K#&BD ^rLs=(2,$}r+1R&Kq3\LutzRJ)RJ)f z}ԩz ι [7%3rt*^m#Oc~U]qz̲_v4|5+yz]u-TܡV^R/j~5篏u"Ǟ{R⏻^svO/UHS篏'Mڜ EFIGRJ)RJ)RJ)zH:=}ޖI X >{MY.%o ݏ;:R{pEVߌz1<9 "ůO%Mm)=lg<^)RJ)RJ)"p^­2_BV7Oqs`w^wߺhQH.4"awCɖ*XwtUu93_ዠD`ew;>U+{*T^KQ} B6!Mj2W0~ø?sPE0IǴu.%,c=RJ)RJ)RJ)M"pwqj 5Ru /xd7mH6ǚͳcGWy{n;GC+#>Ȭ.FTӏLJ@k1'Y`J^ls0GWJ)RJ)RJ\r ,=21Jf=<OI7dsb͏g=X +U]3RvFrO}R؍^)RJ)RJ)R$^ ng.{N -V)N-,[P~`~{ݚ6?sT x/` u쮝nxѱ08zRJ)RJ)RJ)s#\RkrLߤ S]!}7B;ptj /d}^3D]tUx9!rH*E-0cGVJ)RJ)RJ)^mby֫4qtWhy9:R/uu~?`,ɿLNRJ)RJ)RJ) /\J\>X-x6 zfꝪ* ?J=/e}bB0Gő!jŝ1ncQ$ $7lnB^)RJ)RJ)z9HF}˯;S)^\C>ͻ)z94{֒.0t밫_u>88 Z)RJ)RJ)RJ\0o Ls* .]҂gu>O{Nڮ'/>s3U~cv6^%4y7 4'Cpc|d*rw|328wHm]H.P/e9m;:RJ)RJ)RJ)גlG9^~`GxqUxO{Ы/Mʴ.UC2/?ʼ縣+דCX݋ՆGEfNO!FxD}!q$?Nؓ< L ^+ԩ{']ʻR -< c%vg:> վ6 wI)RJ)RJ'v$8?m DLo@.pzS(RJ)RJ)RJ)7c,)!@+J\Ljpe}o=1<2pn{^xtqz}= Kn bCӿB>d94-aU NJM mx#.iJ)RJ)RУ!5ut C] N+ߍc7=}75r2j(}AMN7(>[}:Т]v;}~Gݫr֫)NRJ)RJ)RJ)g&!pD7xR#ʓhh{5IT؈7~KV]Zsc]estxw] 4!uP8&-H˧nQ/y/`3x7uk u 0: wtGҦ)0fswǝT{iM'# >?}ѩRJ)RJ)b{)R w+zkBUT|ׯط"`\蝯WJN͞[#B)RJ)RJ)RJ)W0fI^vR3>):&"O~ abK<;sn16z^%&Ab>K2^5׀=>"B6=i)Ia~#f ۥOߕn}k9:RJ)RJ)~K-J:d;ERJ{M:毞yJ6.yx0:jnU=v~+RJ)RJ)RJ)ԫO\\JӜdAw= e|!͹SaN^E|u=>2_=8"WOcw@\ȑS!iQlIieW%I*XFuUfwSF[,锣(RJ)RJ)j4!.`RYٿ XvMK 8u'۱|1xC/9:RJ)RJ)RJ)RJ"=R\vxsNnc꺞?ES̾!MJ::R^KSWQ̍nO {OG~I9']ˮ`Vyy;,G0d^!>GΟ'v袰ɸ,j-S)RJ)RJ)jМ!n`R_L(w_?Wl5PRz }V)RJ)RJ)RJ) Wr\*zs|ki zoiSBz^ʘy}Kqãoѐ~>Pn7u]uNnZY|kGQJ)RJ)R %C=J)~F& N mcg?\슣*RJ)RJ)RJ)r [ZiuWe{}#xN5gK]+W2 K'>UEb9I6!" R1g׎9:j` > F)RJ)RJ i+}z }:,8m_7UMErJ)RJ)RJ)RJ)^Z!%)ҵ<{{TEzł_ROmߧJ=]'V 8^ysNRJ)RJ)%c(-8y~}kEAEko70pwE)RJ)RJ)RJ)8RHlkk$4$ie |7˼fszn[]OπG UQ >A`TYx|ŭoZ[{fZ g֕IW$ut*RJ)RJ)^NOZGPJ)["6FLhVkBB3[Odu|OXX!9> .y&sA)}}4r?~׬!kK* n>po(RJ)RJ)r! U0ARJ{.Z¾>ؾdle^wA:mgcIO`=^z{yaMs)RJ)RJ)RJ)R/&k}.a_+ x J=0; HH1!GVü>=ϒ/9:RJ)RJ)R/ 6_SVJ'u묫׭nxߧ!1:NbUj_n4L m^pS_å\)큎5RJ)RJ)RJ)R`eu>dw=s=<_͌R9A7,|9:ќw=IHst:RJ)RJ)^l}|}NO?Y .n &_zD{E?z2ѻcP-lq.Ƴ; N=3 8$H<8eo/㾌Afnm]n=aB !tn;wsn)RJ)RJ)RJ)R R4yU\yMpq~=OB~x]Jz}+ >_Uu/zak{a! Q?BlpdH)pl㺣*RJ)RJ);LA:p?ChzV5B=7|Gu`4Ύ:Sƺ[= lᄝ~2JL-s։)\ r];:8zŁqHܛ@= !ss25yRkrά18zwRJ)RJ)RJ)RJ)G2e"p#οGH;UȒ. H="46x㢣ӫW'O?; OU+l{8$ǯKqGUJ)RJ)R%gBJr8hP{ٸ\崣~dڛ?Z? F}<1pmpt:e/UF_4,/v_71&nR>Vehc`GPJ)RJ)RJ)RJ)ԋ@T2grכM\/W Ui,q> S9p˗kӫwwl==<?Y$Ħ{T!n$WIػ8ʽ5k=0I 4H(-* ( (bb ҍ"%HwJ ݵιŰx5kbUt@RRgv-Urj,dv3uhaFy.?+?4陉U+*dWdYE??٣\O%Xol.9`þÝ 9ɐ-ޒ\<[K廕<'Ŕ]dYep*孋͎GiQtWQw翦$%r"?HJ˺X)f%-*Zhv-nVFXj$2|rإHZ.¥J^zTHW9P^]#Thv%Y/G$>!p{k H,/iJۨ I/}>#]zpA)/R2,d9Hwp~kpy7%^C]u.M]n_y+ &Qӣ}W| Ε|"ooI4IOi*)ZJp1.c;ЙRҔgRIdOe)Ir/i]" ؤؕq zt 2THN]}>ck;*60L x'7W_]>+p۹o%Ɗ᾿HwV?8/&_IKWX9TMR.;e-[W}.KR/SkCdɫ% }/KoV{_*Ӡ/wYgi,YK@M4ߋ$I-K-8<&M)9옴RgsR~D ޒqsfg"{wiX._g|Tܱ]qo>K/]'e^\]AgiƢf?fv5[KR}]ϭ}c|٭}-9L6z*Rry=rXZᐬ*VUK 'JZKgJOqL)ù4C`Ϯ.m+Orsu|bv-goU+Wmo)Z7Uzas_~^y2ޜfyq&''֝`xzo^}'Nm95⌇neN'ݽD\C[HU^qeZK?ϘRf)tgfW1J]I^=I6J}Je"D |4ٵ(i~SJqL $&aQ(`7s$fr{Hg:ϐ|(F)̮2#,jZ$ouwV4OZvU$WgW/,8sًIuI8[?)'JYgY>5%i ;Hh?av\WRM;/OnVBɽŘ==bH^TtEʈubS,0"zsK4OUW_*_sHͨQcQbѷ//LH#eOz)^pȢ%]ԯ3&FVo7u׺No]T**iv϶wҘ1* <z3W -҇~${~~ђߖr󽟗?{޽dvERjER]e RVNJ~᷇J컂v#Pr*RWnudZoT͢iuby'rfǢ]@IR\_m5UϪ2oG$_nG̮EIjߊǷ$)wauv)ɔsc1 \%(Gf#A6  {*mb|^]탨0>UWI?|򓒫juYry} ^|ѳi78Q(.{x6mһy9NeBv.%KĪRHqt g|T/~-#t靽O?=hrb CJRނf]%9!2;՛`5)qP1#Xkgui_`U³RFh%S)}HZ,)ߑ`sz٨&U9*ɧf=^奀桛M{j-_8Z s+J.u 2O%,2rS(U-8Pg[;glHRۡZ_Όn4Efv4%0*ϧhtqH]NLIyGJ{(捌/m-|ٵZ!Oz`5Nǝpz_rkq}K5$3)DXuTr#A%>3%CKQJdݔcRڲ*Ri_k:(N-Kvߙn{$ߊe(Dh+5dPqfעg/#Ť;-%ڝ&_ސL H6T\8^k0>9Z3 s9 tr<ZkY%ߧVG{ٛKe' WrY>ɒcv-JJΈ y1!)O^/s78],}љw̮dB`rn#ynuORඈRuPr.2yٵ()ٱrK1JJJZt)u26%kcƩ R.ͮ1—~]8;\v(yZt!kQT\r$Kfעd>P\\tpYBd9=;M{++D_Sr/^H61 ][+`fddKx~JSO4 rH*׮cQ2bcgJ?-N)3S{JU =?Jreͮá+wM|6*y~c^Q[X)h)hYd/{Kl|킠]fǢ Y]|(!=)yi5qJ> 9*?6fnVF! Be,<%kabR._BޫfcQR /d-źxBJ:tZ+;[ʷe7/pW k0p#F+U,~v!y.D`A U'`ٵ(i);p)s I-Y #zZbDxFe(U0(ZprZԿe6Roi5wB)hG{̎EIK ?-I#5>K{Y\8)M`mЂ%}C0 7=#|w 7n6KGpУdʯ[m3)_le͎EIK|Y!Y>QJYv)7r>'ǹ fƈV!z]6N/]S/'+nII.-<'.~ykaDtk=[U]޳NzW7i5OdVg]p1)9]$=$*OfHe+Y`v-JZCKǪ'͗R<2}r v-J1J?#ⱰnыU}pK^VW̮*="w [qX#nRWTճd"R 56}׻ٵ(i1<0$)u)Rc)sS‡eHQrͮFoeVz-*=}\OX0:qc]̮qF+`x:]4Jn)ٟ()dpၣ$ eN{]v^'4~<:%$T}~vQcI*TٵpFDgVWG=uJ_Uz˼-Wg6 \&5 kܲ)V^fWNCr;3u+JS0 Zeufע8Os\.E7QJx"?5)-RWҼqRS{E] 1-l|t u6||TJ+3?xvҪ++Yhv-8r-p͗|Bʜʖ}fH!ѓR%rAϙ]RP}R̸}K ϝ$W)ᙻܾvHET*#k[] >Ա{㟙:4ɛ~kv-P;ی()%Aey!?V_ ^r`gn̮EI)kas)ؾ+JOF7]vr~[dv-ہ.e] I"mxGr5s9%[&s$}" 1^ ;f{F*i!a fעgeJ1uCOH |DuKwT|ٵnfFk3|qU& M_coL:i>mv-pspgqr:"ymp,{6ضT :%OL1%%wpzdW7RJ|c!d}!>a͛%W/ٵJ##tO1|:[J|cR+׏U9*]=lv-pkq2.?"*' /Je#Yœۛ]s1 tQj1vV$ R2̮p#)aj? n}m/oCWר+umy}ˎ{2jSXa^[PnOS)ÈwmRquJN,w:]x9aRl[HI2j(9e̯./(8ev-Tإ]3)m}!%uܰ9u?K:_5aHs/H noH~iyI 8Q6T*Kݔ~޸ߘev2JJfB|JvtyGK/TsX)C#$DZk\i˪=UtjjFH9WmFZZEH΃ݚ:R Q-qRȹZ7zi^)աjIQK eeHEYN9&;0 Ãk/a'%cG% {]ΒT4FGJeި0XY]{ISJzQJy\AړR dgO/SңcͮnFd?3 jB%C9WIj>yQ U{T=}1fǢ%7:7*xx{ORʡ>I_,eOx,S)څoHqL30;q LZ %R?wE5;%-T7)IRJs,RAVυ%WORkaD>ީv*R!}a#xVDH^<|%J+|soV"t5X1'Jg-L-۶RրsIr.Kڠͮ#0 E6Nizr ɫv˞$R{;~%{qlcQC'SMILJ=to[RꤨRܴ%B0#cp{1>5igo%ﱁ%[G1!Qž̮EI䵤R|cԨ 9l䍹7>tQ{gv-nuFa׶2K̮"Rw9xH)n)SWH1. Z3j1i[mwN fG0kqD{w|}ma'Keޭ?M -gٵ(i'@J}|GrC)m2BG- [U(:')Miv-nVFwgvQ{י]q* -}߿-MvY{u ̮PNˌ%f^ODKe{I*~R5 8$. )ZL"%d,)ŨRҔRˊ%([f1"Ϫ0c-N IL9ZzУ#H<__7 (a$Jno{e=(g]U# %*y7 ev-JzZ ]X)Ӊ”RZōH9.%P/{_zctcVTZd,կ>}*;4^݌'+ !mOfJm,mo4ew\э_uL_s١?]j7T=Yswo" 0g}Pz8}dYTEh}-KmR/R_xmTR/_C];4%%or\)vC_JIw0uQ)sVO)/3ãx{.kgDIc6%~S6ھ:?":RtKg ]+zUi_;6J:5/FT?kkb۞ ?\v=玟t>\xw1|֥s}P{܏R/HekiLr9jv-JJV\├R쮃Ӥ9g,e=Cʻ+sNӒcsJGٵ%"F(ºo ?wJO*]$ٱ7͗lO(e?#GEG֒4Jۍ||{ $zT3ꪮX%B}Y`G Y!e)5GR}%H: Crv=\0SܟQ *VV*7$WpRh;*H ]| /Ij 礌Eq-Jq%62nJ2Ž}Vʟ ,wQ#- =~]ɳث绿_3.5ԪQ˶\̮Ҧ-Ԑ3t58ݲ̟枙+54厂_/S4hTsk9 1qswl9#URl֫89p\jmi\)mG꼴'xK/ _8YYwǼ_FI?^7Gh]W&_Jίw(yvE?3I Ln{@ /ٱ(i^12b_8sWAxRͮlDۺuƵK;}6m[_/S4hToSV] 5VrTv9# _o21秜[70Gr_tFWungk=3fVߒ``No9}4\zx#}. x)ؠ}.~؅W>yj>ia2ɮ*,{ܜ%Y#|J-"#mKeN X/.θ[}i)eֹO̡ _g g-h*9:;Vhٵv`DqoݻƵߴդ+lO釖?|)Zu;ϵ-Mk3}\IwiנW?vR@ǀ*7\)uu}]~ު'5 lY77>5xnf?:f*'Y*Oq~BLP`RQov,JZJ*GHqUH RR{+1eR̂W$3KW\[(Et.G5Jj\S2'R9&K__ Z{6q_>RUue{kɳ)յ>Q^R`H}ٱ(iNKsFWJm[R=PNTp6Ÿ$GFZPYdMfgH\Mzai|87.J(Gn㋇X$u|{ݻ2նuj_ܞ8t+}>J_8O,%Z9Ew|;)53ǴYuNRI6IòI_G$xO$IwmN諿ϝ|ЪHG!ͷEpr%H#stI4]O%n֒zu8kR0eg*>HA  e/_e_dJLItK8IJ|*8Xs%+)u5HY4 V(t1D?}WF2FL'lm~z~~]}_2Km|[O 9KrRM< t6uRj_#53%-ɉy)GGg'HIW%㤂 9 ]ӴZp=Q"֝0# =Xzo}yEӊ~vtޱ)/oe/49tKId4ViYPF^7+m_Ŭ-J.Vb詯\l,/}~EZpf5# z徹e]PAz{[c'3W9zC+^vXH2lyJ6I1G.}t9 fJN1xxgHk˯ylڌF1Lry2XG]_N)xy4ͧxkQK*ſpQRZ1)3yZ6@nw%7\u*⫺Ɵi&}3A갪v[ÅRy9e%Xo$KcK[?ZIz5a_}_oy?KZ*/})Sʦ:Kwkzp9Z[iaEM>{×>=w4l~4pw}ߖѢҫ?D7I21H_z=kVXsdkTyoC)8N=^&L./)ڟ}O:OTTˈFq-X;zҫCT3Jd$TʄZpIdiv-JJQGpNJx2:e[̱>RgI 7eTk_k F<:IZ~^㿬Kfv51%uaOJ>BHYOH!N>\y]F]KƂ|tρn뤤3SGIc?ͪ%MM3?F*nPcٵۅQmpdz}oQY>/5m6+%}L({to e6 Zev=\?N}-O8eH~/%e?#$S;9pdwkZ^qyge/':KIμNʬ[7Y)r㙢5fnvF1=0n ׋KխGu[DJ[_y>1RA|3/S:ugҗnmڷ4 '4qIMͮ8T[r%ɷB3G=&9 s6%%;rbxLh!e۞JkTӒͮVFw#{`\_}Z~.Jw<*)5^\׹v}_1@:о#eU׆H}?dRӻ}ͭ[gH3cfMx4=!W?xΰjfW9UbkUPr?yB#+=U-d3>4]9*פIXJ<.7)%L%ZfTh 1Ǎ_G<2/ҠVO=0dv_kҌ'NuA?pw<ps/6BbWfgyxăzToOcvHcҋR!DgVM|ٵdܡFkkqT5N+F;0qUY5?HMiTsk?ʸWZuN{->Oi+퓷EW;ɚeQY/]7Z-&G3V}'{w)h>sҜs?Bn误Zͩ~.}GMojv=D*K.ܷ[IUN)ٶ祠7|ߘ|UTJ[`RVP`Wvµ5GLko?"߲Ik<.^`d r?§{I IVwĵRgO[`]7y2Ɵ}kY]JxqNޟQSOJLER^ǼFy5n܆KVnJ0I>}>>s-}G2i7;S>վc[To'~3ZҸ~Mnv=0Í%΋$%o&HATn[*ilv-JZ3x_rwKO&ޕ]*(sPr|Xnv.^mo7#1ɿFɿolCIS62ee0iZv Vt&9%nRMAvc 1t~ʌ+)h4;{]*WIyē'l{f)Stcso}uûTա^Nzn(K=:81j/$J/>U֬XN)tնxdhE(kQ>ywq)c_%Rߙ/eI:TTs-%x0~Sȶ/ .Ÿ ͮ19R]qHPqKRPD}\噾P|tn)c-'J]8-e{$=*('[B'Җ}@T'̮c|}9)AC"ԯ0qO=^w4wo1Vo{&HroW/yq1K_l8ٵ@xtgm^0^W_CEr1ɻu9Og_u)m%=]R\ץIl'D6%Nsza9fxOؾgy]\DEԟ0m]†eRRʎ]3fJ*WsaٵkNo;V麍_5Я맊N<.dv->fY&Tyו:dEdmy=H#1 u% >g]I>J׫jʜQ˒Ֆٵ?mccJG(8b]V1?p|q`ʩ2oWyA~MJonnvԤz- ]~Y~T]VBI6;WB*4o]O 4H#K!I9f?$*My4(#qc4JaGB[_})u pEzlؿ(|ak 7Xn"ŏ%r:6﨔S?Uy}sǔ,عgKN~C>^u|+Zo -S ]@wU CӶ sQ˨8fto?C덝VT\N$|z徵Vܐ%=Wq+JE_)]}N :͔&OzՎR/zO+<.o} )iz~9?t@H2{w;#?\/͘ nON-N;$]{KC˖JB+Y( \rceٵUR6IՎfE6YdvJIIMvyCzX}*v8K *bjGf~I:g^ #*l,P;~皼Ҩ/YWuK'Il?bXeUTK&O[R|].yhn8{EաFK~uZ:NJ~ݦJ>B odY ɏGrw {YT)59;^l s}gxWΕ}]4hS.fIܾvtrCN҅.m!>XHlX(மmI.'\\f\㟎NxR9Ϥ2zff?Mϱv\_P̮Hsd}p=8ot sj!~v@U&^?YRH]HN_9]⠱pKt3zA%NZICMr}2+S)sfͬ xo_߉>H5~3r=HKW= 9ɐ]W:$y|gm']nR@|xwkcj5IDAT[uNTUThKXH.:v5@fTO٠{iW?B`GUn#y5`v<'yHiij<ͮ[.k{GWΒy녟>#y*sf<4uއ}kv PՋkЉ`3֗s @`KnRMQw쨿]Ǟ䕝RBf↤Cf?aUEH_qΑ<q{S;SwY)oO6WSԢ`fi}W(kv P5640[hgRw!5Ӵ!Xbwrtw<)zu{WiN|,Z2f=STTc,iO87<޳'o?K e lnvm[=uڌ1Kav P5Dn0`,SVRM4(E٣&Dv*T_~3mg˪|k"aS@ɘgd!UtԻ’+$>pw7+ ;L%gKJ1:$aæffۿNf~RӁ cTY̮p+(;>̳S*{/1|i/_ZT*pRx$jvPYdMAfg%$[Y*:Iu#'KIucJS^=,iz19d38\(|\gIO[%)!ii״b1eebx|\gcI{N~4H_rfWx{y0=4@,lyܪt`>JH'J5ߊ\mkvUKr^Jo)'RTB渀";9ND\SKmr̮w_$|Td#q sa./|9* ~f#} <K9$YnnV8?:?$3+{Xj\)ZO]8>5)38faPqz|h麤פ&7Kj:̮6oIbNΫp]XdPqr,X$O%(PcˮR{NTΗOiP%uW8i5~tsRїEE9-וE6N@-'`|!鼸iIv KrieZ-aavP]uP7TrVyr̮Evfp*nUjqPXdUAfg%IN>tJ첉PoYfW656E6 oevNfWh:Z2eUIR";'62d[׹?_R,\e3j&ThavNJaMfg$2 dSl38(, @`M6q0P*Xd7J l* d] `T&+'''ȿNf(,]6@,jp0P*X @aM6q0P*Xd`t. Id`t.+'E6R"l e`t&@`]6@`T @ia1첉R" Evdv,n}ݟws6Umv*U]k&w|J}kQ{LZQR߽C~K^fzH}k3fl2nd` %uIzSnN*TSQIm\^ 9I|O ps>MR۶m[9I]6E{oPQ2_$I5҅ 2$'Ʋ&';ͣfZ+I=߻&鮼=#y{ߛ$}"7W2ǽ:)bFDL؛3Ȝ[=60"`ѐ!' ,6&R Yx[k)WR~ƿX5"';M#nI>;~$-lԪ6҉'OO풤$O](U.r7'=7'MY<+Iqڢ] $Ir5 r' f]6q#~*k~0pYd. 5@&p?`'Ʋ.';mMrƺ!CNpcYd6*W_c'=7'UJ =7Կ9n?nrU5s a,ɎprbnȐX:.UnX0ޏfVwXf1:n* j]T_{f|pӱ&Ɏpq =aMcݰ1!Wΐ!' s]P[#ͮ75!CE6Y>v W9MR6"l vmUyx!=wq]c$5094503lil؎a%YI#zϞ{>3;[4 JT50]'M1T T&qh^:RU7% tv5j`0a@*&:;U&qh^Ԧ @*dc/Tj{t7N4bjR-= ]FwvhFŔR*3(j`B$ͫtv.wvhFԤ::;T2c/uYSJ._T;N4w,1zƏoI1BΎPI)=Zz@2qP)Z`(łP6F1TGKOHj;tPL)%<S elPL)hiɄ:@yH15) 塘BP) 墘T aB-= I :@Y(&:@y(RtPL P.Iuz4$)0bjRC1T A`(#6 eRm0p$Ŕ PIP0bJ6@(&SPIP) 墘tPL)6@y(&BnC1TG`( :@)`0bJN$BMt2QL)%<Sj<S0bJ:@`( ŔR*hiɄ:@y(& eRJ:@y(&6@yGKOH&t. 塘BP) XII`ςmp7G*G&<سHJU: )-VKM$o <sO)yWzdZc}fKg0*s~f΍|h˓w;ˡ%+-rr3prk/|s=;ŭ[:kOav_ZYiON'8⺊7cour*8 +Zvu ,/brI˺uRC&~OH9f여n >aҸ_iHf==NA?ҫSJ o%z1swzɈEG5,q7vMO]Wy-O&-k]T)6pmk\J$7܏Ijgzg}ay}2О=|ҸU[J'78)VL)%߂:i$I.yClZ4ۼ|k'&1s1> |s$fiUh}Io_{{|frljy|g{l]VrPݓn'w{sŒZ4VYk'}{&I+{_IjRJUCzqs==~#Z`ZbJ) P?vk%7U\ULq; |gЀ$,śn>K?4|qqINJ)s5jԤZ`*Ir|CqC_vðɡ3W#:kأHR<ӼkК B`aʆF=>s% tȥG;Nvqu`SRGKOT\RykgI51%i^t+.v(0)&:3 o$Y<\ vLtN}sdFuHtOf\0VRJљ|uiۉ}c[#n.fYKϾo,l՗_{%[z(&%XtIq1]4*zOFm]gZSJ_\2%0jKĎ3-hu&t>钚$'19mN2]ma?'9)f[JG.z,V>BhuZw1)ݽ6ҶpcIjjԄ*kj&6r]GޚdD1TۘP7бkmb#̕,tikw-Pܰ7|$嵌iht(swu$kmU&Y*|tЫJl~/d/yZ u([zoTd#sRUSx͓udrDͧ]ޝ|܀6יkZ=֧Ru(_^<_,ۅ2sl2ó]ү1Wsm;,??O>%Y9s-@3 e>B` qݗ&Ucޗ{n2ۣ_J8䙡 @SJuGKON6II{<;J|:5/sNJŲ&/p'}u}:t|/#sBmTxH.tt|_ϋ-Pd֯g{wmOoLI^r}BMw.?/~d{qS'ۦvnQrٷwߓgh{ϻ^䰺6~g%E^,q}:ŔRC#@qw~qb#uw|『3L'>!7vdiᔟo;ε*Wۊ )򋬷-=w^-<\{\xY?/\py]䍃_ݓƯ7i|Gf.u>c=doW?G6dQ[ǖ~jEh +*f0@k3ߚ4CޅK:#w~_$_goLF= 8'I{z.=_Ϋy|;۹[\2rwt[zUZJ-z Z_ȯn㳟N:,a6yr^_rz- 3bjRJS띕9jvxdk]m:~Cuz훙s8O^3oL_~ݫ0=dvk _ ϯtղ%3?s&K'T/Tɀ~jIBG=ygɷO䙥Y^[ͱɘcl̸ +I=o0bJ){KOHbjlrQLM =Zz@SE15)E`( ŔR0b&%<SS(GKOHbJC15Ą :@Y(j< e P.IuGKOH&lD10bjR0 mPLMJ uP,trQLMJ:@y(TʃPFI P)T K:@y( R5{ɚ$o׿8YY`X vXlzNV~KVz=Y~I9ӟRş'p€K~;'/{~\x7IFWvn2ۦ=2U?.wv& o?מ}:ͿX3z~7k-Zw~SKZK(?s~4*Z Ӣ,vBɦ[ov;$a\!xe>~-:p݃ڳk;K~ܴ-/_yc2Wc@oMm\r/]udN߷W|gn.oK[,Ǐ~fOvk#յͷn=?&PVʯu0Ձ_9G(Պlp ̘M{\o24)4ld#|23Xӭ^'{V5_~ۜ͜ף;p={Nr|'u8dwo˽e9C?z6zк'hu'hZw~SJ?Ak'rΏQL)&6mS|ظ{<{}]ɳ= $:a#6Oze|(6Is_W~x.M&CK/{p2׸-2{v{d,&eˤq+̱eMfYkz̞|Ge :U3z~RV~;?)ouW<ңM䮺 OwJ+>>Kȃ_yII<8O!溮sļ#>뇹ntwߖaģ#?I I?ny4Yg[;$uݯ׸,w[e?PV~;?ouWѼ)03~ݧ^ƽC7=x&+Mm঵RLe8Qm\6^cѕI h(ɿ>~3|dd}rΓƭ2x>+,Zy|`2񭳎࿩[g}+֝RǷ:P~+h^4okuhNG=s'Lix>KR):Pe*ӮK3]Wy滯'j/z?l[Ijfo{Ig?[ϕke`ӯ :u3z~7{o׺ZYO~ͫBE34*Wg )M\y޹~䑺?3}P?4|>_]$=&M;Kf: 3$OM>©>O3z~4{Q>:W~h^jZe*So,jj#ɜgus&I6+!M;o:~}woGqs^9gi&WI1=?Nw%u|Թ%G(&Vʴk`|bv]ۤb+:7p 1KΝM7A\xnIk8>/te|4=?Nw%u|Թ%GLmH,XܰⰏɈF~Zòu|cIOsd:M:~F&~]~eI_'u~I2&X&Mmp&9Q_zaIjRgM7l3?U^q49u|w?@SI.?:W~hŦn3F,GU]>Irz!]tn5~$цŠ_o]=p>3=?nw#u|ԹG(67+ 2Ăw^$M&C:s'>ҩ:>ӹ~@9RM+_P~bjR&*x!I:0]RӈNkRJ SֈiNͰ>3=?nw#u|Թ%G(j;`2*S j~ܨ!]jө 55}96=?nw#u|Թ%G2B )ka7Œi0c:ap>֨ofghZPuS-b`;AT]GҔ.R#:cB6|3$3M>lvILE M8:^ӻI~@RC+_R~hU )M1+Yp̉ksI99ɝ986tBM?lĎ#VYR>JM8HuML^7ʑ:n\e 5)fU2w60^EM}]zĐ톭I?ݣ>Z~D?P9Ȥ\)|t$zL:~F&~]~eI_'u~I2)5qkuJU50^[4uuC.$ɑ U=êҴ07|5$l5| :ӻ~@YRI+_R~bjRa?QK_WGQ39j򇪪ʣ'iίK]OP:Ѧ$Qcz3z~41{(K:sKʏQLMA` )M\/>;$ dc|;{w~>s4slT7}q]ܛB)k7 SS3z~4{(K:sK̏Ul0CPYEEt]O&Y w_ޞgǼz^6I?5~ydC/]|TuMD^'ʒ:N\yfF̰~cQ!iGG|ƧC ut=|2vso3ɗ[~zLy,?t9'^$@3Rש=?Nw%u|Թ%G*F`Ƣw,{H=MSq\x}ojɬę?y /% 䱦_HM]DE.?/\ySJmT6XRHߝ]{7߇w$. m7iV9x?'w{a n GOoߪ|ώx$~] :4u?@Qʚ:sKΏULMuɨLe/zO]dќ>v];<:NX397t\2?ݢ|*,{qOlJϼw1IR|/:>I3z~Lg$w$\ɏWXΕS_Sc,`/֮t|iCa"wW‹+ OuѮ ^V}/z&Z/;y_yFXeIv[qWwl+ote}F'3(~?[H-U0}h=\ɏQX퉕s!\홬O7Ӈn5ldU֟o˛~3_yO.9\tZ1&_o7NF>rܨ?'^9Fɼz8?<w?w{Jw%Ϳ[`Pzչ-+!-Y^5|g z&Ɇ6w<W-<*p?pFs߰{Wu=;Z.H\uF0ʉ:a\ɏRX틕/۸ АSNUT&~t|CdÝ_wۧZzCgޡ!v>['k֥2ߧoþ\_::R5?${ 0dNWEɿvz$L}rL^15L۴IƟγ?96SɟnӣO>жMqڌ2nd` Z XrN#:-ݱG2![="IMzu\קQY+KJ٤:I)5N2W^{$eti'ާK~qXr烷m]g->$)L۴mF9+:q퓫]-wqnHE*'hU n-wիI:~~$WlruH~d ïX*i[ٶSʅg3{u[2ߢ9E񋞐Òɺ{~Dr;⋓=-=KNOrs$c*g|gvu퓧F=%>k΍NҩQ*SY ]]ڥ} f XGT taoJ^Ϙoܗ$[&6lt'66WVmäqopR]ž Wx2&_O=)9vdƃ Y׍?w>p9+v총{L4S{~dov˶gK>W#YG&ϭܽ[3}RȨ+k7: 0=SS(U(3meSۭ'_϶tl-Lh`GTrs[>K/6x -l[nɤo_QO1)N:FtXWʀ:d1ZI}fz4(>pRɸg=<o~O&|vrR˞műƮ\qu~ZdɧW's8痳tig>b'ɠ]8O7}LO>6a|PKU6\7Xdkv[yd՗Yd nw?c}.d𖃻 ]/S2y딷n~䟗= ['oM~3J]v]ɺ$˯²KoW=tzh%c/7+%k7y὏O|goMy|!o&ilk^}gK6v묗wnlzzT7q޶ҁW/yWrWyHJT?uɤxBş $a?ƀyzǏ$/n[Y~q=~v͗/tVY'κd3H&=}e޲,d4F۴p3޿RolYqL{&'77Z(K.]vIiȫ Iw׷UIl GK.']>X=ss_T虋m]|Hf6Il9~w%~g_뛼oMƝ:qMWLMJ-= ࿴IT58&mޟku?]' o76JM:9昑1;&L#*SdOT2?L~]GtN:JUh`k&%\εW&~sW{HV1<(&Vy7*Ifl%lΖYJ{^;%uNlwI$w&6n|]HtK+y$[Hs>q:<RWͼ̯t{ 9hC{dV?}ݐ|Yd$)_$7Jd=-*?Br>wɷ~{M&rv; LRDe* 7Fs]Lw]9ՍN2LڪG>\%Y_ԇN_IAok9s|6JɅ].sɑ7$ӟ}+9?ҡP{},B4>8%$ Vn~-̑&c2Z(KM0T*~]ZuꧯIrӧuR1r'uYߝԜI^|dD%.~:J1THFdlj`Pt1ŗ\U%'.yŗc~aӾQ*SHe&ONtjF:JUCff}79s/8gRxAq43 eAI>MLVAl+9e|y)g$goW51K5p!eɠm7s)뢙c%_G7yngɔT$2s 9߽{Iav~mKF? mRH!߿?'yd}mA*OLvɡ~'np0: ݒNҿ BLץML'N{Ch3X+U^ph~]Ɲﬔ}ѝy䕏_:`+Ig{R]py ?߻W%kniNHrՖI?V`\Ly^~ÓW$wYH6\wk}'7|դx|bIױo;V%Gr}kvxǯ1~)O]n_rmgpIY M/u\s-O'd/&vr5IR6d>[$K޽ſ?5zAkdiSap2[G۾Ȥc:ֹO?W|7M~q}+\lkI.,I薽:,3}vq%mSU:˯gr}WqӤu~dl1wN5&IK;JzNaζo>ۿ~vMz]`X>aCoJN9~3n|>#잼WYdžUX}K:oq-s[sG|RfdDc:7W]gprq=<_jhI K}۶ uǗ~>({|K.3u2v$96smg|nET:om{|v~9^ ,S*@6Y1wc,R,vWWͤq7~zꫮxr!noTyOVE*Sxe5^tyKU~j|- t]ck^5KIfOI?{a>'IN:des\NSG1ⶑ$ǟv".[~oRƞ ;4yt>Mqo6W?=v^?YBwFQŇoUxw}퀷~S;gdC>:anW~1]\$Ϭ/,ONt+&}bbaI{]ּɣG}j*^-Zv];kny9Fyu.߃ GݓGl cr[ؤغ b߬}jHŞyp~i_XիGW$ '𼶠#3߁&]Y# 'g{ڛ잌61;$>rc}}Knϭɳ?K%O߿]vIfU7y+7{ptHxt΄v]#=Kw]0YYF^0Qs&1d7%_3UH!mLU3umkze~I9Rn\߱jrku(5|ꓯ{rڏy+$k?>w@-J^-|NLԤ:K۴F 0;n׿GrłuMQ:&nSǑoY7ߓ!3t)勤oS}PQfL=m*?9)Wi/5)/U2W\5UW]zoT%r篿ukz2s:Je[O׭6ߩ0h_"ɿqӍ 9)7qKlh?ݮqI^}KNZW\&Kf?5ѿ9䜜7-ۨ<:#[<5-҂}{j9I4W:Ւa;T>{o\qI./k,oS_s[>\dT%읜֩|2v1ی0IM}|scQ}5_um[U4~יw`Ƀ?pأLFuS!%SJy>'&?5jpvTm{zgNybgK;:k_yF\ )z23^3umC~W< _?]2l&7tw$y9Oz2u~6YF-d\Kkt\H;a!fԴoHYڍ@ % ʏV"Yze:,q5W +-w59s_:%ҮQ[*S$]Dq GtNFuNڍ6T4"~vw{~ܣ$|.n7OD_|fdN ԗ ={ḺISyn$Ʌ O7OyoIjrbvms׺|#M7Mk'$qVc> \>NOB)_+?mfs'Y.d.$Wv&W5ό^/yyW9[腗d<%y5yzӛw}𼵢[MG?>rqk%pڗqco~y~sw=uuk.G'ܸΕ|4s:5MأmSz=]Ka{𛥟?~#5wr׭v۾Iyb^3I> U/VgJ$I^0tO<ά]銧ߒ% 1f6ڔg~V5 RJ`F&mSl`P?sϹl䚡IUT~1iFwnu9'zgN^_䵕jFQi7EĈT6ѥPtNiDN{i _pӱ=we:ml_>o_z_KGx7+vMfuz`&RFM R՘O~,,龾}zdլ?no}oJ_.U,_Yſ5[sgO\qOyϾ ?p7OE_~vcIQm۵yzҸŖUq!W/K_7|=qGR/֥g:JTp_>C_?9__l4o;]v\tW4n5;eu);QvF|G۩ϯx\yy.{u|n_^lFZI~giwwѢtO'ح4{=K&gD+k;Oc3uw_6`{l3nJV\%s|1<ɣ?=ko>}෫PHRf ԤU~C2Qo7ɵUW_{9({4ǜtЕɞ'#44iר 7פ:]>~:NۈN.(6"~l&o[#wƨo>b_lMӫE~pz&T.RM2QTS:fL#⺤Smy{2g:oH3̣"+pf]dMz?Bƫ :w S9֌~]hvtT~6t:;.7L_I.{[T%->a =z1vWo7xCꮧ0#*~C2 m6mz7uL<仆d׾lճE_;i,ZqOSK䲚K<tڛS'݌)ҵvm'B M߸{q\#: }s%&[6<Ɵ9wnJrls-lR5MpIDc36cK,2_{s%,?< 65}?MߏO'=6Jͺl/μw'3wQ&[Sw Ž{{}~SM?wzåj7L4x' t[`$n^SzLx:Bt.R^j2w?_2p_K~v7dO/<.)+k?Ԥ4-rgCK?'Ivuyv*tc/:3iܖn&W%K"7{og;?w$xM7)Mۍ F/wsw:KOr5\eRwF5WG)zO=_$YvD͏NH9S= ii\]9l{%Wz}'=}~ cpm3ͯ_yF\ )ƫjcѯkKG ?9{號%mjۻa\s%9rs@#N?O><õ?uV7wHV7{HAoגd{_uasG3YxL'SwbfeS!)%%S8 QS+} m&㒌{|\ؑN-|{_vOIڿ߮+Ν/{Q+_r'v>|֤9]|,|Ig,pk]I:'5m4Y5YȤ=_I8ys|Įwl3Sr}my)F":N+2WluWlw)ǰT4r`#&nny͞ Sz=kg=fYI6{o/oOU(IZZ+6x3WRaF<̞+U7I<?e͛G-2$]ztiUIu(:QtN)yNiH?;1]/>1X{u.[7I9%G1^1K)^.}FqEq=qs*xU&|4gd{wA'mJ6?ja̺欏μuW8pzad,vB%XJk}ۓGzݧ:&ׯx>_nURLMJ`&mҶcv[N<{KnK.9YmNӇ\u9Ϯlؗ $)7,7ukӹP]NX` 7j;;7M~]%ɇ:C:'yѶC2}euqɈG.|v;n܆?:F-=M_?dFs=o۰3$=^|fJRۚ3t]?WF?9z$ˤD/d\q]ҹp|&m(:=}?_B/=4i^3U'֣j>՚kרuF}ܙ;$w#;!YEY`dV9i9.w?^F$TlR|mǯXגM4'YsϵCramx|o^M?SoH6@Qh~ا&w}G]lb:/=49c4{rȯF($i:6 >kCτ O:Nstu]aÞKRŦj~ӱ^cS+}6m 77NS~f7pן\^Wy&%~U{̗'6i^3Iixpemt׵)⦅\k&X\wIZe~u^굫=/C?nk;}\'Ǭ}?ys7w}^R I)oHY۴i2;;]wmw&^W|?G̺äq mLol'n:{rw{ 2ܟ74MNۈdtU04]~:lNI~?&Јf6}GYU s: s' {{%Jb,155*Dc7Ƃ-6$"3$yf2rg-3{9{~yj3}cn;|uW1r;aVOrdnם2Дy[ؾ|r~#7N<lAbsI棣Yד̺>S̄B7w.fmwP|u'}]2S[~#y>+znKn>s*-*I1f_[H~8ԟ$.7'$27On{l'a'ku5ޜ}?/ǎ'rĞ~Ɍsg>E=Kn+ )ϧloB2<'l}, GUɋsUFt3i?~v[?]w\V]x?>CѮ}*g $%7T5*]ڦ}8Z4jPW<ǷU#dYˑ-++NI:%\>Gd羿|'&s]OܗĚ=V-{p o-t%rޚVI$hxNj;;_\Uuak*IM?_v5)fj+Oy}i4~sY7?踍&-Con<}O^ L>e6wm=7%_7 f_w 7s7O-$.LR-w&k=kl_|4;biQE>g{^Nxcl`}dmЮ-_k%9<1- q2nz J*?\ldـ[r⁩Le# dۥC2aZ5PXVTјdy՘qZU٬[_Y??譍GϤˏ{zk{%9&xծڃ>elxҷطz`2pwIf77'eSy[-z%/_{pӍe׊+^I6MVK۽}ۏx2؍κ^6}ڧ\ֈl_}!I~'>~y&;~KxNt?"#1f6Pܪxi-޲&%+]+3WM:nۮ{z.~5$i;&~Wy/oqP-&lh4PYԟk|sqMZd{goku\FeZG4zZTtؾJy-,'bBr,2$}*S5Ӳ u LGMcEm<Ӻ1Iku[k.J zۿÙ?>z z'eggȰۇ|\\,'-s߽mHn;SdXaWuo֫޹ILz`) Fos7ubɫorXc Q֙C_b&K{%_l&;{BgMq&;h_$9T%<[72{UU.mk~Uh0"jLbW#MNLy1^imoֻ&ٰj5vɐ>Icܻ%'椓TGW8p~aΘ|ɱyd6,U;,YuOrٽ܏~k{%upsk&evmy׾:𑍎S2cE׊^Ica=_oNU:߷INajozj|Ar?8oc=; վ{{YrlFU'';煉I{?..1^Ͳ_qgmɲ.;sd}M'JK<DuC:~w޾=A&z{ɺXoNݺZg$'zoQr~edƋ31v-*UHrZu׹νG$csʸ:-nT<\2/UUzy Ɏgb/5P`۸/>㑖X˵we=7%_3fmY[9洱/'^z:*?fshWyUnH.\uѕ_ްD3/wdڥSO6,)T҃m?jd۶I%>yhe*+iOMlp~emS&nq|r>CF&2zW%lr۫{$y'ox~H2/[궥v_=/N,Sc<3n~`zLUҳǙ $_#iuH [!rʍ:Ir]>(-7LC9ŝIMzhs?l1vNv[u;T,ѲM~qK:IkelQI=oɟo8c~5Gmy+Y/uɿV//xbNwmݦ}u-?{aRDSHV]r]G٫w$O ͂ul&Ѿ z أtEr=7<؞=reޫ'ל潿澞ywpQ/K9l_%V]77Vߊ /\wm߯Mr ʚv#q9v_wsO7[ޮ!.jFrKqrsw{䁋{}ڹ=[t]z6?Mrа ߻Cu]sk]w@db7$5IsY!}c'mdpos1$WNb{$›[d ~F'kUl[xqvtS33'&ϔ!qUFLZzV$?ܡc)${~m'+*SrBmYUT'i16ӢM[u}w1wk,P~xvﶗkIFe]Ʉ[;ؤoߑnW%sUȡ_]\['Oxd9nœM 5Kn]qɕc9~C<~x'&KdO@m^7uGm\un49twzXvuI{6Vil/{U*vJҢYU= )U ?m2=\ŕ.:㒶{%pAU?O:\!U?i;qm&a?a|vӏydW$х66iU69&7y*ku4.mSu<)e~|SyuK'o;&]y=J\'l[y%/[e7]|µ+%w'k5'9[;눽߽W*;} K,x?3M6{OznϏ\-&#h?*I8oL{~81#޻7&?uOwhTYѽg}5ܓqѭ=9{><9Ψn7LeFk_־岎M0"--\ $zڻtl;fo]\dS7aI{}=7%_3mi;NOrguitȤZyf󭿯}x!$/X}ԽNkoeNy_ՓV$;y1꾠9G۬b雰Y(Oy s\6+)E9ȫFl:*5^;*( olt$yy_l{v=^* Nzhc+|ۤKU$-N)YO2f˜c+/+s5c˻~''k{'94MVzJI60Tn\_zSFeKORDh`B*8Jdǭ3nebVM>W>9W?]X|5ͯ?OYgvIQǫj]_oI&1j5ɀcapIOoIO7w<*#}pre>{I6w=$YZv~T^־_)O2d6#ukďdߤm^/yf&J<|ڈ\M:I1Ix?sS=7ϕ$wM'cgֿ=٨Ow|̥ˮ&vdO a#%V_$/ k]سod'"6=9k!Ӿ q-"^En2מra=ɵoVVK>}Fyٮ:}\|!šo'w:cnwJ6;l^/:&Ia-[UH>8K>==!yi0I ]_zIsRS֬ M )F0BjRNz@"JJ!5)JP(I`( %@(D0`P IӢWKPH1HPHM4RL`( 0$)(JC!TKPHM$@(`( ԤZ0BYM 4RLӢ$ԤZ0BjR`( S T+PiQHj4Rj P )JRQHM4R, 6X0BjR,봨$Ij P I1$RLQ0BjRJC!5)Ӣ$S TK JH!5)JPHM%@i(b`( 0$) P )(JC!5 PVLQ0BjRLE= - `(Ԥ(JC!TK JH!5))PH1鴨$u %b4Rj P IQ0BetZPVj P `(S TK JH!5))PHMiQHB)F0BjR b%@iMV %Ӣ$S-J`(!ˊ4jR-JC!5 %b@IM$ HPHM%@i(F0B TRtZ` P" )(JC!5 A0BjR PVSV]JB!iQHBjR`( S-J)5%IENDB`jeremyevans-roda-4f30bb3/www/public/images/roda-logo.svg000066400000000000000000000033131516720775400233630ustar00rootroot00000000000000Asset 2jeremyevans-roda-4f30bb3/www/public/images/sinatra.png000066400000000000000000000615121516720775400231330ustar00rootroot00000000000000PNG  IHDRvatEXtSoftwareAdobe ImageReadyqe<#iTXtXML:com.adobe.xmp І5C_IDATx uWU&>N恄03"ՠ@` 6JOգZeV4OVWP*v)-@1Cɟ?~p^k !p9kֻ.sfo3hf[wbW.C`aY21_2Dt# n֧vOۺ>4:6۪@U8 F0MPvU`?cO?k5{5T_AZUmsg~?{D-rΦ`ܹZmg4[t|p؏A|_~#Z?Ky \'` \pq4f_toOXݖ7ȻvYˍ7{ſN=c@Ȑ͏./FL[/ ^0w?-ygGZ(7dW])4h "Y3:gS{ 3/ɔ7^u;\i+]-`q?&6B1 ?Jm>(qN>omiO6<Aw[_]Ѣ_kL~hq쬓:7',^oW\_sb/w_@;M[k'(L񷳏iЭO]ArHQ-Oֳ:6Gý"0/a;cODYx!Fĸ?àGQt&cY](3Gh\H1B#S+I W ^gtUt M||'PHI]oLQrzo_Dͦ⑑ v1L)p})\iX{Ѹ㻢,dKX<ui[ M?$ƾXRHȳ}k8A[oڭxKl]~ " 40 Xzley^DNhXz1΢b'1yGYkj>oNZk>bBQJ2l40 ?Z q_! A\-քXr.| $(Td΋%Lq a#%t9{.~~_2.}նmVG=ޯ >׶~/%;ԣ7)lG1zy ~#olce$&e24 FE!,yn>nU k9ŀ$&6!`C 4{>wJA >aB+'m$c΋Қ-wpQcMW" sT)tb6-SDcZ2\o~n/2t (y9.;kシ5&#cmwǭ L&͏qXSo6,!5OGU?&GvȁGIs3DŽЪ¡A Ȁ0{a8d 6`d.&Pp YX~Vl,sA;ru:gfzkW!V.'&Bڠ要LᤏǡD 4ހP4o|BL9Ha {Z Hr"&&.x <:z_Nδ0bclI`ϝ3Z޿|EsޠkN6`9.&OL(l6u هol<ބ4So>aaiI I!97*cFjNbIrɞYy!x{fT>T@1>͠d* B\J6hol^Fb'~_"lw ˫W9'Z.]<:lo5~/ɘjڇh+1I9AƢ44 >]}dJ Iպ pHs11߶`m"^* x %qq(2P>z0>MwS5|UT?ag_}}0ͳ8.;;R^0 O=t2ۛk/^ͽ!{JTp`\(cOym\D#^09Ui&̇&3^B2 !Ce,yRPd9.4{B]*Wɬ6 1Ey#߆zB1?tET1zޝt!h QfΕW_abe>x7b6f2'2և G0m <=rLˈ m6OɤD.!kLZ#zr Ee\3s|ԜaC%͆>Gsb/BטclB׺e|_0ox8yi3y6n-2R*|ܶK} M[1q)*x4#>ccZ& ;S&qɫ@_l#hDxp`ggm}8U7}/\FbK鵍 Bݟߟ|x4B jly&lȨM`91`tZ)"H_2Rr1.qIVh 8+۲3)N_EɘMJ4$zeԡ0Fy1*$#r9=sB; %޽jQmp0ȵO<|\Td-Vq8|LC*{Jb{ֹR~kT8x X۾w%#+q6X&oѠUѰ / iHS*I#Xg-~Hjhӫw`ooS)$sèө{ ;?lPмm(1yu W:h{|[+ iz^}FoM\0N6' 7hT!!;RMA`Ppj![#Lew*C\N "T' U:jr-[9FQg7SK!{iNG_y8,@BUydD[."J(Naf%!C(heN.8ůzoB_C)Tpo,G4Re3~9ԡHl/g\j\ ]/1>tF,.٨.ovX17_ suo=&!2܆ҼCp? *w`pK2=z6P%-&$왣%9|+$y**YZ-0\UҎi9ִ~)# WK(U8Q ^GV:RBa!B4@”ȥv(ttd1i f^NL%PIA(~!6T2OYS|v=3qx C?wa k/r\48+ bXPC % })2\hFaLY:FHqt`T2'2e NNL؟8Elhƀ=~!LU%9 `w k`͡hy!|E :ryO gNW>LMA[Q3?w:tX ZH[T#cP'3 n:+dMzNho\nDHlU*W?i$A1/a(MBl)U"7,39M"'UH]69<.;ΑM\뾽s7[X?}>~ S.V]`ƟSZA+{yeŽw6rlwY1.W \!xSw?y?붝=E;vٌ nmbDKD~6pA bL+.9zX9dTl1h W <;9u6,$eR 3_qKZJR[tNBE4bl|݃N;rrٓWiȡM^y)n&^e"k"'1 Ca1PEO^2Z?9ж$(B :fLr0%3(YU IX.67e ,ڽ `EmSߠ" ŒVBH&r4TV`Er8'@lf5%BVTe!ki0e Í3'퍳pٓ_RӉom~gKBLw+a\NwW2明(=e_MF,bS^@`r6v؂Ф~-J{֘BK4h|P<+9赗DF|~Y٣bx}Aן0j8x:jOG˫0ZYdX`O[k Q4Ҁ7\>{# *zcnZFW  "( T'Ɋ10 Q IQIHZԜ[Jx? ;)Ie B5Q,D9"5qϞ*J~ Nq0ZZ}`!}@p(UnjЍdb#̝!au:lX\FGS1SL/ ղhNlإ:W~vb1T [dϱ'M1Y)FaѐCѢNq1L2()f4Q}_1Ca@'o8ZIOxñ cAZ| 2\U4v)IVVٌoD ߧ ӱL+tAU2mWcI*IZ+|.RCkt,&y8JMGBR21v"H\l2R&3q1:4B=;nRv75-\zy;!7LfҎnp'ũ?o|Bp&&q)Yʝёo$VhXRXW-[s"^ Ǖ,n#4>ʢ}.k5&00Zp& &=*P%㊉b{2Ѻ?m0X S }ttq&-sCV*n@۠*[kӄ0L>B($97Hi^2mUkX~6 j1VYQ;8*ڼMc&p>r6T6}NAmtBg7rC dgLqGIJ*K(]fBYf >ݺoU?egФyArtoiEt7t*_5&g!іVT:3d] #zd jSkb*/6A4%ڠa/ w-<ڄXE':*JM%qlE 2}/ٙ27W0̓TS(yw{i?׳g^x'lop 5k/M\fOfC1RE <И͊LfKQK5vFl퉨|xD %ԩm߅໚GFK+_sk Nj1u4 K0v4֖aji, I ^B&%:h߀tk=;noi|ƪqE5#^kqK̹ܬKaaD- @ 3/z@[V3U T 8 .I]z-ߡNh`ЃѨ}xϞm[~ؿ@zDtS4/t~W9Um1+T0ml;Sb,euԼ0a4i&ި'8fY}"_  ^Y$k`i|MN%O+ VJI/Z &118{E~793ֈ0>%=KCrZ~/i_珵@뇅(Nx~X>p#niVTE+FyOgY $}#:ʎN#亗zA'qRl,d$ al*73U8Ԥx:H0QMٯ#fo8˘b 5IΎ0 י,:kmBCB='+hHӄSpمػ+TRGz t޼l6֙'*<3 'bcLÏvXzc&#feeaGStI+<& IJ2'mĤ0&6)Dlod .>ZtVj6 Jh1I+0Q "O3w?ÿj> ׿~!er5O&;o!RK(C·e9-9!U0!'Y} dqu3qccAdF@Lq`fC9sB&VS/DR31.0& U*,<,cgȼ+nkA4iI\{@M6#c8#C^?~=4yY|7o̦[ >Ξ=J&I3׻ 90fd]ftJlkHLs `ը3U{b~Ts@zWg*6+`V[`^i̙Vh+z<$Y:  J0G r+AWUnҠQȤ0K'{[vl.,:FY1k~CY]x 1[8˹[еA'ȑ;ᚫay+_MaqFM뀧Y&6ŗ $yml O^ s) hЯ{<- أ,4=b ., JgclwZdZF dlȊA*JZr2i-iUcĝwa{x׻ bHd<9#JIA:dƴYTlnѝ#*9ijjhkrEtݘ R=$& yi+?p 'n~;b 3|igLvw[>1\p(loB]$PLNFRqh *B.FxUI[-.aoŅ iJ&ki{ "l+U\(@aD';L+zuv˖=57K]ю|+4S,,o{-vʈiCwop<$L<^`1 1R 645$ :Ȳf}=79҇eOaCǜr* PuL;!!5h6ys]=>%э*Ty$A?7+ -/F)]b"ŕo.p=,&:N…khԂт!l"2‚ qi{Q1y݋,ET|8A֦k/?a ΌçE#PQ4&5}/rS#K1o/7ƠF; `d:T"_c?r\y{mկ.! b/&Eۖj{N[o|wWWh&heiVU,Ƶkgφi?I>_?!DQDFn*ޜ&vhPL؂ B%0qIM&!DLfj)brt8a4M'G'yF`PլЩlZ q&4[0T164aָLX)Š![9%Np$1q̚L sO 0!O/§ӮCmMO{0NjQ-ʱDFkɉ,m9RsQ.$-ҁ#t s.D\nLQP hM9vѕLҨF!9I>~(EDY@=W~~`eyW8 mqoQvDqbl]lE05T45?_q|4Z #g /!ɠ#"N =~el?{\{/{1|7}<+UU6TdȫRlثIIv[$KV/90%.}AS$io-V*5c/lGd(B)JBiĥ#a^oo/8t\|>\i%NdrJX yn 4|x=Kәio4~\eI1,8c&lģb"%+ם*6i;E_\_)O ;0'۰l?Ǫ2s4hY#O1r0 8c6LE>]иwŹ(NgQb1{WQ*1§5.ٲqH1P|(H<>sG~_}pUGCna3=ECfZXWG 59U]쬾^"?5 URik6l(`mt['H=`6Rp?sf N%rMͳ|z>V=~“z૟ ϼjU7]fa9b<ɩ{mp(JPr RZ'IjfǦЙ( lqrk.XXHVэFITh䯌vF-8t}Ow~ԙp5O'y$z߾}YHʢ:< 5EW.f''8#ʯz㍃{'htb_g5o1!&-$|ILYKoRI2014$% 0-եnW\ W?q'\_z9xdF! _1J$eU  `BՉFd=|јQCsyj48ƜI"txDŘ_z!d ?-g\Th0dXUYXv Bm b)Q*D8|BiuEjK0 Jp5Ҟdl"Tb܍韅޿Gs{|s>G8 {~DݖPiɚX @k&]\%Ft{B׳_A:myteJ)qZz 4ħQ +6k&L\Z]ZN-nd5!Q'xKmX؂?} zGKC8pp?\x a£ȏGÁG<(&王?-7tqK$16$N6Om(yqoj ;eheuF 9`I1+M/ Eӫc }½rbx{?w?|gypsv(O;ƅ"SBss[ QzcRE^αͳgX_('_-Y͌(pAezb7R<򬕈M{NEidDdHj y@Md7ܗXA]OmŰq9+K>[8=p!ؿV,ò/f=$Bͥi6*э"3d2$gK&hRB&N 4]kHmZ&rQWW-:%V`=z|sUJfI 7t Ox3Or>g:z?jb]P&B}R>U0څ7]aFsz%oawC8)Q#W/&`}+G>cIH 5ʿzc&}p ,Z_USx0jӯtFÉ]og7uho/*y}f{oȰx#߿¡/Uٟ#^@+iǠoA]=OpIщ-.Ln#£k؝†yHN.$@xXFphM!p ^ ldƍ_>oێ95SsOKxЮ#8-F grK0CZ!JmRHlwM}Uʫ|Ue ֊8 (Jk>WMnG8y6#=3S6őlJVlgKuIi a)@2jn#n-8svPSp\eVUqP\2LYLnh1UY/>IP<̯NO$Q KUԠ[yE3Wq"$z䍜x1xS kkp˭xAFKcnm\xkH  [(_yW41x;;BBr'I$~eL];~f4&:f6}ЛTQק;''A#0m?rd}<&ݷ(G 7=]uGDTL/y8pϜPF3*.&ұpj =f&CpP8EbRԄCWOcb@7GH![q~d7s[go$(CB a^G JBWi:J;PmxLg@FJc@boSl/ӫǤ %UUǵ9,6ȷƜ<#N{D jר|ZC5~k:8>URWZh5:%nD';f'2A}rNv8l\ x7&(fS ,]d%cLʘ V5(M ;^%-!v6`0\Nbn8`7<ׁU䭒emqNk?(J"ETRZ`^=RhlwV2F:SB@5ĨXp O yLŪ (AEf,oz.=>(Q97ÀS/'P#-kr3O Jn%␤{ GSɿ񸳦CN26Ikm D-IP}0TmROȥ"iZp52 bT6.C1p;f6Q"q*zc-)FWdW9ܲFCDUUc6Y*b' @̶Gɾ( I)PM MIۋFmR$fVV >GJVGW(Rf 1/tjC#rU:OvueM:yH&L2p (] @86H~T(;5C3v8欁}|t<.tYuMmg̚8$D2I\ptbt2P5E8*Faĩ7.% =`Be uYؤf6kڭ`E%~a$kΘYI,&8E1$!,0bYFDc2J6`ڊP"Ĝu;ov. [ʬ 6alDra&rG0IX]U}QׯAy =ڐ[X^^=p}m)| [u22V $#ƃ3I[̑y˅pZB~IѭCڔa\ Dv:Vn;W,_H+D$f|H'80̵(Ks(N1efËd:Hc.8NM8NdϬgVøl5wq/1muNzjՏon]ql>:+±u)j2/Cd <1m5;Vx{[ |V R!EUrRE]sc,ԟ$*aJDQpv2AN "v9;3=6[ BWd(I8g'F1k.3)8. O.8<0k[!qEo@,sVб̞^"qsRâN+}OڶE,}e7VZI[r*Q:EJ YNPP^4+Y+ 15kyd·vwwL>!0aw2昙B.0b4bzw|+^PG{ɋƱk"gb6j&3UKWF27f;r_WO;cի;D!JA,$K՘iϢ%ನ\J{4|~ G; wyuPز)uueMe4 ܮ 78bWN)@2䮦Dk7%[YLɽ?p&7 l#Y1I XEF #N+?~GEN萴C`4ChqJTPdjWIӭ \8 15vߛneQ՝q9.aGw銎 k7"sDن{^{_ 8s |H7$ j4ki)MХ>sgPg,ϐNN_ޜDXg4ТIҘ @rw?W]x.λr kp$m1KlraO߂u~w(``B?MgVSjL)@[p#5}W_ |o¯iOڟd~.5JAԜ;+\ttYv_ ܝ+94!QtŪj~a!ѳv13%'YV̹(1$=3Ousx yCP3{y Oڕp맯ϒ6M5h&'"oYLML ag0sٝݠ ~/ɜXuۧ3i!f)}& +2rY(8IǣR5j!ݾ}p+`ͯ?߂o;VK5Jra'bTbwZU_㷊B4rr@g U 4n-)AFx5'jɴPsjI;KaJ\ IĹ>s,(K.~7k_Ok^|9{l4|S԰0[+nDlȹ611B4* 5l$ 'M+]=;sV^l'?yǭ7^?L+B&ԱS*XCJE ,8xp)?`zl?ycԧr=wSՐK4H0wGgA{t"jL 4ʻ&n,Je r< +JϙO1g &Ds$q:HBy;1mgP'47#\y χ.g׿EpS4;~0#6yb iӆp;^D ]¤;OJ xk hxUhZ29Y.̋`*ϲFZԦH#`CQ.6MXċˆ g4ʣ}`g{}c/0;v++|!~߬,Р䌘yQ!U:a3ֱQ$^fgY$bbKY ~KdonnK~V#x3go~7r7̰Ҵ|N͎r9 2 aP5&bY1fcly8p5ɓO^k[o 6w9!h4-qw1[Q/% UCfǠA(qgUiy. SD#BI2 d,)7UPx|^p>9t'v5O{<9τg=pyYd=s^Á2 5$`i̴]w-|o,:}Ll٣,x3)׏qIHjc swJH4h1.i4wCCmD ޘV>D O :JT~=ycxO;Q[holu"*2~.HHb2#+8Q.DVFi@b E1y^E;!7=/ 9]>Q{)5 x uͥؐ'&.@)"imf"9r{ߧlR v`gsO Uo"$aFJ6RV]Tb8aB.w((=,ʮ3nzF\G.YYF ouWIa'fHe J V /ݕs8 OE%'NM8npܣnA5?gggOoN8q Ľpi 76y[Fj(4*l%qz*aa SS 7A.$V#T]JDWjy%Ȁ;(\rpJGW G8y{>lڷh P>;8"ilRaFV6*1%B)܅$10(;u9cbȕ\hcy1>ϻτv>Ҏ|/W4c>?~7ij>1֤\qXMD>LWblqSMEY4AA{;'MɄۺND-C3`t܆~@v'm1."y Ǯvch9lyCP:Hk I_)tag%)OFx/8m"3 Ye=hKһJ 2 i`(@a`j^qF˨r(" nhS38`_=Nr_]/|ƿdye.gxɨ1J&Al)X6,ۺpC,ƅ0xDҰ0"MW-,Qh`=e{9&HA@=AOá},W %Ɯ0U=e .S6m5}gmYBr\5@dƣc D9\U59K}qIz BsCww̩"ORGfl ?z_POmHQQ5q7 qenYr[xMG_LʛYЪpA((kL(u0<9Md﵊勞<M7łh4{2j @Y(]Qc!ĀdN1'lP $zy'=mm.fl 4 v$mIEۄ`9_53۪mw4Lm%hoV/6|_'G! Pb'N"(`\_s\ܙV{Qg" QDc]\`}{}N<6FTͣXtayZc%1MV 0?&TS PN5RYb54i䛙Jy~O5L4 KJ-?GSպ| se:͋kVa.$Uy_uU֔|l,=!Մ*aˍno!nPn'T T()(칓g^XSQ;j4A6^eJ]aJ8@7J&(LZs<Ѳ>ZL2P1䉥&ԸtLJ(i]5 ,tnؕ?Vɢ mboXSPYY D&r%mԱϢ>$#9ݳ"wD[?/Qw*Y(!Ñh#1tCգ6lҙcX J+2G!,xXkM"ǁ9mEؐUae\:qxA>>s \l#TLnSJ"7h)b1uyZ%3ALs`%{6p75&줿~rz_x?uG-=cɵlbt RۡҾ8 & D9<-ɻuy^lQՌ`! bΩngT1:w{'*yϮra1 8J̈́8"Ua&xsTt6R ;>soA ?Җf:fޘyk~{Y+8>gǕ0=(@"^;s%%PH[q`^Mbk\A1aI@q!KyEtȝQv2{>$L<W0U<ۜˡKމJ^"&js9*ݱr(ssNLXi lagj`c?=uo߯yy/z?S??1JC B$-sa %c~UҨL FX^ ~Jb+gj(c\tSbYn sX  sc 4Zlb7Mk"ܐ66\ ݦ3[۷wnmڲ!]Sq -9MӾt:,U&XDre2tT140S]|+aiU2v}:Nf,K 2/f4B|W/KoJM0+iҵǺʘ.p!f@ *XVV(2c$ Kq,N{iБU΅d#,F]{.}ֵQ%U)HQ 9_x67fmk<~eU%GkZx߬qϝHDx$x/VkӧԾHȥ"P^ ).Le;sQSEw\f] (0+ZK$JrX)$?CW'ש 1UpԢc67 `I#/,5i,mgnOm;io'qBuQf-[s휏yD:leOm4xvbeT3 F*F/ix=m'{i0x:'F7 'Δ٪L~_MQR/gl3;A=`2=9">mߺ@Y"]U˸9^9NE^!H U*.XFwgww:ܹ;vw|dms/9]>L~/!7 |l֕H57 ľ[CQ'`P ^,^n4H1yv=p}_a3BIK۹!o}5BNmHb9fA ݨ1Tz} <$ $1ߺ=f͍_ތ0r/udl;M/s"fmBhZc{|OB!!Hqq g=vlB"NiXj~ĺ(fv:ܵ;}b2ȶ6xx=#~ofg;I۾qn\UPP/\wɳfGs5waJDP d/ nqj`D1E ªZg1r5lI3͏{{h4#᷽c}piܿܘҫ?ԽWu0̦֩ɧ{S!hl SHܨ},bQC "Ũ P4 (1qDbtYB]yijsVfӛ몾ak<̭f7,&Oz 2dO[MK}uj:weCqB dO2(s98xQ./O[:gU3ũo5-uwBkfkL[=uӳQo٦mΉ[w=܉rHvA_o |-@u4jfp+%C졃<kcR+/:z^hC{&ޠ9!B32Pm[Ϯ:ocUeo[{qyGάo~ykey k(XۺҥaU{ߢq<+#u`:}4Zbj1)%4؀o "*#(:$EҳmPџ:w1wd6ޓnű;[.f\ѫʠSN.uoJaxsMYhS}xz㪉3LiYbCGF?8ZY+iT2ܑD>=L?oetǷ'ַw>qGr>K٢?'lҹ^Kc?s#>F6~'H[hϟ0N24W,{cy Q?&ɏT(ex({Am]̱1kvެmk++˛k[ǽp{ei=ڂ+/?Jt[ zz @~Qeݟ؟:Aݳ@T~7y^[>o,+Cs< {{Ɖcaw]ųu#=t`:Ot2M?-ؿKK#؝`4RDtp)`__&{ 4\IENDB`jeremyevans-roda-4f30bb3/www/public/js/000077500000000000000000000000001516720775400201265ustar00rootroot00000000000000jeremyevans-roda-4f30bb3/www/public/js/roda.js000066400000000000000000000247131516720775400214200ustar00rootroot00000000000000/* PrismJS 1.15.0 https://prismjs.com/download.html#themes=prism-okaidia&languages=clike+ruby */ var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(){var e=/\blang(?:uage)?-([\w-]+)\b/i,t=0,n=_self.Prism={manual:_self.Prism&&_self.Prism.manual,disableWorkerMessageHandler:_self.Prism&&_self.Prism.disableWorkerMessageHandler,util:{encode:function(e){return e instanceof r?new r(e.type,n.util.encode(e.content),e.alias):"Array"===n.util.type(e)?e.map(n.util.encode):e.replace(/&/g,"&").replace(/e.length)return;if(!(w instanceof s)){if(m&&b!=t.length-1){h.lastIndex=k;var _=h.exec(e);if(!_)break;for(var j=_.index+(d?_[1].length:0),P=_.index+_[0].length,A=b,x=k,O=t.length;O>A&&(P>x||!t[A].type&&!t[A-1].greedy);++A)x+=t[A].length,j>=x&&(++b,k=x);if(t[b]instanceof s)continue;I=A-b,w=e.slice(k,x),_.index-=k}else{h.lastIndex=0;var _=h.exec(w),I=1}if(_){d&&(p=_[1]?_[1].length:0);var j=_.index+p,_=_[0].slice(p),P=j+_.length,N=w.slice(0,j),S=w.slice(P),C=[b,I];N&&(++b,k+=N.length,C.push(N));var E=new s(u,f?n.tokenize(_,f):_,y,_,m);if(C.push(E),S&&C.push(S),Array.prototype.splice.apply(t,C),1!=I&&n.matchGrammar(e,t,r,b,k,!0,u),i)break}else if(i)break}}}}},tokenize:function(e,t){var r=[e],a=t.rest;if(a){for(var l in a)t[l]=a[l];delete t.rest}return n.matchGrammar(e,r,t,0,0,!1),r},hooks:{all:{},add:function(e,t){var r=n.hooks.all;r[e]=r[e]||[],r[e].push(t)},run:function(e,t){var r=n.hooks.all[e];if(r&&r.length)for(var a,l=0;a=r[l++];)a(t)}}},r=n.Token=function(e,t,n,r,a){this.type=e,this.content=t,this.alias=n,this.length=0|(r||"").length,this.greedy=!!a};if(r.stringify=function(e,t,a){if("string"==typeof e)return e;if("Array"===n.util.type(e))return e.map(function(n){return r.stringify(n,t,e)}).join("");var l={type:e.type,content:r.stringify(e.content,t,a),tag:"span",classes:["token",e.type],attributes:{},language:t,parent:a};if(e.alias){var i="Array"===n.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(l.classes,i)}n.hooks.run("wrap",l);var o=Object.keys(l.attributes).map(function(e){return e+'="'+(l.attributes[e]||"").replace(/"/g,""")+'"'}).join(" ");return"<"+l.tag+' class="'+l.classes.join(" ")+'"'+(o?" "+o:"")+">"+l.content+""},!_self.document)return _self.addEventListener?(n.disableWorkerMessageHandler||_self.addEventListener("message",function(e){var t=JSON.parse(e.data),r=t.language,a=t.code,l=t.immediateClose;_self.postMessage(n.highlight(a,n.languages[r],r)),l&&_self.close()},!1),_self.Prism):_self.Prism;var a=document.currentScript||[].slice.call(document.getElementsByTagName("script")).pop();return a&&(n.filename=a.src,n.manual||a.hasAttribute("data-manual")||("loading"!==document.readyState?window.requestAnimationFrame?window.requestAnimationFrame(n.highlightAll):window.setTimeout(n.highlightAll,16):document.addEventListener("DOMContentLoaded",n.highlightAll))),_self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism),"undefined"!=typeof global&&(global.Prism=Prism); Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/((?:\b(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(?:true|false)\b/,"function":/[a-z0-9_]+(?=\()/i,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/--?|\+\+?|!=?=?|<=?|>=?|==?=?|&&?|\|\|?|\?|\*|\/|~|\^|%/,punctuation:/[{}[\];(),.:]/}; !function(e){e.languages.ruby=e.languages.extend("clike",{comment:[/#.*/,{pattern:/^=begin(?:\r?\n|\r)(?:.*(?:\r?\n|\r))*?=end/m,greedy:!0}],keyword:/\b(?:alias|and|BEGIN|begin|break|case|class|def|define_method|defined|do|each|else|elsif|END|end|ensure|false|for|if|in|module|new|next|nil|not|or|protected|private|public|raise|redo|require|rescue|retry|return|self|super|then|throw|true|undef|unless|until|when|while|yield)\b/});var n={pattern:/#\{[^}]+\}/,inside:{delimiter:{pattern:/^#\{|\}$/,alias:"tag"},rest:e.languages.ruby}};e.languages.insertBefore("ruby","keyword",{regex:[{pattern:/%r([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\((?:[^()\\]|\\[\s\S])*\)[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r\[(?:[^\[\]\\]|\\[\s\S])*\][gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/%r<(?:[^<>\\]|\\[\s\S])*>[gim]{0,3}/,greedy:!0,inside:{interpolation:n}},{pattern:/(^|[^\/])\/(?!\/)(\[.+?]|\\.|[^\/\\\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0,greedy:!0}],variable:/[@$]+[a-zA-Z_]\w*(?:[?!]|\b)/,symbol:{pattern:/(^|[^:]):[a-zA-Z_]\w*(?:[?!]|\b)/,lookbehind:!0}}),e.languages.insertBefore("ruby","number",{builtin:/\b(?:Array|Bignum|Binding|Class|Continuation|Dir|Exception|FalseClass|File|Stat|Fixnum|Float|Hash|Integer|IO|MatchData|Method|Module|NilClass|Numeric|Object|Proc|Range|Regexp|String|Struct|TMS|Symbol|ThreadGroup|Thread|Time|TrueClass)\b/,constant:/\b[A-Z]\w*(?:[?!]|\b)/}),e.languages.ruby.string=[{pattern:/%[qQiIwWxs]?([^a-zA-Z0-9\s{(\[<])(?:(?!\1)[^\\]|\\[\s\S])*\1/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\((?:[^()\\]|\\[\s\S])*\)/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\{(?:[^#{}\\]|#(?:\{[^}]+\})?|\\[\s\S])*\}/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?\[(?:[^\[\]\\]|\\[\s\S])*\]/,greedy:!0,inside:{interpolation:n}},{pattern:/%[qQiIwWxs]?<(?:[^<>\\]|\\[\s\S])*>/,greedy:!0,inside:{interpolation:n}},{pattern:/("|')(?:#\{[^}]+\}|\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0,inside:{interpolation:n}}]}(Prism); // Helpers window.map = function (array, callback, scope) { for (var i = 0; i < array.length; i++) { callback.call(scope, i, array[i]); }}; $ = document.querySelector.bind(document) $$ = document.querySelectorAll.bind(document) // Responsive Graphs var MOBILE = 799; window.Graphs = { init: function() { this.size(); this.resize() }, size: function() { window.innerWidth > MOBILE ? this.desktop() : this.mobile() }, desktop: function() { var fills = $$('.score .fill'); map(fills, function (i, fill) { var height = fill.dataset.percent; var style = "width: 100%; height:" + height; fill.setAttribute("style", style); }); }, mobile: function() { var fills = $$('.score .fill'); map(fills, function (i, fill) { var height = fill.dataset.percent; var style = "height: 100%; width:" + height; fill.setAttribute("style", style); }); }, resize: function() { var self = this; window.addEventListener('resize', function(){ self.size() }, true); } } document.addEventListener("DOMContentLoaded", function() { Graphs.init(); });