pax_global_header00006660000000000000000000000064151736465310014525gustar00rootroot0000000000000052 comment=1ddf53daaee794e31dfbee636c295239ec169428 mustermann-4.0.0/000077500000000000000000000000001517364653100137175ustar00rootroot00000000000000mustermann-4.0.0/.github/000077500000000000000000000000001517364653100152575ustar00rootroot00000000000000mustermann-4.0.0/.github/workflows/000077500000000000000000000000001517364653100173145ustar00rootroot00000000000000mustermann-4.0.0/.github/workflows/release.yml000066400000000000000000000007721517364653100214650ustar00rootroot00000000000000name: Release on: push: tags: - v* workflow_dispatch: jobs: release: if: github.repository == 'sinatra/mustermann' runs-on: ubuntu-latest permissions: id-token: write # for trusted publishing steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: bundler-cache: true ruby-version: ruby - uses: rubygems/configure-rubygems-credentials@v1.0.0 # build and push gems - run: bundle exec rake release mustermann-4.0.0/.github/workflows/test.yml000066400000000000000000000024211517364653100210150ustar00rootroot00000000000000name: Testing on: push: pull_request: # GitHub Actions notes # - outcome in step name so we can see it without having to expand logs # - every step must define a `uses` or `run` key jobs: test: name: ruby ${{ matrix.ruby }} runs-on: ubuntu-latest timeout-minutes: 15 strategy: fail-fast: false matrix: ruby: - '3.3' - '3.4' - '4.0' - head - jruby - jruby-head - truffleruby - truffleruby-head steps: - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 continue-on-error: ${{ matrix.allow-failure || false }} id: setup with: ruby-version: ${{ matrix.ruby }} - name: "setup-ruby: ${{ steps.setup.outcome }}" run: echo "" - name: Install gems continue-on-error: ${{ matrix.allow-failure || false }} id: bundle run: bundle install - name: "bundle install: ${{ steps.bundle.outcome }}" run: echo "" - name: Run tests continue-on-error: ${{ matrix.allow-failure || false }} id: test run: bundle exec rake env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - name: "bundle exec rake outcome: ${{ steps.test.outcome }}" run: echo "" mustermann-4.0.0/.gitignore000066400000000000000000000002741517364653100157120ustar00rootroot00000000000000*.gem *.rbc .bundle .config .yardoc .test_queue_stats .tool-versions .coverage Gemfile.lock InstalledFiles _yardoc doc/ lib/bundler/man pkg rdoc spec/reports test/tmp test/version_tmp tmp mustermann-4.0.0/.rspec000066400000000000000000000001071517364653100150320ustar00rootroot00000000000000--pattern '*/spec/**_spec.rb' --require bundler/setup --default-path . mustermann-4.0.0/.yardopts000066400000000000000000000000711517364653100155630ustar00rootroot00000000000000--charset utf-8 mustermann*/{lib,app}/**/*.rb ext/**/*.c mustermann-4.0.0/CHANGELOG.md000066400000000000000000000354561517364653100155450ustar00rootroot00000000000000# Changelog Mustermann follows [Semantic Versioning 2.0](http://semver.org/). Anything documented in the README or via YARD and not declared private is part of the public API. ## Stable Releases ### Mustermann 4.0.0 #### Breaking changes * `Mustermann::Pattern#match` will now return `Mustermann::Match` instead of either `MatchData` or `Mustermann::SimpleMatch`. This object behaves similar to the previous return values, but also implements `#params` and `#pattern`. * Moved `Mustermann::Mapper` and `Mustermann::PatternCache` from `mustermann` to `mustermann-contrib`. * Removed special code for Sinatra 1.x. If you want to use Mustermann with Sinatra, please upgrade to any of the Sinatra versions released since 2017. #### New features * `Mustermann::Rails` now supports Rails up to version 8.2 (previously 5.0). * Added `Mustermann::Hybrid`, a pattern that's a union of Sinatra, Rails and URI Template syntax. It is designed to be as compatible as possible with all three syntaxes. * Added `Mustermann::Set` to `mustermann`, which is a collection of patterns with associated values, designed for building routing tables that dispatch efficiently as the number of routes grows. * Reintroduce `Mustermann::Router`, now based on `Mustermann::Set`, for demonstration purposes and use in small applications or middleware. Simple and fast. * The `capture` option now supports special class and symbol values, that both set an expected capture pattern and define a params converter. * `Mustermann::Pattern#+` and `Mustermann::Pattern#|` now return single patterns instead of composite patterns in significantly more cases, like having non-overlapping captures. * Nicer `inspect` and `pretty_print` for patterns and other objects. Here's an example using `Mustermann::Hybrid`, `Mustermann::Set`, and the new `capture` options: ```ruby require "mustermann/set" set = Mustermann::Set.new(type: :hybrid, capture: { id: Integer, user_id: Integer, slug: :slug }) # adding values is optional set.add "/users", "users.index" set.add "/users/:id", "users.show" set.add "/posts", "posts.index" set.add "/users/:user_id/posts", "posts.index" set.add "/posts/:id(-:slug)", "posts.show" # slug is optional match = set.match("/posts/42-awesome-post") # id is automatically converted to an Integer, and slug is available as a string match.params # => { id: 42, slug: "awesome-post" } # You can access the pattern and value that matched match.value # => "posts.show" match.pattern # => # # Generate a path from a set value and params set.expand("posts.index") # => "/posts" set.expand("posts.index", user_id: 42) # => "/users/42/posts" ``` #### Performance improvements * Small to moderate improvements for compiling complex patterns. Major improvements for simple patterns, which are common in web applications. * Automatically switch between different matching algorithms for `Mustermann::Set` based on number of patterns. This makes it blazing fast both for small and large sets of patterns. * Major performance improvements for `Mustermann::Mapper`, as it is based on `Mustermann::Set` now, and can dispatch in logarithmic time instead of linear time. * Major speed improvements for sub-segment patterns with optional elements (like a format at the end of a path). These patterns are common in web applications.
Scenario Mustermann 3.1 Mustermann 4.0 Improvement
Simple patterns Compilation 12.1 K/s 70.8 K/s 6x
Matching non-repeating strings 4.5 M/s 4.5 M/s none
Matching repeating strings 5.9 M/s 17.1 M/s 3x
Extracting params 0.7 M/s 2.2 M/s 3x
Matching against 1k different patterns 3 M/s 7.1 K/s 425x
Complex patterns Extracting params 486 K/s 955 K/s 2x
Matching against malicious input 3.6 K/s 582 K/s 162x
Higher numbers are better. Performance measured on local development machine (MacBook Pro 2024, Ruby 4.0.2) with the `bench/versions.rb` script, which measures single-threaded performance. #### Housekeeping * Drop support for Ruby before 3.3.0 (all EOL now). * Improve documentation. Add more examples and explanations. Split individual pattern types into separate pages. * Document how to implement custom pattern types. * Add code of conduct and contributing guidelines. Add AI policy. ### Prior to 4.0 * **Mustermann 3.1.1** (2026-04-16) * Improve `Mustermann::Pattern#hash` to reduce the chance of collisions on JRuby and TruffleRuby. Fixes [#152](https://github.com/sinatra/mustermann/issues/152) * No longer inject color-codes into `Mustermann::Pattern#inspect` and `Mustermann::Pattern#pretty_print` in IRB, which was broken for newer versions of IRB. Fixes [#153](https://github.com/sinatra/mustermann/issues/153) * **Mustermann 3.1.0** (2026-04-13) * Minimum Ruby version is now 2.7.0, and we dropped support for old Ruby 2.6. * Removed the dependency on the `ruby2_keywords` gem. * Moved the Rails pattern from `mustermann-contrib` to the core `mustermann` gem. * Reduce gem size. [#151](https://github.com/sinatra/mustermann/pull/151) [@yuri-zubov](https://github.com/yuri-zubov) * **Mustermann 3.0.4** (2025-08-03) * Ruby 3.4+ compatibility: Use `URI::RFC2396_Parser` in mustermann-contrib [#146](https://github.com/sinatra/mustermann/pull/146) [@dentarg](https://github.com/dentarg) * **Mustermann 3.0.3** (2024-09-03) * Fix performance issue for `Mustermann::AST::Translator#escape` [#142](https://github.com/sinatra/mustermann/pull/142) [@hsbt](https://github.com/hsbt), [@ericproulx](https://github.com/ericproulx) * **Mustermann 3.0.2** (2024-08-09) * Ruby 3.4+ compatibility: "Use rfc2396 parser instead of URI::DEFAULT_PARSER" [#139](https://github.com/sinatra/mustermann/pull/139) [@hsbt](https://github.com/hsbt) * **Mustermann 3.0.1** (2024-07-31) * Ruby 3.4+ compatibility: "Use URI::RFC2396_Parser#regex explicitly" [#138](https://github.com/sinatra/mustermann/pull/138) [@hsbt](https://github.com/hsbt) * **Mustermann 3.0.0** (2022-07-24) * Drop support for old Rubies < 2.6. * **Mustermann 2.0.2** (2022-07-22) * Further improve Ruby 3 compatibility. [#134](https://github.com/sinatra/mustermann/pull/134). [@magni-](https://github.com/magni-) * **Mustermann 2.0.1** (2022-07-19) * Properly fix Ruby 3 compatability issue, reverts [#126](https://github.com/sinatra/mustermann/pull/126). Resolved by [#130](https://github.com/sinatra/mustermann/pull/130) [@eregon](https://github.com/eregon), [@tconst](https://github.com/tconst), [@dentarg](https://github.com/dentarg) * **Mustermann 2.0.0** (2022-07-18) * Improve Ruby 3 compatibility. Removed built-in Sinatra 1 support, and moved to new mustermann-sinatra-extension gem. Fixes [#114](https://github.com/sinatra/mustermann/issues/114) [@epergo](https://github.com/epergo) * **Mustermann 1.1.2** (2022-07-16) * Add compatibility with --enable=frozen-string-literal param. Fixes [#110](https://github.com/sinatra/mustermann/issues/110) [@michal-granec](https://github.com/michal-granec) * **Mustermann 1.1.1** (2020-01-04) * Make sure that `require`ing ruby2_keywords when needed. Fixes [#102](https://github.com/sinatra/mustermann/issues/103) [@Annih](https://github.com/Annih) * **Mustermann 1.1.0** (2019-12-30) * Proper handling of `Mustermann::ExpandError`. Fixes [#88](https://github.com/sinatra/mustermann/issues/88) [@namusyaka](https://github.com/namusyaka) * Support Ruby 3 keyword arguments. [@mame](https://github.com/mame) * At the same time, we dropped a support that accepts options followed by mappings on `Mustermann::Mapper`. [Reference commit](https://github.com/sinatra/mustermann/pull/97/commits/4e134f5b46d8e5886b0f1590f5ff3f6ea4d2e81a) * Improve documentation and development. [@horaciob](https://github.com/horaciob), [@epistrephein](https://github.com/epistrephein), [@jbampton](https://github.com/jbampton), [@jkowens](https://github.com/jkowens), [@junaruga](https://github.com/junaruga) * **Mustermann 1.0.3** (2018-08-17) * Handle `with_look_ahead` on SafeRenderer. Fixes [sinatra/sinatra#1409](https://github.com/sinatra/sinatra/issues/1409) [@namusyaka](https://github.com/namusyaka) * Fix `EqualityMap#fetch` to be compatible with the fallback `Hash#fetch`. Fixes [#89](https://github.com/sinatra/mustermann/issues/89) [@eregon](https://github.com/eregon) * Improve code base and documentation. [@sonots](https://github.com/sonots), [@iguchi1124](https://github.com/iguchi1124) * **Mustermann 1.0.2** (2018-02-17) * Look ahead same patterns as its own when concatenation. Fixes [sinatra/sinatra#1361](https://github.com/sinatra/sinatra/issues/1361) [@namusyaka](https://github.com/namusyaka) * Improve development support and documentation. [@EdwardBetts](https://github.com/EdwardBetts), [@284km](https://github.com/284km), [@yb66](https://github.com/yb66) and [@garybernhardt](https://github.com/garybernhardt) * **Mustermann 1.0.1** (2017-08-26) #### Docs * Updating readme to list Ruby 2.2 as minimum [commit](https://github.com/sinatra/mustermann/commit/7c65d9637ed81c194e3d05f0ccf3cfe76f0cf53e) (@cassidycodes) * Fix rendering of HTML table [commit](https://github.com/sinatra/mustermann/commit/119a61f0e589cb9e917d8c901800a202bb66ff3b) (@stevenwilkin) * Update summary and description in gemspec file. [commit](https://github.com/sinatra/mustermann/commit/04de221a809527c2be8c3f08c40a4fcd53f2bd53) (@junaruga) #### Fixes * avoid infinite loop by removing comments when receiving extended regexp [commit](https://github.com/sinatra/mustermann/commit/fa20301167e1b22882415f1181c5e4e2d76b6ac6) (@namusyaka) * avoid unintended conflict of namespace [commit](https://github.com/sinatra/mustermann/commit/d3c9531d372522d693fa5f768f13dbaa1d881d88) (@namusyaka) * use Regexp#source instead of Regexp#inspect [commit](https://github.com/sinatra/mustermann/pull/73/commits/e9213748bda1773b1ad9838ef57a296f92c471e7) (@namusyaka) * **Mustermann 1.0.0** (2017-03-05) * First stable release. * Includes `mustermann`, and `mustermann-contrib` gems * Sinatra patterns: Allow | outside of parens. * Add concatenation support (`Mustermann::Pattern#+`). * `Mustermann::Sinatra#|` may now generate a Sinatra pattern instead of a real composite. * Add syntax highlighting support for composite patterns. * Remove routers (they were out of scope for the main gem). * Rails patterns: Add Rails 5.0 compatibility mode, make it default. * Moved `tool` gem `EqualityMap` to `Mustermann::EqualityMap` in core * Improve documentation. ## Development Releases * **Mustermann 0.4.0** (2014-11-26) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.4.0), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.4.0/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.4.0) * Split into multiple gems. * Add `Pattern#to_proc`. * Add `Pattern#|`, `Pattern#&` and `Pattern#^`. * Add `Pattern#peek`, `Pattern#peek_size`, `Pattern#peek_match` and `Pattern#peek_params`. * Add `Mustermann::StringScanner`. * Add `Pattern#to_templates`. * Add `|` syntax to `sinatra` templates. * Add template style placeholders to `sinatra` templates. * Add `cake`, `express`, `flask` and `pyramid` patterns. * Allow passing in additional value behavior directly to `Pattern#expand`. * Fix expanding of multiple splats. * Add expanding to `identity` patterns. * Add `mustermann-fileutils`. * Make expander accept hashes with string keys. * Allow named splats to be named splat. * Support multiple Rails versions. * Type option can be set to nil to get the default type. * Add `mustermann-visualizer`. * **Mustermann 0.3.1** (2014-09-12) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.3.1), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.3.1/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.3.1) * Speed up pattern generation and matching (thanks [Daniel Mendler](https://github.com/minad)) * Small change so `Mustermann === Mustermann.new('...')` returns `true`. * **Mustermann 0.3.0** (2014-08-18) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.3.0), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.3.0/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.3.0) * Add `regexp` pattern. * Add named splats to Sinatra patterns. * Add `Mustermann::Mapper`. * Improve duck typing support. * Improve documentation. * **Mustermann 0.2.0** (2013-08-24) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.2.0), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.2.0/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.2.0) * Add first class expander objects. * Add params casting for expander. * Add simple router and rack router. * Add weak equality map to significantly improve performance. * Fix Ruby warnings. * Improve documentation. * Refactor pattern validation, AST transformations. * Increase test coverage (from 100%+ to 100%++). * Improve JRuby compatibility. * Work around bug in 2.0.0-p0. * **Mustermann 0.1.0** (2013-05-12) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.1.0), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.1.0/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.1.0) * Add `Pattern#expand` for generating strings from patterns. * Add better internal API for working with the AST. * Improved documentation. * Avoids parsing the path twice when used as Sinatra extension. * Better exceptions for unknown pattern types. * Better handling of edge cases around extend. * More specs to ensure API stability. * Largely rework internals of Sinatra, Rails and Template patterns. * **Mustermann 0.0.1** (2013-04-27) * More Infos: [RubyGems.org](https://rubygems.org/gems/mustermann/versions/0.0.1), [RubyDoc.info](http://www.rubydoc.info/gems/mustermann/0.0.1/frames), [GitHub.com](https://github.com/rkh/mustermann/tree/v0.0.1) * Initial Release. mustermann-4.0.0/CODE_OF_CONDUCT.md000066400000000000000000000210571517364653100165230ustar00rootroot00000000000000# Code of Conduct ## Our Pledge We pledge to make our community welcoming, safe, and equitable for all. We are committed to fostering an environment that respects and promotes the dignity, rights, and contributions of all individuals, regardless of characteristics including race, ethnicity, caste, color, age, physical characteristics, neurodiversity, disability, sex or gender, gender identity or expression, sexual orientation, language, philosophy or religion, national or social origin, socio-economic position, level of education, or other status. The same privileges of participation are extended to everyone who participates in good faith and in accordance with this Covenant. ## Encouraged Behaviors While acknowledging differences in social norms, we all strive to meet our community's expectations for positive behavior. We also understand that our words and actions may be interpreted differently than we intend based on culture, background, or native language. With these considerations in mind, we agree to behave mindfully toward each other and act in ways that center our shared values, including: 1. Respecting the **purpose of our community**, our activities, and our ways of gathering. 2. Engaging **kindly and honestly** with others. 3. Respecting **different viewpoints** and experiences. 4. **Taking responsibility** for our actions and contributions. 5. Gracefully giving and accepting **constructive feedback**. 6. Committing to **repairing harm** when it occurs. 7. Behaving in other ways that promote and sustain the **well-being of our community**. ## Restricted Behaviors We agree to restrict the following behaviors in our community. Instances, threats, and promotion of these behaviors are violations of this Code of Conduct. 1. **Harassment.** Violating explicitly expressed boundaries or engaging in unnecessary personal attention after any clear request to stop. 2. **Character attacks.** Making insulting, demeaning, or pejorative comments directed at a community member or group of people. 3. **Stereotyping or discrimination.** Characterizing anyone’s personality or behavior on the basis of immutable identities or traits. 4. **Sexualization.** Behaving in a way that would generally be considered inappropriately intimate in the context or purpose of the community. 5. **Violating confidentiality**. Sharing or acting on someone's personal or private information without their permission. 6. **Endangerment.** Causing, encouraging, or threatening violence or other harm toward any person or group. 7. Behaving in other ways that **threaten the well-being** of our community. ### Other Restrictions 1. **Misleading identity.** Impersonating someone else for any reason, or pretending to be someone else to evade enforcement actions. 2. **Failing to credit sources.** Not properly crediting the sources of content you contribute. 3. **Promotional materials**. Sharing marketing or other commercial content in a way that is outside the norms of the community. 4. **Irresponsible communication.** Failing to responsibly present content which includes, links or describes any other restricted behaviors. ## Reporting an Issue Tensions can occur between community members even when they are trying their best to collaborate. Not every conflict represents a code of conduct violation, and this Code of Conduct reinforces encouraged behaviors and norms that can help avoid conflicts and minimize harm. When an incident does occur, it is important to report it promptly. To report a possible violation, **[NOTE: describe your means of reporting here.]** Community Moderators take reports of violations seriously and will make every effort to respond in a timely manner. They will investigate all reports of code of conduct violations, reviewing messages, logs, and recordings, or interviewing witnesses and other participants. Community Moderators will keep investigation and enforcement actions as transparent as possible while prioritizing safety and confidentiality. In order to honor these values, enforcement actions are carried out in private with the involved parties, but communicating to the whole community may be part of a mutually agreed upon resolution. ## Addressing and Repairing Harm **[NOTE: The remedies and repairs outlined below are suggestions based on best practices in code of conduct enforcement. If your community has its own established enforcement process, be sure to edit this section to describe your own policies.]** If an investigation by the Community Moderators finds that this Code of Conduct has been violated, the following enforcement ladder may be used to determine how best to repair harm, based on the incident's impact on the individuals involved and the community as a whole. Depending on the severity of a violation, lower rungs on the ladder may be skipped. 1) Warning 1) Event: A violation involving a single incident or series of incidents. 2) Consequence: A private, written warning from the Community Moderators. 3) Repair: Examples of repair include a private written apology, acknowledgement of responsibility, and seeking clarification on expectations. 2) Temporarily Limited Activities 1) Event: A repeated incidence of a violation that previously resulted in a warning, or the first incidence of a more serious violation. 2) Consequence: A private, written warning with a time-limited cooldown period designed to underscore the seriousness of the situation and give the community members involved time to process the incident. The cooldown period may be limited to particular communication channels or interactions with particular community members. 3) Repair: Examples of repair may include making an apology, using the cooldown period to reflect on actions and impact, and being thoughtful about re-entering community spaces after the period is over. 3) Temporary Suspension 1) Event: A pattern of repeated violation which the Community Moderators have tried to address with warnings, or a single serious violation. 2) Consequence: A private written warning with conditions for return from suspension. In general, temporary suspensions give the person being suspended time to reflect upon their behavior and possible corrective actions. 3) Repair: Examples of repair include respecting the spirit of the suspension, meeting the specified conditions for return, and being thoughtful about how to reintegrate with the community when the suspension is lifted. 4) Permanent Ban 1) Event: A pattern of repeated code of conduct violations that other steps on the ladder have failed to resolve, or a violation so serious that the Community Moderators determine there is no way to keep the community safe with this person as a member. 2) Consequence: Access to all community spaces, tools, and communication channels is removed. In general, permanent bans should be rarely used, should have strong reasoning behind them, and should only be resorted to if working through other remedies has failed to change the behavior. 3) Repair: There is no possible repair in cases of this severity. This enforcement ladder is intended as a guideline. It does not limit the ability of Community Managers to use their discretion and judgment, in keeping with the best interests of our community. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public or other spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Attribution This Code of Conduct is adapted from the Contributor Covenant, version 3.0, permanently available at [https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/). Contributor Covenant is stewarded by the Organization for Ethical Source and licensed under CC BY-SA 4.0. To view a copy of this license, visit [https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/) For answers to common questions about Contributor Covenant, see the FAQ at [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq). Translations are provided at [https://www.contributor-covenant.org/translations](https://www.contributor-covenant.org/translations). Additional enforcement and community guideline resources can be found at [https://www.contributor-covenant.org/resources](https://www.contributor-covenant.org/resources). The enforcement ladder was inspired by the work of [Mozilla’s code of conduct team](https://github.com/mozilla/inclusion). mustermann-4.0.0/CONTRIBUTING.md000066400000000000000000000057541517364653100161630ustar00rootroot00000000000000# Contributing to Mustermann Contributions to Mustermann are welcome and appreciated! 💜💜💜 To ensure a smooth contribution process, please follow these guidelines: 1. Do not open issues or pull requests regarding open **security vulnerabilities**. If you discover a security vulnerability, please report follow the [Sinatra security policy](https://github.com/sinatra/sinatra/blob/main/SECURITY.md). 2. **Bug reports** and **pull requests** should be submitted to the [main repository](https://github.com/sinatra/mustermann) on GitHub. 3. Please follow the **[code of conduct](CODE_OF_CONDUCT.md)** in all your interactions with the project and its community. 4. If you use an **AI tool** to assist with your contribution, please follow the guidelines outlined in the [AI contribution policy](#ai-contribution-policy) below. 5. Any code contributions added to Mustermann directly should be licensed under the **[MIT License](LICENSE)**. By submitting a pull request, you are agreeing to license your contribution under the MIT License. This does not apply to third-party plugins, repositories, redistributions, or other works that you may link to or reference in your contribution, which may be subject to their own licenses. 6. Run `bin/rake` and make sure it's happy before submitting a pull request. If your contribution includes new features, please also add tests and documentation as needed. 7. We aim for 100% test coverage. Don't let that deter you from contributing! If your contribution doesn't have tests, we can help you add them. You can find documentation for the project in the **[docs](docs)** directory. Feel free to reach out with any questions and feedback. Thank you for being part of the Ruby Open Source community and for contributing to Mustermann! 🙌 ## AI contribution policy If you wish to use an AI tool for assistance, please adhere to the following guidelines: * As a contributor, you are always the author and fully accountable for your contributions. * You must carefully read and review all LLM-generated code or text before asking maintainers to review it. This includes pull request descriptions, where we want to hear your personal voice rather than an unfiltered AI summary. * You should be transparent and make note if your contribution contains substantial amounts of tool-generated content. * You should be confident that your contribution is high enough quality to merit a maintainer’s time for review. You should be able to answer questions about your work during review. * You are responsible for ensuring you have the legal right to make your contribution under our license, and that your contribution does not violate the intellectual property rights of any third party. Any agents used for contributions may not take action without human review and approval. This includes, but is not limited to, making commits, opening pull requests, or posting in issue trackers. This policy is based on the [Hanami AI contribution policy](https://discourse.hanamirb.org/t/ai-contribution-policy). mustermann-4.0.0/Gemfile000066400000000000000000000004071517364653100152130ustar00rootroot00000000000000source 'https://rubygems.org' require File.expand_path('../support/lib/support/projects', __FILE__) path '.' do Support::Projects.each { |name| gem(name) } gem 'support', group: :development end group :benchmark do gem 'addressable' gem 'benchmark' end mustermann-4.0.0/LICENSE000066400000000000000000000020371517364653100147260ustar00rootroot00000000000000Copyright (c) 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. mustermann-4.0.0/README.md000066400000000000000000000212071517364653100152000ustar00rootroot00000000000000# The Amazing Mustermann [![Build Status](https://github.com/sinatra/mustermann/actions/workflows/test.yml/badge.svg)](https://github.com/sinatra/mustermann/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/sinatra/mustermann/badge.svg?branch=main)](https://coveralls.io/github/sinatra/mustermann?branch=main) [![Gem Version](https://img.shields.io/gem/v/mustermann.svg)](https://rubygems.org/gems/mustermann) [![Inline docs](http://inch-ci.org/github/rkh/mustermann.svg)](http://inch-ci.org/github/rkh/mustermann) [![Documentation](http://img.shields.io/:yard-docs-38c800.svg)](https://gemdocs.org/gems/mustermann/) [![License](http://img.shields.io/:license-MIT-38c800.svg)](http://rkh.mit-license.org) [![Badges](http://img.shields.io/:badges-7/7-38c800.svg)](http://img.shields.io) This repository contains two projects (each installable as separate gems): * **[mustermann](https://github.com/sinatra/mustermann/blob/main/mustermann/README.md): Your personal string matching expert. This is probably what you're looking for.** * [mustermann-contrib](https://github.com/sinatra/mustermann/blob/main/mustermann-contrib/README.md): A gem with additional pattern types and extensions. ## Projects using Mustermann Mustermann is typically used by other frameworks and libraries, primarily but not exclusively for handing HTTP requests. These include, amongst others: * [Sinatra](https://sinatrarb.com/): A DSL for quickly creating web applications with minimal effort * [Hanami](https://hanamirb.org/): A flexible framework for maintainable Ruby apps * [Grape](https://www.ruby-grape.org/): An opinionated framework for creating REST-like APIs in Ruby. * [Padrino](http://padrinorb.com/): A Ruby web framework built upon Sinatra. * [Praxis](https://github.com/praxis/praxis): A framework that focuses on both the design and implementation aspects of creating APIs. * [Webspicy](https://yourbackendisbroken.dev/): A technology agnostic specification and test framework that yields better coverage for less testing effort. * [Alchemrest](https://github.com/Betterment/alchemrest): Betterment's library for building robust, reliable, performant integrations with third party APIs, with a focus on making APIs work with the rest of your domain layer not against it. * [HTTP Fake](https://alchemists.io/projects/http-fake): An HTTP fake implementation for test suites. * [oas_parser](https://github.com/Nexmo/oas_parser) and [oas_parser_reborn](https://github.com/MarioRuiz/oas_parser_reborn): An open source Open API Spec 3 Definition Parser * [Pendragon](https://github.com/namusyaka/pendragon): Provides an HTTP router and its toolkit for use in Rack. As a Rack application, it makes it easy to define complicated routing. * [Wayferer](https://rubygems.org/gems/wayfarer): Web crawling framework based on ActiveJob. * [apiculture](https://rubygems.org/gems/apiculture): A toolkit for building REST APIs on top of Rack. By WeTransfer. ## Git versions with Bundler You can easily use the latest edge version from GitHub of any of these gems via [Bundler](http://bundler.io/): ``` ruby github 'sinatra/mustermann' do gem 'mustermann' gem 'mustermann-contrib' end ``` ## Pattern Types The `identity`, `regexp`, `rails`, and `sinatra` types are included in the `mustermann` gem, all the other types listed here are part of the `mustermann-contrib` gem. There are also third-party gems providing additional types, like [mustermann-grape](https://github.com/ruby-grape/mustermann-grape).
Type Example Compatible with Notes
cake /:prefix/** CakePHP
express /:prefix+/:id(\d+) Express, pillar.js
flask /<prefix>/<int:id> Flask, Werkzeug
identity /image.png any software using strings Exact string matching (no parameter parsing).
pyramid /{prefix:.*}/{id} Pyramid, Pylons
rails /:slug(.:ext) Ruby on Rails, Journey, HTTP Router, Hanami, Scalatra (if configured), NYNY
regexp /(?<slug>[^\/]+) Oniguruma, Onigmo, regular expressions Created when you pass a regexp to Mustermann.new.
Does not support expanding or generating templates.
shell /*.{png,jpg} Unix Shell (bash, zsh) Does not support expanding or generating templates.
simple /:slug.:ext Sinatra (1.x), Scalatra, Dancer, Finatra, Spark, RCRouter, kick.js Implementation is a direct copy from Sinatra 1.3.
It is the predecessor of sinatra. Does not support expanding or generating templates.
sinatra /:slug(.:ext)? Sinatra (2.x), Padrino (>= 0.13.0), Pendragon, Angelo This is the default and the only type "invented here".
It is a superset of simple and has a common subset with template (and others).
uri-template /{+pre}/{page}{?q} RFC 6570, JSON API, JSON Home Documents and many more Standardized URI templates, can be generated from most other types.
Any software using Mustermann is obviously compatible with at least one of the above. ## Requirements Ruby 3.3+ compatible Ruby implementation (MRI, JRuby, and TruffleRuby are tested). mustermann-4.0.0/Rakefile000066400000000000000000000017171517364653100153720ustar00rootroot00000000000000# frozen_string_literal: true require 'bundler/setup' require 'support/projects' require 'mustermann/version' desc 'Run rspec' task(:rspec) { ruby '-S rspec' } desc 'Run "yard stats"' task(:doc_stats) { ruby '-S yard stats' } # YARD is having issues on TruffleRuby, so we skip it there. task default: RUBY_ENGINE == "truffleruby" ? :rspec : [:rspec, :doc_stats] desc 'Build the gems' task :pkg do rm_rf 'pkg' mkdir 'pkg' Support::Projects.each do |project| cd project do ruby "-S gem build #{project}.gemspec" mv "#{project}-#{Mustermann::VERSION}.gem", '../pkg/' end end end desc 'Push the gems' task release: :pkg do cd 'pkg' do Support::Projects.each do |project| ruby "-S gem push #{project}-#{Mustermann::VERSION}.gem" end end end desc 'List projects' task :list do puts "Listing mustermann project..." Support::Projects.each do |project| puts "#{project} VERSION: #{Mustermann::VERSION}" end end mustermann-4.0.0/bench/000077500000000000000000000000001517364653100147765ustar00rootroot00000000000000mustermann-4.0.0/bench/regexp.rb000066400000000000000000000007741517364653100166250ustar00rootroot00000000000000require 'benchmark' puts " atomic vs normal segments ".center(52, '=') types = { normal: /\A\/(?:a|%61)\/(?[^\/\?#]+)(?:\/(?[^\/\?#]+))?\Z/, atomic: /\A\/(?:a|%61)\/(?(?>[^\/\?#]+))(?:\/(?(?>[^\/\?#]+)))?\Z/ } Benchmark.bmbm do |x| types.each do |name, regexp| string = "/a/" << ?a * 10000 << "/" << ?a * 5000 fail unless regexp.match(string) string << "/" fail if regexp.match(string) x.report name.to_s do 100.times { regexp.match(string) } end end endmustermann-4.0.0/bench/set.rb000066400000000000000000000063371517364653100161270ustar00rootroot00000000000000# frozen_string_literal: true $:.unshift File.expand_path('../mustermann/lib', __dir__) require 'mustermann' require 'mustermann/set' require 'benchmark' options = { type: :sinatra } nesting = nil route_count = [10, 20, 50, 100, 200, 500, 1000, 10000] match_count = 1000 while ARGV.any? case ARGV.shift when "--trie" then options[:use_trie] = ARGV.empty? || ARGV.first.start_with?("-") ? true : Integer(ARGV.shift) when "--no-trie" then options[:use_trie] = false when "--cache" then options[:use_cache] = true when "--no-cache" then options[:use_cache] = false when "--strict-order" then options[:strict_order] = true when "-n", "--nesting" then nesting = Integer(ARGV.shift) when "-r", "--routes" then route_count = ARGV.shift.split(",").map { Integer(it) }.sort when "-m", "--matches" then match_count = Integer(ARGV.shift) when "-p", "--type" then options[:type] = ARGV.shift.to_sym else warn <<~USAGE Unknown option: #{ARGV.first} Available options: --trie [THRESHOLD] whether to use a trie for matching, or the threshold for using a trie --no-trie do not use a trie for matching --cache enable caching of matches not yet garbage collected --no-cache disable caching of matches not yet garbage collected -n, --nesting N nesting level of patterns, default depends on count -r, --routes N1,N2.. number of routes to add -m, --matches N number of matches to perform USAGE exit 1 end end case options[:type] when :sinatra, :rails, :cake, :express then prefix = ":" when :flask then prefix, suffix = "<", ">" when :pyramid, :template then prefix, suffix = "{", "}" else warn "Unknown type: #{options[:type]}" exit 1 end line_length = 53 + route_count.last.to_s.size data = route_count.map do |count| routes = [] examples = [] _nesting = nesting || count.to_s(17).size base = "a" * _nesting count.times do segments = base.split("", _nesting).reverse placeholder = String.new("`") routes << segments.map { "/#{it}/#{prefix}#{placeholder.succ!}#{suffix}" }.join examples << segments.map { "/#{it}/#{placeholder.succ!}" }.join base.succ! end { count:, routes:, examples:, nesting: _nesting, rand: match_count.times.map { rand(count) } } end puts "", " Compilation ".center(line_length, '=') Benchmark.benchmark do |x| data.each do |d| x.report("#{d[:count]} routes") do set = Mustermann::Set.new(**options) d[:routes].each_with_index do |route, index| set.add(route, index) end d[:set] = set end end end puts "", " Matching ".center(line_length, '=') Benchmark.bmbm do |x| data.each do |d| set = d[:set] x.report("#{d[:count]} routes") do d[:rand].each do |index| example = d[:examples][index] next if match = set.match(example) and match.value == index route = d[:routes][index] p nil, route, example, match, Mustermann.new(route, ignore_unknown_options: true, **options).match(example), set raise "Expected %p but got %p for %p" % [index, match&.value, example] end end end end mustermann-4.0.0/bench/template_vs_addressable.rb000066400000000000000000000017271517364653100222060ustar00rootroot00000000000000require 'bundler/setup' require 'benchmark' require 'mustermann/template' require 'addressable/template' [Mustermann::Template, Addressable::Template].each do |klass| puts "", " #{klass} ".center(64, '=') Benchmark.bmbm do |x| no_capture = klass.new("/simple") x.report("no captures, match") { 100_000.times { no_capture.match('/simple') } } x.report("no captures, miss") { 100_000.times { no_capture.match('/miss') } } simple = klass.new("/{match}") x.report("simple, match") { 100_000.times { simple.match('/simple') } } x.report("simple, miss") { 100_000.times { simple.match('/mi/ss') } } explode = klass.new("{/segments*}") x.report("explode, match") { 100_000.times { explode.match("/a/b/c") } } x.report("explode, miss") { 100_000.times { explode.match("/a/b/c.miss") } } expand = klass.new("/prefix/{foo}/something/{bar}") x.report("expand") { 100_00.times { expand.expand(foo: 'foo', bar: 'bar').to_s } } end puts end mustermann-4.0.0/bench/uri_parser_object.rb000066400000000000000000000006621517364653100210300ustar00rootroot00000000000000require "bundler/setup" require "objspace" require "uri" require "mustermann/ast/translator" translator = Mustermann::AST::Translator.new translator.escape("foo") h1 = ObjectSpace.each_object.inject(Hash.new 0) { |h, o| h[o.class] += 1; h } 100.times do translator.escape("foo") end h2 = ObjectSpace.each_object.inject(Hash.new 0) { |h, o| h[o.class] += 1; h } raise if (h2[URI::RFC2396_Parser] - h1[URI::RFC2396_Parser] != 0) mustermann-4.0.0/bench/versions.rb000066400000000000000000000125571517364653100172050ustar00rootroot00000000000000# frozen_string_literal: true known_versions = %w[ 3.1.1 3.1.0 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 2.0.2 2.0.1 2.0.0 1.1.2 1.1.1 1.1.0 ] counts = { compile: 1_000, single_match: 5_000_000, params: 1_000_000, set_match: 10_000, set_size: 1000, look_ahead_fail: 500, } format = proc do |count| if count >= 1_000_000 "#{count / 1_000_000}M" elsif count >= 1_000 "#{count / 1_000}K" else count.to_s end end scenarios = { compile: "Compilation of #{format[counts[:compile]]} patterns", single_match: "Matching #{format[counts[:single_match]]} times against a single pattern", single_string: "Matching #{format[counts[:single_match]]} times against the same string", simple_params: "Extracting params #{format[counts[:params]]} times for a simple pattern", complex_params: "Extracting params #{format[counts[:params]]} times for a complex pattern", expand: "Expanding #{format[counts[:params]]} times for a simple pattern", set_match: "Matching #{format[counts[:set_match]]} times against a set of #{format[counts[:set_size]]} patterns", look_ahead_fail: "Matching #{format[counts[:look_ahead_fail]]} times with look-ahead pattern on a long failing input (atomic group speedup)", } case version = ENV['MUSTERMANN_VERSION'] when /^\d+\./ begin gem "mustermann", version rescue Gem::LoadError Gem.install "mustermann", version gem "mustermann", version end when "bundler" require "bundler/setup" when String $LOAD_PATH.unshift File.expand_path(version) else scenarios.each do |step, title| next unless ARGV.empty? or ARGV.include?(step.to_s) puts "", title, "", " user system total real" ["bundler", *known_versions].each do |version| env = { "MUSTERMANN_VERSION" => version } if version != "bundler" ENV.each_key do |key| next unless key.start_with?("BUNDLE") env[key] = nil end env["RUBYOPT"] = nil end system(env, "ruby", "-W0", __FILE__, step.to_s ) end end return end require "benchmark" require "mustermann" require "mustermann/version" version = Mustermann::VERSION[/^(\d+\.\d+\.\d+)/] Benchmark.benchmark do |x| case ARGV.shift when "compile" element = String.new("a") 100.times { Mustermann.new("/#{element.succ!}/:bar") } x.report(version) { counts[:compile].times { Mustermann.new("/#{element.succ!}/:bar") } } when "single_match" pattern = Mustermann.new("/foo/:bar") element = String.new("a") 100.times { pattern.match("/foo/#{element.succ!}") } strings = counts[:single_match].times.map { "/foo/#{element.succ!}" } x.report(version) { strings.each { |string| pattern === string } } when "single_string" pattern = Mustermann.new("/foo/:bar") string = "/foo/bar" x.report(version) { counts[:single_match].times { pattern.match(string) } } when "simple_params" pattern = Mustermann.new("/:controller/:action") element = String.new("a") 100.times { pattern.params("/#{element.succ!}/show.html") } strings = counts[:params].times.map { "/#{element.succ!}/show.html" } x.report(version) { strings.each { |string| pattern.params(string) } } when "expand" pattern = Mustermann.new("/foo/:bar") element = String.new("a") 100.times { pattern.expand(bar: element.succ!) } params = counts[:params].times.map { { bar: element.succ!.dup } } x.report(version) { params.each { |param| pattern.expand(param) } } when "complex_params" pattern = Mustermann.new("/:controller/:action(.:format)") element = String.new("a") 100.times { pattern.params("/#{element.succ!}/show.html") } strings = counts[:params].times.map { "/#{element.succ!}/show.html" } x.report(version) { strings.each { |string| pattern.params(string) } } when "set_match" patterns = [] routes = [] element = String.new("aa") counts[:set_size].times do patterns << Mustermann.new("/#{element.succ!}/:bar") routes << "/#{element}/#{element}" end begin require "mustermann/set" set = Mustermann::Set.new(patterns) callback = proc { |s| set.match(s) } rescue LoadError => e raise e unless e.path == "mustermann/set" callback = proc { |s| patterns.select { |p| p === s } } end x.report(version) do counts[:set_match].times do string = routes.sample callback.call(string) end end when "look_ahead_fail" # /:a:b? triggers the look-ahead transformer: :a and :b share the same # character class ([^\/\?#]) with no separator between them. Current # Mustermann wraps the head capture (:a) in an atomic group so Oniguruma # does not re-examine characters it has already committed to. Older # versions emit a plain non-atomic capture, which backtracks O(n) times # when the overall match fails. # # The failing string ends with "/trailing" — the extra segment cannot be # consumed by either capture (both exclude '/'), so the engine must try # every possible split of the 5 000-character prefix before giving up. # On current Mustermann this takes ~0.11 s; on older versions ~0.57 s. pattern = Mustermann.new("/:a:b?") failing = "/" + "x" * 5_000 + "/trailing" 10.times { pattern.match(failing) } x.report(version) { counts[:look_ahead_fail].times { pattern.match(failing) } } else warn "Unknown step: #{ARGV.first}" exit 1 end end mustermann-4.0.0/bin/000077500000000000000000000000001517364653100144675ustar00rootroot00000000000000mustermann-4.0.0/bin/rake000077500000000000000000000005331517364653100153400ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true # # This file was generated by Bundler. # # The application 'rake' is installed as part of a gem, and # this file is here to facilitate running it. # ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) require "rubygems" require "bundler/setup" load Gem.bin_path("rake", "rake") mustermann-4.0.0/docs/000077500000000000000000000000001517364653100146475ustar00rootroot00000000000000mustermann-4.0.0/docs/custom-patterns.md000066400000000000000000000323371517364653100203510ustar00rootroot00000000000000# Implementing Custom Patterns Mustermann ships with many built-in pattern types — Sinatra, Rails, URI templates, and more. But sometimes none of them fit your needs. This guide walks you through building your own pattern type, starting from the simplest possible approach and working up to a full AST-based implementation. ## The Simplest Case: Subclassing `Mustermann::Pattern` Every pattern in Mustermann ultimately inherits from `Mustermann::Pattern`. The only method you _must_ override is `===`, which determines whether a string matches your pattern. ```ruby require 'mustermann/pattern' class WikiPattern < Mustermann::Pattern register :wiki def ===(string) # A wiki pattern is just a literal path where spaces are allowed. # Match after normalizing spaces. unescape(string).gsub('_', ' ') == @string.gsub('_', ' ') end end ``` The `register` call makes your pattern available through `Mustermann.new`: ```ruby pattern = Mustermann.new('hello world', type: :wiki) pattern === 'hello_world' # => true pattern === 'hello world' # => true pattern === 'hello-world' # => false ``` The `unescape` method is provided by the base class. It URI-decodes the input string when the `:uri_decode` option is true (the default). ### What you get for free Even with just `===` implemented, the base class provides several useful methods: ```ruby pattern.match('hello_world') # => # pattern.params('hello_world') # => {} (empty, no captures yet) pattern =~ 'hello_world' # => 0 pattern.peek('hello_world/more') # => "hello_world" ``` `match`, `=~`, and `peek` all delegate to `===` under the hood. ### Declaring supported options If your pattern type accepts custom options, declare them with `supported_options`: ```ruby class WikiPattern < Mustermann::Pattern register :wiki supported_options :case_sensitive def initialize(string, case_sensitive: true, **options) super(string, **options) @case_sensitive = case_sensitive end def ===(string) normalized_input = unescape(string).gsub('_', ' ') normalized_pattern = @string.gsub('_', ' ') return normalized_input == normalized_pattern if @case_sensitive normalized_input.downcase == normalized_pattern.downcase end end ``` Mustermann raises an `ArgumentError` if an option is passed that is not declared, so this keeps the API clean. ## Adding Parameter Extraction: Subclassing `Mustermann::RegexpBased` If you want your pattern to extract named parameters from a match (like `:name` does in Sinatra patterns), the easiest path is to compile your pattern to a regular expression. `Mustermann::RegexpBased` handles all the matching and param extraction for you. You only need to implement one method: `compile`, which returns a `Regexp` without anchors (the base class adds `\A` and `\Z` automatically). ```ruby require 'mustermann/regexp_based' class ColonPattern < Mustermann::RegexpBased register :colon private def compile(**options) # Turn ":name" segments into named capture groups. regexp_string = Regexp.escape(@string).gsub(/\\:(\w+)/) do "(?<#{$1}>[^/]+)" end Regexp.new(regexp_string) end end ``` ```ruby pattern = Mustermann.new('/:name/:ext', type: :colon) pattern === '/hello/rb' # => true pattern.params('/hello/rb') # => {"name" => "hello", "ext" => "rb"} pattern.match('/hello/rb')[:name] # => "hello" ``` Named capture groups in the compiled regexp become entries in the params hash. The base class calls `map_param(key, value)` on each capture before returning it, which applies URI decoding by default. ### Exposing capture names Because `RegexpBased` delegates `names` to the underlying regexp, you get named capture introspection for free: ```ruby pattern.names # => ["name", "ext"] ``` ## The Full System: Subclassing `Mustermann::AST::Pattern` For richer pattern syntaxes — optional segments, splats, inline constraints, union alternations — you want to work at the AST level. `Mustermann::AST::Pattern` parses your pattern string into a tree of nodes, then compiles that tree to a regexp. You define the grammar by telling the parser what to do with each special character. ### How it fits together ``` Pattern string → Parser → AST → Compiler → Regexp ``` The Parser walks the string character by character. When it encounters a character you have registered, it calls your block and expects an AST node back. The Compiler then visits each node and produces a regexp fragment. ### Defining grammar rules with `on` Inside a `Parser` subclass, you use `on` to register handlers for specific characters: ```ruby require 'mustermann/ast/pattern' class HashPattern < Mustermann::AST::Pattern register :hash class Parser < Mustermann::AST::Parser # "#name" captures a segment on(?#) { |char| node(:capture) { scan(/\w+/) } } # "**" is a splat (matches anything, including slashes) on(?*) { |char| scan("*") ? node(:splat) : node(:char, char) } end end ``` ```ruby pattern = Mustermann.new('/#name/**', type: :hash) pattern === '/alice/photos/2024' # => true pattern.params('/alice/photos/2024') # => {"name" => "alice", "splat" => ["photos/2024"]} ``` The `on` method takes one or more characters (or `nil` for end-of-string) and a block. When the parser reads that character, it calls your block with the character and uses the return value as the next node. You can also register the same handler for multiple characters at once: ```ruby on(?!, ?@) { |char| unexpected(char) } ``` ### Node types The built-in node types cover the common cases. Here is a quick reference: | Node | Purpose | Example use | |------|---------|-------------| | `:char` | A literal character | `node(:char, 'x')` | | `:separator` | A path separator (`/`) | `node(:separator, '/')` | | `:capture` | A named parameter capture | `node(:capture) { scan(/\w+/) }` | | `:splat` | An unnamed wildcard (`splat` key in params) | `node(:splat)` | | `:named_splat` | A named wildcard | `node(:named_splat, 'rest')` | | `:group` | A grouped sequence | `node(:group) { ... }` | | `:optional` | A group that may be absent | `node(:optional, inner_node)` | | `:union` | Two or more alternatives | `node(:union, [a, b])` | | `:or` | Separator between union arms | `node(:or)` | You look up a node class by symbol with `Node[type]`, but in practice you rarely need to do this directly — the `node` helper in the parser does it for you. ### The `node` helper The `node` method creates a node and records its position in the source string: ```ruby node(type, *args, &block) ``` - `type` is a symbol naming the node class (e.g., `:capture`, `:splat`). - `args` become the node's payload. - When a block is given, the parser calls `parse` on the new node, which repeatedly calls `yield` (your block) and appends the results to the node's payload. ```ruby on(?:) { |char| node(:capture) { scan(/\w+/) } } ``` This reads a `:` character, then reads word characters into a `:capture` node's payload (the capture name). The block passed to `node` is invoked by the node's own `parse` method, which keeps calling `yield` until it returns `nil` and collects the results. ```ruby on(?() { |char| node(:group) { read unless scan(?)) } } ``` This reads a `(`, then keeps reading nodes until it finds a matching `)`. Each call to `read` parses one node from the buffer and adds it to the group's payload. ### Reading from the buffer Inside the `on` block, several helpers let you consume input: ```ruby scan(regexp) # Match regexp at current position, advance buffer. Returns the match or nil. expect(regexp) # Like scan, but raises ParseError if nothing matches. unexpected(char) # Raise a ParseError about an unexpected character. ``` `scan` returns a `String` for simple regexps. If the regexp contains named captures, it returns a `MatchData` instead: ```ruby on(?<) do |char| match = expect(/(?\w+)>/) node(:capture, match[:name]) end ``` ### A working example: angle-bracket captures Here is a complete custom pattern type that uses `` syntax for captures: ```ruby require 'mustermann/ast/pattern' class AnglePattern < Mustermann::AST::Pattern register :angle class Parser < Mustermann::AST::Parser # Disallow unmatched > at the top level on(?>) { |char| unexpected(char) } on(?<) do |char| name = expect(/\w+/) expect(?>) node(:capture, name) end # "**" becomes a greedy splat on(?*) do |char| if scan(?*) node(:named_splat, 'path') else name = scan(/\w+/) name ? node(:named_splat, name) : node(:splat) end end end end ``` ```ruby pattern = Mustermann.new('/users//posts/', type: :angle) pattern === '/users/42/posts/hello-world' # => true pattern.params('/users/42/posts/hello-world') # => {"id" => "42", "slug" => "hello-world"} pattern = Mustermann.new('/files/**', type: :angle) pattern.params('/files/img/logo.png') # => {"path" => ["img/logo.png"]} ``` ### Using `suffix` for postfix modifiers Sometimes you want a character that follows a node to modify it — the classic example is `?` making the preceding group optional. The `suffix` method registers a handler that fires after a node is created: ```ruby suffix(??, after: :capture) do |match, element| node(:optional, element) end ``` The block receives the matched suffix and the node it follows, and should return the replacement node. The `after:` option restricts which node types the suffix can follow. Using `:node` (or omitting `after:`) applies the suffix after any node. Using a more specific type like `:capture` or `:group` keeps the grammar from applying the suffix in unexpected places. Here is the angle-bracket pattern extended with optional captures: ```ruby class Parser < Mustermann::AST::Parser on(?>) { |char| unexpected(char) } on(?<) do |char| name = expect(/\w+/) expect(?>) node(:capture, name) end # Make any capture optional when followed by ? suffix(??, after: :capture) do |match, element| node(:optional, element) end end ``` ```ruby pattern = Mustermann.new('/posts//?', type: :angle) pattern.params('/posts/2024/hello') # => {"year" => "2024", "slug" => "hello"} pattern.params('/posts/2024') # => {"year" => "2024", "slug" => nil} ``` ### Capture constraints A `:capture` node can carry a `constraint` attribute to restrict what it matches. This is a raw regexp fragment (without the named capture wrapper): ```ruby on(?<) do |char| match = expect(/(?\w+)/) constraint = scan(/:\w+/) # optional ":type" annotation expect(?>) n = node(:capture, match[:name]) n.constraint = '\d+' if constraint == ':int' n end ``` ```ruby pattern = Mustermann.new('/items/', type: :angle) pattern === '/items/42' # => true pattern === '/items/foo' # => false ``` ### Handling unknown characters By default, unrecognized characters become `:char` nodes (literal matches) or `:separator` nodes for `/`. If you want to forbid certain characters, register them with `unexpected`: ```ruby on(?[, ?], ?{, ?}) { |char| unexpected(char) } ``` This raises a `Mustermann::ParseError` with a clear message when those characters appear in a pattern. ## Registering your pattern Call `register` on your class with one or more symbols to make them available through `Mustermann.new`: ```ruby class AnglePattern < Mustermann::AST::Pattern register :angle end Mustermann.new('/users/', type: :angle) ``` You can register multiple names for the same class: ```ruby register :angle, :angle_bracket ``` ## Putting it all together Here is a self-contained example combining everything above — a pattern type with custom captures, splats, optional segments, and a constraint syntax: ```ruby require 'mustermann/ast/pattern' class BracePattern < Mustermann::AST::Pattern register :brace class Parser < Mustermann::AST::Parser # Disallow unmatched closing braces on(?}) { |char| unexpected(char) } # {name} for a capture, {+name} for a named splat on(?{) do |char| if scan(?+) name = expect(/\w+/) expect(?}) node(:named_splat, name) else name = expect(/\w+/) constraint = scan(/:\w+/) expect(?}) n = node(:capture, name) n.constraint = '\d+' if constraint == ':int' n.constraint = '\w+' if constraint == ':word' n end end # Groups with (...) on(?() { |char| node(:group) { read unless scan(?)) } } # Alternation with | on(?|) { |char| node(:or) } # Make captures and groups optional with ? suffix(??, after: :capture) { |m, e| node(:optional, e) } suffix(??, after: :group) { |m, e| node(:optional, e) } end end ``` ```ruby p = Mustermann.new('/users/{id:int}/posts/{slug}?', type: :brace) p === '/users/42/posts/hello' # => true p === '/users/42/posts' # => true p === '/users/foo/posts' # => false p.params('/users/42/posts/hello') # => {"id" => "42", "slug" => "hello"} p.params('/users/42/posts') # => {"id" => "42", "slug" => nil} p = Mustermann.new('/files/{+rest}', type: :brace) p.params('/files/img/logo.png') # => {"rest" => ["img/logo.png"]} ``` Because `BracePattern` inherits from `Mustermann::AST::Pattern`, it also gets `expand` and `to_templates` support automatically, along with all the standard `Pattern` methods like `peek`, `match`, and composition operators. mustermann-4.0.0/docs/patterns/000077500000000000000000000000001517364653100165075ustar00rootroot00000000000000mustermann-4.0.0/docs/patterns/cake.md000066400000000000000000000053531517364653100177420ustar00rootroot00000000000000# The CakePHP Pattern The `cake` pattern type is implemented by the `mustermann-contrib` gem. It is compatible with [CakePHP](http://cakephp.org/) 2.x and 3.x. ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, and `ignore_unknown_options`. **External documentation:** [CakePHP 2.0 Routing](http://book.cakephp.org/2.0/en/development/routing.html), [CakePHP 3.0 Routing](http://book.cakephp.org/3.0/en/development/routing.html) CakePHP patterns feature captures and unnamed splats. Captures are prefixed with a colon and splats are either a single asterisk (parsing segments into an array) or a double asterisk (parsing segments as a single string). ``` ruby require 'mustermann/cake' Mustermann.new('/:name/*', type: :cake).params('/a/b/c') # => { name: 'a', splat: ['b', 'c'] } Mustermann.new('/:name/**', type: :cake).params('/a/b/c') # => { name: 'a', splat: 'b/c' } pattern = Mustermann.new('/:name') pattern.respond_to? :expand # => true pattern.expand(name: 'foo') # => '/foo' pattern.respond_to? :to_templates # => true pattern.to_templates # => ['/{name}'] ``` ## Syntax
Syntax Element Description
:name Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
* Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use it more than once in a pattern.
** Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use it more than once in a pattern. The value matching a single ** will be split at slashes when parsed into params.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/:name` | `/alice` | `{"name" => "alice"}` | | `/:foo/:bar` | `/hello/world` | `{"foo" => "hello", "bar" => "world"}` | | `/:name/*` | `/alice/b/c` | `{"name" => "alice", "splat" => ["b", "c"]}` | | `/:name/**` | `/alice/b/c` | `{"name" => "alice", "splat" => "b/c"}` | mustermann-4.0.0/docs/patterns/express.md000066400000000000000000000070061517364653100205250ustar00rootroot00000000000000# The Express Pattern The `express` pattern type is implemented by the `mustermann-contrib` gem. It is compatible with [Express](http://expressjs.com/) and [pillar.js](https://pillarjs.github.io/). ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, and `ignore_unknown_options`. **External documentation:** [path-to-regexp](https://github.com/pillarjs/path-to-regexp#path-to-regexp), [live demo](http://forbeslindesay.github.io/express-route-tester/) Express patterns feature named captures (with repetition support via suffixes) that start with a colon and can have an optional regular expression constraint or unnamed captures that require a constraint. ``` ruby require 'mustermann/express' Mustermann.new('/:name/:rest+', type: :express).params('/a/b/c') # => { name: 'a', rest: 'b/c' } pattern = Mustermann.new('/:name', type: :express) pattern.respond_to? :expand # => true pattern.expand(name: 'foo') # => '/foo' pattern.respond_to? :to_templates # => true pattern.to_templates # => ['/{name}'] ``` ## Syntax
Syntax Element Description
:name Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
:name+ Captures one or more segments (with segments being separated by forward slashes). Capture is named name. Capture behavior can be modified with capture option.
:name* Captures zero or more segments (with segments being separated by forward slashes). Capture is named name. Capture behavior can be modified with capture option.
:name? Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Also matches an empty string. Capture behavior can be modified with capture and greedy option.
:name(regexp) Captures anything matching the regexp regular expression. Capture is named name. Capture behavior can be modified with capture.
(regexp) Captures anything matching the regexp regular expression. Capture is named splat. Capture behavior can be modified with capture.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/:name` | `/alice` | `{"name" => "alice"}` | | `/:foo/:bar` | `/hello/world` | `{"foo" => "hello", "bar" => "world"}` | | `/:name+` | `/a/b/c` | `{"name" => "a/b/c"}` | | `/:name(\d+)` | `/123` | `{"name" => "123"}` | | `/:name/:rest+` | `/alice/b/c` | `{"name" => "alice", "rest" => "b/c"}` | mustermann-4.0.0/docs/patterns/flask.md000066400000000000000000000161701517364653100201360ustar00rootroot00000000000000# The Flask Pattern The `flask` pattern type is implemented by the `mustermann-contrib` gem. It is compatible with [Flask](http://flask.pocoo.org/) and [Werkzeug](http://werkzeug.pocoo.org/). ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, `converters` and `ignore_unknown_options` **External documentation:** [Werkzeug: URL Routing](http://werkzeug.pocoo.org/docs/0.9/routing/) ``` ruby require 'mustermann/flask' Mustermann.new('//', type: :flask).params('/a/b/c') # => { prefix: 'a', page: 'b/c' } pattern = Mustermann.new('/', type: :flask) pattern.respond_to? :expand # => true pattern.expand(name: 'foo') # => '/foo' pattern.respond_to? :to_templates # => true pattern.to_templates # => ['/{name}'] ``` ## Syntax
Syntax Element Description
<name> Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
<converter:name> Captures depending on the converter constraint. Capture is named name. Capture behavior can be modified with capture and greedy option. See below.
<converter(arguments):name> Captures depending on the converter constraint. Capture is named name. Capture behavior can be modified with capture and greedy option. Arguments are separated by comma. An argument can be a simple string, a string enclosed in single or double quotes, or a key value pair (keys and values being separated by an equal sign). See below.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/` | `/alice` | `{"name" => "alice"}` | | `//` | `/hello/world` | `{"foo" => "hello", "bar" => "world"}` | | `/` | `/42` | `{"id" => 42}` | | `/` | `/a/b/c` | `{"rest" => "a/b/c"}` | | `//` | `/a/b/c` | `{"prefix" => "a", "page" => "b/c"}` | ## Converters ### Builtin Converters #### `string` Possible arguments: `minlength`, `maxlength`, `length` Captures anything but a forward slash in a semi-greedy fashion. Capture behavior can be modified with capture and greedy option. This is also the default converter. Examples: ``` ``` #### `int` Possible arguments: `min`, `max`, `fixed_digits` Captures digits. Captured value will be converted to an Integer. Examples: ``` ``` #### `float` Possible arguments: `min`, `max` Captures digits with a dot. Captured value will be converted to an Float. Examples: ``` ``` #### `path` Captures anything in a non-greedy fashion. Example: ``` ``` #### `any` Possible arguments: List of accepted strings. Captures anything that matches one of the arguments exactly. Example: ``` ``` ### Custom Converters A converter object may implement any of the following methods: * `convert`: Should return a block converting a string value to whatever value should end up in the `params` hash. * `constraint`: Should return a regular expression limiting which input string will match the capture. * `new`: Returns an object that may respond to `convert` and/or `constraint` as described above. Any arguments used for the converter inside the pattern will be passed to `new`. ``` ruby require 'mustermann/flask' SimpleConverter = Struct.new(:constraint, :convert) id_converter = SimpleConverter.new(/\d/, -> s { s.to_i }) class NumConverter def initialize(base: 10) @base = Integer(base) end def convert -> s { s.to_i(@base) } end def constraint @base > 10 ? /[\da-#{(@base-1).to_s(@base)}]/ : /[0-#{@base-1}]/ end end pattern = Mustermann.new('///', type: :flask, converters: { id: id_converter, num: NumConverter}) pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` ### Global Converters It is also possible to register a converter for all flask patterns, using `register_converter`: ``` ruby Mustermann::Flask.register_converter(:id, id_converter) Mustermann::Flask.register_converter(:num, NumConverter) pattern = Mustermann.new('///', type: :flask) pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` There is a handy syntax for quickly creating new converter classes: When you pass a block instead of a converter object, it will yield a generic converter with setters and getters for `convert` and `constraint`, and any arguments passed to the converter. ``` ruby require 'mustermann/flask' Mustermann::Flask.register_converter(:id) do |converter| converter.constraint = /\d/ converter.convert = -> s { s.to_i } end Mustermann::Flask.register_converter(:num) do |converter, base: 10| converter.constraint = base > 10 ? /[\da-#{(@base-1).to_s(base)}]/ : /[0-#{base-1}]/ converter.convert = -> s { s.to_i(base) } end pattern = Mustermann.new('///', type: :flask) pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` ### Subclassing Registering global converters will make these available for all Flask patterns. It might even override already registered converters. This global state might break unrelated code. It is therefore recommended that, if you don't want to pass in the converters option for every pattern, you create your own subclass of `Mustermann::Flask`. ``` ruby require 'mustermann/flask' MyFlask = Class.new(Mustermann::Flask) MyFlask.register_converter(:id) do |converter| converter.constraint = /\d/ converter.convert = -> s { s.to_i } end MyFlask.register_converter(:num) do |converter, base: 10| converter.constraint = base > 10 ? /[\da-#{(@base-1).to_s(base)}]/ : /[0-#{base-1}]/ converter.convert = -> s { s.to_i(base) } end pattern = MyFlask.new('///') pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` You can even register this type for usage with `Mustermann.new`: ``` ruby Mustermann.register(:my_flask, MyFlask) pattern = Mustermann.new('///', type: :my_flask) pattern.params('/10/12/f1') # => {"id"=>10, "oct"=>10, "hex"=>241} ``` mustermann-4.0.0/docs/patterns/hybrid.md000066400000000000000000000122541517364653100203160ustar00rootroot00000000000000# Hybrid Patterns The `hybrid` pattern type, implemented by the `mustermann` gem, tries to bridge the gap between `sinatra` and `rails`, by being largely compatible with both, while still supporting all features provided by Mustermann. Hybrid patterns are Rails- and Sinatra-like patterns, as well as compatible with simple URI templates. ``` ruby require 'mustermann' # Groups without | are implicitly optional (Rails style) pattern = Mustermann.new('/scope(/nested)', type: :hybrid) pattern === '/scope/nested' # => true pattern === '/scope' # => true pattern === '/scope/' # => false # Groups with | are not implicitly optional pattern = Mustermann.new('/scope/(a|b)', type: :hybrid) pattern === '/scope/a' # => true pattern === '/scope/b' # => true pattern === '/scope/' # => false # Use ? to make groups with | optional pattern = Mustermann.new('/scope/(a|b)?', type: :hybrid) pattern === '/scope/' # => true # Named captures, splats and URI template placeholders work as in sinatra pattern = Mustermann.new('/:controller(/:action(/:id))', type: :hybrid) pattern.params('/posts') # => { "controller" => "posts" } pattern.params('/posts/show') # => { "controller" => "posts", "action" => "show" } pattern = Mustermann.new('/*prefix/:name', type: :hybrid) pattern.params('/a/b/c') # => { "prefix" => "a/b", "name" => "c" } ``` **Supported options:** [`capture`](#-available-options--capture), [`except`](#-available-options--except), [`greedy`](#-available-options--greedy), [`space_matches_plus`](#-available-options--space_matches_plus), [`uri_decode`](#-available-options--uri_decode), [`ignore_unknown_options`](#-available-options--ignore_unknown_options). ## Compatibility notes * **Rails**: All syntax elements are supported. However, a group that includes a pipe operator will not be marked optional by default. So `/scope/(a|b)` will match both `/scope/a` and `/scope/b`, but not `/scope/`. Hybrid also supports additional syntax elements not supported by Rails, like an unnamed splat (`/scope/*`), or a question mark for marking a segment as optional (`/scope/(a|b)?`). * **Sinatra**: All syntax elements are supported, but a group without a pipe operator will be marked optional, even if it isn't followed by a question mark. So `/scope(/nested)` will match both `/scope/nested` and `/scope`. * **URI Templates**: Only simple placeholders (`/scope/{tenant_id}`), non-standard pipes (`/scope/{tenant_id|scope_id}`) and splats (`/scope/{+segments}`) are supported. ## Syntax
Syntax Element Description
:name or {name} Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
*name or {+name} Captures anything in a non-greedy fashion. Capture is named name.
* or {+splat} Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use it more than once in a pattern.
(expression) Enclosed expression is an implicitly optional group, equivalent to (expression)?. This matches Rails behavior. To create a non-optional group, use a pipe operator inside: (a|b).
(expression|expression|...) Will match anything matching the nested expressions. May contain any other syntax element, including captures. A group containing a pipe operator is not implicitly optional; use a trailing ? to make it optional.
x? Makes x optional. For groups containing |, this is the only way to make them optional.
/ Matches forward slash. Does not match URI encoded version of forward slash.
\x Matches x or URI encoded version of x. For instance \* matches *.
any other character Matches exactly that character or a URI encoded version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/:name` | `/alice` | `{"name" => "alice"}` | | `/scope(/nested)` | `/scope` | `{}` | | `/:file(.:ext)` | `/pony` | `{"file" => "pony", "ext" => nil}` | | `/:file(.:ext)` | `/pony.jpg` | `{"file" => "pony", "ext" => "jpg"}` | | `/(a\|b)` | `/a` | `{}` | | `/(a\|b)?` | `/` | `{}` | | `/:controller(/:action(/:id))` | `/posts/show` | `{"controller" => "posts", "action" => "show", "id" => nil}` | mustermann-4.0.0/docs/patterns/identity.md000066400000000000000000000020131517364653100206560ustar00rootroot00000000000000# The Identity Pattern Implemented in the `mustermann` gem. It is the least powerful pattern type, as it doesn't support any special syntax. It is useful if you want to match a fixed string or if you want to implement your own pattern type on top of it. ``` ruby require 'mustermann' pattern = Mustermann.new('/foo/bar', type: :identity) pattern === '/foo/bar' # => true pattern === '/foo/baz' # => false pattern.params('/foo/bar') # => {} ``` **Supported options:** [`uri_decode`](#-available-options--uri_decode), [`ignore_unknown_options`](#-available-options--ignore_unknown_options).
Syntax Element Description
any character Matches exactly that character or a URI escaped version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/foo/bar` | `/foo/bar` | `{}` | | `/users/42` | `/users/42` | `{}` | mustermann-4.0.0/docs/patterns/pyramid.md000066400000000000000000000052261517364653100205030ustar00rootroot00000000000000# The Pyramid Pattern. The `pyramid` pattern type is implemented by the `mustermann-contrib` gem. It is compatible with [Pyramid](http://www.pylonsproject.org/projects/pyramid/about) and [Pylons](http://www.pylonsproject.org/projects/pylons-framework/about). ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode` and `ignore_unknown_options` **External Documentation:** [Pylons Framework: URL Configuration](http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/configuration.html#url-config), [Pylons Book: Routes in Detail](http://pylonsbook.com/en/1.0/urls-routing-and-dispatch.html#routes-in-detail), [Pyramid: Route Pattern Syntax](http://docs.pylonsproject.org/projects/pyramid/en/1.5-branch/narr/urldispatch.html#route-pattern-syntax) ``` ruby require 'mustermann/pyramid' Mustermann.new('/{prefix}/*suffix', type: :pyramid).params('/a/b/c') # => { prefix: 'a', suffix: ['b', 'c'] } pattern = Mustermann.new('/{name}', type: :pyramid) pattern.respond_to? :expand # => true pattern.expand(name: 'foo') # => '/foo' pattern.respond_to? :to_templates # => true pattern.to_templates # => ['/{name}'] ``` ## Syntax
Syntax Element Description
{name} Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
{name:regexp} Captures anything matching the regexp regular expression. Capture is named name. Capture behavior can be modified with capture.
*name Captures anything in a non-greedy fashion. Capture is named name.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/{name}` | `/alice` | `{"name" => "alice"}` | | `/{foo}/{bar}` | `/hello/world` | `{"foo" => "hello", "bar" => "world"}` | | `/{id:\d+}` | `/42` | `{"id" => "42"}` | | `/{prefix}/*rest` | `/alice/b/c` | `{"prefix" => "alice", "rest" => ["b", "c"]}` | mustermann-4.0.0/docs/patterns/rails.md000066400000000000000000000103051517364653100201420ustar00rootroot00000000000000# The Rails Pattern The `rails` pattern is part of Mustermann (no mustermann-contrib needed). It is compatible with [Ruby on Rails](http://rubyonrails.org/), [Journey](https://github.com/rails/journey), the [http_router gem](https://github.com/joshbuddy/http_router), [Lotus](http://lotusrb.org/) and [Scalatra](http://scalatra.org/) (if [configured](http://scalatra.org/2.3/guides/http/routes.html#toc_248)) **Supported options:** [`capture`](#-available-options--capture), [`except`](#-available-options--except), [`greedy`](#-available-options--greedy), [`space_matches_plus`](#-available-options--space_matches_plus), [`uri_decode`](#-available-options--uri_decode), [`version`](#version), [`ignore_unknown_options`](#-available-options--ignore_unknown_options). **External documentation:** [Ruby on Rails Guides: Routing](http://guides.rubyonrails.org/routing.html). ``` ruby require 'mustermann' pattern = Mustermann.new('/:example', type: :rails) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => false pattern.params("/foo.bar") # => { "example" => "foo.bar" } pattern.params("/foo/bar") # => nil pattern = Mustermann.new('/:example(/:optional)', type: :rails) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern.params("/foo.bar") # => { "example" => "foo.bar", "optional" => nil } pattern.params("/foo/bar") # => { "example" => "foo", "optional" => "bar" } pattern = Mustermann.new('/*example', type: :rails) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern.params("/foo.bar") # => { "example" => "foo.bar" } pattern.params("/foo/bar") # => { "example" => "foo/bar" } ``` ## `version` Rails syntax changed over time. You can target different Ruby on Rails versions by setting the `version` option to the desired Rails version. The default is `8`. Versions prior to `2.3` are not supported. ``` ruby require 'mustermann' Mustermann.new('/', type: :rails, version: "2.3") Mustermann.new('/', type: :rails, version: "3.0.0") require 'rails' Mustermann.new('/', type: :rails, version: Rails::VERSION::STRING) ``` ## Syntax
Syntax Element Description
:name Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
*name Captures anything in a non-greedy fashion. Capture is named name.
(expression) Enclosed expression is optional. Not available in 2.3 compatibility mode.
/ Matches forward slash. Does not match URI encoded version of forward slash.
\x In 3.x compatibility mode and starting with 4.2: Matches x or URI encoded version of x. For instance \* matches *.
In 4.0 or 4.1 compatibility mode: \ is ignored, x is parsed normally.
expression | expression 3.2+ mode: This will raise a `Mustermann::ParseError`. While Ruby on Rails happily parses this character, it will result in broken routes due to a buggy implementation.
5.0 mode: It will match if any of the nested expressions matches.
any other character Matches exactly that character or a URI encoded version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/:name` | `/alice` | `{"name" => "alice"}` | | `/:foo/:bar` | `/hello/world` | `{"foo" => "hello", "bar" => "world"}` | | `/:name(/:optional)` | `/alice` | `{"name" => "alice", "optional" => nil}` | | `/*rest` | `/a/b/c` | `{"rest" => "a/b/c"}` | | `/:name/*rest` | `/alice/some/path` | `{"name" => "alice", "rest" => "some/path"}` | mustermann-4.0.0/docs/patterns/regexp.md000066400000000000000000000030511517364653100203220ustar00rootroot00000000000000# The *Regex* Pattern The `regexp` pattern type is implemented in the `mustermann` gem. It allows you to use regular expressions as patterns. ``` ruby require 'mustermann' pattern = Mustermann.new('/\d+', type: :regexp) pattern === '/123' # => true pattern === '/abc' # => false pattern = Mustermann.new('/(?\d{4})/(?\d{2})', type: :regexp) pattern.params('/2024/01') # => { "year" => "2024", "month" => "01" } ``` **Supported options:** [`uri_decode`](#-available-options--uri_decode), [`ignore_unknown_options`](#-available-options--ignore_unknown_options), `check_anchors`. The pattern string (or actual Regexp instance) should not contain anchors (`^` outside of square brackets, `$`, `\A`, `\z`, or `\Z`). Anchors will be injected where necessary by Mustermann. By default, Mustermann will raise a `Mustermann::CompileError` if an anchor is encountered. If you still want it to contain anchors at your own risk, set the `check_anchors` option to `false`. Using anchors will break [peeking](#-peeking) and [concatenation](#-concatenation).
Syntax Element Description
any string Interpreted as regular expression.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/\d+` | `/123` | `{}` | | `/(?\w+)` | `/alice` | `{"name" => "alice"}` | | `/(?\d{4})/(?\d{2})` | `/2024/01` | `{"year" => "2024", "month" => "01"}` | mustermann-4.0.0/docs/patterns/shell.md000066400000000000000000000036561517364653100201520ustar00rootroot00000000000000# Shell Syntax for Mustermann The `shell` pattern type is implemented by the `mustermann-contrib` gem. It is compatible with common Unix shells (like bash or zsh). ## Overview **Supported options:** `uri_decode` and `ignore_unknown_options`. **External documentation:** [Ruby's fnmatch](http://www.ruby-doc.org/core-2.1.4/File.html#method-c-fnmatch), [Wikipedia: Glob (programming)](http://en.wikipedia.org/wiki/Glob_(programming)) ``` ruby require 'mustermann' pattern = Mustermann.new('/*', type: :shell) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => false pattern = Mustermann.new('/**/*', type: :shell) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern = Mustermann.new('/{foo,bar}', type: :shell) pattern === "/foo" # => true pattern === "/bar" # => true pattern === "/baz" # => false ``` ## Syntax
Syntax Element Description
* Matches anything but a slash.
** Matches anything.
[set] Matches one character in set.
{a,b} Matches a or b.
\x Matches x or URI encoded version of x. For instance \* matches *.
any other character Matches exactly that character or a URI encoded version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/*` | `/foo.bar` | `{}` | | `/**` | `/foo/bar/baz` | `{}` | | `/**/*` | `/foo/bar` | `{}` | | `/{foo,bar}` | `/foo` | `{}` | | `/{foo,bar}` | `/bar` | `{}` | mustermann-4.0.0/docs/patterns/simple.md000066400000000000000000000063121517364653100203240ustar00rootroot00000000000000# Simple Syntax for Mustermann The `simple` pattern type is implemented by the `mustermann-contrib` gem. It is compatible with [Sinatra](http://www.sinatrarb.com/) (1.x), [Scalatra](http://scalatra.org/) and [Dancer](http://perldancer.org/). It is based on the original Sinatra (1.x) pattern implementation. It isn't AST based and thus does not work for all features provided by Mustermann, like Trie-based pattern matching or pattern expansion. ## Overview **Supported options:** `greedy`, `space_matches_plus`, `uri_decode` and `ignore_unknown_options`. This is useful for porting an application that relies on this behavior to a later Sinatra version and to make sure Sinatra 2.0 patterns do not decrease performance. Simple patterns internally use the same code older Sinatra versions used for compiling the pattern. Error messages for broken patterns will therefore not be as informative as for other pattern implementations. ``` ruby require 'mustermann' pattern = Mustermann.new('/:example', type: :simple) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => false pattern.params("/foo.bar") # => { "example" => "foo.bar" } pattern.params("/foo/bar") # => nil pattern = Mustermann.new('/:example/?:optional?', type: :simple) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern.params("/foo.bar") # => { "example" => "foo.bar", "optional" => nil } pattern.params("/foo/bar") # => { "example" => "foo", "optional" => "bar" } pattern = Mustermann.new('/*', type: :simple) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => true pattern.params("/foo.bar") # => { "splat" => ["foo.bar"] } pattern.params("/foo/bar") # => { "splat" => ["foo/bar"] } ``` ## Syntax
Syntax Element Description
:name Captures anything but a forward slash in a greedy fashion. Capture is named name.
* Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use * more than once in a pattern.
x? Makes x optional. For instance foo? matches foo or fo.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any special character Matches exactly that character or a URI encoded version of it.
any other character Matches exactly that character.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/:name` | `/alice` | `{"name" => "alice"}` | | `/:foo/:bar` | `/hello/world` | `{"foo" => "hello", "bar" => "world"}` | | `/*` | `/a/b/c` | `{"splat" => ["a/b/c"]}` | | `/:name/*` | `/alice/b/c` | `{"name" => "alice", "splat" => ["b/c"]}` | | `/:example/?:optional?` | `/foo` | `{"example" => "foo", "optional" => nil}` | mustermann-4.0.0/docs/patterns/sinatra.md000066400000000000000000000065511517364653100205010ustar00rootroot00000000000000# `sinatra` The `sinatra` pattern type is implemented by Mustermann itself. Moreover, it is the default pattern type, chosen by Mustermann if the `type` option is not specified. **Supported options:** [`capture`](#-available-options--capture), [`except`](#-available-options--except), [`greedy`](#-available-options--greedy), [`space_matches_plus`](#-available-options--space_matches_plus), [`uri_decode`](#-available-options--uri_decode), [`ignore_unknown_options`](#-available-options--ignore_unknown_options). # Overview ``` ruby require 'mustermann' pattern = Mustermann.new('/:name') pattern === '/alice' # => true pattern === '/alice/bob' # => false pattern.params('/alice') # => { "name" => "alice" } pattern = Mustermann.new('/:foo/:bar') pattern.params('/hello/world') # => { "foo" => "hello", "bar" => "world" } pattern = Mustermann.new('/*') pattern.params('/a/b/c') # => { "splat" => ["a/b/c"] } ``` ## Syntax
Syntax Element Description
:name or {name} Captures anything but a forward slash in a semi-greedy fashion. Capture is named name. Capture behavior can be modified with capture and greedy option.
*name or {+name} Captures anything in a non-greedy fashion. Capture is named name.
* or {+splat} Captures anything in a non-greedy fashion. Capture is named splat. It is always an array of captures, as you can use it more than once in a pattern.
(expression) Enclosed expression is a group. Useful when combined with ? to make it optional, or to separate two elements that would otherwise be parsed as one.
expression|expression|... Will match anything matching the nested expressions. May contain any other syntax element, including captures.
x? Makes x optional. For instance, (foo)? matches foo or an empty string.
/ Matches forward slash. Does not match URI encoded version of forward slash.
\x Matches x or URI encoded version of x. For instance \* matches *.
any other character Matches exactly that character or a URI encoded version of it.
## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/:name` | `/alice` | `{"name" => "alice"}` | | `/:foo/:bar` | `/hello/world` | `{"foo" => "hello", "bar" => "world"}` | | `/*` | `/a/b/c` | `{"splat" => ["a/b/c"]}` | | `/:name/*` | `/alice/some/path` | `{"name" => "alice", "splat" => ["some/path"]}` | | `/:foo(/:bar)` | `/hello` | `{"foo" => "hello", "bar" => nil}` | mustermann-4.0.0/docs/patterns/template.md000066400000000000000000000076571517364653100206630ustar00rootroot00000000000000# URI Template Syntax for Mustermann The `uri-template` (or `template`) pattern type is implemented by the `mustermann-contrib` gem. It is compatible with [RFC 6570](https://tools.ietf.org/html/rfc6570) (level 4), [JSON API](http://jsonapi.org/), [JSON Home Documents](http://tools.ietf.org/html/draft-nottingham-json-home-02) and [many more](https://code.google.com/p/uri-templates/wiki/Implementations) ## Overview **Supported options:** `capture`, `except`, `greedy`, `space_matches_plus`, `uri_decode`, and `ignore_unknown_options`. Please keep the following in mind: > "Some URI Templates can be used in reverse for the purpose of variable matching: comparing the template to a fully formed URI in order to extract the variable parts from that URI and assign them to the named variables. Variable matching only works well if the template expressions are delimited by the beginning or end of the URI or by characters that cannot be part of the expansion, such as reserved characters surrounding a simple string expression. In general, regular expression languages are better suited for variable matching." > — *RFC 6570, Sec 1.5: "Limitations"* ``` ruby require 'mustermann' pattern = Mustermann.new('/{example}', type: :template) pattern === "/foo.bar" # => true pattern === "/foo/bar" # => false pattern.params("/foo.bar") # => { "example" => "foo.bar" } pattern.params("/foo/bar") # => nil pattern = Mustermann.new("{/segments*}/{page}{.ext,cmpr:2}", type: :template) pattern.params("/a/b/c.tar.gz") # => {"segments"=>["a","b"], "page"=>"c", "ext"=>"tar", "cmpr"=>"gz"} ``` ## Generating URI Templates You do not need to use URI templates (and this gem) if all you want is reusing them for hypermedia links. Most other pattern types support generating these (via `#to_pattern`): ``` ruby require 'mustermann' Mustermann.new('/:name').to_templates # => ['/{name}'] ``` Moreover, Mustermann's default pattern type implements a subset of URI templates (`{capture}` and `{+capture}`) and can therefore also be used for simple templates/ ``` ruby require 'mustermann' Mustermann.new('/{name}').expand(name: "example") # => "/example" ``` ## Syntax
Syntax Element Description
{o var m, var m, ...} Captures expansion. Operator o: + # . / ; ? & or none. Modifier m: :num * or none.
/ Matches forward slash. Does not match URI encoded version of forward slash.
any other character Matches exactly that character or a URI encoded version of it.
The operators `+` and `#` will always match non-greedy, whereas all other operators match semi-greedy by default. All modifiers and operators are supported. However, it does not parse lists as single values without the *explode* modifier (aka *star*). Parametric operators (`;`, `?` and `&`) currently only match parameters in given order. Note that it differs from URI templates in that it takes the unescaped version of special character instead of the escaped version. If you reuse the exact same templates and expose them via an external API meant for expansion, you should set `uri_decode` to `false` in order to conform with the specification. ## Examples | Pattern | String | Params | |---------|--------|--------| | `/foo` | `/foo` | `{}` | | `/{example}` | `/foo` | `{"example" => "foo"}` | | `/{foo}/{bar}` | `/hello/world` | `{"foo" => "hello", "bar" => "world"}` | | `/{+path}` | `/a/b/c` | `{"path" => "a/b/c"}` | | `{/segments*}` | `/a/b/c` | `{"segments" => ["a", "b", "c"]}` | | `{/segments*}/{page}{.ext}` | `/a/b/c.html` | `{"segments" => ["a", "b"], "page" => "c", "ext" => "html"}` | mustermann-4.0.0/docs/performance.md000066400000000000000000000264311517364653100175000ustar00rootroot00000000000000# Performance Mustermann is designed so that as much work as possible happens at object-creation time, keeping matching and expansion fast at request time. This document explains what that means in practice for single patterns, for `Mustermann::Set`, and for URL expansion. ## Pattern objects Pattern objects are immutable once created. To avoid redundant compilation, `Mustermann.new` may return the same instance for the same arguments as long as that instance has not been garbage collected: ```ruby Mustermann.new("/:name").equal? Mustermann.new("/:name") # may be true ``` Do not rely on object identity — the guarantee is that arguments producing equal patterns may reuse an existing object, not that they always will. ### Which pattern types use an AST? The regex optimizations described in the next section apply only to pattern types that compile from an abstract syntax tree. That covers `sinatra` (the default), `rails`, `hybrid`, `template`, and `flask`. It does not cover `identity`, `shell`, `simple`, or `regexp`. ## Single-pattern matching ### Bounded character classes The first and most important performance measure is the default capture character class. A named segment such as `/:name` compiles to `(?[^\/\?#]+)`: it cannot match a `/`, `?`, or `#`. This means segments are naturally isolated — they can never greedily consume a path separator or a query-string delimiter. ```ruby Mustermann.new("/:a/:b").to_regexp # => /\A(?-mix:\/(?[^\/\?#]+)\/(?[^\/\?#]+))\Z/ ``` Because `[^\/\?#]` already stops at `/`, there is no ambiguity between `:a` and `:b`. The regex engine matches in a single forward pass with no backtracking. ### Look-ahead for adjacent captures When two captures share the same character class and no literal separator stands between them — for instance `/:a:b?` — the first capture could greedily consume everything, leaving nothing for the second. In this case Mustermann inserts a negative look-ahead into the first capture's repetition: ```ruby Mustermann.new("/:a:b?").to_regexp # => /\A(?-mix:\/(?(?:(?!(?:(?!)[^\/\?#])+?$)[^\/\?#])+)(?:(?[^\/\?#]+))?)\Z/ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ # look-ahead: each char of :a must # not start a sequence satisfying :b$ ``` The look-ahead says: at each position within `:a`, check that the remaining input is not already a valid match for what comes after. This prevents `:a` from consuming characters that belong to `:b`, avoiding the O(n²) backtracking that a naive NFA engine would otherwise require when `:b` fails to match. This transformation is performed automatically by the AST transformer. It fires whenever a capture is immediately followed by an optional capture (or a group ending in a capture) that shares the same character domain, without a separator literal between them. For the much more common case — captures separated by `/` literals — look-ahead is not needed and is not inserted. ### Atomic groups An *atomic group* `(?>...)` tells the regex engine to commit to the match it has found so far and never backtrack into it. Ruby's Oniguruma/Onigmo engine supports this syntax. Mustermann emits atomic groups for captures that are immediately followed by a path separator (`/`). Every Mustermann capture character class — Sinatra's `[^\/\?#]+`, template's `[\w\-\.~%]+`, and so on — excludes `/`, so the greedy match naturally stops at the right boundary. Committing atomically simply tells the engine not to second-guess that result: ```ruby Mustermann.new("/:a/:b/:c").to_regexp # => /\A(?-mix:\/(?(?>[^\/\?#]+))\/(?(?>[^\/\?#]+))\/(?[^\/\?#]+))\Z/ # ^^^^^ ^^^^^ # :a and :b are atomic (each followed by "/") # :c is not atomic (nothing follows it) ``` The last capture in a chain is not atomicized because end-of-array does not always mean end-of-pattern: template expressions nest captures inside inner arrays, and atomicizing a capture inside `{name}bar` would commit to consuming `bar`, preventing the literal from matching. For patterns involving adjacent captures or optional format segments (e.g. `/:file(.:ext)`), the look-ahead transformer (see above) fires instead. The head capture of a `with_look_ahead` node is always made atomic, because the look-ahead constraint already limits exactly how far the capture can extend — no backtracking is needed or useful: ```ruby Mustermann.new("/:id(.:format)", type: :rails).to_regexp # => /\A(?-mix:\/(?(?>(?:(?!...)[^\/\?#])+))(?:\.(?[^\/\?#]+))?)$/ # ^^^^ # :id is atomic (look-ahead head) ``` On a failing input like `/` + `"a" * 10_000 + "/" + "a" * 5_000 + "/"` (one trailing slash that makes the overall pattern fail), patterns with atomic groups run measurably faster because the engine does not re-examine characters already committed. ### Splats are non-greedy Splat captures (`*` or `/*name`) compile to `.*?` (non-greedy). This ensures they consume as little as possible, letting any following literal or named segment match first: ```ruby Mustermann.new("/*path/:name").to_regexp # => /\A(?-mix:\/(?.*?)\/(?[^\/\?#]+))\Z/ ``` Non-greedy quantifiers also reduce unnecessary backtracking compared to greedy `.*`. ### URI encoding alternatives Literal characters in patterns match both the raw character and its percent-encoded forms. A space in a pattern becomes `(?: |%20|+|%2B|%2b)`, a dot becomes `(?:\.|%2E|%2e)`, and so on. This is precomputed at pattern-creation time and does not add per-match overhead beyond the extra alternatives in the NFA. In the trie-based matcher (see below), a need for URI escaping is completely eliminated by these using different edges for raw and encoded characters. ## Set matching: linear vs. trie `Mustermann::Set` maintains a routing table — each pattern is associated with one or more values, and `set.match(string)` finds the first pattern that matches and returns both the captures and the value. Two matching strategies are available: **linear** and **trie**. A caching wrapper can be layered on top of either (only caching matches that haven't been garbage collected yet, so it comes with no noticeable memory overhead). ### Linear matching The linear matcher iterates through patterns in insertion order and tries each one: ``` Input: "/users/42" Pattern 1: /posts/:slug → no match Pattern 2: /users/:id → match! params: {id: "42"} Pattern 3: ... → not reached ``` This is `O(n)` in the number of patterns. For small routing tables (fewer than ~50 routes) the constant factor is low enough that linear scanning is often the fastest option, because there is no trie-construction overhead and no pointer chasing through tree nodes. ### Trie matching The trie matcher builds a prefix tree from the AST of every pattern. During matching it walks the input string one character at a time, following the edge that matches the current character. At each node it considers two kinds of edges: - **Static edges** — keyed by an exact character (e.g. `/`, `u`, `s`…). Shared prefixes are traversed once regardless of how many patterns start with them. - **Dynamic edges** — keyed by a compiled regexp fragment (e.g. `(?[^\/\?#]+)` for a named segment). When a static edge does not match, every dynamic edge at the current node is tried against the remaining input. ``` Input: "/users/42" Trie root └─ '/' ├─ 'p' → 'o' → 's' → 't' → 's' → '/' → [dynamic: :slug] ← skipped ├─ 'u' → 's' → 'e' → 'r' → 's' → '/' → [dynamic: :id] ← matched └─ ... ``` Once the static prefix `/users/` has been confirmed, only the patterns that share that prefix compete for the dynamic segment. For large routing tables the total work grows logarithmically rather than linearly with the number of routes. The trie is built during `Set#add` from the pattern's AST nodes. Splat segments (`.*?`) and non-separator dynamic segments are compiled to regex fragments and stored as dynamic edges; separators and literal characters become static edges. ### Choosing between them `Mustermann::Set` switches from linear to trie automatically based on the `use_trie:` option: | Value | Behavior | |-------|----------| | `false` | Always use linear matching. | | `true` | Always use trie matching. | | Integer `n` (default: `50`) | Use linear until the set contains `n` or more patterns, then switch permanently to trie. | ```ruby # Force trie from the first pattern set = Mustermann::Set.new(use_trie: true) # Keep linear always (e.g. for a tiny router) set = Mustermann::Set.new(use_trie: false) # Switch after 20 patterns instead of the default 50 set = Mustermann::Set.new(use_trie: 20) ``` For most applications the default threshold of 50 is appropriate: small apps benefit from the lower constant cost of linear, large apps get sub-linear trie dispatch. If you want, you can see what the performance looks like with the included `bench/set.rb` script: ```sh bundle exec ruby bench/set.rb bundle exec ruby bench/set.rb --trie true bundle exec ruby bench/set.rb --no-trie bundle exec ruby bench/set.rb --routes 10,50,100,500 --nesting 2 --trie 20 ``` ### Caching A `Cache` layer can be wrapped around either matcher. It stores the result of each `match` call keyed by the input string object, using `ObjectSpace::WeakKeyMap` (if available) so that entries are evicted automatically when the string is no longer referenced elsewhere. ```ruby set = Mustermann::Set.new(use_cache: true) # default set = Mustermann::Set.new(use_cache: false) # disable ``` The cache is transparent: `set.match(string)` returns the same `Set::Match` object on repeated calls with the same string instance without re-running the trie or linear scan. Adding a new pattern clears the cache entirely. The cache is most valuable when the same path strings are looked up repeatedly in a long-running process (e.g. a Rack application handling many requests for the same resource). It has no benefit for one-shot scripts or benchmarks that generate unique strings for every call. ### Thread safety Matching and expansion on a set are **thread-safe** once the set has been built. The internal trie and cache are read-only after construction. **Adding patterns is not thread-safe.** The recommended practice is to populate the set (and the Router built on top of it) during application startup, before requests begin, and then treat it as read-only. ## URL expansion `Mustermann::Expander` (used internally by `Set#expand` and `Router#path_for`) generates URLs from a parameter hash. Like pattern compilation, the bulk of the work happens once at expander-creation time: - Each pattern's named segments and their optional combinations are enumerated upfront. - At expansion time, the expander selects the first pattern whose required keys are all present in the supplied hash and fills them in. Trade-offs: - **Memory grows with optional combinations.** A pattern like `"/(:foo/)?:bar?"` has four possible expansions (both present, only foo, only bar, neither). The expander stores all of them. `"/:foo/:bar"` has only one. - **Capture constraints, type casting, and greediness are ignored** during expansion. The expander produces a valid URL string without re-running match logic. - **Partial expansion is not yet supported.** All required segments must be supplied. mustermann-4.0.0/mustermann-contrib/000077500000000000000000000000001517364653100175465ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/LICENSE000066400000000000000000000020371517364653100205550ustar00rootroot00000000000000Copyright (c) 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. mustermann-4.0.0/mustermann-contrib/README.md000066400000000000000000000330101517364653100210220ustar00rootroot00000000000000# The Amazing Mustermann - Contrib Edition This is a meta gem that depends on all mustermann gems. ``` console $ gem install mustermann-contrib Successfully installed mustermann-1.0.0 Successfully installed mustermann-contrib-1.0.0 ... ``` Also handy for your `Gemfile`: ``` ruby gem 'mustermann-contrib' ``` Alternatively, you can use latest HEAD from github: ```ruby github 'sinatra/mustermann' do gem 'mustermann' gem 'mustermann-contrib' end ``` # CakePHP Syntax for Mustermann See [docs/patterns/cake.md](../docs/patterns/cake.md). # Express Syntax for Mustermann See [docs/patterns/express.md](../docs/patterns/express.md). # FileUtils for Mustermann This gem implements efficient file system operations for Mustermann patterns. ## Globbing All operations work on a list of files described by one or more pattern. ``` ruby require 'mustermann/file_utils' Mustermann::FileUtils[':base.:ext'] # => ['example.txt'] Mustermann::FileUtils.glob(':base.:ext') do |file, params| file # => "example.txt" params # => {"base"=>"example", "ext"=>"txt"} end ``` To avoid having to loop over all files and see if they match, it will generate a glob pattern resembling the Mustermann pattern as closely as possible. ``` ruby require 'mustermann/file_utils' Mustermann::FileUtils.glob_pattern('/:name') # => '/*' Mustermann::FileUtils.glob_pattern('src/:path/:file.(js|rb)') # => 'src/**/*/*.{js,rb}' Mustermann::FileUtils.glob_pattern('{a,b}/*', type: :shell) # => '{a,b}/*' pattern = Mustermann.new('/foo/:page', '/bar/:page') # => # Mustermann::FileUtils.glob_pattern(pattern) # => "{/foo/*,/bar/*}" ``` ## Mapping It is also possible to search for files and have their paths mapped onto another path in one method call: ``` ruby require 'mustermann/file_utils' Mustermann::FileUtils.glob_map(':base.:ext' => ':base.bak.:ext') # => {'example.txt' => 'example.bak.txt'} Mustermann::FileUtils.glob_map(':base.:ext' => :base) { |file, mapped| mapped } # => ['example'] ``` This mechanism allows things like copying, renaming and linking files: ``` ruby require 'mustermann/file_utils' # copies example.txt to example.bak.txt Mustermann::FileUtils.cp(':base.:ext' => ':base.bak.:ext') # copies Foo.app/example.txt to Foo.back.app/example.txt Mustermann::FileUtils.cp_r(':base.:ext' => ':base.bak.:ext') # creates a symbolic link from bin/example to lib/example.rb Mustermann::FileUtils.ln_s('lib/:name.rb' => 'bin/:name') ``` # Mapper for Mustermann ## Overview `Mustermann::Mapper` transforms strings according to a set of pattern mappings. Each mapping pairs an input pattern (used to extract parameters) with one or more output patterns (used to expand the result). All mappings that match are applied in insertion order. ``` ruby require 'mustermann/mapper' mapper = Mustermann::Mapper.new("/:page(.:format)?" => ["/:page/view.:format", "/:page/view.html"]) mapper['/foo'] # => "/foo/view.html" mapper['/foo.xml'] # => "/foo/view.xml" mapper['/foo/bar'] # => "/foo/bar" ``` You can also pass additional values at conversion time to supplement or override captured parameters: ``` ruby mapper = Mustermann::Mapper.new("/:example" => "(/:prefix)?/:example.html") mapper['/foo', prefix: 'en'] # => "/en/foo.html" ``` ## Building a Mapper Mappings can be supplied as a hash, added via `[]=`, or built with a block: ``` ruby # Hash argument mapper = Mustermann::Mapper.new("/:a" => "/:a.html", "/:a/:b" => "/:b/:a") # Block (zero-argument, returns a hash) mapper = Mustermann::Mapper.new { { "/:a" => "/:a.html" } } # Block (one-argument, imperative) mapper = Mustermann::Mapper.new do |m| m["/:a"] = "/:a.html" end # Incremental mapper = Mustermann::Mapper.new mapper["/:a"] = "/:a.html" ``` The output value may be a String, a `Mustermann::Pattern`, an `Array` of either (tried in order until one expands successfully), or a `Mustermann::Expander` directly. # Flask Syntax for Mustermann See [docs/patterns/flask.md](../docs/patterns/flask.md). # Pyramid Syntax for Mustermann See [docs/patterns/pyramid.md](../docs/patterns/pyramid.md). # Shell Syntax for Mustermann See [docs/patterns/shell.md](../docs/patterns/shell.md). # Simple Syntax for Mustermann See [docs/patterns/simple.md](../docs/patterns/simple.md). # String Scanner for Mustermann This gem implements `Mustermann::StringScanner`, a tool inspired by Ruby's [`StringScanner`]() class. ``` ruby require 'mustermann/string_scanner' scanner = Mustermann::StringScanner.new("here is our example string") scanner.scan("here") # => "here" scanner.getch # => " " if scanner.scan(":verb our") scanner.scan(:noun, capture: :word) scanner[:verb] # => "is" scanner[:nound] # => "example" end scanner.rest # => "string" ``` You can pass it pattern objects directly: ``` ruby pattern = Mustermann.new(':name') scanner.check(pattern) ``` Or have `#scan` (and other methods) check these for you. ``` ruby scanner.check('{name}', type: :template) ``` You can also pass in default options for ad hoc patterns when creating the scanner: ``` ruby scanner = Mustermann::StringScanner.new(input, type: :shell) ``` # `to_pattern` for Mustermann ## Overview `mustermann/to_pattern` adds a `to_pattern` method to `String`, `Symbol`, `Regexp`, `Array`, and `Mustermann::Pattern`, and provides the `Mustermann::ToPattern` mixin so you can add the same method to your own classes. ``` ruby require 'mustermann/to_pattern' "/foo".to_pattern # => # "/foo".to_pattern(type: :rails) # => # %r{/foo}.to_pattern # => # "/foo".to_pattern.to_pattern # => # ``` ## `Mustermann::ToPattern` mixin Include `Mustermann::ToPattern` in any class to get a `to_pattern` method driven by its `to_s` output: ``` ruby require 'mustermann/to_pattern' class MyRoute include Mustermann::ToPattern def to_s "/users/:id" end end MyRoute.new.to_pattern # => # MyRoute.new.to_pattern(type: :rails) # => # ``` If your class wraps another object (via `__getobj__`, as in `Delegator` subclasses), `to_pattern` will unwrap it before converting. # URI Template Syntax for Mustermann See [docs/patterns/template.md](../docs/patterns/template.md). # Mustermann Pattern Visualizer With this gem, you can visualize the internal structure of a Mustermann pattern: * You can generate a **syntax highlighted** version of a pattern object. Both HTML/CSS based highlighting and ANSI color code based highlighting is supported. * You can turn a pattern object into a **tree** (with ANSI color codes) representing the internal AST. This of course only works for AST based patterns. ## Syntax Highlighting ![](highlighting.png) Loading `mustermann/visualizer` will automatically add `to_html` and `to_ansi` to pattern objects. ``` ruby require 'mustermann/visualizer' puts Mustermann.new('/:name').to_ansi puts Mustermann.new('/:name').to_html ``` Alternatively, you can also create a separate `highlight` object, which allows finer grained control and more formats: ``` ruby require 'mustermann/visualizer' pattern = Mustermann.new('/:name') highlight = Mustermann::Visualizer.highlight(pattern) puts highlight.to_ansi ``` ### `inspect` mode By default, the highlighted string will be a colored version of `to_s`. It is also possible to produce a colored version of `inspect` ``` ruby require 'mustermann/visualizer' pattern = Mustermann.new('/:name') # directly from the pattern puts pattern.to_ansi(inspect: true) # via the highlighter highlight = Mustermann::Visualizer.highlight(pattern, inspect: true) puts highlight.to_ansi ``` ### Themes ![](theme.png) element | inherits style from | default theme | note -------------|---------------------|---------------|------------------------- default | | #839496 | ANSI `\e[10m` if not set special | default | #268bd2 | capture | special | #cb4b16 | name | | #b58900 | always inside `capture` char | default | | expression | capture | | only exists in URI templates composition | special | | meta style, does not exist directly composite | composition | | used for composite patterns (contains `root`s) group | composition | | union | composition | | optional | special | | root | default | | wraps the whole pattern separator | char | #93a1a1 | splat | capture | | named_splat | splat | | variable | capture | | always inside `expression` escaped | char | #93a1a1 | escaped_char | | | always inside `escaped` quote | special | #dc322f | always outside of `root` type | special | | always inside `composite`, outside of `root` illegal | special | #8b0000 | You can set theme any of the above elements. The default theme will only be applied if no custom theming is used. ``` ruby # custom theme with highlight object highlight = Mustermann::Visualizer.highlight(pattern, special: "#08f") puts highlight.to_ansi ``` Themes apply both to ANSI and to HTML/CSS output. The exact ANSI code used depends on the terminal and its capabilities. ### HTML and CSS By default, the syntax elements will be translated into `span` tags with `style` attributes. ``` ruby Mustermann.new('/:name').to_html ``` ``` html /:name ``` You can also set the `css` option to `true` to make it include a stylesheet instead. ``` ruby Mustermann.new('/:name').to_html(css: true) ``` ``` html /:name ``` Or you can set it to `false`, which will omit `style` attributes, but include `class` attributes. ``` html /:name ``` It is possible to change the class prefix and the tag used. ``` ruby Mustermann.new('/:name').to_html(css: false, class_prefix: "mm_", tag: "tt") ``` ``` html /:name ``` If you create a highlight object, you can ask it for its `stylesheet`. ``` erb <% highlight = Mustermann::Visualizer.highlight("/:name") %> <%= highlight.to_html(css: false) %> ``` ### Other formats If you create a highlight object, you have two other formats available: Hansi template strings and s-expression like strings. These might be useful if you want to check how a theme will be applied or as intermediate format for highlighting by other means. ``` ruby require 'mustermann/visualizer' highlight = Mustermann::Visualizer.highlight("/:page") puts highlight.to_hansi_template puts highlight.to_sexp ``` **Hansi template strings** wrap elements in tags that are similar to XML tags (though they are not, entity encoding and attributes are not supported, escaping works with a slash, so an escaped `>` would be `\>`, not `>`). ``` xml /:page ``` The **s-expression like syntax** looks as follows: ``` (root (separator /) (capture : (name page))) ``` * An expression is enclosed by parens and contains elements separated by spaces. The first element in the expression type (corresponding to themeable elements). These are simple strings. The other elements are either expressions, simple strings or full strings. * Simple strings do not contain spaces, parens, single or double quotes or any character that needs to be escaped. * Full strings are Ruby strings enclosed by double quotes. * Spaces before or after parens are optional. ## Tree Rendering ![](tree.png) Loading `mustermann/visualizer` will automatically add `to_tree` to pattern objects. ``` ruby require 'mustermann/visualizer' puts Mustermann.new("/:page(.:ext)?/*action").to_tree ``` For patterns not based on an AST (shell, simple, regexp), it will print out a single line: pattern (not AST based) "/example" It will display a tree for identity patterns. While these are not based on an AST internally, Mustermann supports generating an AST for these patterns. mustermann-4.0.0/mustermann-contrib/examples/000077500000000000000000000000001517364653100213645ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/examples/highlighting.rb000066400000000000000000000022741517364653100243630ustar00rootroot00000000000000require 'bundler/setup' require 'mustermann/visualizer' Hansi.mode = ARGV[0].to_i if ARGV.any? def self.example(type, *patterns) print Hansi.render(:bold, " #{type}: ".ljust(14)) patterns.each do |pattern| pattern = Mustermann.new(pattern, type: type) space_after = pattern.to_s.size > 24 ? " " : " " * (25 - pattern.to_s.size) highlight = Mustermann::Visualizer.highlight(pattern, inspect: true) print highlight.to_ansi + space_after end puts end puts example(:cake, '/:prefix/**') example(:express, '/:prefix+/:id(\d+)', '/:page/:slug+') example(:flask, '//', '/user/') example(:identity, '/image.png') example(:pyramid, '/{prefix:.*}/{id}', '/{page}/*slug') example(:rails, '/:slug(.:ext)') example(:regexp, '/(?[^/]+)', '/(?:page|user)/(\d+)') example(:shell, '/**/*', '/\{a,b\}/{a,b}') example(:simple, '/:page/*slug') example(:sinatra, '/:page/*slug', '/users/{id}?') example(:template, '/{+pre}/{page}{?q,p}', '/users/{id}?') puts example(:composition) composite = Mustermann.new("/{a}", "/{b}/{c}") puts " " + composite.to_ansi puts " " + (Mustermann.new("/") ^ composite).to_ansi putsmustermann-4.0.0/mustermann-contrib/highlighting.png000066400000000000000000002534461517364653100227370ustar00rootroot00000000000000PNG  IHDRa? iCCPICC ProfileH wTS-@轷{"ͮȠ#(:TGG@ƂbȠ> 6T z'[gg}V>ZO(LG22E!n!PA A txB& ucqMtB >|(-e |cYb3sWV q@tO$r1`uQe&db,O%`|c|߈s22c:M cj c/[}O3sg_3Cِ&9cJ $bse a%v" yjWʆQW56H61j56"e52LL LL^jƘ00bfmnv잹yy+  E Kzn˗VVVn[37[YٴLjr v;7v'>g!͡ar;j8G8NqN?9:;h$4u҂Қ#;2q3e22._>,4 GveME+KVtgbWrr~uʭ}w4_:?3J[W>Y}kl\3um:d] Olм1mfo6En)T)P8mEE[6moI2r랭_/T|*^Ǫ%m,)۷=s;˥W Y(xkٮVusvVTuڳ}ϧZvw{^粯NO)?ݮliOܟ?sniTj,iܔ4bn-kCrڦ:qݸQr~K/7;=ǘNJ;Ε]]8䷦'jNʝ,;E=Uxj3gg;uFs.:vu '._<~{+WnAΫWz |uo޸J@zCyo:}e& 1-b 6׊$Ky@"̚{@,;7@o>1ՉR,"rh}/Ty6SiTXtXML:com.adobe.xmp 865 412 q@IDATx} XT`Fn3mFTLLR LS`&R_A߹@ON=/s*c'Kʎp쨐dr83r:o ìk]w"@ D"0aa"@ D"!9"lD"@ @C D"@D"@ >##D"@ #6@ D"8D"@ 䈰 @ D"#9>"0@ D"9"lD"@ @C D"@D"@ >##D"@ #6@ D"8D"@ 䈰 @ D"#9>"0@ D"9"lD"@ @C D"@D"@ >##D"@ #6@ D"8D"@ 䈰 @ D"#O)q D c83*h(Ubsuo"*YE=]7Gz;9n5,- h{վƆ/lLצ[;dO5ڨV#v)|IkA ,D`ݻwgٲ$ENbhF2⩒kRo ;+"5CKFJQ_:qxH_K;:ViK@ kB'k%8ADg'ADz3ih/H.}kA \gS nxށ5 62oowO /D"ՐDx.F+~&ysvNZiq{y'SM=`Y#. zb9m ~|qk/9+/4"@P vE;4Jhץ\7{A. bI:q N?C<5"8Or$YhwNx7{k+KYD"@( y gZyK`hvVUa(d+` O֢~oK:?+|XUc4W@ 3+ 22DM9|hqPl|dhmAJ9%sxE]|2bȘ“3woVlRHq@3Ϝ+̛|IF0@ lƻrGXdhRT!Il>"dL\[9<ŘqT1. {2iXWEڼ԰Ⲣ;x6; #U-1וKb"@ 4−$at1= MLMM*j*J˪dQVfPs"SHsuez\hARpFq37>$ 25'#VZWQfq') *W0¨p_&e(FVRimUIذ jY^U9y2o7<摸!"_K #oB#7? ;~KUt k=k4?]TemS8L(c` ݊6bu] ێx@R*z3fPS~}=Nmid5i<5Mi8ű:&VY4_PL@ p )L&&#J9Sy0WVcUUJJD(KRD8ݲS1X899)2!899Ԡ .ȉITڂRb(sV.sٙ.˺僧/]|Y~lIl.4:tlrc %/44n#8D Hvlu5`龡_01r\1vEK.ˎp;~D\//B'k^xKCԍJ}Dm?`d._:K >p5Vpv .:Z͗#D"#`(TdL;ҏjjqhkDYQ%.#Rʑ|Jzj(%聿viC+̡L%zy̋y&jZb= 'Z$ )12<唀5zǼ͗1F DE Ppu:ZEςcͺGr;1WʪkI_Fus`壈ȋ2P ڥk^:j_ٰ ΅ ]b84<,0,JV9J$Hc3Y'NogK A 58"L8hq2 J?Sċ*)* S%*hn©k g XZfr%6:z1|7Ɉb0LC]1m lZ5CҚw[[vD"C #Xj`sA^#ODD tCQ@RSHE'gJzyѲbl3<)ڥc.UUC _)bዀ7QnfL8.|`r;Fs!DA  %yJ"ȭNr&dX)p2(HٞHB8lH\]iH5e#yQBWWaB!órҊ!c'ҟZy>['$wT cY~ߤHy~RY?~1IyVӗGH+eRh}-FGIJn4!4Q\9\|r &8<|@  ` pJ wKjk+d'Eϙ\GQ| 9_joTBCysĴSLYe3A?cr*ũbڂr4"YBtFE w7+QT),H&$)YPJRM)>RXZ3!2rQUVS.進v2Hf'_O64 Dk-nj:4Wu>w%W~(c܂ț$M*z,ooER!&$)* iuP"@ "`Y b XVTŴDU36t_*=6)ΐgblQKH.ȝF'kc1J"k*YIb&QI biV 1caV K+\"7!#Y9WloS}-4Šީk>JCXuӛu3L&|4v@͂qĀ2"ApK!:Ys;jW/T*s['@ T `gEɅjqbA\la3չ^"UQLrB#Op2K.dF= Έ ): wلQ[Nl<iHZI2 Oy/[i~#b8s #<~>1†Jg *~5 9CKoL$z44dba4;ss>!!Eh@u_''@  0]ӳ&T*ʔ{$ݣ)+KZ/(6J1" `iuU _g Z$0x~b)ǧ'G+umpeǀ` ZsZ=bhPrwŋװ[Oۑaچŵw1Oj[P6֎2&>sjONϩ j@ sKXI#@qĈ.*@ D`\  D"@s G/,d͆@ GrY vy_^VX)D"Eb=5#jĆbMnC*XGD"L%#N%P7D"@ ىӛ Z @ D`*q*х!D"N G Z @ D`*q*х!D"N G Z @ D`*q*х!D"N.t|w*Ṻ7ўw_7`K`j_cy{ϗB6&kӧ/4Y'^!Y\ 5 9<_5x]+Jp Aخ>&3qb錢Q9_aQi-i9KBWq,mePd'Sl OmսM}#vi|w!{t1#SO*Zئfjjc>ԇ (|L8r5[ڶa#S~:“F~5i-81R  p(AEBG=wH<و v\,irwo+."CW_)IY T׽OVr‘bgg<[txnS` %8ADg'ADz3ih/H.}kA \gS nZ޺[Z#QdiaDV0_)$+E̱X Ǹt?Nn~eSujX}_Ls9b6f(Lx-T{O>]=ge%57Η{y'Sͧj :pD%AݨkuFVA~Jjh!1/6phs׷~{iyO}VMjy8N@}Ø7#o1@0 @*s"y/|ܗħixAg :ϻbc6#vEҁ2VB]p rK2a|7eBǵVQ߷S1-2 Aq舻?ퟶt?p:;i3{Q罰eIbj+gej&q5bx822ɈtG M.IdZ4++g*sEt>x*y[#Px O ҷ 4ц {A;/hTR0=6(qTg=:Eqf0%i9[V/RFl k Qgّ6 D#Ǩisjq^?՟k8Q!dy304e0˨è|[zL 8YͭNuj~DQjq(9EЎ/CE&=0,^Qx]8nN ;kN'Xw'YV9DI-xm $Ԛ7*ds[[U%1I2Mt`߇0.dNB[oz@Vf{A gsgލ8_C2%`8Z08#6Ҭ*4K +.+j:K[pPUeݟW*eRpW[R\URkksceq2)=,M!))*JVdm[Ɛ(Ym 9vZk Md) Iyws|IRSmvF![y0AxIYCOv 4c?Fz.iPMWxxq`ʅ~L&i>mD/1!N]B5{?@)~GU%'J:B\)WoJFLjl()kO Ls(s]t;'8mXDq׻twSz G崇KD&Еd*Zj#onb5]o_d\hH,|z]4RiY4HgGWoHj~8HҿWtB<GѸdd*1s|gf {7i6ڇI0l[HKƼD{Dk99}uǴVjS/C>˖/ T`=b=~76e$2Vk>Jdɖq O BvhMEb)fqb֔P*:*p5' )t 8c9%"H5g$ٱR8GMD >55VD>{Gyvʬ$ rD)b( 2Kx)dT^JJ`3)[ݧaЊQ^]m,"J1 L~|-I{t9M |;յ m8`~?]'~aTr۪ߒ^ki 5;x?N RSR/?`q-5E$lK;bFFx3Q5KE1 w<7vcnsg[0xx`sLhndcy,EhN_nQL}Uڧ%=hױsZh;ԖqӒ+CѴF3I ASYMƩV[AZԆOjFV`l0<6^eiB}T6DONNL(✂+2yd_TV|uf]tj1E##D"?2HY.6+Kt"9^(]eeEZ=ۉJ"H,kKىRGNi$e'&%^PEMR󗞔]ªwY|E]|񐹄[ Uq{dcW_ihFpF+r:v|9$$ z~-tʫ Ja9.qpe%CeG8H?z`>Ybn&P ~ax+v^qz`0Wcu1hUFUܴ7\MUmW.3{=Aبz[ޚ{t `WV|`V/DNd'v3z 'u^f%*/ ' Uڧ^c ug{BLs$o->DN+]=L=(Z91NόEFir3Ss I ѠDHHʅbrCN9T~Đk0gY v4UY(ۄDW*RIO%vҎّr:BAOBãq%I OzISuYYYIID³#DOv\ʇr3K__D\V'@◅NNTb%NAzhqDzp]ဖwh|x!{m2,!>B0k#XVo#}ĘCرS{n K$[qjim{8eM9q{FzNMzBfHDt9o 8=.rsV&ֻUo!:Xtˮjlԫхb:fL.b>J:LQ#3+JI(ŇP%I8ա>GV9&2*]!HPDp"߭̉bNJĘwPA=ˉ=u*v&Gk9HVZb(ԏ#֪Yl0ܜJ ^kKD̗ddB81)3}I٬Ɨ0*3ƃ6ޟ.]$d|5G{L*/;muA o }.rhwdnRa@n<`q AQ&Ĝɿ헦66×hz!{[If'Enhsr-NXx=^D٬װoCzo\>{OǵZ*\ځS_Q)~BwX#*u"3ɮQ`ˊ _#Hyc>1<`P/F-B` =xiIO<>ĺun|"'Шg?ꕇ|] E*wA{{qiZj̗/ ! )""/Z ã=Y`sZG!BJYWw02[h'/`"5/+}_Q-EUR>ϼܰ|I5OP;)urT<ʫ71\aY)T&?E.@A1~(8}{[I$J*#/wQZՈ~MFaF53QY?PqJgm"HGAme.5x-ѷB/!3i,}"x9kF`Jx=En{kЇfd6` Ϻoo jŁڼ? KH7'O D*wxӀ~AlM=XSoh$Dg梔 WoN\!'uӧ'`''&3&3_&))(Uy;¤|Iڞ$fruգ z1|7= F%:c p7xjKBҚw0.omJRvX^k]#ǻm퐞ݥ?EFGl-+05ț_'舤Eljs~\Utg֏;m=h+Ic.Ud5R )ŜFyvNDLRLOddzP~jg:hwFea9q]ⵅa:@ Xd yC((g OT?2=YG_"kz.fl?w5#WQ;RH$Lhxc=Urp,w<@^O~[iT%d)-oni{׾j:3&tBDrEñ LqǑFHQQxv̮4>: !`H`~)r3"K]To;,^o‰9{,v66FqD=D% ؏2/_*L:_ ClCWC?l}泷NHaE4ǪMj'aF#)OoB˻n+bJٌMtni AO ȹ# v4i!O|kDuum} ?ot[c:'b}fd`mt)ƚ-g9 iQ_y rjD4usυ4]RKG/iX 1%[҇LAuu}/\Uӽ5\q&'VQ-tȌAj1H0p;򊛕(Vd$rpvES$ 1j8:+%IiΎ(~>q"tx\#BkJL RE"ȢZ3뛲e 4/%`!:I١<3R{+-5}_%i5Ők;(nW.$ƿ;&N\BC#ё~UWy>]rb01O]7Iڛε7T-H\j7DdʽxB+l^h*gX`F^˞#T׶^*n:v%z?N>w˛c8^/8)mv"pirџ.4g!~cBiqZm*{ħ}3>=5uOdǍľ%ZҒC#'O?~˕]ґtTmy*9(hʥDTFf儨mʆ6 %(d &EuKY/ju9?3uh[,za*SieUjEjuB/^^[Oۑaچŵw3(#blͲzry,"-@j1R<|ƞ$zWZREt4s3}3v͵dHOuwGFqgr`f}?֗Iu/D3OLQg@e7a?\5agP6q4g"3K6~7V Di^:+>:h+U@f*v+^]foܝY` sQޮi:[ }DV@CEhW{d{rṳXYC5H4W}+Qo/y;-] 9:xz ;ϒ el oڿlHщD:ctD֊nuO&9Q)b ׂCs-_xƣ {u(=`ܠ3+40LsXN5ρ?MoPQG0^ϛkY5ioB-=pstm״4ky8eA[d9q{A8M,#"})˒B̀{ZeU_3 w=ዂϥBfB-~- ">; "[ID{Ap;^ Ҹ\:gv}HK.6(x>sD4~Ka p&/EB !`9!ɺ5;N&A {j"T#=w㵧t "Jh,Α٠3Ǐӻ_~Egh藬 Dj'HM~?9$; d{T{;- NoYJq<9LtM%x^/os:]풠nymvU_❕y*opE 聛d^\u0ViFN_Uj4wBNOS52nbOy2Wvy4Y Tqϻ,oF>1뗬Ѫ!s\!sJu{%j3(kh︾o_b[VѸ}~~|U.`Cʿ}9asn]DE=|??8nic g#oʏ}N!1IcK³-gZyK7V"Lv 6Vsf{R9kt ѼoZq:+<%Qz%NS{ MsQL=A~%>g(qRff`ՓF#݅\/뼋`+(6B H{ vQ0I2qdG M.I׋YJ $T\#GI 1(Df9E i^g=ۭ _l|gtWm8ƆFv(tRJ{ ,ڐF51suE8nѿ~GQGXc= #F$-g JEʈ 7{_!!ϲ#ZlFQeEݙh1ġFuh])EH>@]|2C,wϴ}3H 2 !k[ ٔFzFwg_uqUU6Y~w>z??"zfA^(]a3Fcm5?(ḏtkϫ'V FxT^:^(YmTu]a;ņh+Wok+l''oHt@+ہ*I#x֞Dži\a\bz[Ck=}#1LɾnqhYwq3V7:.Uu?ێ-D[B5jCu;LeLI'›LҷlTwпޙg̮'yEld 93hsqbSTYJS3J8$ J~73ɻT4XR3T&k9<k 2=(*0E;SŸNܓkhmvF![y0|?~aeV^l![Fj-ior1^mǵ3!پ+\^dŹAľg/KLӣ}P BsDe+0U(9r WoJFLjl(@ qo(Dф?dhoŠuc?Yu[/o~},kK\"G~N祾 82j#UG֪>!Maerw!QwtF7`@IDATkQ;BNBF z7TG~ C}awz EtK}юsơ>)`$gfm@;,s^Op-#o6ƶ !o?սzWn0`12wEig}Ji6:],6(R{ (GOm6dFZ #!n +7 5b/7;;.*K1ѽ zrKɊo_X!3涄rQ_? jd(QmQA|4RGTinTMFςcD=|WC#  Z`4'E3W'6x4>% #~"yIPA;Z[ہ6+ W`"AY/ XpgoϵR|TvX[-mlŚS5脌u~ zp"my o3J'|cG+i._A"lއ03|WMRDvB꒵ib:s*:@vk^ddNB#_24(hE $(*keύ#q~fMG&<E++w ؂h+H,kJwiMcѼRRWJQAI?NLJ Ju22a7ldn_q!cK–R5PW}goGG6и)'8"YHݵd[9$$K uZWQ7Q(Zw ꁹ^^x*šts V7*;7)e[?XDqz`0W.l8^`{뵔Xy~J-B#5>i|H7^P#lns! \$214`'t"/eӄz_!azt;I8i!IoRCʪ+ɗ O B9m5E£5 27~"ъOaaoo_3ZAզhA$AϊmkG5cD,n_WA9ܬ4QL+~To_#^,׈axt]MDn5 J/[KVi6-M(qD%d<~bMAwS1z:Av<1S*1TE͉[9 *J4:pEێ WQCChfaQ!/ ?©XyOgQO9q1Pw8"=IR8 g.|٣#l8[ޑM<[fưlՙ_.6Aq#qKgί<)v,s!P1D+0E#Ê?M@>d`~ :_Af[ /Z 1ȞH ! ipQ𸠏 #Ok10}8E_曝-pc\O}@;M[ A8VUS;#ABes'Y@qCMJ!nZ8HQƦiK_ Ih鯂q^ N4M5!<'}ct}T>qW]I=Sӧ5q8:.WȰ\~T~ F\A ŪPN BEڔ|A/" ㈱*hqV,7[*2 EkvɏoHKQNh 1>mSKU116ޟ/͑#~ԛ"c-HDw4O̲t›@߭ $&۰^ Qxfh4(6i!zN,؏?&מtp]Qla-`Gb %~ "BKedO t|h%tln+jlN_cD,Gި9S/\w'62|Xs'j, ~ϗ?WtM [6DzAŎ=u$Az/Zaۻk1a95zhS~)yξ#?VhqQZՈcTц,aЏۍK_xf , R~ɚ8I&"HhkD2KK%afܡ6e?(՜dаU2Ҭ#Mf1J<0yӷZe>xOw~4Vp~2A9:,Wb/#Aoa>f{kHLǏWi*CYWI Co2^ɜdh}RL%Yɿe;ӪOL _W6,}r2Ha"X\Yz(8=+D{[I/K*# ^`Ű޿XNTw 1[Áol%4XPbGr{MZI/4 E~x4MI7rz;H+T%xTR/bj0ĉkmthB}ӴqVlA7^05Ao}vcjFs\xml1P}>LJlii)B ,4Q՜F,D5Y/H5=')8"3z8152Iq^NNAa҃:yDgxq *)* N1cvgdo?Yw:4g_tH.DHP!hGaL /~O.omJ ->Ux/{;vkyrVN=ԯLjڠG0L1D`iEްNx.XehZWɯ= BO rkZզByFL73cq~\0O|X <9CRz+%_~jg>FGp Q~e=zgQ9:; W[mrtO7c Nf"ƌ#`f$3Ta)?:oQq\+V VD ^,o}mf:ElIr;Qwfh}3m#/5x ̑v!Pˉ n M~Q()ќӏ Dr3N^ݍe4mKfY yMxQdx~Hv (MK5Є~Vy0jFJjҏ$ȊS @dyQBWWaB֡8JAN]˜~f 3~;!1>s`hڏUԀi2r |xR]uI+eR5Tu'Dqd-:+|=9@ֆZ ɫU2F:* ~|W[`u:2?2>dGwZ]W?{hǙOGr3| mO,ѫG$8n\k<ҴqD'׼/5XI*N=hRrf3٩b@))E8/KL^eJ,ʦi$VdNU{5G҃QI 4f9"vKZ u7F#+@}aExQ5 RROo0T|tB͂qD!H+W}/o6揨-OEևp /{su*C*|=c*_2c ]tON_M:?H*DgsȜ|AbX$YGnIS.ՑpNfɅHr\8#X0eH2 &켗`^\x>ofỵGN<~>1!`C}pj@s^D'"k4QmQb"HiA! X鷳gY&S<ۆk.ؖGO!aq2%H&9p4b9{=o(87zڎ k6,=x2lͲzry" VU'a9γ`5<ԊEXd@7Ej1R<| I0M Eq溻}F{U## 4,GhtD88[zLF2Uqw%uY 雱k.͠ʨ);sbc𲹮 꾟zKrd0h= &;%8՚^`mEf@]^o~?5F_'f@ D` `E nV`iV+X\{} ->[gې=j^eGbj9ԕ5C C@>"y3yڤ._pp2!BhLBOL#FcaD`#qZv uu_^ًys-i}Z8q\D@γ# WH@VbN\]']Em{6wg>{)oWq][k%kgZX(B0|%|@|O z?}s}]sc66vf이@ 9"lD"@ >=[D1D"@ 䈰 @ D""9-""@ DrD D"@lx @ D"9"lD"@ x#+RhcR3>=}s:譾[}VL15ԽSU髣sbhD"@ >:`9UK=S ͩցDVzPn*3N~XO{D"@ލ#Z|7R.p(U"'\z4M?dn*p&[GD"L~]%=P,URrK 7fC!o +W?9p+5nFXښ+{|?c`aX@<D"x#"dwq.,v#Zp>}Eы{A·s%N˅D"1Jccߣ挶!"!x6E@ y∨|g^5§"h(\vjKT:(镎gBkꏽRX\%Wbr@/+G?}}yNa5r*}Wt8_rBZ.pQ=B:?쟮Y )m-1v45Wv c -pw˄ܸ).pҹ$78rӦŋGvaD"@@ Qkt94i8+ SJLͨNYT][~t/o kȫv攓d;S 3VS9,?ԁp8MSG (YPczNb;>lGErA.Ŵ| M#/=X]TqGOU I2#qV)~v< / {#`S7_h GD̆FƵ]|)-8;}/ 5:6EUF^$C}+W)Rw Y;!h}yVֺbsd7v]7:?%T[ ^?^ճ鬕X&>R3g/pur:u#\@ GƤHT}q}\ϵ:L4L(s4&y\C5B\slM$ *i'IDE;z8̝eU S/)j(W|CtTstGt#u2 Wɀ=W_zzY)v >3w3#)@v.oΧ/ AX,62E.yCW[_\d"[Fh@A:_hNƅs"bT1$K/ݷcƠi2|Lz'羈tʊ!G'ISK.-ggqO1({d-W0#M_]_m%i"@ "K-o(E Dα-d2?RM$}Yf,+GJxTQ:޽{gJ֪|HJg3QqNYQ+cx] `+J (TU=Ј"q$V Gg P4g3ChY)b>L {sl;q[?z2Ō~[`/SۖNon,-\[ Gw՟GΓFjO6ښL͓6C*jϑD%O=t!ix3{>:S~ۓtTԲ;AB_5`^t".8VWM$߫o x|mlcD"8"*f؋M%uEDԨOe2(ML#P5Ay ŶյL1⼪l6#gҁL2,F4qZ Y ` ؆T .ў'-&m05}I.oxXFKID m7tbĽ7nB,KQRK=*r*u:q.;YG )GDfF=h)D"xw9"sʪeLh_D.#)"WWC-@Q+^ 1i8&FyN #I"@%<ǵGT!wtJJ$"JˈeCEH*UӜSUX6Z84՝"i5Y|R /l~пA0R߼~[$RQ̜t!E#0y;4AK/7Flez&x.zG{R_},:a "@ "`H K2KtNJ!`dn,SUBQݣ34:V`wGZ@H)k _2b}zjp?ѧm%oDvYI8=X%'X$&nj^ }iGMg!t& D"13)z֛X"AF\Ma=tu&숇eN3X|( *Ky]Pp ә9Ϙd3U[[΄ 6}ϖ[(gRvJZˆKCuiOFL ZWJՎ ~IOyi'ʠ,D"@Z jSsSb1Djll5T>NNfIPRA,p$/~fTdWSU.t,*h3Tw$;ZѨꪊ "={M.D"&TMWzxK"9$Լ}1UENQ=dd`FO:Y 3uG%ЙY\lCs)dliKSc¬c(JcTDx5Ew&RAJ'd*T>pr1Wqq@Fu7&65hCPO|NG't۟ԳXO>8+$8TY۽ٖD"pƃ2S j*U\S򂼗SJ()TxY {T =YZGQFY/+.fe3;* 28ئߜuLBQT+.XUMkɱG':y%̰!?>VDBJid9uΌ)T* itMRqbarK:q{dWb|_Ihh=szWxU΢u1IT_\׌'!]ϕ>~ڋ]C+gs[A`ϕuooh/[#f9@ 'P! Oʸb]9'kK-{ފBz b l>z*>_W['-)-֓ P jbS#Iè=z3XTAv&HDPE?T:$LeJHʺ%D9K޼CF&g HW66k,yh zSlMR ?~Kt"((z:|:49}ݭ4b\sϯ^{m4)/7s޵ų ;=Y!LC D}< L˳03+^]CFHU V)$g(:ܮ.IgIRZ<;A2g;Z\n!Qn)=@'X1EL&.̗P*Lr"8HqJ&JzƍK_/jzOfw&UĐA??U_2GnEڂץ<5Q\r~/?sjaoGO[SDž Sgm ~!pgq!Gdl޻s,rĤ/&>,7SvzE LA D}ܾ}}i/IʺZFHS~_WQM}," IbtU9u-Wjy~(!I_dKU߳UUH( lJ᭎IO8!ڳ^woĈEɡt}ޝɶmjrw`nbq9A!!gScmcfs 6o FŚPzVr_a}0f曇ͼB"@' G%q Z{jl渏b;V6"dGf+FKqĀ̝O ɻ@ {{ѻѠ| WW۞DD\ q/y髥+V[42$1>vĨ7Ë5{y "@^ҹ";DT}fw$yx[,@ gcL MřN;rٴF`Dv6[6޹+i#pxJx"@;p<❸ ٱZj@;a]_(H2 qoLq4_|}>v<)"q€X~uMɡ gNN"ścWA]3QO5faWt?]ORF|YQi mdQ~}v ԟm>w2Ԅ"XwSR37"zo3DC327KQcMC[I߉[@`o W  W/xZv3iU@OQpŒuV{( ^@`-ģ;iW{zl!~xwSnK0LL"bѣw![3<]_Ѵ=ߋ14ؑq/ƭ]iX jmC2=3oKHh]<+ws VTkGToRF!֗5ւ3V1~%yNQ#cepZ^(U截csʭ$4`f;騕=^* lSTeh$ٜNo_ev^cU[6>Gח&m.lQAAF^zҴ9Z$;H\4"H) Gb츀 Ƿ0fڎk]KLuϚ7z qzr2jFho8x"4tM|CF w0[RJ*er9ݡB kU^~LjDD\DvqI9bF$kphA2j+yd2Pkn6^n5dчMS, Y46Mֵ~v"<{;"KO&>'N rW[㍷iۘk]0h5\i7HUΤ)f}BF(}C",2e =`X9ypӘe--8ADO^q=>lfПI|gn-lv?~7v %٨r$1G,kK=Ⱥ,<)^i!&O\ّ!/ /Ou2~S%"nCJg9JQ)y7ikca84e'[}0-!סJpK.i ?RM䮲}YVvR%89\.N==zdb9i#p`9%΂!'V'a#ͥ q[5`ܾQ$[gHI=k-vjubLөm<. nA'ah8d402ZxvlMX;uO¬ ȺD0o[2+=cJ:=rGςChEh lBGD,mc^7dH ݟML/5j{@#LxK\za$O$b~n%!L;=ju.PWl a'L-L;vOU 56.Wr"$s ($88H$\TUoڢB ɱ?~FI;N^s<@"& c!uP;~$ܣR Ҷ 3G`uEJXR>ED">WP[[[SӤkdYnFs-I\`4 tdidj`t /M_<Dp. Bnc] l8pa""]pj9|= 7l-.D>nR;v_M3D-$+<{g.@t"ɚ&7@fq/)"nf6H_O7?]R( {b{N#жuOsWZ@IDAT^g "@''9Q[`hs?-ܐMD`~[r@ɓV7J>e[.&u1̞~}<_,yfj}q*Af39o,9tPd*!?Y)90a(NlBso&%)EpNYت23_-侚N]/LʪCɁLעbGNDr=Ԛaé*h]q[䁟'2 Y ?:]} & םѿ-2XAx]H9td[1IaxTxL8@; 31t5k3L{r.wkW[mSlNtm޴ƇDЄMUM<(@CV9ָpkaFR1H`[ rmjc_\=r﷐ >DŽ|Ϧ¦iߖq6D qWD}FszKϹ XWЃR(z"GKtGRTrp3FUE2)JˉX\ʁ:^38wX1e"r+r+ ɉ0I=PqZZ/]ds&3L\CHB<'T{E}}dd/e٫YJр cWJbKd뭇)SFT\1% {^I0fNKpݙGtjbƨx+Caf[1 2A=K=$HOA[=ĭVJ`?կXt9Z|@6bӞ|ԃ?\LPxznp"GzkY-0d!p$DwO[gq,eptR@WmҀ0↱vJ=ȠDI;B2:4m3_ ۷.to|r79Q]Ȅ/`$}7]h?*@vz6,VhCdpWV()wW00oJ) //~ [_$^`]o]rVuW!eەt#B0cFJ ΀A+v߾miwZ[(1Tb߸F4xF쁾ݑOtGm;f0mKz]E2{JEx01AL~𙿆0W6Zɔ]E"0Dޭ:ގx[p#n:xx†w +f0ōJ'c,jh@_ zms8OomT_^lϹm!x<3fLug`9ܲ)!n:7QRKJxΨnKw96uY4/ɧ@EgЙ\K0AF:A,%ۡ؃XH {9A?3!MJJW؟V q/k{Jֱ.X9+m܌g4 W!:`:2<Bo : "ruNߑ,{@r2z=;bJL!row$y@fk!S|cBБkjuKC%ߖfvqfc?ڬnnٲ´)ߧX t%zU= ʺ'bP vjeu BV'q$iR(fؐ}b։2`jVQIuͼWPVyl27!{9M%IWZ]Y*$WUN#+) Pk* kn=[ľטYͅ~ك'_Xe+bwlO$fk;>|uh4o^,+Uv+vEOֳzr?^t"ja;jCI㩳vUpѬk[.#ܗiwbᜁ(ZKugȵBk;u']m"9Gv0cFf GqM/5[Z:dp -M2E.J.ﳶO{ֆ:}hD'&PۍS-"vzlO7MQmov[*5ZiWfY6-FV.,CCR-➱N0O I:i{Q$ŠͩC^%j ff#?h?g*0 >X 1 m.+{iZR*1tsBzl!I%=vy X{#i&{1\$9{dw`뗮Qc~u Cuq-^A`v[wś [%HwSN%]8?-Ym,ʿ _ijn(QG3zqǺ|cﱾҴxа}[P[] I= *5"PKą{Th:a/7QQnٹݑh] 42i>/y7_4+ t}I-=}[mQ&CQ]=UQ ;R ΀h;0"xQ![T ]~=y6J ]%ͨ0401a4q4~L`ۈ+e9t`0ABwMj(q75A'`.>\* "%e3'"^jf3q:#88bhoQєRsY "&!bZG &ٟ{( lfS 8 }^X3Yz$t_Ҷͱ6x(Pʈmv,_w\ S5h% :lTELwpj% i¢~Aכnicm3FDtF{oVz1u1b&Lepx@ȵOC={9q<]AhXO|!8 3_)1DBM2xҟ0iƺ&_JmdBn  -F \9en\iuXxf9mh!޻3𗤿n8f^ Ad 7W{fOϯ,m0gDDVKȎ*5،l5P@d6D;˛o/uh%D"ٺ3Ѿ1~ [H>.ݓ' e G0@ D"^~G@ DAr{ZBO D"x $@ DAr{ZBO D"x $@ DAr{ZBO D"x r_fausdoͩh]eIM;Xkf{n~=6҃e}x=62OmX}W}R/j }F(YUZ9Szn! D` 0GWwprGm̢ɍ?Bn7TٱɹaT͵+ۗ[CZ*$=&kMyw쨽ot}젓{<JtO =$ IjR_:& DA`-@\|Dg :{2~c "< 8% &zAax%wm\?[8awctlhW CE볶;'(07XF8G?8,ڶ%'NTD"Oyˮ(*)%q^I@ fPXOurWjܯ5 gӟ,#U[-Tl({쇖``%:xd@LC #> s\|Rd-&ò@ "]\{~z_[}EыAr"'=9!jiG'Kc^yʒ3\* YP`BZ*}8n6#`KVMtSUbe"L~6J2en셿)vnY՞\6tV:. HBц#7t`}v1e#_% d{MLqDm b 8J`N+04tM|CF,<Ӎ%u{=u^ Hl(',.c# ~K.'IB߼:5->D"0GD8d4g3GJkUxB/)8)9IDP_+Wbr\rJ27&5EB8@g>[Y_"QN^^fմ7(2L̲Q-1U1QOYb"TӰA&MdG-C z3 #%/& =}Wuou3&j}Y2Ԣ1 ?buԴ(io"xE+CyJ㍷iۘk]0h5(@q~2X E(jvx{YK Ngcبđ枯'>krDY64J>I Y!D` %h K5E9!W[[A,nHH(wn9DʊsM%g* ؔdj7G>!yޠDRMnj^Sn)9vqF"k=Uedb{:FŽ ydFh[29- r @w[Lg:zC$;@@+_1j1_A:s2e{%cO?8eWo(" kw^bXJ{d v ¬ %f- oC#A~m(2:0(;g{'q?D"0>Gg|K#HSnj:* g %N1ytt%t&)0jSz!25SLJyx0gx "%~tE:ok M0.л:KdsH?&Q֛YMRDsQ! qHC<³n_>𙋤I'X#ƭ>Bێ%` Xk?Rn ib(yݜg D`#m8u$kν×`O"A<' mS6$1)6W.r rs%"cIݮ6lt-L~^`C^v]mI^ULT$2NN'IT%l_ÐɶgTC+=o' "~:ϟφu2S+W;UcSD"0}LvdCYQ^EL qn(5iե bt$QK UPaLUIr:Uc %%RM B8%ت6Wm9-"Bԗ CCY<9S*(;٤m)nIm{fus3ը,0E*'a-n zU= "R?nKWOMNvkc +z7 ީn Q @~G5:A%Om)'2;Kaw ))z?rSqFUi`"@XZDx5Ԭc ttfw"[D`牴ڎFy@wW;ҔR`?~bШa|ʙ\D@ lO$k;>|uh4o^,+Uڇ9ja;DPxꬵh">_}[yNM6=3Hy61RDkHD"8}ΆCˊ sqʨ:$|mQ.mIssLf/Uڲ$U(ItC:PZj)E *lzw'O τeJHʺ%b`9Ө7 юN{9hd DudvaHn>|/AÚZiP[]bd? 5JyYU5L9UXO9gg=K ~rpM>4>Y7P M{]10kO[F0D?c5ĀOmiIGB%va TG&_VW=v+I(D"0/p33o\j:3p\@Ll DeJvGtM4\-qC,\RX&0 dj'%9WkK'#Ӳ#?#Ŋm27.}ItF]~#bXDm}c;zs z>iŲp׏$,CX kve= !S뒷MAD8xշ7ظe`˘cF!L5kzEaH,JvU3$T(r]W_dzS&1  'f;`odF}/Ar#SeE@ ۷oq+=‹gZ3;b^jjtIJ UUH(;չ|Tr{l(N,pd h2}W>,a&& f,;+00LlnhP>(c2\5E8/mHqlza?)3?#bE!w[dKkk31|<2ӁeY~./ץo)SCCC@ }{_;Gqz-93Vt3 G!Q+ayKh_JGkWWMW AMgnn+԰{3E[rL1[+3yq:h?m./伻Az“B`'HplAhkVka*zp ӿl,*ӉSis% UM-&uuTN{s?s3h?Bsz?P[Γj>kF?xQBCxCpcKt_ _ p>@ٖ ҏoҌ7,nlGAΨtWZ:8o j NP`n^* ]A`Ѷe,'lKN2["*޿u];"on,x`1z GtI@18K[a]Lm%; 7&q}C [xѣY'HFڎVțsH,ٵnbCWTlh- O~eQ7"!/'m}{h$xIےF;0H=<CC[$x~%AS5P\Mo^E_[ 5s AF]}}>:'UHgˆ =ڒ˧B>zVΨy*+w:OaD1W (P1nqQ}n7OD;+(Za|>/M~5߯Dg(Pؔ-=Ͻմ(~,IX^?~1^74}:e\k. }=͆ X;,Yʢ0LJreDi*Lo?D Ƅ=ѿ/"|ot%p죋4SfeY<8AslČrͽr]gXѶs8D$$~n=5ݲs<HR_t@4S8=#uG P[@8nPG] T)%'-$mg"w߅!!<)1.y*&A_9;B<-\Z,w:Gz v(ܙ+ENvyUS_YXX\)Wa #Ȑ5JY{J$8DZWb4եJ*dzLEF |E$%U%F:5;Q>!eaVA :^QUUQTI\LJ Hr2|ϖ|)YRpDd/q_~h;DyL%ɨBIB2p `BĐɏsvv}uD8bGǽvJZ$.fs*M=3oKHhvdDwӧ(: ad=21TqA4v'%SJtq5ݾ۽c E\u˰XRaQoO?Y(;w*K 0uGpі&kΐnq0g'2io颅iڴg;D,  LVyj ٔ;+;DP-' ^Vɑo)n _…S(hI"v=u[?KpI?0z%tSj>ր/(huM'%&H f t؇$?v1 l,Fofd$E[ 5~} /[y"[ &>eeO_Ut 7`әGXÄ'zTi?[lmݧL҈X4&(ke2C >}_2:=Y=.Խ;w^F& kIv_~(U戲ԬR9q)Ksk+ڏeK6YF"&) զ);xIC}*/+wn)a*AWׂRYW9F!֗5牭=xEAF^zh6e{H"Iyi 8E""Kk*9hEi셿!huˢ׮3K:u n x1l՚ǰT` UBlwZL:K馨 x$cjN>d>f裂x๯ﳆEhEW\%?)0Y ?W! Y]r}9FTfK2]7 ۰xdI'pU=[boF/Eկxtt@~ h]6[OW_2/ZWl~.#QJTr* fg4yQm4:|c*>䦝FU{0jMbՏL"Lot`n?Niv)IWmɃ̂G#*ۿ>=t@z[Sk(uzװvԓ#٫(LD 377CD1!c{h(HɭJ_I!jRIsTk,j*Q"TzStuͩEDSd⪦D%trVQ/NI#dq3lņls5D{@^L&nb vؼA{4HI% @1 ւ S;; 83;Yv*lkSSڪ`, AĄ$\s sɕ{>|{y}|;VҟNr9 SWѻ yn[\vcFXT^aD~_!q0)ѝ+P*UaFj)hfTë9p~>ϣ'.qK2{N[cVf7Uۧۂ/RF0rj XDyUQ#\Ls?"{MDtBL`!ta]L$!ΦD;8wZL7vK"ja*'ח$av"lMضua\03' )$AdmӶ[R>t:px/--aWirEg0 C04fd%u mׯUpuuȖ^>_JؘWE8A!s ʯUfRtpDt ki%Xc;s*S)?iA$ HR~yuq&$#'8]-:f$"N %e҃YtsG&;}78ao7`aoJM0V60IRTwןlԁ$)SFĚ9]CgUJ^աnP^G]q bL\|1,* [w_{&8;X -`T{]`8.m =[9/;~ދuyB#lRo]փ YXҟGM_pl).ڿ7jFNABvtKv+vAG\|CY]k[Q_]]]Y٢iȻӃm2v` vrz`#=ugB׼/^BDG>#HWi')oW4WT:+.5R@`{ɨ")"o/׻ ֛,&(#!>N0jk,WUXm$Ք|& eTZ"=g{& A7?JNR|$[D[nL.q([:f]O^H Me6XDBbCH7= Os/-}u1!{>&z ϴġLAFxu<3 rΟϧƸC騂+K©U]I4OC\|=] @#iujN"wjQ£u+i㙶t#ܤkI4I oj3Rqg`AĬ,ucDk<ƮG7m   JdN|`dAzw)5xk8j?M y'{xU`^-"ĠwtOv"J'"QGu%Nw5L*xLz(t0}!0ݽN\#Zb/:t59_ _% AٗA܃Hx))OCЃTG{/i{Eh&ı bwzd@Qw+ %y$)Y<@`:ŕzM6n_+tq';Q% ]0nxX=5ݽ4 k W"hsJ r澭A4H恕Y,JW0iCeYeːNlj rR;-=zNM "Y!}0 "^m]]-'><ТUꁎWA݂U?*0Gh"hjz.?-|)t֬B3QKl &L_ ÍCb0x31? D6p%I}lT`hOߝBXQĕ(kH+-ã}zs4(^IտsCӅ ?=$Tf);֛lDۘ0K| c#)C''ިaCbRVD4 75).|bcC}gWu;2 ݴ/Vi7 ($}^[W#ocS'=ugDa:Vv[7K\,:R] KH>J JJF1{h:M Rzt 5%1(,R!ۈ;x`O HSx{]/o쯧nŠگfdGNtYδaözY1Yfz~%f&a<=̯9%S;or|3cm(Hz5Wv D@Ĥn.q6bkSt/1E9 k4ؔ'lq8ss:mnAQ[R7+;ƊCAp?=C\D8 1f䕷'I OU)YO[=/DK\w4"G#Sϡi*]]'DE_uaEiF|jDeK-Z' pfgZ][!g S ⸄{{:a_?}c0hF228S6)& ͱVUk+ը2YBqw/$ug^=эwphzR~GOX9[.ouc_U{(|_J͵:}6N :݄tnUp3)Y/DzA`ҖQBN5j)NsjЏCAz]7$bŜƫP/=Ĥq75 }O&3)~'꛻/*1H2zAc>OWUL}qԲ]tF:[`SYw&PG=)d~Gj';|7S |ix{SGg+%@>\xk67:Qs_64C^oBG]'H4e?KSK3|X܇M^?YiaW5͝~nOHgE.q.;țy|NmFV%+0n⤣;DyY=Z~$_$b4pN'X@h.BL}bC7;l4%3umek$W뿻x fn)QRDWs13Gҭ)><[Cʱ |sh7*;*+Ikk;?"wo7S㡜[͘r y:qj΢$#26\grvLBEE`w8{@9uG$Hםq-"afcC5fDl- Zҟa[95z:(ILK5'|tK 4j.ԜՉcTכtXpSզFZؾ#nu]^OS1T㺪XlS;3f6 ~&O- }4RIï4cz \ fǼLm|gVKOZߌS۴iڰ7:RwM6`ҝa}S!`cCO٫"Za /`ϲ6 4iߙwP\ l\gJ2,-0٠K@03ta'i΃%=|杇vaOln呻Mhɹ§H̆FpQadA<=L]l]3]fjz"hYgӶ}nqo".W,M^Bmhk3 N2xdQә^o3NF@2C^Lgq/-c6D?v㻨p}n?_ v0n`k:J\(X!^j *jj@: *ЊU蛉S:`D7P5rIJ4KEdLZJ)tC7uU *:maO p{Ngݞ|# Niϝ+ 1{S`ut6;PlV02t}c:[vwdw^~I0 G~ ÆNKoP;FjԹ&3y-B9k4CKK`!\?#|unۖ˨zsdx_,c̏;W+?{Ԍ(wn 4h|O~w`Tx𽃄^̔O  \7ecpeYMuX<׮F_{]Db7xO`^3\ƲdC#:׌- A=mf#~s{_$AMKοelTLב9QQ$SL P΃uKKYcm@!sJY!n/j S;K3 {aV!UV(`Q=1ՠnl`֌+.SrKP_ a+S,TFz:z a\pGg=Πޮ۬ a% {ȎwSe3мD[CxS&8[fuŲjUIdK\l+&Tfϱ }QHxe}a7 *%87Pu>CoLat/p|#zU&:".eoR[AstZDLX1p7B=s"왝]5s,f_ZDk?;эp}k^ vm٫z 3MDDD l5u}cx,J{en` 7? 4ueJ*@bcv!>{b<2Pv%FCGAOF1Ut4RՅ aQoG*-usD`5;#¸6z}GN[LoL⻚'NxBoP;2,>uF'dP"3C„eKk{wN vc)~s޷C#.4 .0q ۾D( ?(gGu^ g]9ND FƮe!c(zw^^R=0";g8nɯ4[m]簨+Ad֭[P XDMَ9[ހ<"[׊_zf"OOq doH\^W !F PGOb܆|Xrg™)(,&SbF98/IgY!05xk3 es0~xC֨GI|Xwἦ YϮ+ u^NT0";Vu;prgpfw}x:o-Q^m ZBNڈ`h'3l5~. zbzGDDPu=x'<}B` =_KX9 (dzGl#$FR߽{OA yI\s)݆:g3hcB(^4Cpy'R{"/Ox23say" %K%X"hj[:qojhAH{*AxŸuZvEVm0";Mޑkl!zg妦6xl,ž$u ddg|FTOqSz[!LR0hk _;zZMߏ\H"M,ckƸ7":~H`~D/3a̽tk0";M# zݑD6 p[}x0X5aoа1Kljfv߈L@=<؛{#F4;[.+:'; jÉ:3 {A[>D`v4etlBxYoEBgg7 08@:SEEXAA.U+"hq꼍¬2 T -V˔*H̖Q$EIN|L + 0u] kS妛@6\dlS&, O>_wKZ-usD`6nTWxADNju_w?>خ͝w_{/>{L޹dbv \#g+oRZ4^A>/Ns'%o &ӱDT(!f8;y= /.ڂ<=q%2t0٘e\UJxL fDK hէf2$T)90t dtS<ўj;j #tP(!!`$W&85N0&2k(vpDD'D 4d*ꣳ @pM0>wA*c9Cœ/جj|EG Q[/ϵ<9wQp G?Lq8 lS? f:*Owt}hkHVedˈ`ժ(!,4T=F01.tTWɉ$i~ea* 4D&MC^Qm/M8[Q_]]]Y٢iɤ颋 Y^؉-co"b'VSk^=aMki;/e3t D"0wy6I'M^n] XyކmH>oSN0>vkbj\¨$V Nq"!u{,kuOzwAdS,_K%n)P`.x,1qmO[eI\8WUA+ x9??''B_[{<O2Kum9 BHhQ-"%an\\PHfmƜb2O$!:Dswn"jdrtJ'@&]衇||cw§ !}n }:{M`"fo;6^ B%gا [qBooЀߐBޫĬBcCZQ#&w2'j\$״=ƑERGnW݄F' |}5 0NLjECo3ﰐS=K %g VoV$vT0X=F/eP/όĚOP(-sơ8naP50P[LUqM,n!4{6j8l֓W|¢¢7?LZ |bb] upjδfE( 3{RjXD-iO%o 49;(D;R7^s>%_.c 4xo`z mEq:]Gq6ˬ*'g+;?8h(*3AG4:"Ty>AeMkpL^ѩ%n,5[:{=c&%ۦZZdYp=3f''\Npߎ鋏}r`x:<ʪ*+?UH qpra",R7X %r- IumIrOQpnK.HLb^`XD`Y"0g3b;hm/Mr3Q{n QbC_~RF{m ^\UZ{ )=,4."I:MN[D[9 Tp\RC}Wo"Y١t#,Q.AqV qVjjEMAF9YLE:Mt7vT(VZQ[U!^%(RS.>y$vۓ⧗TdARѨJ jdG5^Biq D▾. D`!0gmNbGyASϰr5;>;~UMczzhHD/4Js$o iּY }8$e#"1Ѩ!20[<&M-ngߐ_[zn53BiUfU ϖ>fW3RVR +9.ڈ2Z#+ȒkFU&rrl#CXfR:o 4 M56.n ` ,"Lpɳ3NW.}%\mA$<.ֻRxaX9 ̼ݽ˦2ŵK 8N=R f!: AyDy7f_ z]%#fЗjE`cF(m^B6^'MjRC "Mv.pEIWvx)$XIleɉUqv("K]Vhl-Zg*fkS./0-ɺL: ܾVA薄 D3CoqKY]4DABUf8bݐ2?hMz͡Jg8׺) C' .:78KjeKkgF KJ:teN6+ nim: >qv:F+hyw<%TށQ=zMC]-U]y#Ԗ)^?7@dɖu=>pmƛef'ӷSLvܙUk(Mut A¢)6g%Tn/P(m[Y#`5Dz"Ih"fvX;*fvps;rxߗ XO"F.<#.q*ϖv"ZseՉr#2 -FU\Q1^-IMyоDn@n GHSj҇9LHD"@"f)gNentY lJЕ/Ce@ +HY>`i1W`{=} n7xQs%ݿx@ .G`=<!VR(HW3QȣBr"0~WNlWD7ncXb+^d(v:5҅fv9?bgJC D&;fE 2eGdrQRNq`ahbq?3aUL ^Z+ H*C G{?;؉}3Go=e/jV1tOؗ@1Q1\"@ 3z9ҪE\Z"t7q5A"gm滀 cn#/`Q ?{zI<D",y@+ n]Fo YQKtز`x/8VH{%OAD"p-#k _;zZMwr"4-DQ&1fV Bj "Lى\fmmiv &,;]Vlˑ՟8)JN?Eʛk\ɼ\ǕWC W`e<>0ӯa+ˊٞm /='O&_]x?C&|6 *^;q`'JXǻn#%{^#w:t=@Gtd/ {ǤsLO{"n}vG'ӯO[$"STH&!'|r /ƍrdmT=۹ /D&<>TԸ M7ʻ#8X-\6fFfh_Vžkԭu7FwxZ|ډ; mIpwӓv$o)W,@ 5+J79.Y dafǠbJL,a?GdGNgA0l,LoȕE!5Q}We!ugDO=G=|#33񈜨CYSUuBy,+3Kid6!Zi/UܤY+D"p[@[ED$;X&f#E_@DϦ͉zl[Bº@@XLeb{ D%EMt1 b`HTBL4$؛lD"XYף%c$a1Y<_]_IYaz`ej Kk$Wc]*,^qB[D׊tVed׫yNYQ"ҹ颺*T:*g%5Ytk2dGv9-lXkIrǺ[z$HS/{oTUIj{9&G~|u7~SC*?O”b"8 X=G>IJBw`;+x@'{b3_}8 F+f*R$I.#4DoOiBy]Wת/DNSO$ UWivEPD",'w=3S'"3ntU@)+N+:rN`@%!H0"EVV:Fĩ4 4D&j$-6e˭쨯lѴ,Ģ঒@n{ ]+w߉Eexᙨ#AwǗ(o{7fhspclͤ "H1ЎF"LW?|~s )lĝЫΑ2Ay>pm:lcvn@ޙ/#dD#_X еWOdKT(~+rF=LD hU^@ +%q\6W(H8 Ml@(OD9S^JJ&^[XO&)a5Z+:Ig")"VTF\JNG=1eYn\*+jKkn=Ua:8t }-a4/aX&M3 (>&RwoҴC`w*psTv˧ÍPc9(8} "hCw.B8asgנrVQik?)ۄNqlAB r@%ɯǧ˔8CcyO(Izh{(b+g"YYEii/,;lV:L"5 .Gsjx>(l֋UQ LǶh&l#?STivfIQ%د=6KJ[M0 5 7Y y>q񱡾TeN]i̩"@ለ T& X"XeݱϿ6`}uM]~5{-Wvtht X~f@H=~|iƍ厠Ɖf<+IflR=MV|lMc 2،mg{#<,2~2+}n*yiv3LP$xCŒ6D"^ .%Z^QRTTZ!'dx)쩢zt>Vd IzҌ(-YR:sNH➌2`1]#|`änU- KClŠ)"X8[ug)ј݂|+j :nV4w޺ƊCg k1VtP}ͶC Gvet`z v1µQD &*[]ό^ؚ/L5"NUQmҐT_&i0Sj(GZU~9ٙM20Mg+gwmv}gR-Y_ja;鴴<=չ vK L@ +8^=B+?UHl/%BL0_,:b"e2{,C/m֖$GSMWN4]#.!]睿o9+'&pwc >Ǘ;f;*J{4--"!wnr38"29|5mJZbFxOgs&Pl۫'Rw \zMVdBk=E5r!DAz`u]YAA/`AIU\բ(VdA0EQ?y$8JKVXۏPˎUEiȢ_P]]L~dM C'\豝%J`V*gdQ#_{NhU%5sֹqNH}%?l0jFtV߼2|O5m{hyKt|]q+ a'C3[_h}7{hB(H+xKj;>ix{SMO!w[t 13'Z&)^ݫ%WQ;+ۧ`G͕oW~خ zqj vݱc[]]CtI~DQ=R6})!-p gt)ΔW\K3K( A >@VbCQ~YE?؁hN6Ub'eC`*TevTk5NeTJG:ӮFa}YAI2;TBQ*5HJioRlN0PkuH5@YO}Jye֯Q0=.G {'~(*9w4)INPN-|=;!VũJ|d4K<.\ݳƫM6ҷ6[up dzܠbwj.ԜcTכ{s)]s;*iжq`"@V"N.,۩ޮ۬ ??ЇQְ:F4!H D"X#|T]sS*+}Fmoֈ.?\\c} kmZ'FE=2:D"XHMCYx9=9qRz#fo2 C( f˰X\_? _׎\C  K@ D"0Ooޫ @ D"X  G\ a1D"@#. @ D`q@ D"XF@.*D"@ Br@ D`!U蜶\{pzAEkpܡp}^q[>^A~~KĮxD{֫5;P׌Nyo "(X>P.ҡ++mm Ft}۬@j 2RdB}GulsG{o GU ; H #I#;-x+/i}DmE0K×]:)ZHB W6OFI+>l1WmX{OG-eq}X;8rf`dA!`X*,ZAl>8k^Ȑ~lz&@"`p̡ƮI6o K҆5n@ <#7uJ;nK1|0#+۝2v)j m}MsBaRA`F/'WV:@QW*t^};BinB[m7|DpLs8.2X8HzcϞ{HsvpQ5BSA%>v d#|oG3/cحK8D7zk"XlmZ0<+$NŒ A\+˟l=_(aZX8 -͖\dVF]Ok&A$EFFgIaFu~DumkGOWQ .W$e($2PyiVn"9PdNZwjkK *PD]V~HV.x\T %IGdU+jw@,a~,"(kT!8#CnfiyndAA:%(Y'cɀ.QRڕ4p o9<&n0}vbFz$~%}Bf[mؑ=/t;t;G_P|}\?^;i>jݸ|1v[NJ1oG[?nl\L!R#|)7^FhʑbS6Blz&HFMfL] *j\Luw7ޚ [`T0GK-fITB/OB4]Ag߆mjEɬ"OeLܵ.uO3=owGLO[vm΍"^T}EtG\phqJaE&$uj <y: Im !}輤x|b/.P[>1 Tt7qt9 Rf$AEQQ*ޘG)tPTJdeN_@c#:IIQ]E}Vv&FYWQsISL=3<]J28ZK*u猫"*9"f N/fjDUEzMGtÔ:ӗNT(,"8u%G+W̴nzP6?<>eI p7כMuU u^A3{3&vݷ7Ϗn?X#پG4ޫ}>LVG^ ;MQ[;X2uL*VǼ5egkё>}WA\Cz 7kwJ~ԝ<|ʟM/]g=a[6;h sCނ#Cj@ؾmZQ[$LX!`RVl[ˮޥO#9>lKF|t 5; M턍"fj9ÔOhXնn狉Ybz.lhpӏiG BLJgkv p}cb2xP65~Lgָ%f5ffP.8v򃨆n#<_gT~ 62勀K8 "\IvnnzUw몫xIqJxNfJ5rx$=+NQ^Hz,ЌFdRٹI"DY_ dyx7HfFIj*\XWFsD@NR+JNGex h**E{p(ýv8(40bdP)o؍M5?Q_ CiM۹O3$<S>鏩ѯx]WΘ^aD~grm]G4oEDN/7mߩh(y[&nxΛxp>?H>GDvjpwS&Mo9Rx e݈GV(J`"0=lk' lx4]"g4:L5;]]}@Q*M.K_=,)OIlPrΠtFE4g'Dwkgdu مr=n h84mYZ;x*6LQSU} {/=~D`h!HdT!<P $чż2@Jמ»d8G uAn=sT$;T{*Xh*'IX]P,X$uCPT" W!Q* ERu +:+~:ҙ)Ͷ4g /0=({mEv/}k͚BMdS<:ZRoAXOQ.5[; T/foN:^9aKP˓ߜG$ԋ;[=|A%Wf^;aoy,Ktl\4kDpϘ3g.2hl=;/ErvF7;oL4R>x`4ƨI~+7CC)yCQVAHঋ%F|WՌMK]c̢yhͪDHI7cϭ? 5jz? 2ոeVZ:~|>e. )<%Ɔ ӍqOh7DUUmZGw mL9ʻ+R`vxPTϭe+ExyV%zDĬ5W[w] ڌ J;ұRG c5bQSq#%gb?j>: ?z > {>* C:1 {ncZ]eq$& IgA,_G9X)5HXWq?t iVD{hsL~fO](INъ%gxtSy7X^BӐ/E-"P"F'R}~K?K?4M=/Ш=!"H-M7΢Ë'!g@ h((p RphUF)" \ʼn?o`82 f/Xɗ=% =wg}B$x/4_9zMrvGϙ~px^h]-ҧ%DP<(2~a/ !ոJcWwoOYjkXyl$lmɒٹg[!MrDh#}>MЍ{DF3H!)nLɩ&ꁱ܊ iS`qtiE%HԠ_7sV~MHlT-dZlWYi[GgӘyH4){YjF1fT1:|)p<fTjX QXaLǎ_~& |;,"WL5_T3Ԩ.—G˖s^rhDP fCH;Rβi "BeQ@xuTioJXi=}K@dHF ҩ2tBOܣ"<(!w_S ֿNC}V96KpS݇şܸ4ÇÞ[Hq|~ ?QcILrAo&e/Yz.T5Bh^U'4);h:sl1mDgha/)>/2d&>FjH:h0M,_I'eTa#b}b ZEwʝ>_Q8gPyF}U% Be`rF%w:6=Ӽ3V L֫S]NҐAA@h@@?@,2cϷ;tt~4|Ѱg_#dDrrLecrG_GũEa 6jphQ:<z¬CmE奥+^iu}yqS˂h DMO*iAֺtMNapNd+o[,IN& "r`"'RQ0QJo#pvoѝꅔU/5-?w~v)ffz6MGc΢$HEcVyb+F A|:V\yUbD|-7Y0U!{2h vugbO%9;d)?;GA^Z ut6?jWG90V%K2 b}VaG!⪶49dV+/'gꓮu%r3E4n9hݹ "qB:UvALcl.UqQ鮏DaV ݹc)N(s28bbxIas8ioƸLkگ#IK[ ]g^$ Fa% @"z盺x*d JS7=VY}W)SerWԆtTc㕒7db[ĮxCN[/ܺLg> g":tU8;|Ğrbm aLrkQ +u4(N d6wǦTS7ݥ )гW39+ψ ->FOSIdh64aYAe_nmD_^-l@ $lCy+\*>-d? iL~b~ O&VMr`F [agtPtt|$bˤ*1[/͋?/kw$\{N?2jY3xXCA~87DL|}96 h9%ObIp\2qI;l>&1VB%5䷦x[ET/>N0%is,lԷ]M.d"@>\PPY8WQ/~8t(}p>\hCnFnE׿[E)De] cYAuaf兹E+ҁ\O(jeuy~qc"ϋ?ڤb41P&i]wy7 +&5w.QBKzhe,nJIɓKym׮ u#*xUu({/xi[w4׌4}$S(ܐ7W~eZoOh;\-r$t%>v~~P+] 5a^5cݷΕ ΢q@~e@~irepRUwk*+UfAɬ0L~wI[jjCO?AV7J\9wd N7&@ Dq^Vb2śIQ?=hVSI89ҹΔR.x ߻.-]ƶj;BLɳRs}3@C8%c%b3v]7Qg sDڪ=fVpW`N`A5I\TQ*)@ERf+O%Y% :!5Q 4Q?{QiRv`\+f3M]඼jSkɹ7 :wb;.FL%䛸/҇믴l+E NvcP/AߎP$+Jv.lW] 3~8;՘Fu,G}/l}5F\{B[?yBpTɫ|r/Up\wPD#w0 ,׿Ө9KHL R\k0Z4SM T۲uwքz<-Wl:iN?JwDwST(Mds2"R}"? kǙ[fwhӝ䀗Bߤ=ѫg {Ukv!pUᏞ[oPq!5HJty0 0! Si$m#v #d+o&ZQշOJIx̪dnz/Pt9we7v5|j'[wYqhP^j0 `75#^iE;V=i~hl2,݄n, 1whI/Z=<ێE681NL 6<Ţ08ěPםVANȶP`,@VkC#H=\zAEa" Nz?6.D&)-k6F8(z 4;=48279~Ǯ;;Z/~'&k]ldmk{NJFj50}!"Nq!< } ?6{cv1e~fܴ]Wgns)QK Yv!֐F">wv 11P}{O ak˃ (sg XmC@=`L,m /<Ĩl~=\Ո@  x_ 2N>;4YGGi#l^W`;&0/0Xt-p-8TrNg_q<MSmhL.+)Nx細x¨' }DOkxHfxjrYb]C@z`=e?;cSNߞE.jq ;p*@_g zx(J gcOa#zʕ= Fxn~{˓CSZͬ|Y 涪CDL"|7>w:C-!D"8 i!D"@3GW "@ Aw("@ |FA D"x0@B D"@q>_X7D"@ 7B`$UW~Y\GlNW'SiH(ɻ-xfo"+_hX$7#O2n¡E{:dQh! JV} 1atmk֓g!e,6PXo۷yD7P 쇉j/TJ 4%:m ^{z_-C }k ;=9M2rV-:ݶҷ|v mYA{'Ԫ?:fC}oM Zecě c4kJ2VC'\㶺RM)IvD"µj5-JDnEW*5uwE5gNiD$ 6 "N .>Fz`^<9*+  Q;-j?P#oENG d+;>Rb}N5_nmP#;w>l!&yٙMJa/>aP@7"AX݊6ճ9uYwgΎ\Fq ټ&4%-ZS'X $ꓺ mGoجfʁCeB^8I6HZmőЧ՗1ߋ%B_e$6*ݞ͓uNPۖԿ}_rĬ%D,8wspR5}DY*qpYUR X%AL-O0luY׈n; Q= ٩аŦgG>Rd<{kzDx0y zMŏ/acZ H֣;;v㫳08p1B^)Qu.ߚޒpcUY6[m,(9;!8vJun^/abx4!}311q cBd|/ϗF:W(9`!$Jrd*uUZbMMlk{F ekoh|(M773WKMdN:uƖNZE(Ƌq^‹KMk~ZɜG¨KeYwhlK!٢tn[?aڼ0D?XâtWไ˗#K ʂB 5O )*E 8##%t$mz@zcw]EAx$[y"~WU֟{kJ3+ Euex{N@ E(VqtgVu/8pAMNIHY⛸NlB/ԺN7]tV! 'U^AS[hT_@PWsƯ(%C<{vΈP>r󬶪l`7oc቉V 82}F[s%t՘N=Uъ#Z|B?@5BL8(qvP.be[aŞ5ywyb 1ך͏*n"Cf;7zE/H"ڒO_Fa\=2~+u(kN5oԡV}ke izKnaGx)fb!T;" tӀ>q oLED֔,<5z 's왯p5Rƕa@r~ YB]y0FBu6#*ODizAia#SQ;q ":"{**>_Lg?߃K{WgY*]}=^𙒂RNOow]]Il>0=/?E4ԝ>EԹНVD—_iZ/?6'eӫ)PǠf..d^|IMG(7ph?мDhT?BoXz?胄 C(Q5ەVeԶ슶1)$T%t>s󟓨&|{IiWD HhӨD95J-;q܋`t&qֈ!)^YUcm/q0.FR+A$$BnjH=xo_D)eW]>U{12ʭ!˵\`]"ҠCdtZE|tJ6(}Gmݶt. lYhA7GK>-f&-|,=~J#mG׫zzxQiL'bdKMun~~,4k3鹫 AѼRO.:P%9 #Uu[ ϫLLzey <7剴Ԕ 6l9Hdkg5{#$h sD,?+<Ԥɬa#vmgTxvYk3I)I՛(oJ6xrD=R&Xq$y6Ajp`hAaoΣmg/|V7IßHzͪz ._aqV\!G)4  Ź.eF<$+D,*4Nj<)I^EY<`Z*\6L#O?"8AzQnzpa5\F9!sNuǪJp6uۼg&eH Y01v,K(ZER;-pŇ0dz5'V0\9 0>P'5wެc&O?bZNXhlzwQT>rA T݌skP"He-[[B\r׈miZ1 ryRV8skxy > bD1e+୻-Mۅ9 <3wHA#Eqױn[Rqvϻk'vdE:0I܏htmW_MFSYyB!<.Pш!m2TΛ҂7s,2F+!L4TDs Vh6cuxvC )˹Ow~, ҲJM`qn[A+5OI$&D˶+Ckh~,E+e1{=-zp'HǪ2~[nbq,Sf98l}+wp\W\mr;OB%{?5 _Y}8ozMlS%-dNcZ'ȑFf!`B<"bkG&kK|X *G13s[oKwwLO?nbA+ |AP12U:fV=wQ6C{`WLL|6!1`~p);$]DC]إ,ŕxt*3fK9F.4);h:k>$>Ƚ׆?}.|O˄m7Õĵvs|Dbݟƀmb^cỹgB˟$0>f␁uحYZ$$[d!HCt O^ %vk?F34G(qZ` g\9!bBXE< I^=zw#۟Iյ**S]n _3; {Ґ*XGEf6I:Y]N;l$wgR-w17 6DGir8UΖ8I)Ugbd. {Lѧ> u zj 1Lݸ% 3%!g_`DV^&Dc uPz>jeKs#JӝgZM+5_0P*DVuF %.BwTZC@WʮFtgd!hui1Jb+F A~֪+_L]o&6? rT҉uwmUsO?%3Èg!`yuJW]mѠ#WV#9yNqZpy;0;+ԕV1wAaܬgO)|Ӥ4KU cE[@ڇ>%?d5> CCѵ鮏DYV BjVdߝa… (WtV)j /&ح$4i7ꥮD@ Gc%u<# IhjJRta^TAL/.m$SwǦTɘ5ݺ+3b#"rO8 YgMSY\y\p&fc_qLI {U,Τ6N4Q*VdJt$fn|(o%zm=>K"t7R6?qg/"- 1ǯ_m[%ƪٶG~ݟ֢Ö{[.eB,S/T6O~87bcU-杈S7GHN"S)#7()꙳tR9i*-͓Z۳)ewIgOF4*7Θ03DcNSה;{fF.te{]uianĢRc֫8~M]cccu IAK>Pex=_I=crȎ72rK?Q^(Ŵ7pu5Αʟn/Qp܇ڤbĪwXoqelX1s鍺}XtnlUOwuL pǝW$54klZ-ޡ5(]""f|^kvePQ:AE_>׾W\ۺfD~$i%&f\,dBV߽|,@FTFէߚ9]) `B褞nA%-55!EO˧- ݈ʹ#'.6TGuzqvV71}G ZD=_d7Y~zL=|ΙlŸK;SoVW.iLv8e\s}vg_ D z d7;{TZQNXK(VsuPSW?SQLAfsf}"kMWFALn:_X! ,''qwrסJLDW}ۉJ.(T"ɀvQƺ6q{Hk(ϦIف:"`oȐ`aՈjN8B IDATV\N,kѠoG(( UnoR`iUKX [XйQ _In 5bADoKb_[Pd7'fHY䙃L+/g[HtΆ*Rl`Lu !l*hy4j,F!AGl^<uq9K!ւaM4_miM(Xߚw+_ӂ4 w]='8gn!^mټ͓(s X;n)0AӘ=mef71h1G劲t72&YQyix D 'C b94Ҽc +:K%&Zy!)m(NT0DNh92**N=X*An}56Ձy3؋?o] oMpdoMxKE~e^W:%q;[_41b Rp&ږ^%Ң ?N~0'͊Ⱦ.%y1LGsQwKY̆-i+ 5D cNb!+}!Fd +}kmOhRJ|,-bɦ&'3نШ<${~?5 i֧׮1F2V.;$޾oy*I%yY+)*2ʿSUQo(1cFYءN L{"`+mNVsU}n1 WƘn/l5N'e:J3rEB18R"ǂ6Nͺ cب ^l^p$@n~w^w xBtwB7^ iSi#lIN7J:eCx`bIlc8uv5+AmE~PeoBD/8Ao%VdG~PhkaiuGkD)r+DŽe*,Jt+  @Í7~#l?Yf4`wg8> Vna8u$-r, C||i RvTF<2QdE Xt,z[{:&(&щ.AwkqKEBt+^k}? 6Cݹ)< 9y:`>h-d͊-ӡD**_8o0ċzϛں"s;_jfkb1ybUp Ygc!{D" #:6,@ D"`{c&6,D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@.B D"P@ Dx(D"@ EzaZD"@\@UcӯIENDB`mustermann-4.0.0/mustermann-contrib/irb.png000066400000000000000000000560031517364653100210340ustar00rootroot00000000000000PNG  IHDR] iCCPICC ProfileH wTS-@轷{"ͮȠ#(:TGG@ƂbȠ> 6T z'[gg}V>ZO(LG22E!n!PA A txB& ucqMtB >|(-e |cYb3sWV q@tO$r1`uQe&db,O%`|c|߈s22c:M cj c/[}O3sg_3Cِ&9cJ $bse a%v" yjWʆQW56H61j56"e52LL LL^jƘ00bfmnv잹yy+  E Kzn˗VVVn[37[YٴLjr v;7v'>g!͡ar;j8G8NqN?9:;h$4u҂Қ#;2q3e22._>,4 GveME+KVtgbWrr~uʭ}w4_:?3J[W>Y}kl\3um:d] Olм1mfo6En)T)P8mEE[6moI2r랭_/T|*^Ǫ%m,)۷=s;˥W Y(xkٮVusvVTuڳ}ϧZvw{^粯NO)?ݮliOܟ?sniTj,iܔ4bn-kCrڦ:qݸQr~K/7;=ǘNJ;Ε]]8䷦'jNʝ,;E=Uxj3gg;uFs.:vu '._<~{+WnAΫWz |uo޸J@zCyo:}e& 1-b 6׊$Ky@"̚{@,;7@o>1ՉR,"rh}/Ty6SiTXtXML:com.adobe.xmp 542 137 ;@IDATx} \SW@xI@DPAEE ijrۂ{/ogL`g& V#vl+P ƆX UA5(h$P}><$ Ϗُ^k-ׯ_'3`NrKO~2`F`$!p 6-#8,F`, cF`D!Mˈ*, F# `2J`Fش`06-cF`D!Mˈ*, F# Wa6VD>!>1F=~ I q0O91(-~!E:gCvCP ԥ?MP|I nt=hk:]z~Ż_+NZuhLlSr?>G[!RCee@\,;yN2m٦Ι)7v|}R]7KG( H8UClW\$17ȅʆnOnV2W4}J'6,+LOh_wGJ5?k񔔌D)JJ~V=SR-YiYqdj+rX;21;*Jsr"S4Z &LF7絣( F,Fm1]eCVPg\;"棡4*Q49Mk F A_L -SLBۑP2Udfv>}h OIc Yv#ZI3hRGX8jZ!;{Ք:4߫sm4)gÿPyIiy-䳪dbk,vRֽq3!$ 8JKJ8qGɶm%;>$5/Y"XT^RAFOFNN[R~__PRmǎmv짲":n#mΚ9m;h1O;/pj668#?i:7O[!ܜ8+%Z$Kہm" )cUǝa&ɋ :, ,ȎOo^RAdUBՀb d/qP%{/͝ <&l̺ŌJ3$& ^fBL^0uttC+Ih@Tg5hh4򫘕L"LxJ.:Jf]Lk!3X]탷AP&y2Wnpj5O}rCW^`VCAdT#$,CX8tzS$R)xU$Q"s1CLYTQk4JB^!Vku\E;S]B4t2t*]%R % | FMOEu'>»ZW5zF1F0opȕU&x{ -z5T#FD_PE7q=)]Qb m` >CS'i0hXӠUsZF6v嗪++5e yy ⢢b4~y`X4Bç^]:Js\r5Q/R)U5 J*c1/GIdUʊ/w\ GcIl !+6R*F)%VTZ X)]i:I7b۞@c_3^].˯zӓZ3A:Dt t[^v1-h;JTJfl'PAVȠGV0%l x!in tiaM )(9 f2EU-휭1rvp W}AɬkR)3`mQ)Ja @(2P.;SjNK`u\ |u= A\C*㱕أi2@M>A]_UYYj4jU׆.rkk1LA_v\ɘof P\^Zô?2=[4!ǘfƵ'u)RLK@4',542ԍLѝبV] :r X%FLH\ XTY.QvIHϔ&7D7쏣mRkvfΑ4?+k l=/+ _﷖vZ e齻 Pdڲ'5}*"I 4)sڼ&ͮ w  )IC,TYiK9LjyXt,QZM>;J$=GIAs* #NEڢ4 zH@H-C_(4?OB5'bR-xKg9m+Y{?Ls-/wG!__EnA~et0&H*)t{WJ#ҮUXLM冉sQvkOV DC N^HPkV&Q`Zii8A;EPIi^&{;js 2Wod}WѹQm^w%.ZX/Ori4|B+r {]G6eˢϭ/@k;D;%ȣj$Fcp+kQL,0e+6pvU E;ĉOVہ͹]Vm|y%ۑRj d"W[A3-.+ԞSt3J|RIٛvj&^MU~$*mi= Dpkc!R2_cz 1jXaH8ʇ@iCkuRG=ܼ};~]eb;WzP\="n(]&=A8."F/7~h |OF]J0,~&ܓNjRPIT늙Z&LatغjUo^8|dҝ/nI}M$/VӼ j2'95}or%^t@`5]E)89gmyW: Om_{uxx>Ǐr}QJ!;'Hg/ˑIKo|b@U G:Av qLy/tB<ƶȮRc0 7j4S`pCBd,̲04Mq)z)51)@ 0!%iBM;ٻk>۟J` %GJڶn; 8cϰ!lZ,?/s~G ,m%WڱkCBOH%^x:lՎCtc$0i= /1wiUl]X|ZA d%u9) +%D1*yHbT4Ir1VtO|zo9iCCiڶrڲgn՚jm 4%нV><]*n FQJ$/<"AO^l[M-21K9pI_ f)S֖*n _k0+?:w5zpCܤm4TWsCbF"MA/ "#L`hP_jb %G`7t2l+C Ah1#0w1"xA(,'K]v*T^9UУʄB50I*CFTFW(ݙc!NmiB)DJx1.bKfwu^)]iCϵY?.wlٓq|M{Tx21:1Y o~o]dr9KJ`NDШ ]4#Atu(1LQ/#IЁtPFDKt"`L F=rBq)n D(},,^QB+!<!(yBL"į.rB~,ID+~H^#o<%K6\xRyv{owmuhkT;'Jzl(X{,p48m{K5JZv͕Qs鉡$LRy_'PS/<VS$X"\4JRjݦÓ|Uн췌5.JBY4tw%A,wB.Q*5Ɲ33T8*¥гFHJ7~i<30!1<$wΎ3ζ_;9aVl^ZB`6zϝÅL 8YA_Dc鰴/cIt84֖sW"cjt|l#X>=xPHP(|eXzPYz.}}` ,iN&#-]m j~DTl|bP Ŕ;_[[}DDIq.#9a^ȴƁ!#ƴ /f 'bÙ`07ش<SJcF`!M+,x~)(^ `~lZˏ]886 k±6-#0(F`l 'F9b-0BTXF#06el## iAE`0clZF9b-0BTXF#06el## iAE`0cnZlJsV("F#m3Ojȯ^?f%|e!#4F?GH`0~#0MߚF#0L iqX,KáwT={GW㎒mJv|IJָd}-XZlܻ|9hܻDx)T^R)^`n cKӫQYEeڢ·PŻАuZLT1@p2ZgL:Vrq(gKWWe Z<򜨍 ئѼd* { Y?va0 ue ́ gqT LNfڙJPi4JQRJI~YbjF|(4tb&!NF#ps 0ҔɁ®BŮB"#JE"F jB iX,ɡPUٲ75[+NY!eiuK˓Iwfi{_΍{raJc0y㏽F̌򨎙W+ kז4iv=_+@&,y˞mK-y(o)X^ZKTA11uɏvA󫊡]qLB!r+F` !iuju֗WVigm^Q@ Y ~XfBԹYiK]-~FZ6@v^).7h$%'x.^aش@0F&CO>M^$B<52Nn Lf]DgIR\js>%x\-Uj#yY0%`07!Kif *h9Qe-_=ܽ0:mlGDO$\N\hpF#C^]}S`غjUo^K)Kw1'-+Q}NRO/D"/VaԼِ'rqF`4#gh|jwnҷ2ԛmTfﲈGOK"j4i0lHs_F#0ӴO.+UѼ3661+\kVs{{bE/ _LD]{7n YHIrMEr\2 FKsevZSO`bF#0;֖*x.bj fWӌ`JmͦJ[|#enY3d_o1WsE pLThj[&RE T)'B(F#0 [z -yw-NB06óin79EjhdjBI+aY#TzJID\^ F*4ԵzF<&-cM/⧦+%zrbi[Biٹ6WGm;H qlcQ; l"UuM 1@`دa0Xڌ@am Wѱ3<&QK[285)DGMOB7 -F#FƎiF0kF#aEL`0clZB)b0BUXF#0e,"#(iQŁ`0clZB)b0BUXF#0e,"#(iQŁ`0clZB)b0BUXF#0묎m6[v$|B|b v"~ I qM,ښ;_emyXڏ! -O82@.iZ7x $h{& %[끧kDF} 6_|mvZUt+^(n谻`mQG_ւ># nT6E`Ҷ֧4f'yV/C0#/ A2,JᏪ*aC)> ~Z5/q{d1Q9$sj(孕73d5o8̺2WU ))0s˧O2(26h9_CQ0򇟙g\L[i80ْHD^Z)ue׺{K%bhY2֪-VrE6o1 СU_j1;dkT~*5Bsea񀒽Hl3J?EO7EPi:̇M:-? jJj2T:5  T":hb|q?rt/$ӏBe/657*zF7єдy"|YCĊ@,n5ﻟbceEN(uT,j1)~DZJvcCӐjHSI<8]LusA6j*3FC1ZLbG'\n/Epg9@"C C/3}qu2򡮙\p!26:+ymYGw(11CjItj 9 I;J&xנ CLئ".†t B_f0_onَTl&3ˇwS9q7vxR핟8-mx# G HkWw?E%IɩYRP"*?=FQ(m(?fv=ƀjZ=jnM`RaRPiWiMiRJ{<YUljv0he[Mk],e7)ل.wX,gvW(oۉwgm䳷Y G bKہm2(ῌU瑑لyEzmU,mM% ׌N:wlVN2%ardj#lm()ٶMo@7m rJ[]/߼~*Y_PxSh) Wɾ99Kj[YS%G<{ YJ`BۧHjmBېN 7"O[pvxQ41g&!߶WRYȫZD$^Tz,wu#l~^B!-`93YqӚa[NhBL^0utt̜)Fإ, u~3!l֣@T `N<;<RT5nPUmxv9E[8!+.5jXL /Cf7UkyЏZ UY:_9aF3Vnd!g`Rku5JgܭtE"[BݠcS2-l'g<^4mn֫i}jTk ^a^UZZVICJS8 ʬ޻_'FbS:(y-wЇ yVwP9Cy[Y>\Z\pbmPBW\G,7xg. [G-pj*̐_PE7=J)N yT}Ihe`P!j8/@eaCA."v-hוk3K*Sjt:]CMuZdTl#qZvkktjU L"#ESJo.'Ua9.nrBeVjTz ʔ*u^O)T|)Oi@mW^TaLJkg,ʃ(}BIL 0։y G=R^/vLYU, Ʉjk#D.C+#˯&zӇgw{Q0ՂAD25pG)p9t50 ,;ndZ( U_MnkmQ)JaJ~*+J tZhtEn7b3e RHϠlZ᚝]_; Ws꣕A_ըnFezOqx|=}z, Q}@3:hBTg}ez0ww= (]U\8ݩK8qٗ.M Vf@Mƴ\ﮯjU58kCA \`hii1L. Zano؅G3uɘoyQWVg(.;e#ԀV[`e@մ0mmSxV]ʇbmU e4)SP^Gsz,j# e3؝Q!1sVq ;SysJy)Od hn``X\ġ hM*,Ŕ7m\MhMY鮞4nw@ %2QD"/NS5~0u$h◬곝=эgZ6-_mhˊB®Kvl*OdO ,![^[CɴeOkTI/$ (ҤM;k4/U*T*$ȊV2y,)= oIݟ.9y2B jpB0'-=a`3a%Yѣj{Ǜ}vHz0UXVZRVL[J<,:m("29qq|~~%GLnWM2u6mD:7\vxsFNԝl-$9AyQqb ~m/XO'Ö#")ݳ>2jգ\xxA2/(\"!y,װ)3҈$pC|TH"]XLMJ4 [{7V P%mR()ԚIa[Rh! yh`Ɔ" ̌[6z͏]d~IXN}i7-$?ywG7>c]5ب7ql:)ȋiݵH}WV-m^,Nde=X!ȑ T~.z˖- Vr̃"w!l|cw`cpSQ%ß,+Xoº"M.߳+Ox͐A::N~Q 2/ɼwm26$SzYh=JZQYVAP%B$?5$_Lہ͹5HYՈO[[Rzk42VⲂLȻSSr0+%Bxx&eoکxi|lDû(@T"v CbQBE K.أ۷7u]&*?yq;/v2qXZS5D˕`BF&|#URޖ:"͵3\_D(EL|=<g,jyן-ߚ.#3_2/,%RH&ĀsDh-V΢=)"Hg1yeG{KhEjx3>D H{A{CJ'ʮPzŃ!% .H3n2nj]qJpG<%^m]7sd2e7P/d| ȸ!ô71Dpr򜵯tp_ZHm_{ox>GXITi^lHbӹ ][E bmPκׄuG#WD^x yE*MnZtecCK׭v[18ULǬ9[*/J絶T#Y0p£_+it$ZbB%OUـ !'Ks*&U|]B5xBP9vpP] O-E%ÞKDCF>h9 s 6jBd(!V`'Q<%%&O+f 4 {t܅̾p#?NGrGKm'Jk~b^.vٳi^<7 Sy:1O[ f:N?$=jÜ QY1Kk>LUV q+뻛wf[?yWsh('j*M>c'\EU*G_ b`.}+h/}0$'#|26W4DȀxY5s9ɷ ȓCzXC6$QQKgUR2wH-Hqg4xC%IrDpq vڙ㍧$"%pS {|9D^rɥ.`_65ɋ+MkI Q ID\^5N.Wb(3ȉl'umpj4ruMε>j#֪K&oVW^L,~j雈ٰ\FM͒Q+]g4KT9IЉtnxL*I>+Q)5Ɲ3y3p,*T&&&SWi \zp W}ee!yn.GwE[7rKFH@Ryb={3~s;OGOgǙsg/^0+U6/-0){;[W'G zC3aڂےsn)Svx=":vFұYz.}}` ,NOx\͏OtoxH&ǃUdf 쯭>""v줸"kS*@h;5 WJA2wYNls !!V7Fi=ȓmL7jrU}{8 i_#]ޒ`Ȋ۞XhFbLXcz8l==>߬ͯ._g lZN K{p75WJ=j |J1rbԴ0R!GM>d6-!:EYCg_uR-Nq ~J%솹 쮞l&{Gs$Q`BL;CEdxӓs&pM_I8v2E8f& R'RhK9%!i1^f6_b0B'4x1 d0&ǿ&au@}>7 <ދ)Z~=w׼xR6oY]N%~='y蹟`±cl  ZȮ ~T-) LucS҄xmr~n$a"zكUaoE@2~e cV7!炃i_La5cn5|x["[p02RA?ye!)wr ͝LOr¤Xkz£;M<39oBn_&HkO^Ϝh/]Ñ@(G X@.1UB@h±/|1 ߋŅK 8jɜE ?-HY˗ = F*{E M7|GML/ml 5f#~/&rw!WJ ͈|K2lY¢2mQ$X1KC'?7pZuDYI7;qtGZ0A]AퟜF qW'Y{CK_.WH؊tWN=d2`7u=MI8xO^&%5p8Qəfƙϫ~WC*!{?TF_wU]8A"|Ӏe|Í4OIKQ;Ovj9f+I/;iZIT vQUn{wF<%b~~iWc:y7ʈSOc}qn^Z#*[_$,B|;>1> =wDIzÓ" 8<^ h%@Kgޏ9째>]]% xS'sAl~4*qi}՟Me۽yP ィdgCp8Ǣ/>C`s4X|EK+M/?JS~|k40+J}gl{lnV#9 M d0r  @И%`B,='kAI尞lH/=S+'*2*`poNl?@DGJR=nCkW!jAv%$.s&E8;/t%g 6URVS7aB/Z8~+)Mtn$o?$Ϛ ^edȲ2"Kg'Fw&}o|B-H8vvCX@5)iF@kcoh+eДKj(_~ţ/KW9l3(*Q6Tc 'Dc aP̢rjOX3%3tN^&z 9_Y^ \a s{J kW.r?\?&#&GyF:{hCB]fPBqmo{+SKgܚn?MxoS '/ߠvt׶Q'0i|u] LPl}?{{ƭX0ඞ<x ݎFr3]Y9=hs{'..<ϓ$I6R qܳ׹Ȇg,Xa +xǟ#@8Div^'6dQs};d?+f:_0:C|kH`<1wW0+r HoNzq}Zʁ?<\t4'9{3 qځE0rV P6^+ܖȾ\G崻}3x>ا81ˇ 'BH܃e!=ߣF0#r;CK.ͬ'-ȘnI&xV|C?:)3q+2/&K;ۣUrN,OW^\Hϣ{vS90.-?'姍 E78[ɤ;iI㿪p^h$y #oMl9Ae`4-?1ӵYb&ʂڵ%M] 9?-99;=?AzCf$ @=;MmĔ{J47-8b$ESe3# ݕo S3xK3AL2ea\pγ_ЮP$/]#`:h8;p&kh~t-7 0 8$]8 kp>QyyIDAT%.Df>!@_!qG>r#d`4-Nqr+4)sڼA÷gAyϙE5p;i'+Q#)==w~sMHd$7&\%DO#7T{ޜ8)cUh;zF/NGk'vL^Z5`X4}Ȝմ]aܾ~)$|w΢exdzIы1+בx3]i({:;ަ,$VyP]w3E'x<ݏ!o1xaVnCx!8l,׍33BNL8.>)'/+dc< 1tж9z:v=_ =5MbUF l&T۞wr4jvm/ܥI9Sd?Mf:oa~-2: Lx:ƽ*¿fg}6w@DtFp l5GCuGT0g,sE&r @vC@ iR&IsZڹ[ߙ9D>|CGȬ<9QŽ 7&@R0*9+å%xFLY0ؗkي6-R~aʒ_>p)y0/W_QLc疧NsW\5Oq9ST}s%(z?am"b He4յM%1&ᑮ.E=qa+ɼDħy"!J';20wVJjvߋ1?ouվ޼ne$S|qcNG"[B$,}YWwOvꇉttURҧ'q3DW+})4-c},kfA/_/9q|>$AGLXwΣ!('ѓ1@鶆8܇S&CrV3ݮp Y/'&zD8) =HHC('zq7գ_ֽ};؀9=$_87ˍSyS+v{P1-ޜ7DB13/fV`=9>g/>4xzfe3t&9!F Y$f Վ> ̊B FHoIḒCmqAWQ AI9%,=z5!˒]?f]ߞjxZG7跧,+VT$ðg=Aɽs{&n)-/#Y =W4,stϹ6D N=`@dbd|Z@=|e'g7lĬ\r%pF539ۃKp5e rs 500Xj`2΋h$3M]uXKW?|/]Sy)A ~Y:]mΒ*NSd7N}:}C`=s:}^!9C&E"ֺW%)/oL+F9?$p)Os}".MPt_C<ȍ7woxrfWdw};:?isyU"b/EkdK{?K*E}w]ݾxGYܰsT+QS|[MmFi NR([k60c}gϴ)sNS&;}kh ̱ry#m IJY?5 \yvxOդOk{LߴV0#%qР~ʮ$ :P#XM7RLJto.5uybx2x}u+V̝:zwo>>rDduHI8sxBJÙa&s,(v`gG6+tu?YyIcp˚gn^PRKw'ewN m |o[lA$fE|Ssk$9tDN=3SD qMN^t@0g~"^~G|8y3L9x5>{_3fJǓ#%t2|7Q 1w-u_ޮ58\GVf:ճğt%%1R=̟Dk:PJM<ȽQ`n<^GGؿ/Qܥ=RN<Grg8 4LWAG!,r\lGa ,"yV#'6Ʉ.!C~)xΚ#w+ wv>zmiXGG?3>Ci]|>G]0.7{nYTlx{qfG?uӣ#CW!>:!]'À SLqx%pFyǣsYqm/l\SSn|xɞaܬ{f3yrSRLz0" O}ɬ$ (n;9Z}'u Šl 6GR'RFrǧ^֮O MKs`VNh|$+wJ.yBMG_+45#kv(89r/yƃt)pP`qtAA yBNm B`鞵wOBqKI8? 1c6yzWBY$ߠY񕍫FZ[ ׯ{Q4] &$L\c=y-x)vpb!a㤱"d+pgTdr?wJ?5~z|h=ߜu8ϼuv &J]=W]9Sf̜륌OQ5Dd>.O^46ȹi=;ά=W:vI(} <-"8rRTl=S`oh&$lr8ɏxqi>l1gF#p"H#}B`0>ش>F#2ش B#``0F``2d1F##M `0!#Mː! 0F6-|<#`""OIENDB`mustermann-4.0.0/mustermann-contrib/lib/000077500000000000000000000000001517364653100203145ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/lib/mustermann/000077500000000000000000000000001517364653100225055ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/lib/mustermann/cake.rb000066400000000000000000000007771517364653100237500ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # CakePHP style pattern implementation. # # @example # Mustermann.new('/:foo', type: :cake) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#cake Syntax description in the README class Cake < AST::Pattern register :cake on(?:) { |c| node(:capture) { scan(/\w+/) } } on(?*) { |c| node(:splat, convert: (-> e { e.split('/') } unless scan(?*))) } end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/express.rb000066400000000000000000000020161517364653100245220ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # Express style pattern implementation. # # @example # Mustermann.new('/:foo', type: :express) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#flask Syntax description in the README class Express < AST::Pattern register :express on(nil, ??, ?+, ?*, ?)) { |c| unexpected(c) } on(?:) { |c| node(:capture) { scan(/\w+/) } } on(?() { |c| node(:splat, constraint: read_brackets(?(, ?))) } suffix ??, after: :capture do |char, element| unexpected(char) unless element.is_a? :capture node(:optional, element) end suffix ?*, after: :capture do |match, element| node(:named_splat, element.name) end suffix ?+, after: :capture do |match, element| node(:named_splat, element.name, constraint: ".+") end suffix ?(, after: :capture do |match, element| element.constraint = read_brackets(?(, ?)) element end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/file_utils.rb000066400000000000000000000155521517364653100252010ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/file_utils/glob_pattern' require 'mustermann/mapper' require 'fileutils' module Mustermann # Implements handy file operations using patterns. module FileUtils extend self # Turn a Mustermann pattern into glob pattern. # # @example # require 'mustermann/file_utils' # # Mustermann::FileUtils.glob_pattern('/:name') # => '/*' # Mustermann::FileUtils.glob_pattern('src/:path/:file.(js|rb)') # => 'src/**/*/*.{js,rb}' # Mustermann::FileUtils.glob_pattern('{a,b}/*', type: :shell) # => '{a,b}/*' # # pattern = Mustermann.new('/foo/:page', '/bar/:page') # => # # Mustermann::FileUtils.glob_pattern(pattern) # => "{/foo/*,/bar/*}" # # @param [Object] pattern the object to turn into a glob pattern. # @return [String] the glob pattern def glob_pattern(*pattern, **options) pattern_with_glob_pattern(*pattern, **options).last end # Uses the given pattern(s) to search for files and directories. # # @example # require 'mustermann/file_utils' # Mustermann::FileUtils.glob(':base.:ext') # => ['example.txt'] # # Mustermann::FileUtils.glob(':base.:ext') do |file, params| # file # => "example.txt" # params # => {"base"=>"example", "ext"=>"txt"} # end def glob(*pattern, **options, &block) raise ArgumentError, "no pattern given" if pattern.empty? pattern, glob_pattern = pattern_with_glob_pattern(*pattern, **options) results = [] unless block Dir.glob(glob_pattern) do |result| next unless params = pattern.params(result) block ? block[result, params] : results << result end results end # Allows to search for files an map these onto other strings. # # @example # require 'mustermann/file_utils' # # Mustermann::FileUtils.glob_map(':base.:ext' => ':base.bak.:ext') # => {'example.txt' => 'example.bak.txt'} # Mustermann::FileUtils.glob_map(':base.:ext' => :base) { |file, mapped| mapped } # => ['example'] # # @see Mustermann::Mapper def glob_map(map = {}, **options, &block) map = Mapper === map ? map : Mapper.new(map, **options) mapped = glob(*map.to_h.keys).map { |f| [f, unescape(map[f])] } block ? mapped.map(&block) : Hash[mapped] end # Copies files based on a pattern mapping. # # @example # require 'mustermann/file_utils' # # # copies example.txt to example.bak.txt # Mustermann::FileUtils.cp(':base.:ext' => ':base.bak.:ext') # # @see #glob_map def cp(map = {}, recursive: false, **options) utils_opts, opts = split_options(:preserve, :dereference_root, :remove_destination, **options) cp_method = recursive ? :cp_r : :cp glob_map(map, **opts) { |o,n| f.send(cp_method, o, n, **utils_opts) } end # Copies files based on a pattern mapping, recursively. # # @example # require 'mustermann/file_utils' # # # copies Foo.app/example.txt to Foo.back.app/example.txt # Mustermann::FileUtils.cp_r(':base.:ext' => ':base.bak.:ext') # # @see #glob_map def cp_r(map = {}, **options) cp(map, recursive: true, **options) end # Moves files based on a pattern mapping. # # @example # require 'mustermann/file_utils' # # # moves example.txt to example.bak.txt # Mustermann::FileUtils.mv(':base.:ext' => ':base.bak.:ext') # # @see #glob_map def mv(map = {}, **options) utils_opts, opts = split_options(**options) glob_map(map, **opts) { |o,n| f.mv(o, n, **utils_opts) } end # Creates links based on a pattern mapping. # # @example # require 'mustermann/file_utils' # # # creates a link from bin/example to lib/example.rb # Mustermann::FileUtils.ln('lib/:name.rb' => 'bin/:name') # # @see #glob_map def ln(map = {}, symbolic: false, **options) utils_opts, opts = split_options(**options) link_method = symbolic ? :ln_s : :ln glob_map(map, **opts) { |o,n| f.send(link_method, o, n, **utils_opts) } end # Creates symbolic links based on a pattern mapping. # # @example # require 'mustermann/file_utils' # # # creates a symbolic link from bin/example to lib/example.rb # Mustermann::FileUtils.ln_s('lib/:name.rb' => 'bin/:name') # # @see #glob_map def ln_s(map = {}, **options) ln(map, symbolic: true, **options) end # Creates symbolic links based on a pattern mapping. # Overrides potentailly existing files. # # @example # require 'mustermann/file_utils' # # # creates a symbolic link from bin/example to lib/example.rb # Mustermann::FileUtils.ln_sf('lib/:name.rb' => 'bin/:name') # # @see #glob_map def ln_sf(map = {}, **options) ln(map, symbolic: true, force: true, **options) end # Splits options into those meant for Mustermann and those # meant for ::FileUtils. # # @!visibility private def split_options(*utils_option_names, **options) utils_options, pattern_options = {}, {} utils_option_names += %i[force noop verbose] options.each do |key, value| list = utils_option_names.include?(key) ? utils_options : pattern_options list[key] = value end [utils_options, pattern_options] end # Create a Mustermann pattern from whatever the input is and turn it into # a glob pattern. # # @!visibility private def pattern_with_glob_pattern(*pattern, **options) options[:uri_decode] ||= false pattern = Mustermann.new(*pattern.flatten, **options) @glob_patterns ||= {} @glob_patterns[pattern] ||= GlobPattern.generate(pattern) [pattern, @glob_patterns[pattern]] end # The FileUtils method to use. # @!visibility private def f ::FileUtils end # Unescape an URI escaped string. # @!visibility private def unescape(string) @uri ||= URI::RFC2396_Parser.new @uri.unescape(string) end # Create a new version of Mustermann::FileUtils using a different ::FileUtils module. # @see DryRun # @!visibility private def with_file_utils(&block) Module.new do include Mustermann::FileUtils define_method(:f, &block) private(:f) extend self end end private :pattern_with_glob_pattern, :split_options, :f, :unescape alias_method :copy, :cp alias_method :move, :mv alias_method :link, :ln alias_method :symlink, :ln_s alias_method :[], :glob DryRun ||= with_file_utils { ::FileUtils::DryRun } NoWrite ||= with_file_utils { ::FileUtils::NoWrite } Verbose ||= with_file_utils { ::FileUtils::Verbose } end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/file_utils/000077500000000000000000000000001517364653100246445ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/lib/mustermann/file_utils/glob_pattern.rb000066400000000000000000000034521517364653100276550ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module FileUtils # AST Translator to turn Mustermann patterns into glob patterns. # @!visibility private class GlobPattern < Mustermann::AST::Translator # Character that need to be escaped in glob patterns. # @!visibility private ESCAPE = %w([ ] { } * ** \\) # Turn a Mustermann pattern into glob pattern. # @param [#to_glob, #to_ast, Object] pattern the object to turn into a glob pattern. # @return [String] the glob pattern # @!visibility private def self.generate(pattern) return pattern.to_glob if pattern.respond_to? :to_glob return new.translate(pattern.to_ast) if pattern.respond_to? :to_ast return "**/*" unless pattern.is_a? Mustermann::Composite "{#{pattern.patterns.map { |p| generate(p) }.join(',')}}" end translate(:root, :group, :expression) { t(payload) || "" } translate(:separator, :char) { t.escape(payload) } translate(:capture) { constraint ? "**/*" : "*" } translate(:optional) { "{#{t(payload)},}" } translate(:named_splat, :splat) { "**/*" } translate(:with_look_ahead) { t(head) + t(payload) } translate(:union) { "{#{payload.map { |e| t(e) }.join(',')}}" } translate(Array) { map { |e| t(e) }.join } # Escape with a slash rather than URI escaping. # @!visibility private def escape(char) ESCAPE.include?(char) ? "\\#{char}" : char end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/fileutils.rb000066400000000000000000000000401517364653100250240ustar00rootroot00000000000000require 'mustermann/file_utils' mustermann-4.0.0/mustermann-contrib/lib/mustermann/flask.rb000066400000000000000000000160531517364653100241370ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # Flask style pattern implementation. # # @example # Mustermann.new('/', type: :flask) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#flask Syntax description in the README class Flask < AST::Pattern include Concat::Native register :flask on(nil, ?>, ?:) { |c| unexpected(c) } on(?<) do |char| converter_name = expect(/\w+/, char: char) args, opts = scan(?() ? read_args(?=, ?)) : [[], {}] if scan(?:) name = read_escaped(?>) else converter_name, name = 'default', converter_name expect(?>) end converter = pattern.converters.fetch(converter_name) { unexpected("converter %p" % converter_name) } converter = converter.new(*args, **opts) if converter.respond_to? :new constraint = converter.constraint if converter.respond_to? :constraint convert = converter.convert if converter.respond_to? :convert qualifier = converter.qualifier if converter.respond_to? :qualifier node_type = converter.node_type if converter.respond_to? :node_type node_type ||= :capture node(node_type, name, convert: convert, constraint: constraint, qualifier: qualifier) end # A class for easy creating of converters. # @see Mustermann::Flask#register_converter class Converter # Constraint on the format used for the capture. # Should be a regexp (or a string corresponding to a regexp) # @see Mustermann::Flask#register_converter attr_accessor :constraint # Callback # Should be a Proc. # @see Mustermann::Flask#register_converter attr_accessor :convert # Constraint on the format used for the capture. # Should be a regexp (or a string corresponding to a regexp) # @see Mustermann::Flask#register_converter # @!visibility private attr_accessor :node_type # Constraint on the format used for the capture. # Should be a regexp (or a string corresponding to a regexp) # @see Mustermann::Flask#register_converter # @!visibility private attr_accessor :qualifier # @!visibility private def self.create(&block) Class.new(self) do define_method(:initialize) { |*a, **o| block[self, *a, **o] } end end # Makes sure a given value falls inbetween a min and a max. # Uses the passed block to convert the value from a string to whatever # format you'd expect. # # @example # require 'mustermann/flask' # # class MyPattern < Mustermann::Flask # register_converter(:x) { between(5, 15, &:to_i) } # end # # pattern = MyPattern.new('') # pattern.params('/12') # => { 'id' => 12 } # pattern.params('/16') # => { 'id' => 15 } # # @see Mustermann::Flask#register_converter def between(min, max) self.convert = proc do |input| value = yield(input) value = yield(min) if min and value < yield(min) value = yield(max) if max and value > yield(max) value end end end # Generally available converters. # @!visibility private def self.converters(inherited = true) return @converters ||= {} unless inherited defaults = superclass.respond_to?(:converters) ? superclass.converters : {} defaults.merge(converters(false)) end # @!visibility private def self.inherited(subclass) # :nodoc: super # Work around JRuby bug subclass.instance_variable_set(:@converters, {}) end # Allows you to register your own converters. # # It is recommended to use this on a subclass, so to not influence other subsystems # using flask templates. # # The object passed in as converter can implement #convert and/or #constraint. # # It can also instead implement #new, which will then return an object responding # to some of these methods. Arguments from the flask pattern will be passed to #new. # # If passed a block, it will be yielded to with a {Mustermann::Flask::Converter} # instance and any arguments in the flask pattern. # # @example with simple object # require 'mustermann/flask' # # MyPattern = Class.new(Mustermann::Flask) # up_converter = Struct.new(:convert).new(:upcase.to_proc) # MyPattern.register_converter(:upper, up_converter) # # MyPattern.new("/").params('/foo') # => { "name" => "FOO" } # # @example with block # require 'mustermann/flask' # # MyPattern = Class.new(Mustermann::Flask) # MyPattern.register_converter(:upper) { |c| c.convert = :upcase.to_proc } # # MyPattern.new("/").params('/foo') # => { "name" => "FOO" } # # @example with converter class # require 'mustermann/flask' # # class MyPattern < Mustermann::Flask # class Converter # attr_reader :convert # def initialize(send: :to_s) # @convert = send.to_sym.to_proc # end # end # # register_converter(:t, Converter) # end # # MyPattern.new("/").params('/Foo') # => { "name" => "FOO" } # MyPattern.new("/").params('/Foo') # => { "name" => "foo" } # # @param [#to_s] name converter name # @param [#new, #convert, #constraint, nil] converter def self.register_converter(name, converter = nil, &block) converter ||= Converter.create(&block) converters(false)[name.to_s] = converter end register_converter(:string) do |converter, minlength: nil, maxlength: nil, length: nil| converter.qualifier = "{%s,%s}" % [minlength || 1, maxlength] if minlength or maxlength converter.qualifier = "{%s}" % length if length end register_converter(:int) do |converter, min: nil, max: nil, fixed_digits: false| converter.constraint = /\d/ converter.qualifier = "{#{fixed_digits}}" if fixed_digits converter.between(min, max) { |string| Integer(string) } end register_converter(:float) do |converter, min: nil, max: nil| converter.constraint = /\d*\.?\d+/ converter.qualifier = "" converter.between(min, max) { |string| Float(string) } end register_converter(:path) do |converter| converter.node_type = :named_splat end register_converter(:any) do |converter, *strings| strings = strings.map { |s| Regexp.escape(s) unless s == {} }.compact converter.qualifier = "" converter.constraint = Regexp.union(*strings) end register_converter(:default, converters['string']) supported_options :converters attr_reader :converters def initialize(input, converters: {}, **options) @converters = self.class.converters.dup converters.each { |k,v| @converters[k.to_s] = v } if converters super(input, **options) end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/mapper.rb000066400000000000000000000060651517364653100243250ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/expander' require 'mustermann/set' module Mustermann # A mapper allows mapping one string to another based on pattern parsing and expanding. # # @example # require 'mustermann/mapper' # mapper = Mustermann::Mapper.new("/:foo" => "/:foo.html") # mapper['/example'] # => "/example.html" class Mapper # Creates a new mapper. # # @overload initialize(**options) # @param options [Hash] options The options hash # @yield block for generating mappings as a hash # @yieldreturn [Hash] see {#update} # # @example # require 'mustermann/mapper' # Mustermann::Mapper.new(type: :rails) {{ # "/:foo" => ["/:foo.html", "/:foo.:format"] # }} # # @overload initialize(**options) # @param options [Hash] options The options hash # @yield block for generating mappings as a hash # @yieldparam mapper [Mustermann::Mapper] the mapper instance # # @example # require 'mustermann/mapper' # Mustermann::Mapper.new(type: :rails) do |mapper| # mapper["/:foo"] = ["/:foo.html", "/:foo.:format"] # end # # @overload initialize(map = {}, **options) # @param map [Hash] see {#update} # @param [Hash] options The options hash # # @example map before options # require 'mustermann/mapper' # Mustermann::Mapper.new({"/:foo" => "/:foo.html"}, type: :rails) def initialize(map = {}, additional_values: :ignore, **options, &block) @options = options @additional_values = additional_values @set = Set.new(use_trie: false, use_cache: false, **options) block.arity == 0 ? update(yield) : yield(self) if block update(map) if map end # Add multiple mappings. # # @param map [Hash{String, Pattern: String, Pattern, Arry, Expander}] the mapping def update(map) map.to_h.each_pair do |input, output| output = Expander.new(*output, additional_values: @additional_values, **@options) unless output.is_a? Expander @set.add(input, output) end end # @return [Hash{Patttern: Expander}] Hash version of the mapper. def to_h = @set.patterns.to_h { [_1, @set[_1]] } # Convert a string according to mappings. You can pass in additional params. # # @example mapping with and without additional parameters # mapper = Mustermann::Mapper.new("/:example" => "(/:prefix)?/:example.html") # def convert(input, values = {}) @set.match_all(input).inject(input) do |current, m| params = Hash[values.merge(m.params).map { |k, v| [k.to_s, v] }] m.value.expandable?(params) ? m.value.expand(params) : current end end # Add a single mapping. # # @param key [String, Pattern] format of the input string # @param value [String, Pattern, Arry, Expander] format of the output string def []=(key, value) update key => value end alias_method :[], :convert end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/pattern_cache.rb000066400000000000000000000027351517364653100256410ustar00rootroot00000000000000# frozen_string_literal: true require 'set' require 'thread' require 'mustermann' module Mustermann # A simple, persistent cache for creating repositories. # # @example # require 'mustermann/pattern_cache' # cache = Mustermann::PatternCache.new # # # use this instead of Mustermann.new # pattern = cache.create_pattern("/:name", type: :rails) # # @note # {Mustermann::Pattern.new} (which is used by {Mustermann.new}) will reuse instances that have # not yet been garbage collected. You only need an extra cache if you do not keep a reference to # the patterns around. # # @api private class PatternCache # @param [Hash] pattern_options default options used for {#create_pattern} def initialize(**pattern_options) @cached = ::Set.new @mutex = Mutex.new @pattern_options = pattern_options end # @param (see Mustermann.new) # @return (see Mustermann.new) # @raise (see Mustermann.new) # @see Mustermann.new def create_pattern(string, **pattern_options) pattern = Mustermann.new(string, **pattern_options, **@pattern_options) @mutex.synchronize { @cached.add(pattern) } unless @cached.include? pattern pattern end # Removes all pattern instances from the cache. def clear @mutex.synchronize { @cached.clear } end # @return [Integer] number of currently cached patterns def size @mutex.synchronize { @cached.size } end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/pyramid.rb000066400000000000000000000013661517364653100245050ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # Pyramid style pattern implementation. # # @example # Mustermann.new('/', type: :pryamid) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#pryamid Syntax description in the README class Pyramid < AST::Pattern register :pyramid on(nil, ?}) { |c| unexpected(c) } on(?{) do |char| name = expect(/\w+/, char: char) constraint = read_brackets(?{, ?}) if scan(?:) expect(?}) unless constraint node(:capture, name, constraint: constraint) end on(?*) do |char| node(:named_splat, expect(/\w+$/, char: char), convert: -> e { e.split(?/) }) end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/shell.rb000066400000000000000000000034311517364653100241420ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/pattern' module Mustermann # Matches strings that are identical to the pattern. # # @example # Mustermann.new('/*.*', type: :shell) === '/bar' # => false # # @see Mustermann::Pattern # @see file:README.md#shell Syntax description in the README class Shell < Pattern include Concat::Native register :shell # @!visibility private # @return [#highlight, nil] # highlighing logic for mustermann-visualizer, # nil if mustermann-visualizer hasn't been loaded def highlighter return unless defined? Mustermann::Visualizer::Highlighter @@highlighter ||= Mustermann::Visualizer::Highlighter.create do on('\\') { |matched| escaped(matched, scanner.getch) } on(/[\*\[\]]/, :special) on("{") { nested(:union, ?{, ?}, ?,) } end end # @param (see Mustermann::Pattern#initialize) # @return (see Mustermann::Pattern#initialize) # @see (see Mustermann::Pattern#initialize) def initialize(string, **options) @flags = File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB super(string, **options) end # @param (see Mustermann::Pattern#===) # @return (see Mustermann::Pattern#===) # @see (see Mustermann::Pattern#===) def ===(string) File.fnmatch? @string, unescape(string), @flags end # @param (see Mustermann::Pattern#peek_size) # @return (see Mustermann::Pattern#peek_size) # @see (see Mustermann::Pattern#peek_size) def peek_size(string) @peek_string ||= @string + "{**,/**,/**/*}" super if File.fnmatch? @peek_string, unescape(string), @flags end # Used by {Mustermann::FileUtils} to not use a generic glob pattern. alias_method :to_glob, :to_s end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/simple.rb000066400000000000000000000036071517364653100243310ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/regexp_based' module Mustermann # Sinatra 1.3 style pattern implementation. # # @example # Mustermann.new('/:foo', type: :simple) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#simple Syntax description in the README class Simple < RegexpBased register :simple supported_options :greedy, :space_matches_plus instance_delegate highlighter: 'self.class' # @!visibility private # @return [#highlight, nil] # highlighing logic for mustermann-visualizer, # nil if mustermann-visualizer hasn't been loaded def self.highlighter return unless defined? Mustermann::Visualizer::Highlighter @highlighter ||= Mustermann::Visualizer::Highlighter.create do on(/:(\w+)/) { |matched| element(:capture, ':') { element(:name, matched[1..-1]) } } on("*" => :splat, "?" => :optional) end end def compile(greedy: true, uri_decode: true, space_matches_plus: true, **options) pattern = @string.gsub(/[^\?\%\\\/\:\*\w]/) { |c| encoded(c, uri_decode, space_matches_plus) } pattern.gsub!(/((:\w+)|\*)/) do |match| match == "*" ? "(?.*?)" : "(?<#{$2[1..-1]}>[^/?#]+#{?? unless greedy})" end Regexp.new(pattern) rescue SyntaxError, RegexpError => error type = error.message["invalid group name"] ? CompileError : ParseError raise type, error.message, error.backtrace end def encoded(char, uri_decode, space_matches_plus) return Regexp.escape(char) unless uri_decode parser = URI::RFC2396_Parser.new encoded = Regexp.union(parser.escape(char), parser.escape(char, /./).downcase, parser.escape(char, /./).upcase) encoded = Regexp.union(encoded, encoded('+', true, true)) if space_matches_plus and char == " " encoded end private :compile, :encoded end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/string_scanner.rb000066400000000000000000000244301517364653100260540ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/pattern_cache' require 'delegate' module Mustermann # Class inspired by Ruby's StringScanner to scan an input string using multiple patterns. # # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new("here is our example string") # # scanner.scan("here") # => "here" # scanner.getch # => " " # # if scanner.scan(":verb our") # scanner.scan(:noun, capture: :word) # scanner[:verb] # => "is" # scanner[:nound] # => "example" # end # # scanner.rest # => "string" # # @note # This structure is not thread-safe, you should not scan on the same StringScanner instance concurrently. # Even if it was thread-safe, scanning concurrently would probably lead to unwanted behaviour. class StringScanner # Exception raised if scan/unscan operation cannot be performed. ScanError = Class.new(::ScanError) PATTERN_CACHE = PatternCache.new private_constant :PATTERN_CACHE # Patterns created by {#scan} will be globally cached, since we assume that there is a finite number # of different patterns used and that they are more likely to be reused than not. # This method allows clearing the cache. # # @see Mustermann::PatternCache def self.clear_cache PATTERN_CACHE.clear end # @return [Integer] number of cached patterns # @see clear_cache # @api private def self.cache_size PATTERN_CACHE.size end # Encapsulates return values for {StringScanner#scan}, {StringScanner#check}, and friends. # Behaves like a String (the substring which matched the pattern), but also exposes its position # in the main string and any params parsed from it. class ScanResult < DelegateClass(String) # The scanner this result came from. # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new('foo/bar') # scanner.scan(:name).scanner == scanner # => true attr_reader :scanner # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new('foo/bar') # scanner.scan(:name).position # => 0 # scanner.getch.position # => 3 # scanner.scan(:name).position # => 4 # # @return [Integer] position the substring starts at attr_reader :position alias_method :pos, :position # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new('foo/bar') # scanner.scan(:name).length # => 3 # scanner.getch.length # => 1 # scanner.scan(:name).length # => 3 # # @return [Integer] length of the substring attr_reader :length # Params parsed from the substring. # Will not include params from previous scan results. # # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new('foo/bar') # scanner.scan(:name).params # => { "name" => "foo" } # scanner.getch.params # => {} # scanner.scan(:name).params # => { "name" => "bar" } # # @see Mustermann::StringScanner#params # @see Mustermann::StringScanner#[] # # @return [Hash] params parsed from the substring attr_reader :params # @api private def initialize(scanner, position, length, params = {}) @scanner, @position, @length, @params = scanner, position, length, params end # @api private # @!visibility private def __getobj__ @__getobj__ ||= scanner.to_s[position, length] end end # @return [Hash] default pattern options used for {#scan} and similar methods # @see #initialize attr_reader :pattern_options # Params from all previous matches from {#scan} and {#scan_until}, # but not from {#check} and {#check_until}. Changes can be reverted # with {#unscan} and it can be completely cleared via {#reset}. # # @return [Hash] current params attr_reader :params # @return [Integer] current scan position on the input string attr_accessor :position alias_method :pos, :position alias_method :pos=, :position= # @example with different default type # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new("foo/bar/baz", type: :shell) # scanner.scan('*') # => "foo" # scanner.scan('**/*') # => "/bar/baz" # # @param [String] string the string to scan # @param [Hash] pattern_options default options used for {#scan} def initialize(string = "", **pattern_options) @pattern_options = pattern_options @string = String(string).dup reset end # Resets the {#position} to the start and clears all {#params}. # @return [Mustermann::StringScanner] the scanner itself def reset @position = 0 @params = {} @history = [] self end # Moves the position to the end of the input string. # @return [Mustermann::StringScanner] the scanner itself def terminate track_result ScanResult.new(self, @position, size - @position) self end # Checks if the given pattern matches any substring starting at the current position. # # If it does, it will advance the current {#position} to the end of the substring and merges any params parsed # from the substring into {#params}. # # @param (see Mustermann.new) # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match def scan(pattern, **options) track_result check(pattern, **options) end # Checks if the given pattern matches any substring starting at any position after the current position. # # If it does, it will advance the current {#position} to the end of the substring and merges any params parsed # from the substring into {#params}. # # @param (see Mustermann.new) # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match def scan_until(pattern, **options) result, prefix = check_until_with_prefix(pattern, **options) track_result(prefix, result) end # Reverts the last operation that advanced the position. # # Operations advancing the position: {#terminate}, {#scan}, {#scan_until}, {#getch}. # @return [Mustermann::StringScanner] the scanner itself def unscan raise ScanError, 'unscan failed: previous match record not exist' if @history.empty? previous = @history[0..-2] reset previous.each { |r| track_result(*r) } self end # Checks if the given pattern matches any substring starting at the current position. # # Does not affect {#position} or {#params}. # # @param (see Mustermann.new) # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match def check(pattern, **options) params, length = create_pattern(pattern, **options).peek_params(rest) ScanResult.new(self, @position, length, params) if params end # Checks if the given pattern matches any substring starting at any position after the current position. # # Does not affect {#position} or {#params}. # # @param (see Mustermann.new) # @return [Mustermann::StringScanner::ScanResult, nil] the matched substring, nil if it didn't match def check_until(pattern, **options) check_until_with_prefix(pattern, **options).first end def check_until_with_prefix(pattern, **options) start = @position @position += 1 until eos? or result = check(pattern, **options) prefix = ScanResult.new(self, start, @position - start) if result [result, prefix] ensure @position = start end # Reads a single character and advances the {#position} by one. # @return [Mustermann::StringScanner::ScanResult, nil] the character, nil if at end of string def getch track_result ScanResult.new(self, @position, 1) unless eos? end # Appends the given string to the string being scanned # # @example # require 'mustermann/string_scanner' # scanner = Mustermann::StringScanner.new # scanner << "foo" # scanner.scan(/.+/) # => "foo" # # @param [String] string will be appended # @return [Mustermann::StringScanner] the scanner itself def <<(string) @string << string self end # @return [true, false] whether or not the end of the string has been reached def eos? @position >= @string.size end # @return [true, false] whether or not the current position is at the start of a line def beginning_of_line? @position == 0 or @string[@position - 1] == "\n" end # @return [String] outstanding string not yet matched, empty string at end of input string def rest @string[@position..-1] || "" end # @return [Integer] number of character remaining to be scanned def rest_size @position > size ? 0 : size - @position end # Allows to peek at a number of still unscanned characters without advacing the {#position}. # # @param [Integer] length how many characters to look at # @return [String] the substring def peek(length = 1) @string[@position, length] end # Shorthand for accessing {#params}. Accepts symbols as keys. def [](key) params[key.to_s] end # (see #params) def to_h params.dup end # @return [String] the input string # @see #initialize # @see #<< def to_s @string.dup end # @return [Integer] size of the input string def size @string.size end # @!visibility private def inspect "#<%p %d/%d @ %p>" % [ self.class, @position, @string.size, @string ] end # @!visibility private def create_pattern(pattern, **options) PATTERN_CACHE.create_pattern(pattern, **options, **pattern_options) end # @!visibility private def track_result(*results) results.compact! @history << results if results.any? results.each do |result| @params.merge! result.params @position += result.length end results.last end private :create_pattern, :track_result, :check_until_with_prefix end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/strscan.rb000066400000000000000000000000441517364653100245050ustar00rootroot00000000000000require 'mustermann/string_scanner' mustermann-4.0.0/mustermann-contrib/lib/mustermann/template.rb000066400000000000000000000035371517364653100246550ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' module Mustermann # URI template pattern implementation. # # @example # Mustermann.new('/{foo}') === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#template Syntax description in the README # @see http://tools.ietf.org/html/rfc6570 RFC 6570 class Template < AST::Pattern include Concat::Native register :template, :uri_template on ?{ do |char| variable = proc do start = pos match = expect(/(?\w+)(?:\:(?\d{1,4})|(?\*))?/) node(:variable, match[:name], prefix: match[:prefix], explode: match[:explode], start: start) end operator = buffer.scan(/[\+\#\.\/;\?\&\=\,\!\@\|]/) expression = node(:expression, [variable[]], operator: operator) { variable[] if scan(?,) } expression if expect(?}) end on(?}) { |c| unexpected(c) } # @!visibility private def compile(*args, **options) @split_params = {} super(*args, split_params: @split_params, **options) end # @!visibility private def map_param(key, value) return super unless variable = @split_params[key] value = value.split variable[:separator] value.map! { |e| e.sub(/\A#{key}=/, '') } if variable[:parametric] value.map! { |e| super(key, e) } end # @!visibility private def always_array?(key) @split_params.include? key end # Identity patterns support generating templates (the logic is quite complex, though). # # @example (see Mustermann::Pattern#to_templates) # @param (see Mustermann::Pattern#to_templates) # @return (see Mustermann::Pattern#to_templates) # @see Mustermann::Pattern#to_templates def to_templates [to_s] end private :compile, :map_param, :always_array? end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/to_pattern.rb000066400000000000000000000027371517364653100252220ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' module Mustermann # Mixin for adding {#to_pattern} ducktyping to objects. # # @example # require 'mustermann/to_pattern' # # class Foo # include Mustermann::ToPattern # # def to_s # ":foo/:bar" # end # end # # Foo.new.to_pattern # => # # # By default included into String, Symbol, Regexp, Array and {Mustermann::Pattern}. module ToPattern PRIMITIVES = [String, Symbol, Array, Regexp, Mustermann::Pattern] private_constant :PRIMITIVES # Converts the object into a {Mustermann::Pattern}. # # @example converting a string # ":name.png".to_pattern # => # # # @example converting a string with options # "/*path".to_pattern(type: :rails) # => # # # @example converting a regexp # /.*/.to_pattern # => # # # @example converting a pattern # Mustermann.new("foo").to_pattern # => # # # @param [Hash] options The options hash. # @return [Mustermann::Pattern] pattern corresponding to object. def to_pattern(**options) input = self if PRIMITIVES.any? { |p| self.is_a? p } input ||= __getobj__ if respond_to?(:__getobj__) Mustermann.new(input || to_s, **options) end PRIMITIVES.each do |klass| append_features(klass) end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/uri_template.rb000066400000000000000000000000351517364653100255220ustar00rootroot00000000000000require 'mustermann/template'mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer.rb000066400000000000000000000021671517364653100252350ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/highlight' require 'mustermann/visualizer/tree_renderer' require 'mustermann/visualizer/pattern_extension' module Mustermann # Namespace for Mustermann visualization logic. module Visualizer extend self # @example creating a highlight object # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_ansi # # @return [Mustermann::Visualizer::Highlight] highlight object for given pattern # @param (see Mustermann::Visualizer::Highlight#initialize) def highlight(pattern, **options) Highlight.new(pattern, **options) end # @example creating a tree object # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # tree = Mustermann::Visualizer.tree(pattern) # # puts highlight.to_s # # @return [Mustermann::Visualizer::Tree] tree object for given pattern def tree(pattern, **options) TreeRenderer.render(pattern, **options) end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/000077500000000000000000000000001517364653100247025ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/highlight.rb000066400000000000000000000077031517364653100272050ustar00rootroot00000000000000# frozen_string_literal: true require 'hansi' require 'mustermann/visualizer/highlighter' require 'mustermann/visualizer/renderer/ansi' require 'mustermann/visualizer/renderer/hansi_template' require 'mustermann/visualizer/renderer/html' require 'mustermann/visualizer/renderer/sexp' module Mustermann module Visualizer # Meta class for highlight objects. # @see Mustermann::Visualizer#highlight class Highlight # @!visibility private attr_reader :pattern, :theme # @!visibility private DEFAULT_THEME = Hansi::Theme.new(:solarized, default: :base0, separator: :base1, escaped: :base1, capture: :orange, name: :yellow, special: :blue, quote: :red, illegal: :darkred ) # @!visibility private BASE_THEME = Hansi::Theme.new( special: :default, capture: :special, char: :default, expression: :capture, composition: :special, group: :composition, union: :composition, optional: :special, root: :default, separator: :char, splat: :capture, named_splat: :splat, variable: :capture, escaped: :char, quote: :special, type: :special, illegal: :special ) # @!visibility private def initialize(pattern, type: nil, inspect: nil, **theme) @pattern = Mustermann.new(pattern, type: type) @inspect = inspect.nil? ? pattern.is_a?(Mustermann::Composite) : inspect theme = theme.any? ? Hansi::Theme.new(**theme) : DEFAULT_THEME @theme = BASE_THEME.merge(theme) end # @example # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_hansi_template # # @return [String] Hansi template representation of the pattern def to_hansi_template(**options) render_with(Renderer::HansiTemplate, **options) end # @example # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_ansi # # @return [String] ANSI colorized version of the pattern def to_ansi(**options) render_with(Renderer::ANSI, **options) end # @example # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_html # # @return [String] HTML rendering of the pattern def to_html(**options) render_with(Renderer::HTML, **options) end # @example # require 'mustermann/visualizer' # # pattern = Mustermann.new('/:name') # highlight = Mustermann::Visualizer.highlight(pattern) # # puts highlight.to_sexp # # @return [String] s-expression like representation of the pattern def to_sexp(**options) render_with(Renderer::Sexp, **options) end # @return [Mustermann::Pattern] the pattern used to create the highlight object def to_pattern pattern end # @return [String] string representation of the pattern def to_s pattern.to_s end # @return [String] stylesheet for HTML output from the pattern def stylesheet(**options) Renderer::HTML.new(self, **options).stylesheet end # @!visibility private def render_with(renderer, **options) options[:inspect] = @inspect if options[:inspect].nil? renderer.new(self, **options).render end # @!visibility private def render(renderer) Highlighter.highlight(pattern, renderer) end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/highlighter.rb000066400000000000000000000023441517364653100275300ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/highlighter/ast' require 'mustermann/visualizer/highlighter/ad_hoc' require 'mustermann/visualizer/highlighter/composite' require 'mustermann/visualizer/highlighter/dummy' require 'mustermann/visualizer/highlighter/regular' module Mustermann module Visualizer # @!visibility private module Highlighter extend self # @return [String] highlighted string # @!visibility private def highlight(pattern, renderer) highlighter_for(pattern).highlight(pattern, renderer) end # @return [#highlight] Highlighter for given pattern # @!visibility private def highlighter_for(pattern) return pattern.highlighter if pattern.respond_to? :highlighter and pattern.highlighter consts = constants.map { |name| const_get(name) } highlighter = consts.detect { |c| c.respond_to? :highlight? and c.highlight? pattern } highlighter || Dummy end # Used to generate highlighting rules on the fly. # @see Mustermann::Shell#highlighter # @see Mustermann::Simple#highlighter # @!visibility private def create(&block) Class.new(AdHoc, &block) end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/highlighter/000077500000000000000000000000001517364653100272005ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/highlighter/ad_hoc.rb000066400000000000000000000056151517364653100307510ustar00rootroot00000000000000# frozen_string_literal: true require 'strscan' module Mustermann module Visualizer # @!visibility private module Highlighter # Used to generate highlighting rules on the fly. # @see Mustermann::Shell#highlighter # @see Mustermann::Simple#highlighter} # @!visibility private class AdHoc # @!visibility private def self.highlight(pattern, renderer) new(pattern, renderer).highlight end # @!visibility private def self.rules @rules ||= {} end # @!visibility private def self.on(regexp, type = nil, &callback) return regexp.map { |key, value| on(key, value, &callback) } if regexp.is_a? Hash raise ArgumentError, 'needs type or callback' unless type or callback callback ||= proc { |matched| element(type, matched) } regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp rules[regexp] = callback end # @!visibility private attr_reader :pattern, :renderer, :rules, :output, :scanner def initialize(pattern, renderer) @pattern = pattern @renderer = renderer @output = String.new @rules = self.class.rules @scanner = ::StringScanner.new(pattern.to_s) end # @!visibility private def highlight(stop = /\Z/) output << renderer.pre(:root) until scanner.eos? or scanner.check(stop) position = scanner.pos apply(scanner) read_char(scanner) if position == scanner.pos and not scanner.check(stop) end output << renderer.post(:root) end # @!visibility private def apply(scanner) rules.each do |regexp, callback| next unless result = scanner.scan(regexp) instance_exec(result, &callback) end end # @!visibility private def read_char(scanner) return unless char = scanner.getch type = char == ?/ ? :separator : :char element(type, char) end # @!visibility private def escaped(content = ?\\, char) element(:escaped, content) { element(:escaped_char, char) } end # @!visibility private def nested(type, opening, closing, *separators) element(type, opening) do char = nil until char == closing or scanner.eos? highlight(Regexp.union(closing, *separators)) char = scanner.getch output << char if char end end end # @!visibility private def element(type, content = nil) output << renderer.pre(type) output << renderer.escape(content) if content yield if block_given? output << renderer.post(type) end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/highlighter/ast.rb000066400000000000000000000065231517364653100303220ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module Visualizer # @!visibility private module Highlighter # Provides highlighting for AST based patterns # @!visibility private class AST Index = Struct.new(:type, :start, :stop, :payload) { undef :to_a } Indexer = Mustermann::AST::Translator.create do translate(:node) { |i| Index.new(type, start, stop, Array(t(payload, i)).flatten.compact) } translate(Array) { |i| map { |e| t(e, i) } } translate(Object) { |i| } translate(:with_look_ahead) do |input| [t(head, input), *t(payload, input)] end translate(:expression) do |input| index = Index.new(type, start, stop, Array(t(payload, input)).compact) index.payload.delete_if { |e| e.type == :separator } index end translate(:capture) do |input| substring = input[start, length] if substart = substring.index(name) substart += start substop = substart + name.length payload = [Index.new(:name, substart, substop, [])] end Index.new(type, start, stop, payload || []) end translate(:char) do |input| substring = input[start, length] if payload == substring Index.new(type, start, stop, []) elsif substart = substring.index(payload) substart += start substop = substart + payload.length Index.new(:escaped, start, stop, [Index.new(:escaped_char, substart, substop, [])]) else Index.new(:escaped, start, stop, []) end end end private_constant(:Index, :Indexer) # @!visibility private def self.highlight?(pattern) pattern.respond_to? :to_ast end # @!visibility private def self.highlight(pattern, renderer) new(pattern, renderer).highlight end # @!visibility private def initialize(pattern, renderer) @ast = pattern.to_ast @string = pattern.to_s @renderer = renderer end # @!visibility private def highlight index = Indexer.translate(@ast, @string) inject_literals(index) render(index) end # @!visibility private def render(index) return @renderer.escape(@string[index.start..index.stop-1]) if index.type == :literal payload = index.payload.map { |i| render(i) }.join "#{ @renderer.pre(index.type) }#{ payload }#{ @renderer.post(index.type) }" end # @!visibility private def inject_literals(index) start, old_payload, index.payload = index.start, index.payload, [] old_payload.each do |element| index.payload << literal(start, element.start) if start < element.start index.payload << element inject_literals(element) start = element.stop end index.payload << literal(start, index.stop) if start < index.stop end # @!visibility private def literal(start, stop) Index.new(:literal, start, stop, []) end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/highlighter/composite.rb000066400000000000000000000027501517364653100315330ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # @!visibility private module Highlighter # @!visibility private module Composite extend self # @!visibility private def highlight?(pattern) pattern.is_a? Mustermann::Composite end # @!visibility private def highlight(pattern, renderer) operator = " #{pattern.operator} " patterns = pattern.patterns.map { |p| highlight_nested(p, renderer) }.join(quote(renderer, operator)) renderer.pre(:composite) + patterns + renderer.post(:composite) end # @!visibility private def highlight_nested(pattern, renderer) highlighter = Highlighter.highlighter_for(pattern) if highlighter.respond_to? :nested_highlight highlighter.nested_highlight(pattern, renderer) else type = quote(renderer, pattern.class.name[/[^:]+$/].downcase + ":", :type) quote = quote(renderer, ?") type + quote + highlighter.highlight(pattern, renderer) + quote end end # @!visibility private def nested_highlight(pattern, renderer) quote(renderer, ?() + highlight(pattern, renderer) + quote(renderer, ?)) end # @!visibility private def quote(renderer, string, type = :quote) renderer.pre(type) + renderer.escape(string, string) + renderer.post(type) end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/highlighter/dummy.rb000066400000000000000000000010731517364653100306610ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # @!visibility private module Highlighter # Provides highlighting for patterns that don't have a highlighter. # @!visibility private module Dummy # @!visibility private def self.highlight(pattern, renderer) output = String.new output << renderer.pre(:root) << renderer.pre(:unknown) output << renderer.escape(pattern.to_s) output << renderer.post(:unknown) << renderer.post(:root) end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/highlighter/regular.rb000066400000000000000000000065451517364653100312000ustar00rootroot00000000000000# frozen_string_literal: true require 'strscan' module Mustermann module Visualizer # @!visibility private module Highlighter # Provides highlighting for {Mustermann::Regular} # @!visibility private class Regular # @!visibility private SPECIAL_ESCAPE = ['w', 'W', 'd', 'D', 'h', 'H', 's', 'S', 'G', 'b', 'B'] private_constant(:SPECIAL_ESCAPE) # @!visibility private def self.highlight?(pattern) pattern.class.name == "Mustermann::Regular" end # @!visibility private def self.highlight(pattern, renderer) new(renderer).highlight(pattern) end # @!visibility private attr_reader :renderer, :output, :scanner # @!visibility private def initialize(renderer) @renderer = renderer @output = String.new end # @!visibility private def highlight(pattern) output << renderer.pre(:root) @scanner = ::StringScanner.new(pattern.to_s) scan output << renderer.post(:root) end # @!visibility private def scan(stop = nil) until scanner.eos? case char = scanner.getch when stop then return char when ?/ then element(:separator, char) when Regexp.escape(char) then element(:char, char) when ?\\ then escaped(scanner.getch) when ?( then potential_capture when ?[ then char_class when ?^, ?$ then element(:illegal, char) when ?{ then element(:special, "\{#{scanner.scan(/[^\}]*\}/)}") else element(:special, char) end end end # @!visibility private def char_class if result = scanner.scan(/\[:\w+:\]\]/) element(:special, "[#{result}") else element(:special, ?[) element(:special, ?^) if scanner.scan(/\^/) end end # @!visibility private def potential_capture if scanner.scan(/\?<(\w+)>/) element(:capture, "(?<") do element(:name, scanner[1]) output << ">" << scan(?)) end elsif scanner.scan(/\?(?:(?:-\w+)?:|>|<=|/)}") when 'p', 'u' then element(:special, "\\#{char}#{scanner.scan(/\{[^\}]*\}/)}") when ?/ then element(:separator, char) else element(:escaped, ?\\) { element(:escaped_char, char) } end end # @!visibility private def element(type, content = nil) output << renderer.pre(type) output << renderer.escape(content) if content yield if block_given? output << renderer.post(type) end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/pattern_extension.rb000066400000000000000000000036201517364653100310010ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # Mixin that will be added to {Mustermann::Pattern}. module PatternExtension prepend_features Composite prepend_features Pattern # @example # puts Mustermann.new("/:page").to_ansi # # @return [String] ANSI colorized version of the pattern. def to_ansi(inspect: nil, **theme) Visualizer.highlight(self, **theme).to_ansi(inspect: inspect) end # @example # puts Mustermann.new("/:page").to_html # # @return [String] HTML version of the pattern. def to_html(inspect: nil, tag: :span, class_prefix: "mustermann_", css: :inline, **theme) Visualizer.highlight(self, **theme).to_html(inspect: inspect, tag: tag, class_prefix: class_prefix, css: css) end # @example # puts Mustermann.new("/:page").to_tree # # @return [String] tree version of the pattern. def to_tree Visualizer.tree(self).to_s end # If invoked directly by puts: ANSI colorized version of the pattern. # If invoked by anything else: String version of the pattern. # # @example # require 'mustermann/visualizer' # pattern = Mustermann.new('/:page') # puts pattern # will have color # puts pattern.to_s # will not have color # # @return [String] non-colorized or colorized version of the pattern def to_s caller_locations.first.label == 'puts' ? to_ansi : super end # @return [String] ANSI colorized version of {Mustermann::Pattern#inspect} def color_inspect(base_color = nil, **theme) base_color ||= Highlight::DEFAULT_THEME[:base01] template = is_a?(Composite) ? "*#<%p:(*%s*)>*" : "*#<%p:*%s*>*" Hansi.render(template, self.class, to_ansi(inspect: true, **theme), {"*" => base_color}) end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/renderer/000077500000000000000000000000001517364653100265105ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/renderer/ansi.rb000066400000000000000000000011511517364653100277650ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # @!visibility private module Renderer # Generates ANSI colored strings. # @!visibility private class ANSI # @!visibility private def initialize(target, mode: Hansi.mode, **options) @target = target @mode = mode @options = options end # @!visibility private def render template = @target.to_hansi_template(**@options) Hansi.render(template, tags: true, theme: @target.theme, mode: @mode) end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/renderer/generic.rb000066400000000000000000000022761517364653100304600ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module Visualizer # @!visibility private module Renderer # Logic shared by most renderers. class Generic # @!visibility private def initialize(target, inspect: nil, add_qoutes: true) @target = target @inspect = inspect @add_qoutes = !target.pattern.is_a?(Mustermann::Composite) end # @!visibility private def render quote = "#{pre(:quote)}#{escape_string(?")}#{post(:quote)}" if @inspect and @add_qoutes pre(:pattern).to_s + preamble.to_s + quote.to_s + @target.render(self) + quote.to_s + post(:pattern).to_s end # @!visibility private def preamble end # @!visibility private def escape(value, inspect_value = value.to_s.inspect[1..-2]) escape_string(@inspect ? inspect_value : value.to_s) end # @!visibility private def escape_string(string) string end # @!visibility private def pre(type) "" end # @!visibility private def post(type) "" end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/renderer/hansi_template.rb000066400000000000000000000014211517364653100320300ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/renderer/generic' module Mustermann module Visualizer # @!visibility private module Renderer # Generates Hansi template string. # @see Mustermann::Visualizer::Renderer::ANSI # @!visibility private class HansiTemplate < Generic # @!visibility private def initialize(*, **) @hansi = Hansi::StringRenderer.new(tags: true) super end # @!visibility private def escape_string(string) @hansi.escape(string) end # @!visibility private def pre(type) "<#{type}>" end # @!visibility private def post(type) "" end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/renderer/html.rb000066400000000000000000000027041517364653100300040ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/renderer/generic' require 'cgi' module Mustermann module Visualizer # @!visibility private module Renderer # Generates HTML output. # @!visibility private class HTML < Generic # @!visibility private def initialize(target, tag: :span, class_prefix: "mustermann_", css: :inline, **options) raise ArgumentError, 'css option %p not supported, should be true, false or inline' if css != true and css != false and css != :inline super(target, **options) @css, @tag, @class_prefix = css, tag, class_prefix end # @!visibility private def preamble "" % stylesheet if @css == true end # @!visibility private def stylesheet @target.theme.to_css { |name| ".#{@class_prefix}pattern .#{@class_prefix}#{name}" } end # @!visibility private def escape_string(string) CGI.escape_html(string) end # @!visibility private def pre(type) if @css == :inline return "" unless rule = @target.theme[type] "<#{@tag} style=\"#{rule.to_css_rule}\">" else "<#{@tag} class=\"#{@class_prefix}#{type}\">" end end # @!visibility private def post(type) "" end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/renderer/sexp.rb000066400000000000000000000016111517364653100300130ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/renderer/generic' module Mustermann module Visualizer # @!visibility private module Renderer # Generates a s-expression like string. # @!visibility private class Sexp < Generic # @!visibility private def render @inspect = false super.gsub(/ ?\)( \))*/) { |s| s.gsub(' ', '') }.strip end # @!visibility private def pre(type) "(#{type} " if type != :pattern end # @!visibility private def escape_string(input) inspect = input.inspect input = inspect if inspect != "\"#{input}\"" input = inspect if input =~ /[\s\"\'\(\)]/ input + " " end # @!visibility private def post(type) ") " if type != :pattern end end end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/tree.rb000066400000000000000000000035411517364653100261710ustar00rootroot00000000000000# frozen_string_literal: true require 'hansi' module Mustermann module Visualizer # Represents a (sub)tree and at the same time a node in the tree. class Tree # @!visibility private attr_reader :line, :children, :prefix_color, :before, :after # @!visibility private def initialize(line, *children, prefix_color: :default, before: "", after: "") @line = line @children = children @prefix_color = prefix_color @before = before @after = after end # used for positioning {#after} # @!visibility private def line_widths(offset = 0) child_widths = children.flat_map { |c| c.line_widths(offset + 2) } width = length(line + before) + offset [width, *child_widths] end # Renders the tree. # @return [String] rendered version of the tree def to_s render("", "", line_widths.max) end # Renders tree, including nesting. # @!visibility private def render(first_prefix, prefix, width) output = before + Hansi.render(prefix_color, first_prefix) + line output = ljust(output, width) + " " + after + "\n" children[0..-2].each { |child| output += child.render(prefix + "├ ", prefix + "│ ", width) } output += children.last.render(prefix + "└ ", prefix + " ", width) if children.last output end # @!visibility private def length(string) deansi(string).length end # @!visibility private def deansi(string) string.gsub(/\e\[[^m]+m/, '') end # @!visibility private def ljust(string, width) missing = width - length(string) append = missing > 0 ? " " * missing : "" string + append end private :ljust, :deansi, :length end end end mustermann-4.0.0/mustermann-contrib/lib/mustermann/visualizer/tree_renderer.rb000066400000000000000000000053771517364653100300700ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/visualizer/tree' require 'mustermann/ast/translator' require 'hansi' module Mustermann module Visualizer # Turns an AST into a Tree # @!visibility private class TreeRenderer < AST::Translator TEMPLATE = '"%s%s%s" ' THEME = Hansi::Theme[:solarized] PREFIX_COLOR = THEME[:violet] FakeNode = Struct.new(:type, :start, :stop, :length) private_constant(:TEMPLATE, :THEME, :PREFIX_COLOR, :FakeNode) # Takes a pattern (or pattern string and option) and turns it into a tree. # Runs translation if pattern implements to_ast, otherwise returns single # node tree. # # @!visibility private def self.render(pattern, **options) pattern &&= Mustermann.new(pattern, **options) renderer = new(pattern.to_s) if pattern.respond_to? :to_ast renderer.translate(pattern.to_ast) else length = renderer.string.length node = FakeNode.new("pattern (not AST based)", 0, length, length) renderer.tree(node) end end # @!visibility private attr_reader :string # @!visibility private def initialize(string) @string = string end # access a substring of the pattern, in inspect mode # @!visibility private def sub(*args) string[*args].inspect[1..-2] end # creates a tree node # @!visibility private def tree(node, *children, **typed_children) children += children_for(typed_children) children = children.flatten.grep(Tree) infos = sub(0, node.start), sub(node.start, node.length), sub(node.stop..-1) description = Hansi.render(THEME[:green], node.type.to_s.tr("_", " ")) after = Hansi.render(TEMPLATE, *infos, theme: THEME, tags: true) Tree.new(description, *children, after: after, prefix_color: PREFIX_COLOR) end # Take a hash with trees as values and turn the keys into trees, too. # Read again if that didn't make sense. # @!visibility private def children_for(list) list.map do |key, value| value = Array(value).flatten if value.any? after = " " * string.inspect.length + " " description = Hansi.render(THEME[:orange], key.to_s) Tree.new(description, *value, after: after, prefix_color: PREFIX_COLOR) end end end translate(:node) { t.tree(node, payload: t(payload)) } translate(:with_look_ahead) { t.tree(node, head: t(head), payload: t(payload)) } translate(Array) { map { |e| t(e) }} translate(Object) { } end end end mustermann-4.0.0/mustermann-contrib/mustermann-contrib.gemspec000066400000000000000000000021751517364653100247470ustar00rootroot00000000000000$:.unshift File.expand_path("../../mustermann/lib", __FILE__) require "mustermann/version" github = "https://github.com/sinatra/mustermann" Gem::Specification.new do |s| s.name = "mustermann-contrib" s.version = Mustermann::VERSION s.authors = ["Konstantin Haase", "Kunpei Sakai", "Patrik Ragnarsson", "Jordan Owens", "Zachary Scott"] s.email = "sinatrarb@googlegroups.com" s.homepage = github s.summary = %q{Collection of extensions for Mustermann} s.description = %q{Adds many plugins to Mustermann} s.license = 'MIT' s.required_ruby_version = '>= 3.3.0' s.files = `git ls-files lib`.split("\n") + ['LICENSE', 'README.md'] s.metadata = { "bug_tracker_uri" => "#{github}/issues", "changelog_uri" => "#{github}/blob/main/CHANGELOG.md", "documentation_uri" => "#{github}/tree/main/mustermann-contrib#readme", "source_code_uri" => "#{github}/tree/main/mustermann-contrib", } s.add_dependency 'mustermann', Mustermann::VERSION s.add_dependency 'hansi', '~> 0.2.0' end mustermann-4.0.0/mustermann-contrib/spec/000077500000000000000000000000001517364653100205005ustar00rootroot00000000000000mustermann-4.0.0/mustermann-contrib/spec/cake_spec.rb000066400000000000000000000052351517364653100227470ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/cake' describe Mustermann::Cake do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should expand.to('') } it { should_not expand(a: 1) } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } it { should expand.to('/') } it { should_not expand(a: 1) } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should expand.to('/foo') } it { should_not expand(a: 1) } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } it { should expand.to('/foo/bar') } it { should_not expand(a: 1) } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/*' do it { should match('/') } it { should match('/foo') } it { should match('/foo/bar') } example { pattern.params('/foo/bar') .should be == {"splat" => ["foo", "bar"]}} it { should generate_template('/{+splat}') } end pattern '/**' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } example { pattern.params('/foo/bar') .should be == {"splat" => ["foo/bar"]} } it { should generate_template('/{+splat}') } end end mustermann-4.0.0/mustermann-contrib/spec/express_spec.rb000066400000000000000000000157651517364653100235460ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/express' describe Mustermann::Express do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should expand.to('') } it { should_not expand(a: 1) } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } it { should expand.to('/') } it { should_not expand(a: 1) } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should expand.to('/foo') } it { should_not expand(a: 1) } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } it { should expand.to('/foo/bar') } it { should_not expand(a: 1) } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/:foo+' do it { should_not match('/') } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end pattern '/:foo?' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should match('/') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('/') .should be == {"foo" => nil } } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should expand .to('/') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should generate_template('/{foo}') } it { should generate_template('/') } end pattern '/:foo*' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end pattern '/:foo(.*)' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{foo}') } end pattern '/:foo(\d+)' do it { should_not match('/') } it { should_not match('/foo') } it { should match('/15') .capturing foo: '15' } it { should generate_template('/{foo}') } end pattern '/:foo(\d+|bar)' do it { should_not match('/') } it { should_not match('/foo') } it { should match('/15') .capturing foo: '15' } it { should match('/bar') .capturing foo: 'bar' } it { should generate_template('/{foo}') } end pattern '/:foo(\))' do it { should_not match('/') } it { should_not match('/foo') } it { should match('/)').capturing foo: ')' } it { should generate_template('/{foo}') } end pattern '/:foo(prefix(\d+|bar))' do it { should_not match('/prefix') } it { should_not match('/prefixfoo') } it { should match('/prefix15') .capturing foo: 'prefix15' } it { should match('/prefixbar') .capturing foo: 'prefixbar' } it { should generate_template('/{foo}') } end pattern '/(.+)' do it { should_not match('/') } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } end pattern '/(foo(a|b))' do it { should_not match('/') } it { should match('/fooa') .capturing splat: 'fooa' } it { should match('/foob') .capturing splat: 'foob' } it { should generate_template('/{+splat}') } end context 'invalid syntax' do example 'unexpected closing parenthesis' do expect { Mustermann::Express.new('foo)bar') }. to raise_error(Mustermann::ParseError, 'unexpected ) while parsing "foo)bar"') end example 'missing closing parenthesis' do expect { Mustermann::Express.new('foo(bar') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar"') end example 'unexpected ?' do expect { Mustermann::Express.new('foo?bar') }. to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "foo?bar"') end example 'unexpected *' do expect { Mustermann::Express.new('foo*bar') }. to raise_error(Mustermann::ParseError, 'unexpected * while parsing "foo*bar"') end end end mustermann-4.0.0/mustermann-contrib/spec/file_utils_spec.rb000066400000000000000000000064671517364653100242130ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/file_utils' describe Mustermann::FileUtils do subject(:utils) { Mustermann::FileUtils } include FileUtils before do @pwd = Dir.pwd @tmp_dir = File.join(__dir__, 'tmp') mkdir_p(@tmp_dir) chdir(@tmp_dir) touch("foo.txt") touch("foo.rb") touch("bar.txt") touch("bar.rb") end after do chdir(@pwd) if @pwd rm_rf(@tmp_dir) if @tmp_dir end describe :glob_pattern do example { utils.glob_pattern('/:foo') .should be == '/*' } example { utils.glob_pattern('/*foo') .should be == '/**/*' } example { utils.glob_pattern('/(ab|c)?/:foo/d/*bar') .should be == '/{{ab,c},}/*/d/**/*' } example { utils.glob_pattern('/a', '/b') .should be == '{/a,/b}' } example { utils.glob_pattern('**/*', type: :shell) .should be == '**/*' } example { utils.glob_pattern(/can't parse this/) .should be == '**/*' } example { utils.glob_pattern('/foo', type: :identity) .should be == '/foo' } example { utils.glob_pattern('/fo*', type: :identity) .should be == '/fo\\*' } end describe :glob do example do utils.glob(":name.txt").sort.should be == ['bar.txt', 'foo.txt'] end example do extensions = [] utils.glob("foo.:ext") { |file, params| extensions << params['ext'] } extensions.sort.should be == ['rb', 'txt'] end example do utils.glob(":name.:ext", capture: { ext: 'rb', name: 'foo' }).should be == ['foo.rb'] end end describe :glob_map do example do utils.glob_map({':name.rb' => ':name/init.rb'}).should be == { "bar.rb" => "bar/init.rb", "foo.rb" => "foo/init.rb" } end example do result = {} returned = utils.glob_map({':name.rb' => ':name/init.rb'}) { |k, v| result[v] = k.upcase } returned.sort .should be == ["BAR.RB", "FOO.RB"] result["bar/init.rb"] .should be == "BAR.RB" end end describe :cp do example do utils.cp({':name.rb' => ':name.ruby', ':name.txt' => ':name.md'}) File.should be_exist('foo.ruby') File.should be_exist('bar.md') File.should be_exist('bar.txt') end end describe :cp_r do example do mkdir_p "foo/bar" utils.cp_r({'foo/:name' => :name}) File.should be_directory('bar') end end describe :mv do example do utils.mv({':name.rb' => ':name.ruby', ':name.txt' => ':name.md'}) File.should be_exist('foo.ruby') File.should be_exist('bar.md') File.should_not be_exist('bar.txt') end end describe :ln do example do utils.ln({':name.rb' => ':name.ruby', ':name.txt' => ':name.md'}) File.should be_exist('foo.ruby') File.should be_exist('bar.md') File.should be_exist('bar.txt') end end describe :ln_s do example do utils.ln_s({':name.rb' => ':name.ruby', ':name.txt' => ':name.md'}) File.should be_symlink('foo.ruby') File.should be_symlink('bar.md') File.should be_exist('bar.txt') end end describe :ln_sf do example do utils.ln_sf({':name.rb' => ':name.txt'}) File.should be_symlink('foo.txt') end end end mustermann-4.0.0/mustermann-contrib/spec/flask_spec.rb000066400000000000000000000303361517364653100231440ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/flask' describe Mustermann::Flask do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should expand.to('') } it { should_not expand(a: 1) } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } it { should expand.to('/') } it { should_not expand(a: 1) } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should expand.to('/foo') } it { should_not expand(a: 1) } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } it { should expand.to('/foo/bar') } it { should_not expand(a: 1) } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/f') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/f') .capturing foo: 'f' } it { should match('/fo') .capturing foo: 'fo' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/42').capturing foo: '42' } it { should_not match('/1.0') } it { should_not match('/.5') } it { should_not match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should_not match('/%0Afoo') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/42').should be == {"foo" => 42} } it { should expand(foo: 12).to('/12') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/42').capturing foo: '42' } it { should_not match('/1.0') } it { should_not match('/.5') } it { should_not match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should_not match('/%0Afoo') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/42').should be == {"foo" => 42} } it { should expand(foo: 12).to('/12') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do example { pattern.params('/42').should be == {"foo" => 42} } example { pattern.params('/52').should be == {"foo" => 50} } example { pattern.params('/2').should be == {"foo" => 5} } end pattern '/' do example { pattern.params('/42.5').should be == {"foo" => 42.5} } example { pattern.params('/52.5').should be == {"foo" => 50.5} } example { pattern.params('/2.5').should be == {"foo" => 5.0} } end pattern '///' do it { should match('/foo/42/42') .capturing foo: '42', bar: '42' } it { should match('/foo/1.0/1') .capturing foo: '1.0', bar: '1' } it { should match('/foo/.5/0') .capturing foo: '.5', bar: '0' } it { should_not match('/foo/1/1.0') } it { should_not match('/foo/1.0/1.0') } it { should generate_template('/{prefix}/{foo}/{bar}') } example do pattern.params('/foo/1.0/1').should be == { "prefix" => "foo", "foo" => 1.0, "bar" => 1 } end end pattern '/' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end converter = Struct.new(:convert).new(:upcase.to_proc) pattern '/', converters: { foo: converter } do it { should match('/foo').capturing bar: 'foo' } example { pattern.params('/foo').should be == {"bar" => "FOO"} } end context 'invalid syntax' do example 'unexpected end of capture' do expect { Mustermann::Flask.new('foo>bar') }. to raise_error(Mustermann::ParseError, 'unexpected > while parsing "foo>bar"') end example 'missing end of capture' do expect { Mustermann::Flask.new('foo') }. to raise_error(Mustermann::ParseError, 'unexpected converter "bar" while parsing "foo"') end example 'broken argument synax' do expect { Mustermann::Flask.new('') }. to raise_error(Mustermann::ParseError, 'unexpected = while parsing ""') end example 'missing )' do expect { Mustermann::Flask.new('' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/f') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/f') .capturing foo: 'f' } it { should match('/fo') .capturing foo: 'fo' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/42').capturing foo: '42' } it { should_not match('/1.0') } it { should_not match('/.5') } it { should_not match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should_not match('/%0Afoo') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/42').should be == {"foo" => 42} } it { should expand(foo: 12).to('/12') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/42').capturing foo: '42' } it { should_not match('/1.0') } it { should_not match('/.5') } it { should_not match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should_not match('/%0Afoo') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/42').should be == {"foo" => 42} } it { should expand(foo: 12).to('/12') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo,bar') .capturing foo: 'foo,bar' } it { should_not match('/f') } it { should_not match('/fo') } it { should_not match('/fooo') } it { should_not match('/foo.bar') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should_not match('/baz') } it { should generate_template('/{foo}') } end pattern '/' do example { pattern.params('/42').should be == {"foo" => 42} } example { pattern.params('/52').should be == {"foo" => 50} } example { pattern.params('/2').should be == {"foo" => 5} } end pattern '/' do example { pattern.params('/42.5').should be == {"foo" => 42.5} } example { pattern.params('/52.5').should be == {"foo" => 50.5} } example { pattern.params('/2.5').should be == {"foo" => 5.0} } end pattern '///' do it { should match('/foo/42/42') .capturing foo: '42', bar: '42' } it { should match('/foo/1.0/1') .capturing foo: '1.0', bar: '1' } it { should match('/foo/.5/0') .capturing foo: '.5', bar: '0' } it { should_not match('/foo/1/1.0') } it { should_not match('/foo/1.0/1.0') } it { should generate_template('/{prefix}/{foo}/{bar}') } example do pattern.params('/foo/1.0/1').should be == { "prefix" => "foo", "foo" => 1.0, "bar" => 1 } end end pattern '/' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end pattern '/' do it { should match('/foo').capturing bar: 'foo' } it { should generate_template('/{bar}') } example do expect { Mustermann::Flask.new('/') }.to \ raise_error(Mustermann::ParseError, 'unexpected converter "foo" while parsing "/"') end end context 'invalid syntax' do example 'unexpected end of capture' do expect { FlaskSubclass.new('foo>bar') }. to raise_error(Mustermann::ParseError, 'unexpected > while parsing "foo>bar"') end example 'missing end of capture' do expect { FlaskSubclass.new('foo') }. to raise_error(Mustermann::ParseError, 'unexpected converter "bar" while parsing "foo"') end example 'broken argument synax' do expect { FlaskSubclass.new('') }. to raise_error(Mustermann::ParseError, 'unexpected = while parsing ""') end example 'missing )' do expect { FlaskSubclass.new(' "/bar" }}} its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } } example { mapper['/foo'].should be == '/bar' } example { mapper['/fox'].should be == '/fox' } end context 'accepts a block with argument, passes instance to it' do subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise) { |m| m["/foo"] = "/bar" }} its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } } example { mapper['/foo'].should be == '/bar' } example { mapper['/fox'].should be == '/fox' } end context 'accepts mappings followed by options' do subject(:mapper) { Mustermann::Mapper.new({ "/foo" => "/bar" }, additional_values: :raise) } its(:to_h) { should be == { Mustermann.new("/foo") => Mustermann::Expander.new("/bar") } } example { mapper['/foo'].should be == '/bar' } example { mapper['/fox'].should be == '/fox' } end context 'allows specifying type' do subject(:mapper) { Mustermann::Mapper.new({ "/foo" => "/bar" }, additional_values: :raise, type: :rails) } its(:to_h) { should be == { Mustermann.new("/foo", type: :rails) => Mustermann::Expander.new("/bar", type: :rails) } } example { mapper['/foo'].should be == '/bar' } example { mapper['/fox'].should be == '/fox' } end end describe :convert do subject(:mapper) { Mustermann::Mapper.new } context 'it maps params' do before { mapper["/:a"] = "/:a.html" } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end context 'it supports named splats' do before { mapper["/*a"] = "/*a.html" } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar.html" } end context 'can map from patterns' do before { mapper[Mustermann.new("/:a")] = "/:a.html" } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end context 'can map to patterns' do before { mapper[Mustermann.new("/:a")] = Mustermann.new("/:a.html") } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end context 'can map to expanders' do before { mapper[Mustermann.new("/:a")] = Mustermann::Expander.new("/:a.html") } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end context 'can map to array' do before { mapper["/:a"] = ["/:a.html", "/:a.:f"] } example { mapper["/foo"] .should be == "/foo.html" } example { mapper["/foo", "f" => 'x'] .should be == "/foo.x" } example { mapper["/foo", f: 'x'] .should be == "/foo.x" } example { mapper["/foo/bar"] .should be == "/foo/bar" } end end end mustermann-4.0.0/mustermann-contrib/spec/pattern_extension_spec.rb000066400000000000000000000024661517364653100256200ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/visualizer' require 'pp' require 'stringio' describe Mustermann::Visualizer::PatternExtension do subject(:pattern) { Mustermann.new("/:name") } before { Hansi.mode = 16 } after { Hansi.mode = nil } specify :to_ansi do pattern.to_ansi(inspect: true, capture: :red, default: nil).should be == "\e[0m\"\e[0m/\e[0m\e[91m:\e[0m\e[91mname\e[0m\"\e[0m" pattern.to_ansi(inspect: false, capture: :green, default: nil).should be == "\e[0m/\e[0m\e[32m:\e[0m\e[32mname\e[0m" pattern.to_ansi(inspect: nil, capture: :green, default: nil).should be == "\e[0m/\e[0m\e[32m:\e[0m\e[32mname\e[0m" end specify :to_html do pattern.to_html(css: false, class_prefix: "", tag: :tt).should be == '/:name' end specify :to_tree do pattern.to_tree.should be == Mustermann::Visualizer.tree(pattern).to_s end specify :color_inspect do pattern.color_inspect.should include(pattern.to_ansi(inspect: true)) pattern.color_inspect.should include("# "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/*foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } example { pattern.params("/foo/bar").should be == {"foo" => ["foo", "bar"]}} it { should generate_template('/{+foo}') } end pattern '/{foo:.*}' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } example { pattern.params("/foo/bar").should be == {"foo" => "foo/bar"}} it { should generate_template('/{foo}') } end end mustermann-4.0.0/mustermann-contrib/spec/shell_spec.rb000066400000000000000000000110151517364653100231440ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/shell' describe Mustermann::Shell do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should_not respond_to(:expand) } it { should_not respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } example { pattern.params('/').should be == {} } example { pattern.params('').should be_nil } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should match('/foo%2Fbar') } it { should match('/foo%2fbar') } end pattern '/*/bar' do it { should match('/foo/bar') } it { should match('/bar/bar') } it { should match('/foo%2Fbar') } it { should match('/foo%2fbar') } it { should_not match('/foo/foo/bar') } it { should_not match('/bar/foo') } end pattern '/**/foo' do it { should match('/a/b/c/foo') } it { should match('/a/b/c/foo') } it { should match('/a/.b/c/foo') } it { should match('/a/.b/c/foo') } end pattern '/:foo' do it { should match('/:foo') } it { should match('/%3Afoo') } it { should_not match('/foo') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/{foo,bar}' do it { should match('/foo') } it { should match('/bar') } it { should_not match('/foobar') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end describe :=~ do example { '/foo'.should be =~ Mustermann::Shell.new('/foo') } end context "peeking" do subject(:pattern) { Mustermann::Shell.new("foo*/") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar/".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar/".size } example { pattern.peek_size("/foo bar") .should be_nil } context 'with just * as pattern' do subject(:pattern) { Mustermann::Shell.new('*') } example { pattern.peek_size('foo') .should be == 3 } example { pattern.peek_size('foo/bar') .should be == 3 } example { pattern.peek_size('foo/bar/baz') .should be == 3 } example { pattern.peek_size('foo/bar/baz/blah') .should be == 3 } end end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar/" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar/" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{}, "foo bar/".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{}, "foo%20bar/".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end context "highlighting" do let(:pattern) { Mustermann::Shell.new("/**,*/\\*/{a,b}") } subject(:sexp) { Mustermann::Visualizer.highlight(pattern).to_sexp } it { should be == '(root (separator /) (special *) (special *) (char ,) (special *) (separator /) (escaped "\\\\" (escaped_char *)) (separator /) (union { (root (char a)) ,(root (char b)) }))' } end end mustermann-4.0.0/mustermann-contrib/spec/simple_spec.rb000066400000000000000000000213751517364653100233400ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/simple' require 'mustermann/visualizer' describe Mustermann::Simple do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should_not respond_to(:expand) } it { should_not respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern "/:foo/:bar" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} } example { pattern.params('').should be_nil } end pattern '/hello/:person' do it { should match('/hello/Frank').capturing person: 'Frank' } end pattern '/?:foo?/?:bar?' do it { should match('/hello/world') .capturing foo: 'hello', bar: 'world' } it { should match('/hello') .capturing foo: 'hello', bar: nil } it { should match('/') .capturing foo: nil, bar: nil } it { should match('') .capturing foo: nil, bar: nil } it { should_not match('/hello/world/') } end pattern '/*' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } example { pattern.params('/foo').should be == {"splat" => ["foo"]} } end pattern '/:foo/*' do it { should match("/foo/bar/baz") .capturing foo: 'foo', splat: 'bar/baz' } it { should match("/foo/") .capturing foo: 'foo', splat: '' } it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', splat: 'h%20a%20y' } it { should_not match('/foo') } example { pattern.params('/bar/foo').should be == {"splat" => ["foo"], "foo" => "bar"} } example { pattern.params('/bar/foo/f%20o').should be == {"splat" => ["foo/f o"], "foo" => "bar"} } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should match('/path%2Bwith%2Bspaces') } it { should match('/path+with+spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/*/:foo/*/*' do it { should match('/bar/foo/bling/baz/boom') } it 'should map to proper params' do pattern.params('/bar/foo/bling/baz/boom').should be == { "foo" => "foo", "splat" => ['bar', 'bling', 'baz/boom'] } end end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/:file.:ext' do it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%E6%AD%A3%2Ejpg') .capturing file: 'pony%E6%AD%A3', ext: 'jpg' } it { should match('/pony%e6%ad%a3%2ejpg') .capturing file: 'pony%e6%ad%a3', ext: 'jpg' } it { should match('/pony正%2Ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正%2ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正..jpg') .capturing file: 'pony正.', ext: 'jpg' } it { should_not match('/.jpg') } end pattern '/:id/test.bar' do it { should match('/3/test.bar') .capturing id: '3' } it { should match('/2/test.bar') .capturing id: '2' } it { should match('/2E/test.bar') .capturing id: '2E' } it { should match('/2e/test.bar') .capturing id: '2e' } it { should match('/%2E/test.bar') .capturing id: '%2E' } end pattern '/10/:id' do it { should match('/10/test') .capturing id: 'test' } it { should match('/10/te.st') .capturing id: 'te.st' } end pattern '/10.1/:id' do it { should match('/10.1/test') .capturing id: 'test' } it { should match('/10.1/te.st') .capturing id: 'te.st' } end pattern '/foo?' do it { should match('/fo') } it { should match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } end pattern '/:fOO' do it { should match('/a').capturing fOO: 'a' } end pattern '/:_X' do it { should match('/a').capturing _X: 'a' } end pattern '/:f00' do it { should match('/a').capturing f00: 'a' } end pattern '/:foo.?' do it { should match('/a.').capturing foo: 'a.' } it { should match('/xy').capturing foo: 'xy' } end pattern '/(a)' do it { should match('/(a)') } it { should_not match('/a') } end pattern '/:foo.?', greedy: false do it { should match('/a.').capturing foo: 'a' } it { should match('/xy').capturing foo: 'xy' } end pattern '/foo?', uri_decode: false do it { should match('/foo') } it { should match('/fo') } it { should_not match('/foo?') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should match('/path with spaces') } it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern "/path with spaces", space_matches_plus: false do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context 'error handling' do example '? at beginning of route' do expect { Mustermann::Simple.new('?foobar') }. to raise_error(Mustermann::ParseError) end example 'invalid capture name' do expect { Mustermann::Simple.new('/:1a/') }. to raise_error(Mustermann::CompileError) end end context "peeking" do subject(:pattern) { Mustermann::Simple.new(":name") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end context "highlighting" do let(:pattern) { Mustermann::Simple.new("/:name?/*") } subject(:sexp) { Mustermann::Visualizer.highlight(pattern).to_sexp } it { should be == "(root (separator /) (capture : (name name)) (optional ?) (separator /) (splat *))" } end end mustermann-4.0.0/mustermann-contrib/spec/string_scanner_spec.rb000066400000000000000000000133601517364653100250610ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/string_scanner' describe Mustermann::StringScanner do include Support::ScanMatcher subject(:scanner) { Mustermann::StringScanner.new(example_string) } let(:example_string) { "foo bar" } describe :scan do it { should scan("foo") } it { should scan(/foo/) } it { should scan(:name) } it { should scan(":name") } it { should_not scan(" ") } it { should_not scan("bar") } example do should scan("foo") should scan(" ") should scan("bar") end example do scanner.position = 4 should scan("bar") end example do should scan("foo") scanner.reset should scan("foo") end end describe :check do it { should check("foo") } it { should check(/foo/) } it { should check(:name) } it { should check(":name") } it { should_not check(" ") } it { should_not check("bar") } example do should check("foo") should_not check(" ") should_not check("bar") should check("foo") end example do scanner.position = 4 should check("bar") end end describe :scan_until do it { should scan_until("foo") } it { should scan_until(":name") } it { should scan_until(" ") } it { should scan_until("bar") } it { should_not scan_until("baz") } example do should scan_until(" ") should check("bar") end example do should scan_until(" ") scanner.reset should scan("foo") end end describe :check_until do it { should check_until("foo") } it { should check_until(":name") } it { should check_until(" ") } it { should check_until("bar") } it { should_not check_until("baz") } example do should check_until(" ") should_not check("bar") end end describe :getch do example { scanner.getch.should be == "f" } example do scanner.scan("foo") scanner.getch.should be == " " should scan("bar") end example do scanner.getch scanner.reset should scan("foo") end end describe :<< do example do should_not scan_until("baz") scanner << " baz" scanner.to_s.should be == "foo bar baz" should scan_until("baz") end end describe :eos? do it { should_not be_eos } example do scanner.position = 7 should be_eos end end describe :beginning_of_line? do let(:example_string) { "foo\nbar" } it { should be_beginning_of_line } example do scanner.position = 2 should_not be_beginning_of_line end example do scanner.position = 3 should_not be_beginning_of_line end example do scanner.position = 4 should be_beginning_of_line end end describe :rest do example { scanner.rest.should be == "foo bar" } example do scanner.position = 4 scanner.rest.should be == "bar" end end describe :rest_size do example { scanner.rest_size.should be == 7 } example do scanner.position = 4 scanner.rest_size.should be == 3 end end describe :peek do example { scanner.peek(3).should be == "foo" } example do scanner.peek(3).should be == "foo" scanner.peek(3).should be == "foo" end example do scanner.position = 4 scanner.peek(3).should be == "bar" end end describe :inspect do example { scanner.inspect.should be == '#' } example do scanner.position = 4 scanner.inspect.should be == '#' end end describe :[] do example do should scan(:name) scanner['name'].should be == "foo bar" end example do should scan(:name, capture: /\S+/) scanner['name'].should be == "foo" should scan(" :name", capture: /\S+/) scanner['name'].should be == "bar" end example do should scan(":a", capture: /\S+/) should scan(" :b", capture: /\S+/) scanner['a'].should be == "foo" scanner['b'].should be == "bar" end example do a = scanner.scan(":a", capture: /\S+/) b = scanner.scan(" :b", capture: /\S+/) a.params['a'].should be == 'foo' b.params['b'].should be == 'bar' a.params['b'].should be_nil b.params['a'].should be_nil end example do result = scanner.check(":a", capture: /\S+/) result.params['a'].should be == 'foo' scanner['a'].should be_nil end example do should scan(:name) scanner.reset scanner['name'].should be_nil end end describe :unscan do example do should scan(:name, capture: /\S+/) scanner['name'].should be == "foo" should scan(" :name", capture: /\S+/) scanner['name'].should be == "bar" scanner.unscan scanner['name'].should be == "foo" scanner.rest.should be == " bar" end example do should scan_until(" ") scanner.unscan scanner.rest.should be == "foo bar" end example do expect { scanner.unscan }.to raise_error(Mustermann::StringScanner::ScanError, 'unscan failed: previous match record not exist') end end describe :terminate do example do scanner.terminate scanner.should be_eos end end describe :to_h do example { scanner.to_h.should be == {} } example do end end describe :to_s do example { scanner.to_s.should be == "foo bar" } end describe :clear_cache do example do scanner.scan("foo") Mustermann::StringScanner.clear_cache Mustermann::StringScanner.cache_size.should be == 0 end end end mustermann-4.0.0/mustermann-contrib/spec/template_spec.rb000066400000000000000000000777731517364653100236770ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/template' describe Mustermann::Template do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern '/:foo' do it { should match('/:foo') } it { should match('/%3Afoo') } it { should_not match('/foo') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should match('/path%2Bwith%2Bspaces') } it { should match('/path+with+spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern "/path with spaces", space_matches_plus: false do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern "/path with spaces", uri_decode: false do it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context 'level 1' do context 'without operator' do pattern '/hello/{person}' do it { should match('/hello/Frank').capturing person: 'Frank' } it { should match('/hello/a_b~c').capturing person: 'a_b~c' } it { should match('/hello/a.%20').capturing person: 'a.%20' } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } example { pattern.params('/hello/Frank').should be == {'person' => 'Frank'} } end pattern "/{foo}/{bar}" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end end end context 'level 2' do context 'operator +' do pattern '/hello/{+person}' do it { should match('/hello/Frank') .capturing person: 'Frank' } it { should match('/hello/a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/a.%20') .capturing person: 'a.%20' } it { should match('/hello/a/%20') .capturing person: 'a/%20' } it { should match('/hello/:') .capturing person: ?: } it { should match('/hello//') .capturing person: ?/ } it { should match('/hello/?') .capturing person: ?? } it { should match('/hello/#') .capturing person: ?# } it { should match('/hello/[') .capturing person: ?[ } it { should match('/hello/]') .capturing person: ?] } it { should match('/hello/@') .capturing person: ?@ } it { should match('/hello/!') .capturing person: ?! } it { should match('/hello/*') .capturing person: ?* } it { should match('/hello/+') .capturing person: ?+ } it { should match('/hello/,') .capturing person: ?, } it { should match('/hello/;') .capturing person: ?; } it { should match('/hello/=') .capturing person: ?= } end pattern "/{+foo}/{bar}" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/foo/bar/bar.foo') .capturing foo: 'foo/bar', bar: 'bar.foo' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end end context 'operator #' do pattern '/hello/{#person}' do it { should match('/hello/#Frank') .capturing person: 'Frank' } it { should match('/hello/#a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/#a.%20') .capturing person: 'a.%20' } it { should match('/hello/#a/%20') .capturing person: 'a/%20' } it { should match('/hello/#:') .capturing person: ?: } it { should match('/hello/#/') .capturing person: ?/ } it { should match('/hello/#?') .capturing person: ?? } it { should match('/hello/##') .capturing person: ?# } it { should match('/hello/#[') .capturing person: ?[ } it { should match('/hello/#]') .capturing person: ?] } it { should match('/hello/#@') .capturing person: ?@ } it { should match('/hello/#!') .capturing person: ?! } it { should match('/hello/#*') .capturing person: ?* } it { should match('/hello/#+') .capturing person: ?+ } it { should match('/hello/#,') .capturing person: ?, } it { should match('/hello/#;') .capturing person: ?; } it { should match('/hello/#=') .capturing person: ?= } it { should_not match('/hello/Frank') } it { should_not match('/hello/a_b~c') } it { should_not match('/hello/a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } example { pattern.params('/hello/#Frank').should be == {'person' => 'Frank'} } end pattern "/{+foo}/{#bar}" do it { should match('/foo/#bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/#bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/foo/bar/#bar.foo') .capturing foo: 'foo/bar', bar: 'bar.foo' } it { should match('/10.1/#te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/#te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2F#bar') } it { should_not match('/foo%2f#bar') } example { pattern.params('/hello/#Frank').should be == {'foo' => 'hello', 'bar' => 'Frank'} } end end end context 'level 3' do context 'without operator' do pattern "{a,b,c}" do it { should match("~x,42,_").capturing a: '~x', b: '42', c: '_' } it { should_not match("~x,42") } it { should_not match("~x/42") } it { should_not match("~x#42") } it { should_not match("~x,42,_#42") } example { pattern.params('d,f,g').should be == {'a' => 'd', 'b' => 'f', 'c' => 'g'} } end end context 'operator +' do pattern "{+a,b,c}" do it { should match("~x,42,_") .capturing a: '~x', b: '42', c: '_' } it { should match("~x,42,_#42") .capturing a: '~x', b: '42', c: '_#42' } it { should match("~/x,42,_/42") .capturing a: '~/x', b: '42', c: '_/42' } it { should_not match("~x,42") } it { should_not match("~x/42") } it { should_not match("~x#42") } end end context 'operator #' do pattern "{#a,b,c}" do it { should match("#~x,42,_") .capturing a: '~x', b: '42', c: '_' } it { should match("#~x,42,_#42") .capturing a: '~x', b: '42', c: '_#42' } it { should match("#~/x,42,_#42") .capturing a: '~/x', b: '42', c: '_#42' } it { should_not match("~x,42,_") } it { should_not match("~x,42,_#42") } it { should_not match("~/x,42,_#42") } it { should_not match("~x,42") } it { should_not match("~x/42") } it { should_not match("~x#42") } end end context 'operator .' do pattern '/hello/{.person}' do it { should match('/hello/.Frank') .capturing person: 'Frank' } it { should match('/hello/.a_b~c') .capturing person: 'a_b~c' } it { should_not match('/hello/.:') } it { should_not match('/hello/./') } it { should_not match('/hello/.?') } it { should_not match('/hello/.#') } it { should_not match('/hello/.[') } it { should_not match('/hello/.]') } it { should_not match('/hello/.@') } it { should_not match('/hello/.!') } it { should_not match('/hello/.*') } it { should_not match('/hello/.+') } it { should_not match('/hello/.,') } it { should_not match('/hello/.;') } it { should_not match('/hello/.=') } it { should_not match('/hello/Frank') } it { should_not match('/hello/a_b~c') } it { should_not match('/hello/a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{.a,b,c}" do it { should match(".~x.42._").capturing a: '~x', b: '42', c: '_' } it { should_not match(".~x,42") } it { should_not match(".~x/42") } it { should_not match(".~x#42") } it { should_not match(".~x,42,_") } it { should_not match("~x.42._") } end end context 'operator /' do pattern '/hello{/person}' do it { should match('/hello/Frank') .capturing person: 'Frank' } it { should match('/hello/a_b~c') .capturing person: 'a_b~c' } it { should_not match('/hello//:') } it { should_not match('/hello///') } it { should_not match('/hello//?') } it { should_not match('/hello//#') } it { should_not match('/hello//[') } it { should_not match('/hello//]') } it { should_not match('/hello//@') } it { should_not match('/hello//!') } it { should_not match('/hello//*') } it { should_not match('/hello//+') } it { should_not match('/hello//,') } it { should_not match('/hello//;') } it { should_not match('/hello//=') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{/a,b,c}" do it { should match("/~x/42/_").capturing a: '~x', b: '42', c: '_' } it { should_not match("/~x,42") } it { should_not match("/~x.42") } it { should_not match("/~x#42") } it { should_not match("/~x,42,_") } it { should_not match("~x/42/_") } end end context 'operator ;' do pattern '/hello/{;person}' do it { should match('/hello/;person=Frank') .capturing person: 'Frank' } it { should match('/hello/;person=a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/;person') .capturing person: nil } it { should_not match('/hello/;persona=Frank') } it { should_not match('/hello/;persona=a_b~c') } it { should_not match('/hello/;person=:') } it { should_not match('/hello/;person=/') } it { should_not match('/hello/;person=?') } it { should_not match('/hello/;person=#') } it { should_not match('/hello/;person=[') } it { should_not match('/hello/;person=]') } it { should_not match('/hello/;person=@') } it { should_not match('/hello/;person=!') } it { should_not match('/hello/;person=*') } it { should_not match('/hello/;person=+') } it { should_not match('/hello/;person=,') } it { should_not match('/hello/;person=;') } it { should_not match('/hello/;person==') } it { should_not match('/hello/;Frank') } it { should_not match('/hello/;a_b~c') } it { should_not match('/hello/;a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{;a,b,c}" do it { should match(";a=~x;b=42;c=_") .capturing a: '~x', b: '42', c: '_' } it { should match(";a=~x;b;c=_") .capturing a: '~x', b: nil, c: '_' } it { should_not match(";a=~x;c=_;b=42").capturing a: '~x', b: '42', c: '_' } it { should_not match(";a=~x;b=42") } it { should_not match("a=~x;b=42") } it { should_not match(";a=~x;b=#42;c") } it { should_not match(";a=~x,b=42,c=_") } it { should_not match("~x;b=42;c=_") } end end context 'operator ?' do pattern '/hello/{?person}' do it { should match('/hello/?person=Frank') .capturing person: 'Frank' } it { should match('/hello/?person=a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/?person') .capturing person: nil } it { should_not match('/hello/?persona=Frank') } it { should_not match('/hello/?persona=a_b~c') } it { should_not match('/hello/?person=:') } it { should_not match('/hello/?person=/') } it { should_not match('/hello/?person=?') } it { should_not match('/hello/?person=#') } it { should_not match('/hello/?person=[') } it { should_not match('/hello/?person=]') } it { should_not match('/hello/?person=@') } it { should_not match('/hello/?person=!') } it { should_not match('/hello/?person=*') } it { should_not match('/hello/?person=+') } it { should_not match('/hello/?person=,') } it { should_not match('/hello/?person=;') } it { should_not match('/hello/?person==') } it { should_not match('/hello/?Frank') } it { should_not match('/hello/?a_b~c') } it { should_not match('/hello/?a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{?a,b,c}" do it { should match("?a=~x&b=42&c=_") .capturing a: '~x', b: '42', c: '_' } it { should match("?a=~x&b&c=_") .capturing a: '~x', b: nil, c: '_' } it { should_not match("?a=~x&c=_&b=42").capturing a: '~x', b: '42', c: '_' } it { should_not match("?a=~x&b=42") } it { should_not match("a=~x&b=42") } it { should_not match("?a=~x&b=#42&c") } it { should_not match("?a=~x,b=42,c=_") } it { should_not match("~x&b=42&c=_") } end end context 'operator &' do pattern '/hello/{&person}' do it { should match('/hello/&person=Frank') .capturing person: 'Frank' } it { should match('/hello/&person=a_b~c') .capturing person: 'a_b~c' } it { should match('/hello/&person') .capturing person: nil } it { should_not match('/hello/&persona=Frank') } it { should_not match('/hello/&persona=a_b~c') } it { should_not match('/hello/&person=:') } it { should_not match('/hello/&person=/') } it { should_not match('/hello/&person=?') } it { should_not match('/hello/&person=#') } it { should_not match('/hello/&person=[') } it { should_not match('/hello/&person=]') } it { should_not match('/hello/&person=@') } it { should_not match('/hello/&person=!') } it { should_not match('/hello/&person=*') } it { should_not match('/hello/&person=+') } it { should_not match('/hello/&person=,') } it { should_not match('/hello/&person=;') } it { should_not match('/hello/&person==') } it { should_not match('/hello/&Frank') } it { should_not match('/hello/&a_b~c') } it { should_not match('/hello/&a.%20') } it { should_not match('/hello/:') } it { should_not match('/hello//') } it { should_not match('/hello/?') } it { should_not match('/hello/#') } it { should_not match('/hello/[') } it { should_not match('/hello/]') } it { should_not match('/hello/@') } it { should_not match('/hello/!') } it { should_not match('/hello/*') } it { should_not match('/hello/+') } it { should_not match('/hello/,') } it { should_not match('/hello/;') } it { should_not match('/hello/=') } end pattern "{&a,b,c}" do it { should match("&a=~x&b=42&c=_") .capturing a: '~x', b: '42', c: '_' } it { should match("&a=~x&b&c=_") .capturing a: '~x', b: nil, c: '_' } it { should_not match("&a=~x&c=_&b=42").capturing a: '~x', b: '42', c: '_' } it { should_not match("&a=~x&b=42") } it { should_not match("a=~x&b=42") } it { should_not match("&a=~x&b=#42&c") } it { should_not match("&a=~x,b=42,c=_") } it { should_not match("~x&b=42&c=_") } end end end context 'level 4' do context 'without operator' do context 'prefix' do pattern '{a:3}/bar' do it { should match('foo/bar') .capturing a: 'foo' } it { should match('fo/bar') .capturing a: 'fo' } it { should match('f/bar') .capturing a: 'f' } it { should_not match('fooo/bar') } end pattern '{a:3}{b}' do it { should match('foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{a*}' do it { should match('a') .capturing a: 'a' } it { should match('a,b') .capturing a: 'a,b' } it { should match('a,b,c') .capturing a: 'a,b,c' } it { should_not match('a,b/c') } it { should_not match('a,') } example { pattern.params('a').should be == { 'a' => ['a'] }} example { pattern.params('a,b').should be == { 'a' => ['a', 'b'] }} end pattern '{a*},{b}' do it { should match('a,b') .capturing a: 'a', b: 'b' } it { should match('a,b,c') .capturing a: 'a,b', b: 'c' } it { should_not match('a,b/c') } it { should_not match('a,') } example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end pattern '{a*,b}' do it { should match('a,b') .capturing a: 'a', b: 'b' } it { should match('a,b,c') .capturing a: 'a,b', b: 'c' } it { should_not match('a,b/c') } it { should_not match('a,') } example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator +' do pattern '/{a}/{+b}' do it { should match('/foo/bar/baz').capturing(a: 'foo', b: 'bar/baz') } it { should expand(a: 'foo/bar', b: 'foo/bar').to('/foo%2Fbar/foo/bar') } end context 'prefix' do pattern '{+a:3}/bar' do it { should match('foo/bar') .capturing a: 'foo' } it { should match('fo/bar') .capturing a: 'fo' } it { should match('f/bar') .capturing a: 'f' } it { should_not match('fooo/bar') } end pattern '{+a:3}{b}' do it { should match('foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{+a*}' do it { should match('a') .capturing a: 'a' } it { should match('a,b') .capturing a: 'a,b' } it { should match('a,b,c') .capturing a: 'a,b,c' } it { should match('a,b/c') .capturing a: 'a,b/c' } end pattern '{+a*},{b}' do it { should match('a,b') .capturing a: 'a', b: 'b' } it { should match('a,b,c') .capturing a: 'a,b', b: 'c' } it { should_not match('a,b/c') } it { should_not match('a,') } example { pattern.params('a,b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator #' do context 'prefix' do pattern '{#a:3}/bar' do it { should match('#foo/bar') .capturing a: 'foo' } it { should match('#fo/bar') .capturing a: 'fo' } it { should match('#f/bar') .capturing a: 'f' } it { should_not match('#fooo/bar') } end pattern '{#a:3}{b}' do it { should match('#foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{#a*}' do it { should match('#a') .capturing a: 'a' } it { should match('#a,b') .capturing a: 'a,b' } it { should match('#a,b,c') .capturing a: 'a,b,c' } it { should match('#a,b/c') .capturing a: 'a,b/c' } example { pattern.params('#a,b').should be == { 'a' => ['a', 'b'] }} example { pattern.params('#a,b,c').should be == { 'a' => ['a', 'b', 'c'] }} end pattern '{#a*,b}' do it { should match('#a,b') .capturing a: 'a', b: 'b' } it { should match('#a,b,c') .capturing a: 'a,b', b: 'c' } it { should_not match('#a,') } example { pattern.params('#a,b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('#a,b,c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator .' do context 'prefix' do pattern '{.a:3}/bar' do it { should match('.foo/bar') .capturing a: 'foo' } it { should match('.fo/bar') .capturing a: 'fo' } it { should match('.f/bar') .capturing a: 'f' } it { should_not match('.fooo/bar') } end pattern '{.a:3}{b}' do it { should match('.foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{.a*}' do it { should match('.a') .capturing a: 'a' } it { should match('.a.b') .capturing a: 'a.b' } it { should match('.a.b.c') .capturing a: 'a.b.c' } it { should_not match('.a.b,c') } it { should_not match('.a,') } end pattern '{.a*,b}' do it { should match('.a.b') .capturing a: 'a', b: 'b' } it { should match('.a.b.c') .capturing a: 'a.b', b: 'c' } it { should_not match('.a.b/c') } it { should_not match('.a.') } example { pattern.params('.a.b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('.a.b.c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator /' do context 'prefix' do pattern '{/a:3}/bar' do it { should match('/foo/bar') .capturing a: 'foo' } it { should match('/fo/bar') .capturing a: 'fo' } it { should match('/f/bar') .capturing a: 'f' } it { should_not match('/fooo/bar') } end pattern '{/a:3}{b}' do it { should match('/foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{/a*}' do it { should match('/a') .capturing a: 'a' } it { should match('/a/b') .capturing a: 'a/b' } it { should match('/a/b/c') .capturing a: 'a/b/c' } it { should_not match('/a/b,c') } it { should_not match('/a,') } end pattern '{/a*,b}' do it { should match('/a/b') .capturing a: 'a', b: 'b' } it { should match('/a/b/c') .capturing a: 'a/b', b: 'c' } it { should_not match('/a/b,c') } it { should_not match('/a/') } example { pattern.params('/a/b').should be == { 'a' => ['a'], 'b' => 'b' }} example { pattern.params('/a/b/c').should be == { 'a' => ['a', 'b'], 'b' => 'c' }} end end end context 'operator ;' do context 'prefix' do pattern '{;a:3}/bar' do it { should match(';a=foo/bar') .capturing a: 'foo' } it { should match(';a=fo/bar') .capturing a: 'fo' } it { should match(';a=f/bar') .capturing a: 'f' } it { should_not match(';a=fooo/bar') } end pattern '{;a:3}{b}' do it { should match(';a=foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{;a*}' do it { should match(';a=1') .capturing a: 'a=1' } it { should match(';a=1;a=2') .capturing a: 'a=1;a=2' } it { should match(';a=1;a=2;a=3') .capturing a: 'a=1;a=2;a=3' } it { should_not match(';a=1;a=2;b=3') } it { should_not match(';a=1;a=2;a=3,') } end pattern '{;a*,b}' do it { should match(';a=1;b') .capturing a: 'a=1', b: nil } it { should match(';a=2;a=2;b=1') .capturing a: 'a=2;a=2', b: '1' } it { should_not match(';a;b;c') } it { should_not match(';a;') } example { pattern.params(';a=2;a=2;b').should be == { 'a' => ['2', '2'], 'b' => nil }} end end end context 'operator ?' do context 'prefix' do pattern '{?a:3}/bar' do it { should match('?a=foo/bar') .capturing a: 'foo' } it { should match('?a=fo/bar') .capturing a: 'fo' } it { should match('?a=f/bar') .capturing a: 'f' } it { should_not match('?a=fooo/bar') } end pattern '{?a:3}{b}' do it { should match('?a=foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{?a*}' do it { should match('?a=1') .capturing a: 'a=1' } it { should match('?a=1&a=2') .capturing a: 'a=1&a=2' } it { should match('?a=1&a=2&a=3') .capturing a: 'a=1&a=2&a=3' } it { should_not match('?a=1&a=2&b=3') } it { should_not match('?a=1&a=2&a=3,') } end pattern '{?a*,b}' do it { should match('?a=1&b') .capturing a: 'a=1', b: nil } it { should match('?a=2&a=2&b=1') .capturing a: 'a=2&a=2', b: '1' } it { should_not match('?a&b&c') } it { should_not match('?a&') } example { pattern.params('?a=2&a=2&b').should be == { 'a' => ['2', '2'], 'b' => nil }} end end end context 'operator &' do context 'prefix' do pattern '{&a:3}/bar' do it { should match('&a=foo/bar') .capturing a: 'foo' } it { should match('&a=fo/bar') .capturing a: 'fo' } it { should match('&a=f/bar') .capturing a: 'f' } it { should_not match('&a=fooo/bar') } end pattern '{&a:3}{b}' do it { should match('&a=foobar') .capturing a: 'foo', b: 'bar' } end end context 'expand' do pattern '{&a*}' do it { should match('&a=1') .capturing a: 'a=1' } it { should match('&a=1&a=2') .capturing a: 'a=1&a=2' } it { should match('&a=1&a=2&a=3') .capturing a: 'a=1&a=2&a=3' } it { should_not match('&a=1&a=2&b=3') } it { should_not match('&a=1&a=2&a=3,') } end pattern '{&a*,b}' do it { should match('&a=1&b') .capturing a: 'a=1', b: nil } it { should match('&a=2&a=2&b=1') .capturing a: 'a=2&a=2', b: '1' } it { should_not match('&a&b&c') } it { should_not match('&a&') } example { pattern.params('&a=2&a=2&b').should be == { 'a' => ['2', '2'], 'b' => nil }} example { pattern.params('&a=2&a=%20&b').should be == { 'a' => ['2', ' '], 'b' => nil }} end end end end context 'invalid syntax' do example 'unexpected closing bracket' do expect { Mustermann::Template.new('foo}bar') }. to raise_error(Mustermann::ParseError, 'unexpected } while parsing "foo}bar"') end example 'missing closing bracket' do expect { Mustermann::Template.new('foo{bar') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo{bar"') end end context "peeking" do subject(:pattern) { Mustermann::Template.new("{name}bar") } describe :peek_size do example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo "}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end end mustermann-4.0.0/mustermann-contrib/spec/to_pattern_spec.rb000066400000000000000000000046111517364653100242200ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/to_pattern' require 'delegate' describe Mustermann::ToPattern do context String do example { "".to_pattern .should be_a(Mustermann::Sinatra) } example { "".to_pattern(type: :rails) .should be_a(Mustermann::Rails) } end context Regexp do example { //.to_pattern .should be_a(Mustermann::Regular) } example { //.to_pattern(type: :rails) .should be_a(Mustermann::Regular) } end context Symbol do example { :foo.to_pattern .should be_a(Mustermann::Sinatra) } example { :foo.to_pattern(type: :rails) .should be_a(Mustermann::Sinatra) } end context Array do example { [:foo, :bar].to_pattern .should be_a(Mustermann::Sinatra) } example { [:foo, :bar].to_pattern(type: :rails) .should be_a(Mustermann::Sinatra) } end context Mustermann::Pattern do subject(:pattern) { Mustermann.new('') } example { pattern.to_pattern.should be == pattern } example { pattern.to_pattern(type: :rails).should be_a(Mustermann::Sinatra) } end context 'custom class' do let(:example_class) do Class.new do include Mustermann::ToPattern def to_s ":foo/:bar" end end end example { example_class.new.to_pattern .should be_a(Mustermann::Sinatra) } example { example_class.new.to_pattern(type: :rails) .should be_a(Mustermann::Rails) } example { Mustermann.new(example_class.new) .should be_a(Mustermann::Sinatra) } example { Mustermann.new(example_class.new, type: :rails) .should be_a(Mustermann::Rails) } end context 'primitive delegate' do let(:example_class) do Class.new(DelegateClass(Array)) do include Mustermann::ToPattern end end example { example_class.new([:foo, :bar]).to_pattern .should be_a(Mustermann::Sinatra) } example { example_class.new([:foo, :bar]).to_pattern(type: :rails) .should be_a(Mustermann::Sinatra) } end context 'primitive subclass' do let(:example_class) do Class.new(Array) do include Mustermann::ToPattern end end example { example_class.new([:foo, :bar]).to_pattern .should be_a(Mustermann::Sinatra) } example { example_class.new([:foo, :bar]).to_pattern(type: :rails) .should be_a(Mustermann::Sinatra) } end end mustermann-4.0.0/mustermann-contrib/spec/visualizer_spec.rb000066400000000000000000000220021517364653100242300ustar00rootroot00000000000000require 'support' require 'mustermann/visualizer' describe Mustermann::Visualizer do subject(:highlight) { Mustermann::Visualizer.highlight(pattern) } before { Hansi.mode = 256 } after { Hansi.mode = nil } describe :highlight do context :sinatra do context "/a" do let(:pattern) { Mustermann.new("/a") } its(:to_ansi) { should be == "\e[0m\e[38;5;246m\e[38;5;246m\e[38;5;247m/\e[0m\e[38;5;246m\e[38;5;246m\e[38;5;246ma\e[0m" } its(:to_html) { should be == '/a' } its(:to_sexp) { should be == '(root (separator /) (char a))' } its(:to_pattern) { should be == pattern } its(:to_s) { should be == "/a" } its(:stylesheet) { should include(".mustermann_pattern .mustermann_illegal {\n color: #8b0000;") } example do highlight.to_html(css: false).should be == '/a' end example do renderer = Mustermann::Visualizer::Renderer::Generic result = highlight.render_with(renderer) result.should be == pattern.to_s end end context '/:name' do let(:pattern) { Mustermann.new("/:name") } its(:to_sexp) { should be == "(root (separator /) (capture : (name name)))" } end context '/{name}' do let(:pattern) { Mustermann.new("/{name}") } its(:to_sexp) { should be == "(root (separator /) (capture { (name name) }))" } end context '/{+name}' do let(:pattern) { Mustermann.new("/{+name}") } its(:to_sexp) { should be == "(root (separator /) (named_splat {+ (name name) }))" } end context ':user(@:host)?' do let(:pattern) { Mustermann.new(':user(@:host)?') } its(:to_sexp) { should be == '(root (capture : (name user)) (optional (group "(" (char @) (capture : (name host)) ")") ?))' } end context 'a b' do let(:pattern) { Mustermann.new('a b') } its(:to_sexp) { should be == '(root (char a) (char " ") (char b))' } end context 'a|b' do let(:pattern) { Mustermann.new('a|b') } its(:to_sexp) { should be == '(root (union (char a) | (char b)))' } end context '(a|b)' do let(:pattern) { Mustermann.new('(a|b)c') } its(:to_sexp) { should be == '(root (union "(" (char a) | (char b) ")") (char c))' } end context '\:a' do let(:pattern) { Mustermann.new('\:a') } its(:to_sexp) { should be == '(root (escaped "\\\\" (escaped_char :)) (char a))' } end end context :regexp do context 'a' do let(:pattern) { Mustermann.new('a', type: :regexp) } its(:to_sexp) { should be == '(root (char a))' } end context '/(\d+)' do let(:pattern) { Mustermann.new('/(\d+)', type: :regexp) } its(:to_sexp) { should be == '(root (separator /) (capture "(" (special "\\\\d") (special +))))' } end context '\A' do let(:pattern) { Mustermann.new('\A', type: :regexp, check_anchors: false) } its(:to_sexp) { should be == '(root (illegal "\\\\A"))' } end context '(?.)\g' do let(:pattern) { Mustermann.new('(?.)\g', type: :regexp) } its(:to_sexp) { should be == '(root (capture "(?<" (name name) >(special .))) (special "\\\\g"))' } end context '\p{Ll}' do let(:pattern) { Mustermann.new('\p{Ll}', type: :regexp) } its(:to_sexp) { should be == '(root (special "\\\\p{Ll}"))' } end context '\/' do let(:pattern) { Mustermann.new('\/', type: :regexp) } its(:to_sexp) { should be == '(root (separator /))' } end context '\[' do let(:pattern) { Mustermann.new('\[', type: :regexp) } its(:to_sexp) { should be == '(root (escaped "\\\\" (escaped_char [)))' } end context '^' do let(:pattern) { Mustermann.new('^', type: :regexp, check_anchors: false) } its(:to_sexp) { should be == '(root (illegal ^))' } end context '(?-mix:.)' do let(:pattern) { Mustermann.new('(?-mix:.)', type: :regexp) } its(:to_sexp) { should be == '(root (special "(?-mix:") (special .) (special ")"))' } end context '[a\d]' do let(:pattern) { Mustermann.new('[a\d]', type: :regexp) } its(:to_sexp) { should be == '(root (special [) (char a) (special "\\\\d") (special ]))' } end context '[^a-z]' do let(:pattern) { Mustermann.new('[^a-z]', type: :regexp) } its(:to_sexp) { should be == '(root (special [) (special ^) (char a) (special -) (char z) (special ]))' } end context '[[:digit:]]' do let(:pattern) { Mustermann.new('[[:digit:]]', type: :regexp) } its(:to_sexp) { should be == '(root (special [[:digit:]]))' } end context 'a{1,}' do let(:pattern) { Mustermann.new('a{1,}', type: :regexp) } its(:to_sexp) { should be == "(root (char a) (special {1,}))" } end end context :template do context '/{name}' do let(:pattern) { Mustermann.new("/{+foo,bar*}", type: :template) } its(:to_sexp) { should be == "(root (separator /) (expression {+ (variable (name foo)) , (variable (name bar) *) }))" } end end context "custom AST based pattern" do let(:my_type) { Class.new(Mustermann::AST::Pattern) { on('x') { |*| node(:char, "o") } }} let(:pattern) { Mustermann.new("fxx", type: my_type) } its(:to_sexp) { should be == "(root (char f) (escaped x) (escaped x))" } end context "without known highlighter" do let(:pattern) { Mustermann::Pattern.new("foo") } its(:to_sexp) { should be == "(root (unknown foo))" } end context :composite do let(:pattern) { Mustermann.new(":a", ":b") ^ Mustermann.new(":c") } its(:to_sexp) do should be == '(composite (type sinatra:) (quote "\\"") (root (union (capture { (name a) }) | ' \ '(capture { (name b) }))) (quote "\\"") (quote " ^ ") (type sinatra:) (quote "\\"") ' \ '(root (capture : (name c))) (quote "\\""))' end end context "nested composite (different operators)" do let(:inner) { Mustermann::Composite.new(Mustermann.new(":a"), Mustermann.new(":b"), operator: :^) } let(:pattern) { Mustermann::Composite.new(inner, Mustermann.new(":c"), operator: :|) } its(:to_sexp) do should be == '(composite (quote "(") (composite (type sinatra:) (quote "\\"") (root (capture : (name a))) ' \ '(quote "\\"") (quote " ^ ") (type sinatra:) (quote "\\"") (root (capture : (name b))) (quote "\\"")) ' \ '(quote ")") (quote " | ") (type sinatra:) (quote "\\"") (root (capture : (name c))) (quote "\\""))' end end end describe :tree do subject(:tree) { Mustermann::Visualizer.tree(pattern) } context :sinatra do context "/:a(@:b)" do let(:pattern) { Mustermann.new("/:a(@:b)") } let(:tree_data) do <<-TREE.gsub(/^\s+/, '') \e[38;5;61m\e[0m\e[38;5;100mroot\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/:a(@:b)\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m└ \e[0m\e[38;5;166mpayload\e[0m \e[38;5;61m ├ \e[0m\e[38;5;100mseparator\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/\e[0m\e[38;5;66m\e[38;5;242m:a(@:b)\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m ├ \e[0m\e[38;5;100mcapture\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/\e[0m\e[38;5;66m\e[4m\e[38;5;100m:a\e[0m\e[38;5;66m\e[38;5;242m(@:b)\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m └ \e[0m\e[38;5;100mgroup\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a\e[0m\e[38;5;66m\e[4m\e[38;5;100m(@:b)\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m └ \e[0m\e[38;5;166mpayload\e[0m \e[38;5;61m ├ \e[0m\e[38;5;100mchar\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a(\e[0m\e[38;5;66m\e[4m\e[38;5;100m@\e[0m\e[38;5;66m\e[38;5;242m:b)\e[0m\e[38;5;66m\" \e[0m \e[38;5;61m └ \e[0m\e[38;5;100mcapture\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m/:a(@\e[0m\e[38;5;66m\e[4m\e[38;5;100m:b\e[0m\e[38;5;66m\e[38;5;242m)\e[0m\e[38;5;66m\" \e[0m TREE end its(:to_s) { should be == tree_data } end end context :shell do context "/**/*" do let(:pattern) { Mustermann.new("/**/*", type: :shell) } let(:tree_data) { "\e[38;5;61m\e[0m\e[38;5;100mpattern (not AST based)\e[0m \e[0m\e[38;5;66m\"\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\e[4m\e[38;5;100m/**/*\e[0m\e[38;5;66m\e[38;5;242m\e[0m\e[38;5;66m\" \e[0m\n" } its(:to_s) { should be == tree_data } end end end endmustermann-4.0.0/mustermann-contrib/theme.png000066400000000000000000000261701517364653100213640ustar00rootroot00000000000000PNG  IHDR.X9? iCCPICC ProfileH wTS-@轷{"ͮȠ#(:TGG@ƂbȠ> 6T z'[gg}V>ZO(LG22E!n!PA A txB& ucqMtB >|(-e |cYb3sWV q@tO$r1`uQe&db,O%`|c|߈s22c:M cj c/[}O3sg_3Cِ&9cJ $bse a%v" yjWʆQW56H61j56"e52LL LL^jƘ00bfmnv잹yy+  E Kzn˗VVVn[37[YٴLjr v;7v'>g!͡ar;j8G8NqN?9:;h$4u҂Қ#;2q3e22._>,4 GveME+KVtgbWrr~uʭ}w4_:?3J[W>Y}kl\3um:d] Olм1mfo6En)T)P8mEE[6moI2r랭_/T|*^Ǫ%m,)۷=s;˥W Y(xkٮVusvVTuڳ}ϧZvw{^粯NO)?ݮliOܟ?sniTj,iܔ4bn-kCrڦ:qݸQr~K/7;=ǘNJ;Ε]]8䷦'jNʝ,;E=Uxj3gg;uFs.:vu '._<~{+WnAΫWz |uo޸J@zCyo:}e& 1-b 6׊$Ky@"̚{@,;7@o>1ՉR,"rh}/Ty6SiTXtXML:com.adobe.xmp 770 46 M qIDATx]xř % A?A?, G.< Ujݠr\,jc+K.=Z\ㆶKjI˦ $֍6D6]ޙ7&@37̼7f@@ |^E@@ $@ (c14zQy9皳ioO;qJ[F}K2{Y_ϡZ>ɗ=vh[|r Jƺ{9ӯp `PL$g@o{C19ua}Dq|񘔁ɼvVC J рۑ(k$1{K HLTEQ4%R:v9,,3wC"-l 5;묘fPkƺk\jG%#v-hCcF]G~ nڂH7>1H,THƫ%Щ3-kThЦ]L48EkWnDd@eҬJ6łn,(_9b ^N7V7LjH le@0Tq X=쎬#l *C͝!$C v{TNlR4^SJ2[WF,g 9YY)$JgYQf#v{C*]Onp< Uj$B=QҺo!`7t G(D 'lz[/71 Xb 5s5CzuԕICG&^9al-*wUէ 49i2D:g39x$FۈGjKltHIup HzY\!A^4Q=٧ KGd{NVvʧӠ<# ťӧN7k-_l˾/*Dg-CiVVUku-DH P[f!3fi)Ypi=IMdGVh@44PKk6,#El۫kSdo;mTYkcZ8flm/_խX}zoXâe%%%ko)AcfNq$_3*VvYckkUf{zV6 Ƞ8?U(v/ǡ:6Wh =uЕݵ_)F)JL{wmA %[Rެʚ"G?uGCtP34V#!Vܲi uV{nj"y(llCa\5LMru # %Ik#GeT/\ GcZh6?G2;+R$ahoa$7(MUY 9%2bBlG: t<t%’1*efkmu2(n77虐e awJ*+58֛"56 w:rZW/{,Qu$F/з+$tVNޤwzp'Y[[I3 qȳ<G7#w2ca&a8tNC<{ W 9շ$9_ʹ|l%GXJeS-Jb2W΅X(Ȓ ߼wS77n:>Cu!⠧ګ^]ˇ0ݣ1ț6oBhmlI0۲aSJJ=:z7dcKx85 ;L, IVo (WVLvʅQҢ̺,NiE( 4*eIR$jfusEh+.Űue#NuҡlN?e2S>Y674٩֒Q01VlX@Gc7 VWUKDKޡC|$_|N!D6YXeU~<2IMwtr'&0ea8@|3Բ45y.uWe!F XiMdL'Fܞ2 (;Y1]Έ~-]B²(CdX/aJٲ'T83thjAc\>Zsa?=!`&Ϋ2, Yy)_z\XBYF>nȍLkn =>m,t;v{܃lALC a{Q]~TFW Rʮ4J#0cƍ$V+^`;HwH 0SxڡԢ!EJM) #+)QT^{+vHH,h(Tg1$U {mOGw(TȤpg[)zV/m ׹$h Oˮ?G8=HjpV C Ql ɃX sY, `!5m^K<)'2QSLvD4(,@KuGN4joNF }4!SaFC 7Q&%)ҝRݝᦦP7"]< &G$~,sPBp]u6i;K{drh3ΖQKģ݈#nj~q9j ť˦^V[<&Wښ1sYZ-Cy2q9dU C ?67Z ~[mV*Hηg2,ʴ;IOJд%L8|rF_}Y7.aΝ3n^YKo,ZgegC4XU+gР`*z2Ш@VCy?D# #F_m!l :Z> O6&^l 3dfC pjPG@ACE@@ 0@ E@@ !px @@ `p@@  qq&i`t5Þ7SG#HT(+a?BZ_ʽ{:X_#j @"`6d '#ߠGB| =1)9 l__W|xFBŦ"jVBpd(ky3l1q;q7z=1%Gu;Zu-rfT(w^.KpД w,~f 2Cr遽G`0F&@@ F m pS[>L MFGAxOpgOQvR l(!v Y'!všzDMɟRX@@ 0rlrx7]03m̃({Kߠ͉(tlo'YR70QX6[f FÉx\sRDTO JmU(U @Z8 cͯm < p/4F>XL}  a|NKn%R?I[n RyJ^ tqȷ/č@@ 3A],p^eFd#yv{[j2#rҚ> !{OI9 gIo*^YQJKf&Jw@@ L`AOR{dfSoL^"-e W\?|d2z-S$:K{6-%늤0n?o_ZI_ѻQn+׭ RH/9ӧɺ@@ <)N#M`:߇zĕ_KNz|x 4T&xu8c%0A @$)xymC2o2a/8@|6GJWW =ooo Md`S$ljTeCG`,Ë~X 덎M@@ #<tUI0Tr̂[? kn G\ =}0a]Cy2kw'4Vl:XE'tE gk ڈ@@  HY$9^PQ)o>_Q)}Axn\wt4ëOB`)QpL!??k.[)4*@ d7(9y? 5Wwç1H8·IL7IxP8enZYe0#,PxYUAc'b@@ |E8;cЋ @` 4`=@@ gbgX'@l! ܠ+YF@Agx@@ ۬_ྒ*B3T2@@ wز?IENDB`mustermann-4.0.0/mustermann-contrib/tree.png000066400000000000000000003101431517364653100212150ustar00rootroot00000000000000PNG  IHDRb iCCPICC ProfileH wTS-@轷{"ͮȠ#(:TGG@ƂbȠ> 6T z'[gg}V>ZO(LG22E!n!PA A txB& ucqMtB >|(-e |cYb3sWV q@tO$r1`uQe&db,O%`|c|߈s22c:M cj c/[}O3sg_3Cِ&9cJ $bse a%v" yjWʆQW56H61j56"e52LL LL^jƘ00bfmnv잹yy+  E Kzn˗VVVn[37[YٴLjr v;7v'>g!͡ar;j8G8NqN?9:;h$4u҂Қ#;2q3e22._>,4 GveME+KVtgbWrr~uʭ}w4_:?3J[W>Y}kl\3um:d] Olм1mfo6En)T)P8mEE[6moI2r랭_/T|*^Ǫ%m,)۷=s;˥W Y(xkٮVusvVTuڳ}ϧZvw{^粯NO)?ݮliOܟ?sniTj,iܔ4bn-kCrڦ:qݸQr~K/7;=ǘNJ;Ε]]8䷦'jNʝ,;E=Uxj3gg;uFs.:vu '._<~{+WnAΫWz |uo޸J@zCyo:}e& 1-b 6׊$Ky@"̚{@,;7@o>1ՉR,"rh}/Ty6SiTXtXML:com.adobe.xmp 697 504 Pz@IDATx} \TGwi/ 4ЬbPFŘ\2F<}y3}m3i[D1Q("6K7KC櫻u^fb_֭{ԩԩsN_:B!@A`x@ BC  B!` +BC B  B!` +BC B @ f +/oe*xMcE hS]MU 7[n(vNY2 IF< aWJp,#B'U eUh]R:k*`0q֕{uնߊxW B!0Q7th fe(P`q``s;iaS%}0Gp;,hՅUSWPIDBQq~<]`ro\1꒸"r^ÃA[=HGҼc= P=P(N X݊V-yKA ,D:8>Jl:.?g!_X.f<țޠźs']t6Of~9JE^7-]ljV#ݙyh:Zx- t~GDu[#e/QB[so M[=3OQzN#[3?QNCxnx 8VC ⬟1bdz}'iz3/ Ety %- #=XN\YCJWțҁ"quRqu{ظX^ێs Dz{|2U-\$8vLL^҇UH/u6:T<)V=&OIMt`X:qSMfiuVdlO&SR۟?h$z;UZlX_O搖IIU5(EBNY嵼#JRN^: j@%w aɼy~xv 'uw%噄$~4t꒵53fID\acwTV \.|㹨 x TWsTI UH"qS!͹! )|wlM w·ۊI/pVd۟.=DP?ną :®N/ڟ}aud0u$0Qǽ;g#wy%VVz.Jn_25B!0غ)a'vC`$#(.xp k9\@j84+0w2NL]U@ӱ*5"͎D/lq~!񫹔",ό4@),ͬ>||&7d7NCD0VV+>|PPݭW g >>y0KkqEY44x˙+qD4˸e.AT^["BMù r/>xkx$^hnN`R!W /+ !sM <_e^`X?%\NBrm#ijÏ0|~Ӟ9mRz.H; Ūui۴WeD1DE@^?τ!:*T yc)B7P-s?TC~m~0gbAyZ,CPJ." EJX~l,ЮHc]\=oF9u7O\aٜedqy5>v> oP p֩u!\NO[C-u:z^ӀK;(nKd}A7?]%*kxTv(%vq.jvZGXo`xzD YYEIFLZY}a\W`n_B* {{iiR}<6B(눈]-h\Joժ@W1EI #yڡ~&,'ZQ PZt S e}YADt-5J]tEw KyJ%`Li#5v#~]6rm{;Rtf}abc[3[ hDPs>\q6񽜵NJT.iJTI"eO#kʯ#5 +;4Er-I[KxM4uGr3E2ꑴKp/;' |J*5<\Ӈ{EGJϏl2Q!7hnhř@\~]leq'ʛ~5uQo形N~;0׋7|aͱ&>gA']X&"QʌM̌S:ԒX%NíH7j g)}{9qZb[%|6u4̲äCQ/E=!rsE^,uϴzogf殻t=nvdêKڂ^_C`Jj"P8_sO$}Xy-ѰP({ ]u\aX>nuԯrV%:o/@۫fŐQSR<#~g_s< u?' QE@㆖h.GMˇXAGVaj+}>c#;:5wP3+In6ĬWX%=ku*Sm(m&mLEyrّLDD&E_,9ME*ln6CdML.L7BT:z$38qYjeGh\ҌuQA t%kNt 5lb'qNW^G^N1<WSQ>eM5:PēT ZnWm\^kH6lhͺM/Y+HehHMt*ژݝ0s- .Y$zdžIF!@L(0SLr6wfS vTΔڤsÊgm0kO_ቯDG@P(9pA#ߡf09ٟ c+=(@ &;^$5|]aءB!x@[< O!@ G D ʈ@ #tcR"BI@ OSFeD u3-VL#嫉xL!E]g MNu?0!@ F Jr圠JĂ8m6jèO9u8cYLru^ >~:(@ 㺂\Z^5 y>Iu;(e^߉M33<2'0 1B!@L 0)L7B,/ 5D B@ۨB B@+h1^WSx@ kT2[ ꦲ6?o }7ru==(Ttb֒rY[^RU{cD;.VQm۹2&v,Ai9]B!@L\W;3N;]%CY෷|uP?AwJ!rh쩠n00xsݨW/43abNw)%L|pg/0cH9պZ'@n@ $A5shw 5R mfS 򄟝|FQpYG[H,`{ٞ'w{JJUuўAbCMTί|oq3 V 4#9r 銂UқDID0B!@ &$zr=)*IO}0'836/[8҂> K~R+hzw$7,½1+ sߴS̴nZeO!yI,EoY|oEopp`B,ğ$͔&c3iQn;3h¬GB"0PzT[FnE\8l6eMGh{3\h;ɋ?'L۠=/T D6-v.U|B_Ag´ל݆.Fhe]ɟKW]@ܵ[E 9-S%|@YI􋂈GB!__PQ^zE07%MVI!KeKhauϫMg-|zBq_p X.KyT[$@"!gN@DB!@L tElf0U>RGKe4Y7-$;z@;uV/Ȇ?uԭ[3 I:( bJtMz|9r . $!@ #`\W`pRVXhPL{S,0%%l=t D> '.Y^񢥎*0H0lq,2ky{ɲ A @(?4 {il;/;FEi86j$}|vQo{3] bxEb$bS,e~$ʮ{]#B`!`tkAaΉ_n6~gȮP ?) mB_?za6wgO$%QISSp?w{j$fjEF[ ﴿*aӉpBM-hD!B!@Lͮc R9a9AA*QHT'w[O},7U|o1 y1 JyK0J)W20˩ jv.־H7q rB!xeϴcMo e\'Q(44{Ef=Cٞ<̗ 4#B`! F/bh)YIwi»~ #%E B@(Po/DhNuP!@ QD`t!=,#)FVR>L)B!@ ~-o#0J &9:`G/Pgi0:l-!@ 8 ,1b@ x"0|B!@ HW@u!@ S ]:B!@ HW@u!@ S ]:B!@ HW@u!@ S L!VP*\]Vۯb*H?;3):]@'e"<(W[$޽6('YR+W!Q<A.ɱ-66f$#o)= U-B 8{;Oˮd+ms}jYRuhx|uf唈qT,T/ÐemFo1nKi7&RP. tPEhحx|u(Sƺ<r ``{+YP$LZL'3CÒpa:N) _ڒ3[Խ:`1aVx_\lCqS{_87mJw&`G{ mu̔PqB}ʺ%|< # M?G1)ڹ##'bwkuЉd آ#c1%Wp/0Æ*:l_Xy>- Xq@QSǴ[S,8xJ']Q o`,bDW0R g@+<.u!y? m(@?SV3YE?КI2bz&Y_`&&ު\$k}sڣ3oUزfgQ٧&*qJ`z#KpC}Z ۗ?׍`O\fdKU搄Bɞ+~+!\rs@'(.h6u]Zxm;^“符Wv5lv6t;*sf.]ywĘn6/0}fKm8Ύܐk7eIui՟ZqJNɁXl{ҕjڎsiaOs& pK\f@25( !ę|9ghY{\ X/\&;5C6j)7>wg̟3dH5]-QyֱK8l33~࢈]$ܰwInܳw*D3}zH䜸@J2XȠqT]]jU* UH GEW.)z W+bٲ1n'T)uܒőN⫧rrxO~PBśaqJPNO>-,όtJ,07:D|_˩BRYp]:;8u3uW!4GB` ")1=E5Mtͤ@S?Ҙۆޝ3`v໼++@F{SM\qv,Ͼ:2kid,S (l]ₔL_ber<NDh Z A{(ڀ[xgm=t' L3'l'|_M{*545m+>>cv4-ţ^J?+L\E!+ߍ 5%"8q:rj޽|j$-;]3ԨÂzu&)-!z6ѽW@M;JعA:UTK,:jPHs%.RД<]%ў8`.-8uSa(X܊JY;/lj0U29ujkZ$d"A~5΢9*&zAk Q7lMHN1-N^$;^KӖ׊a^z1µi JQ`Fo:흢{@˯TZQx;)$jPx:SDnxcAtcRell%.od.I#J= >>[0h0~_C3Ҕ-lÌT;fTWJ$XLY+<) ;|lACP%e.rQZrbyD YL\Ҹ0naWt_b2 cDǁoǏ+Vq_*dk<6ۋx4ʈg:[ {$UtE!pztV܂WkK`CSgBvykWFg?n۔4#:dnvil|l{H'0*/~Z+ + W^myGI>zc9̴P|ci}_?u>Cl[ i./- \IYc'-WZ';fY\C@OtSVx\Ӂ|a}CI%$8wKPb8xEM V!iC'Q["9ߺz{Fwnۖh\Kcc$6/)Ƴv)9)_;{S24[K:ausЉK%0}1Œ,T璺dW~DO@Xfʬe/ 1NG`kOI #}"/2D5~_Qѩ&EW՟v} lȇC)ĎD#2]t3>4Pt43.'#B@rKh5~mvf9k.LJcc_RWލ WXmY˳Є]ۖۓi/?#髣8M[pwP̷Na_qo?HWm.!9(͖)o~IՐ}0oglߎB.ޘLݜM+N f/?(i/2*:yx>kgJm/H-jƤ5Y]V|-{%AMhKd;`XO* ИI) ;w߯4 8C"oę\vģܟH7bi?$oqeQt:^490q٫U}mpPw"B3XlĥgɊI1ϥ<-BzOǠuIҶd~PpWJc i|J 2yct_" z%|?dtF8߿d'S\\i'<T֥m<y$}P{.W75f!vitx/şg"Iʗ+]R^ԢS//^wmpNw|DaV6e-[oniƃ{d핲PtSʱB Wy 3 Xi,Xyڳ,W]!e LY`7x!$4-nܞ ,('4bg C yǿ{1xF:VH6{m9SoG8y-6-8D'2t"iͯ!@դvXJQP{/k %|J53J#Z"bq[6u5A">)Ņ3 ˈB'ӡ./B)Shp]=VVF:c,nq:aJ$R `LvPÎv#"@3=<3a~RiDfOSHEFp)VXOCSX$\յՑ:"bW)nV4UC+ ]L`JN-Bt0ڥ#yZUT+1?GҢϪxqx jݪ{غ<,O) XN]B;tE7׶ҪZ%v0dCIpc* Ld>6}rZ=k+(MK9FY+k'G6 uT)+8mi̠nj. I`8y ު$ҳ",!бHg7GWQs,CIBs3}E6:u\3uEQюx)v}$AD{HyeҚH/+,Z>&|E5v+$t`ݻD[D5kTʪ"CuX0cDFXRC;eY]N>]7fXVXO'Ύ?UQY^tRB G1ӟ4`|Pn!4!Rݖ˸%Uw%*G6ۊdMw {[ۮ 9ޮһU0RT3ޙhPWW]Oٛ;GD&E_KN&ʉaL˛k>(~;*6WK7kDX'<Èbf]\/Pw ɘNxoi]vLso.<,'aOY o|۳Yꊯ2-u`w|C.:7ݗ=U)j˅FnH"L.m N6Jw.hŁ9E.{#s%d|m./·`n.Y*8t2$!bqœjlY~ґ+;|,|"aS<8X\|U.;{:3U;?_p;)AĊk[sB\i44_ΞwIxo\a!#f8| J+@IDATV"rƣe͚S^ wR~lŐ)-h}y0TGNT#ޡ0qX“dcn_zlRYq!mQ>aGm7Ns4QtGNl8 {w- ~;uj*`;_?o[ڑw8!{w. ov#7 ?s Oi-ڦ,Z*l. wna6'л~*b 3{Q6i /YC),Bx5ڷ!hD>!c~FV#}>dώwL*`}{ `&'&].;wPZfz `}sm4oYstF՞0NeGKjWfVeG[~ "{j2|)0 3\tomĭL2Q_D!Qte4N:>wu4~J Zu@SKI ,4|Uޖ 0 Nv6?kJ拫n;Xv0nؔ+|zhn^9cec*(]!aɚ~Zꝑ(!(]i_ 7!X o:_t#`=}k r]~S%+*q˜O;+įPłKWr*+J^{k2,Q*-KecjRƼ(B`! u\"Ÿ uq_Iz^"!§F"sqpȥZ̑Xq  *F̈́I;X?->{˴odD`|u0Ņo2q/MlIt' Qq!#Z95C: Kh97QDA.SR W+!>bFIFB} G%<7/_w18 :O_p"bdBXVv+SzFQ`/$Nuhqv͓(m~y77Cgh]*_ R?v`^4ˋCLT2 ~eC{kg 36>.&ܙ' \HN#tqD<;40[ViWS~U͖JݫS*?ܦ< aWJp,#B'M4\g߽3ѺhUy~K@^v%XiS[ߊxwT+?S"ƍ֧BQSTyXyw9︢辜_>̶_ښ)Y[+u6e5O"\eJJ>x &:]piFG)yK7۹kEY7 ~TUy֗ϵCͻވ7S˵߂yȪש+ "Y!(8Tr|?ňZ%ȼu}G)p*5q5H* 2^$ d/qk]j\ɅꠐsW}l$A+5n^ =ί.XLhHoN->w7ǁ+ Ê(fJo >~<>\qDs Wy W5OВ!4MQr7EO܉.Q醇Ya^TU>qbqP ک(0?K裆j뒪D,`}]̒8ӏS֒ [Saxv҆6Eq&,ʾB |"!>Y P]x1gD]u j1 ɠ 8f wPTQšCʢd?~Sbv2>/yX<*?SXsvڭa.R۟?h$JϴJi.n,ꑓrJEdocG7^ <%϶ۉ?;V WV3Z#01y=HVyRn~ ~`xӫɔyNZzs'Q-$ZC4Jj!U^;d;) GQ{ZUrٯPW$͛ó[3v͛ZX* }< b'"yw̿HH@a۰wInrU[E7] Bm8ΎX憬?]B5;}zH䜸@T3`RPϛʎX\tpE?ܼ"-kx+U `dq꩜:x-`%Zp[1&UOG".m,W'詠͸^]wꝚVhVI£{w6_G|~໼G96Z)3pΥK%9\uQF,.8Nͱ_?Ȭa;SjhBuE;/U gf9O:4t=@D4f0Oaf0,07:xsWdpD5{ö.~qAJhe&zR&#.h8yDnhn˒ Q\) o5R_˩BRYT;8u3uW!pDɤAxHj.5h[zsJji@fh9ܭWջ|PPݭW gl̓V)3S/`&ڦ \\ADq?e\2^$ե.4Qc*dk68r[Q)k8Qc9}5a~ CԌlet]A~L)x <ͿV,|{g09j3!0vn3L* rN;;@?/G߰}7qGGt o9uMo>?$\4V 2̵+zZ*r1A]&c Q{MՃE%r^:`Vb3u+${+J=T;3eQ:B$35?}6h- ykOjiBjl[PAi+]/u) -X%/ x4e~{]O<*É SZQx;)$jPx:SDnx߆ wY]Qy`OMs~LPty!kйTkhnh/-0OFY!7P/,=Vi>V2;Ff4b27z],Qn;9-PtMz."|l>|Jⶅt\6lb0^;ElTgKihV iOj}F뒬1 O&Wj >1!E"ݕMbN矘FͶ k6%]Lk"b@Ds|ytWIRr/Fl߱62;iqًCw0X~|b0?wiI6԰j{ڒ=6c4eTkA-C@톖Yuwcj`oB#Y҂xF*Oő3 3Q͇bN5)Af NHi=JD|9Ya˟K1y:vfE}/z"YR!uUwBRVh虛VګI!N&꒕nŧA`ܲWRm?}X-^ڕ$+ѼvzRQ;j^q{7.?V_96dR/+#02:> ȅ-=p)@n?ͩ{gNԎRp)<8+"dǓ?nWx;:HsƕE|z<E܀Cl^#4IFLy6nCD Y絃pJ@g$UKJRE;ox \vgbvߺmDѣD"ίA^[ELԝ1k0\Hx#F?<{_ߴggĤ =;rrɊ=NYEd}jĭ~m ]&$ EJX~4q]gsWt)ư麬Q f0jCu>T9̼zJt~|(nۍs7s1?M/Ӌdv4J XWϼy ,# Pc1^V ;τRRh_‚e!U-1D]b뷏qZ|R&Ww|b. 9J_R5!e/lvo +$ nܞ Ԅrd*d֬u/!ޗEudv 4Kll"$4q-.cs'fs̼̋yL^2/O,3g4Ѹ1nA l , [nl_ߺUNUݪSU._ |ɯ/ .@5S;>h zV;MfEfHTӋuğt)Tװj^ Sp2ts *ujՓQ?\Up2WZ&LA{k0`A8u zJ0+Q(`(`~r615ͨiߨ'~r)% 7U%MFWtR! К9]4P{ P{.R 1 !)wk\n SːPAN2a3fSw?\ߠQATr.}?t 1BKH]9aL \n~/ ieV/F:vkK=R| M fl0Rmfn_贚>ؑa/b*b=93w&A&Up/?65ܙq2`I`t vHi5ܣM4ZnlΆQ^⽹8rO6*5,h =LJ׃y^ ؚ١Ht2:e)|`6ϑO]H0odd$&M>W[ᚡǼSO8Y@떩A8 [Й-18YcHZsL AN2sJ3[)&)k WTUF!..a>#p^qB&%[ }{Ma 7б}m],°Z.&W9? $ RJIȹA2 ᲈ\A[I:fzwlu{/&Fg2iQm{yG }TLׅE/aFX,kRT)eSsjfRǀBI[EϚ8f\70~:tu:IJv:j̅-S/@O׉RMt) u^d]"?77˜ Дյ/LnINJ6YK&b= os]b Es$yO1 Oq4l(AlI!%Z !a0,=vabj  5IqJ0ֲp59ˍ oR`a(p _ \G+zR|j:9lu^H7r]i&&y>^׻ Ǐ}I^櫮jӕU7ruVr[ mYF+yTAZDl0[,wka ~o9IH2Ϯ%le͙Kr4= 3eLR&)Y,|dVQ] 6t&3eٓ϶5}~^!ZKHghr{%@1ŴA`^" L-GF$rj ޽-vg/Fo$O:7VN3藬 sNܦЙ%.hh* ~cGi}kRxn@5-]}l"NEiTYK5}xLQo@SubBkYShs$wJf)B67ܫMCv}Z'ZZwԠlB,hu6fhTxAW"2IOsϪay;Ki\&t%B^n+z-C}95%g'{jV-ObœDdUN$r#5a#('.-Te/L܂ӶNIqF VJDݭE@gٙVҋ2]We0*JՖ#Q59v 664nm $[ҋ3uyǢXI]Yuwp9ޜ0'V`c'b+s}i4On6߷.5rkNvP'NZi;_DcF}6.++'vb0y#+hNaz|=Frmbb-.J ގngGC̊B*|0߀0"5 ) -qCYzKpq* iM>x;Ywj;E8|6L{/&˩]çn'7؋ɐ+4uus'IL~ZOIXqFdZTy 7?/\y;^ա&j!Aa' ISo\uŪdҲ[ELp /QSMLjEe/6evXQD -~Z>Zn}ׂemo%A&_"mvݺ__JΟ v ܭuN~ԗ Ze}+ Di)5:MA0{h3];C8ҫW#8L<˨A<5#g_R&G&;Dl #-O}s NQ˔Ppʓ|+EZ;M;׎|իS a/ɸ.h4}kRRT|vm]S'ʕR\r5nn1r1R' ig @xL`|Po: W٩i#99G(̀/H4/u՘^SBÕiOV)L汋u_%L:oy¶#4=u[0]]>=d%fE^Z'L4OBT_8فםkpAsU(DsɯZM 3J1wJ ǡ6'F?ⲍ?瀽I?+39[}iMeAF,=[ ^)~ vPNFM.)O`GڸbaMh<5&俵$ux^+żk0 ּz|UT74&9# 8a 5}U2xg =zqg?ุ]I5򮦶NY?`2'g7#}`ϿS҈oo(=܍[~tH-݀稑3|}\ :ܺ®Ojs̲)BQb@ `ő :]dQK2fxqFķE^L1YNw?i|48FjUravvEkHfl oem\ҧ,DXP#kX*pTe{JFG>0DCNeԊ5Wn43W0Ɍ \|.h}{"0!r`!Gna .gM} buyC?LhLjNN&x'L]) s2%G<.Xlw+%l1FPkHّggOT kh8F&cNOڥOf9bCfFg֐kPG 6#V2v_ҎNE'8>QjffyQC+"EBx4/<pdi?`a{]AȯO[f5A בČ!@  TVJp'IW٩vj`lu/?n`ƣ7A](!@ +2U*MG O2eObuwOgbz"]>fbrs!dp}<8Då8 s&X a=P Aqӱ Gf5m]2]EVB |{8G_M1h />P6t|FY~,_5bn.3_m.@E]sڭ%y u<0JeM|[-.Qs Q ko Cus e<@Q'dsMhmRiA A3͊;OY`Q㨆 _y0(8=T(ˢ :d!:ѐ5=9cf :N/l`+ץ\bʪ($E;g'IkӶL79#b&#ւuX"_l!|}CT$-B`!@ IUw!> Ce~+oʒɣ\k`Mc@Q_Si6='+ h}ۉo ha.g"9`b+<.uK'=}Q :Cy߈{` b&pVsL%9`sJʴpWZwJm58c (/YE3ŞSik`'i{`*lj|`]MVf3 +jZBh' II]ͩbpx R'Y79rUr̂Xݠ4٩N؁i I8]W[x1⌼-ȫ1#,4?rȩ}l k$4%kWJUˑU<~9h}iOY7HIcLW53.ȑH %MC*vjXٵ 'wcF.^<'VB p̏rT^<] 2*lɂx=|o1&%A!@~峫JXwwsSoáyHtMuΦCi}/%_ºq=`5l6"hE1~k yxx%ƪXD VռrYRN#%R{o&%^3 r2*C&>#' qjGQG\^mњ 'Zx SӼ":O!h{DЪHu.d|)9 Oҟ >Qu$(KRix6Ȯ 52 i^(udyRs*펴Swb453t]Lu#魟.0P".%hڬ`J !}[Կ%^ۍM2V'-SG8X.ߔ4%Kʢ>FjtݨxU9bv K(ƞYt]A#9Ϳ2i6D{T_OīQ,PUb,.Ίݟk^a7~4zڗZM7-6ѓ4Q$5~WY4mipo1LkNtӆT-='lliίiߣ5SiwA%?t%F X Ѭ?e4~_}3u9#hACUSXF;eeZ?(7V+PcdYL^̄4u!-E>nRhtXN%:H+W_+50V+Z|Gӂ^kƤ~w?@&nyϧ+ ߷`wY_L{JѼ_7a4}PmFUejaHxYАD6|o/嵕\t}<煸D<l%*\ʍ9|<95$6PxuQSVS>V[ ^[GN,AV2iA Ij&3VӒ6 ^;Rn.47u (Q/™h;?Iw%pN/?&pۼ2.]. yR⡻vo1bi W^S// 9_) *:dc*0Vm&5,m ~fSDTd1Qd& EaSi*Md_lQ1Z34aHKimT}*aTm' HMa;E Dp&+H'e/9AhLHWw]|S{䜔XwN2>>ߘW:Hrׯ^ % JY`nyPf(?-s$57nXۙ|Lp*>𷷉PkXEfuf9(> x;.13ShIr#v^#gx1nK[> VPEo$JiZfgzaoh U`%lj˂u 0j?`%w03ua@HW08pzԘ'&oE{K][(`~JW(qy>E܏[6X,T +̗{)X]dymIT4ohT$u%'L"5CsX*ig,"1P=j:J$%(oCMU/EEmPw/)Ó55i/k{-Q 9PQ =0eM۟#'3g͏T4MV%',=K姹UXC<]BS0?ÖGFfj9Dm(&GO$J35XOΌ'_l#8݅+sUXdʍզI5у.p H:b~/01li)AP!~%Z !FaUJn0y( =-`imN!5DR9g@oΈ :,5 eyS'R1gWnlA$fƾ$Am)-ZV1;G͂NՍJJ|m,a TC{QzKɺŝ(A_!*;|mR^ ?uYփ } "!-8hn޾B83q 9Y,|VQ] #c3F.Q2`S P﬩P3}Fvv'Ư7n&bwM[q.A4ˤ<0#Zh Z'ɏ k,6[4i|욭VG@$e]-5+"f;k6T"Cs繑*`/3uAb"gbh/2$ @r4]tlqC_~{2KCTR@ǁop !0I&!̄fyPSZw4є-jA Ksk#YC!唁hTU G<Tm?!ɱKLO`33UצmZ /\l*J4bX%-T$Lb\#ggdA!x*cշGWe0*Tmy΍0[+C(/x&M &2w$%{|ȭ2(:gܐ{1'ym}ַ4bfekRل ++I g_mq1Fϲhde%Ubm#g`0uɈ ̓E0^sPˎ{<@Z/e"plZ>WiJ&d͉ #+(+.J ގn;cGv(~ kO0N@CslX<^( /-;uo_Qq#xq}ͭ-Wn3y'/raCTI!UT'#ѕs̚:;~(t^Q UsOӯ v/C" Kָ+y[m}(ܙ쭭uz!:|;nޭxNִ]:w~EL7YS#U-N{N2Ÿl(Eg螗sz:3ɺk*+==2*b#N_6.[>V`IJkTk;$wQDt7T\+:5T3ݦ,t}C"V7R|E@=0)!uVN~IJECm`Q:x(*~EJ7/wg毿|?#fVJ06Y7We&y9׫}dz9,oT@C坁>lf~&N|N ; oB~v?&ƎD]"㚹t^n,r<|MBkq`{ԫM`JxIW+04Ҽn>g\w҅w$D/6(ȯ*/Gh7"iic)34_/kհU*eGLpkZ*>BX׸"0]AYsX "Ԏx}$$?R "G1@qY~8YAQ"}] ÄםkV*{sU(DsNHZύ6&(b޶jӍ"_wę~vgZrx:k;L ^93ȽMoSkG$~iZ ,*C?TDpDZ+Zmć _&ֿGqe9`4l|ﱶP%s[84zkM{#nV z~n|}oft~)`i_24M4!O>%?.niݥdƾ>$y+rW^{*K$wԧxF*<0|Yl__7y0<~ E nJ8\GpD/HkSciԒ⬿c_Kc VTZ:pAE' ,H0&d:M%N\@LI4*K.u/qfWc}S 71*Г>OȄ1sؚf쑭cڈg2ȺD ` ݹu)W59GYJ'p kʔ=-f0mdJ -ؤŤgE2@&JѦac܃l8Zt'-ĭfxI2P,Mk{g `0Y`Ѝ*[XN^B zS젒W3s^bS(dum=0qwq* inzzZ)6ݗޕ\+O 1-Zکv`LR}iMeAF,=oXmG*BGgŶf~;D ;]j2\T~ci+{WDYJZ8%@}"JI ?`<<\m\m4J1g)ӟ%\,df5`M0^|]2R\/ E0F)L˻Q72nJQ?k:$ns~.Z|n]a'p-fٔevʱRjCxRf‚G&詡vJA0Ϋ%uW^3evSf 3o-;D'|:6ǃfP9w!vU7f..<>֢k u!}qB;) Pޡ90 #E@q2'z Ukmެҽ`ёO<Uc,͸<|WHO0xe+#Niˬ jX!JB! TVJp'IW ]\Vp . KߍEfncyu V*@ D`xu%)'{ !L3^*@1\tB!`dh@ +NyCG@M%JzixPӗzȚ3w~WtLquԾR}u=wbatʩ4Mj4@B!@<581o:(vKjHxߩÃS:Eq+t{,0^tTȨ]Ss)]g, 7 ~Y]FNwo]Q`'9rIZ$@B1Ch;j`0?4kt8>wdS`{X{I>a;E. U❱x_96qԂ3}/N`Eo]vrFA9/ ? Y>#O [)'s9_?:CX!*D@ *cGW(:QZ_Jm~V=9?;q6yf"'%Op Q2,"mZ;@$Jۿ_v_[kV m%! _uR=[ޅOȍANM]z[9fIɣI@EB!x|xy0fpܸOΒNjM&xk뗟GMigt`HjXS)Yy3|hLsuXEd7+m7~+]Q .'!-?y7 AB! `{| SUq"D)͍zt/uS Z4A'[J0縨l> ƣ_'X{) 'BNJl Y'@DwB!x@Aq;,hNGP& aOt$m hMho>wd`t5G{Mcݓp:bQ yոT)"n?w)k߷A;B!@< ]˻5\慨|I;D>O{W/tPλhK)^K) 7[zbF/qvG₮ܮ Rܗn#vu憊oO)z yoX/'ܬK=ŧjwķE!Q B!@<яAnb[W |ۙpsz(bsݻv7 Q8J%/7@Ԋ@n'~q2/oŏRg^;&|,eWOd|ј~zPRQa@  h^bQsppiL(O{ċ,:x9k5MC[RtdK{{n9&OO GB7#R4r! q Q)Im q+:yϏ/cSAr4j8& yMJƻy9}oQ.Ĝ{0SlVt&OV8p'9+/T+<N@ .D@ 8#j `t1! ƢrM.h};^Jh`|X>ƶ^N&+ ldipf #0m㺐>8gy &P5wf?m_R@ eWW@ Ǧٺim4}~K@Yr!PQgVNw1y -D(F~d]cϸȹAqT9,Uյrۯ)L+9ڑ՗ c`#LcG'+esWq%I+C^Waq !W{NJ` o #T+hwu@g=̨EuWY}…X'6_w?.\V(H?ֈyRO@!ngg, +xmRVNTV֊eb\|hvC`ȩ(Z2Z(ë,ׅ\6ylV$ؤ`yGQwglƥ7+Tw9\Q~+E)t1篏 ]Kv9~*g*m?rduWiխƊ~8>3KΆZ) C<\RRC)_%1pvSekKOUr{ JJJ8L\$Sn`^'2r=}f,/=~`aeB D*w?wXB&_u+*FR s&?nmyӊ4{oGBk޴Գ\'8QŠ`^Tg7Y+ץ\bʪ($>g'IkӶL~BҢuX`)_l!|I"cbH珯> 9^`j/[1|SLQ2/MWƙ,oS?EvI 2&,GN_7IW@Eo5U؁Xd4>'RwnjE #qo2r3<$޹U{3[CFSZwJm58c (/P1΂Rgmtj֗27t\,,R))fyC S%ɾ NʿνEuY0FC'ܻ"9d y>XnP8{[=2q㽁P`F닪<{v+LZK3?9ZW!)YxkY[m,e3@y'bw"n\aW])kWad,G &.?tI^YTx׊>\z/Z<]@{$.TOpaZdw*l%^tIv#)=hTn|QޜAdM9-R5M^ uws{uZ6jC̷6 '9i#V^VWo\. |&k=%S3 e#%vIBZ)j9PC@}iOY7H(vqxn"ШqAOLSaۇU.ZmBb%v!ĵ.Ҍ_Bŋ )[!%P4fopҲ% }t 1طPBZϵGY*.Lt7o, 5\_t6f^+K%~=`l6"hE1~XYՌ.k9]:95$Om7]ǦK޽?9R67`{zxQ#BcB\2}- ܼumJX0qq]Nb$&۪KowБ8;W1$w wnӶ8wW|R.igC#8iB=?7xx_MPmrm]j }4/Tߕ}Vj-) i6I\_%驩3 r2*C&>g{{Skۃ\JAlpzk6,c[Z,{jvHilNx4cSf2| *sO7`KP?z:S!c(pXF=ߍ7ld\`|"^bu]LY[?]`rVWjKдYN/˫,'3MxzR/bgyIc{J})ُtOk4߯) ;w>*j^ygy~ T?o&.g-Hcj NY3 ]/Xp1n3ZN PL^7)6i”jF/Zټ'[( F0# F,Aź~Ѵ431iҚxc2ݟQ O-RWKG})uko̥b;DMmMu7S+BPD\zFɣ2WU!ecCC۴{$w"FExPY ָF{D}ZEݤ>J 4i~~i~³MG?r!qi޺pcKkC`?!r2Kj2䣢*b|yϚ4_s?Mh(zU]a3ttk8הndq2XIpʖ\RӥR@̏# toD)Y, PIi5b XtF (zL%TA6od<|d$(5'3cC`{qL>~jb]qZDG\w:m wDJ[a?)0!iծ >¥GTi{$XȤċ ]RuO-z)ܭK- x|Q#:UwzV2A[n6[Ʋ0dthВVEEmuIYSUVMrx0v7SːLg˄oSʞ񍚺|w! Hq9?a3upKMP;ˉ辄?t 1IH]9aL \n~/ iUPaT0\TC@ /n?9=ExQ| !mP!H5}3/b*c=93,.j&VQ}6Yh?#"ZoI`8E*'&rw/?Et2bq :P;y<#ZڒLS;94} x^ zDGMbqU ూ -nqMZExke> zTyy 1]H:BN D?0Nƴ\@ۧ/eNI=R~pf+囒pO qq x+x %' igw-{f-Ozkkk“XN'%CfGӥs&ez.WM2?l 7eYCDoM 1:J|!`sbPrBTJ%"3pN(JNa,Rۥ"D@g ;eЍg\)=u/0>ϠQTu[Lhp|);EZrlű p5%k넚:~SlN 5h3:JȊKoOYj_1G)뽇yO1G\Bߖ(AjI!%Z !aFjze} XTGA7M74Kl#" D7%c4NL3f^E_&7&&gfӌƸ1.A"T@,M/_w뾽&`ݏ[uԩ߭[uԩsEZ: ֫/K7LI~n:n d~?>IXtlV 57h5!ܚ_D 1 egr({]>|e 6ˍ=tzy&:iIАvi 1* ;%? LYE;.o2& ȶԗյ`vt>p 4VUwZ`PmI==s?F#QSv{zxj" ]= F]]Ð1pd)]5ACZyR,(a#uC_j֥( iHKHiſȡt۞~Qv!IG/q{L/NEx(Ѹnbgb8p|"`YLI~ 뛫MP3MRSN.N3ظckw Ԗ,h!!$⬛ SvzRNu7В_ |+"j+XS_b#=3_gq;%uV<*鑫[j9kWQ#4HKϗB~zAl3^v.Q?Rɥ vqtkwk'Zy3ir~8ѩŚNh]rdVz-  hnhJ( }mWN5Պ[Z/E:i% [Θt.&E?O-] _w⻯^{pNSr7[jM>%M6V ^u]ܳ PyM2͉J2OZ\bgE!٠lPxY:y 0ΚB)1QKWFm+f'DT_^8DEh"Ndf ~_OY-sbn46-*\< Hy6R2vfGiLV^+_#tl"hЄkZ*;Wzd1F\CS“ue3qH3>Ǿ_EĨU8)6D%¿tEAȜsڻRP|_:mƭܡ(׵4n7 ZBItDŹ suK#;{覀}^q-yf׷iN5iٳ#e V6DqYm4VD*).f;]`;޷Dr7cy X8E;G pŔcu/yYA$h(aպj'vҏ5>1;5&.Ls!L;X2LEYP+pޱvJ;atX3'j`jhn^2aIc`!/0y޲ct+*eFH(]u^ 7!XQ9umI)sxb+ ?^> 87*noL pjfT(U)% ?liVUx 35 چrݰ6[m$@(h< sQӟ sw_1鑵˽eF)QOnmp9 [g{X[~z׆e )h䕋_? =G,C+wk7ij@E --}xϔ97}K9+(9gҺs!S}bt^j1ao˅lΚ?)b]I4g(#B`4 0pwڒDO>]ZEş;{X_<"2X)$jh5ȳ,~vŲ)D7ZaOTbSw9q{jA{ч຃assw7"1"1C0!?:Pa^!oˋė8TL ]WB!@ hFU!@ OHWx23%B!@ ʇ@ '+<!@ }E }EC nddR].mUhQPغźGi#SHY%ly&6*! m]9WSi1+(V8Ǫ G42]9;{&*bŤֳtGwB`Z]A!/. EtPw+8Tvk,aڭ~A61@]i8/G #h;[=U@xhgDD?I H|B!0m4 F BG !lh@!@G`h ˀb@@^Ü*[V` l oC uk\C>UjJ[Me^[g> ,<8A[U :?G$ oNVJZS7]><"ak7ʩ"9r'EY#ui!zB!+Xp!S=۝}ׅq,耞ԥ*]M_ܨ(=JD@Ap\-NUهaZ{H?uëZ&ZsZO) dUAѮUNnVdm]B!0h5A$~an"YuoQoClUZߨΗkY-)v6Yma`ž3*.RY_uN|g$nw6XQoUU4E!C~<ċHt&G;["'ɻ+(Ok?*=ٳ>x#?_`Z!ma /ucCg:U[m$ޢ_HpS O8d!|,o0\EOpr&4&-CXhB!+XX ElӍa%䙉ͼ@VW90 oLۭbXK.kSRk#?D575MEUt'6BQ o^qxw\rVȩIi.%,迹4~S?B!@ <80c߆9=Eg$rR~Gu:X$^IK}^cf~O !XjwfPnAOez ^9nb~B`@@€8Lmh+ UB8;>35fQI㛰E\ܙ ]?CYu(7l{#1nSprfW1]&*mVrs~B`@@€9tJrDrqi?y!co|k9W.%"A~ ( 7[z`Nf/QFk¬2Ro#vUC]TrYeHc`摛5?Xr|FR0+$JD ~ @ :4w|CTަPox;3m=.yx}'EY}ԭ}_Yi ܭ,¤t. V>þW,_Tx*Cb eR~_Kd&tMO FPRQi!@ +3yDC>^gCo륀1 ;ޟ2=Nhr6'$IXN!Pܴd{wv 9~#T~OH"BB``!)$?n vل `10Uy2t|3  [Vl;g3'(eUJ9utr}QnĜu48Rh&WqƆ*/T)j*n[;?D@ P[EQh 0 G`pȂ!6lN!&FUZ+ 15Mb_LW˫ܖd!MW@B!(VW`nXҭq~`%}de0!Wd@B`"0q:_4Fdt$&eف# $+B!@ &C+ fU0޽zX#9FQ5 CdBI@`h}D*u 0 M@CH$y`: B!*0THrB!02@+熤F P!tB@ FsCR#B`@P!A D #!B!0T ]aF B`d"6ӏ熤F 5ʪ=@bEFzr@}yQe#r-2 X^:Xck9[hhY9f>GesCL牾މ!ASx, Yk[gW-ͺ&a-DR5pqP]I9VS1؂':Pˮﴉ+|ROSt?h(O_т0.cy!cC-(XqĐn7\-1IՇ~~*GSSWPHzrASnSӁ[wϻPD%y/ RF%)|xmI/NAZ*ϪBH^`[~CuUn>t,?s PD<,B5&9a9,pt\ P/ğ) <޺8AP W:5Hz*))n ')4l?qb-)fM7T eeaƟ?9+2G/ΰ-ꊪ M}v+U`_}]^5qp܏zԭ 02_xps"<_]V]n>nphvN+DjѷNjq3ǚ iԩdMbLLGBuU28JD Ip-5?enuW2&y5bht|*>>!2ȓǵg(mJ6+JO爷 BiKcG&;[ЍZhV;[=$[ڥ!TBIlŤkw}#gy xhuPB!0B`:eށu 7 T$v>^ĕ#}.MJQ&5DoO /NϾ\ y˻|V},б_sf[3ӃL h{7ַ1sa["LVu?W>ٹ##'&mo5(ifXVI:pClр1%qWX`CmeNwq:S/Ϟ麚JO:`9qF]Q H;L/#d A#<6i^8o[bPU_jS@S`2\/{PእZs57M؁o0[jN,k*NT>\m$j}3wYLK~:)&?vYFm-|%+\ƹb,/hN!g \/w?dE[O$=Oy(}JOsISr-눊R1hyZ I &jɢAg$ȟ1_l۞5X#PhlykVP[CuzRU-]. Xr@w%l^i;\/w…3c={');vHWuyS"\h^N#[ (Rr ԋFB !uиyxl˫xx&;vPٜYOj DU#ʀ(Ia%PvJ_[yiA8"ԃ\Cuzqy}p2uR<!e-F.+T/)n$-:T罠@ >66XSbrw1; ~T{/xˍ7Wk5~Fеg`z,y[mq[]z% iB?hm.v>'Wg}N(N?sBWhERl6~BMy·onLOJazi/cKQ6UQЫn[/n񣦻wvYE]eݲԋV8DڹIXϟ0/nxI8].yFT?" nKP9u!Sa(YoF%.1וaObq joVO)ȸ ri[e9Z$=Z^ `1䣗 gXĥR9Mz&){$5:4Hm< {F{/H}fٜC;H1 N.:ٖ7iK}{aY_s0F@g>9{r{|*wbmI)3_NĜjlOR>) I&ou\j\d ziE|6O/1HGa2'k?K֊JA|v\Ի5^ Y&\].^ƽ!hLNմo/5 rQBUayN婼=s4m;i,47&,M(gv45ݯci(+4 $~i֑Lu9s6tfϛA}C>^JyA4Qݢәh?oK '>q%~l*\Q7aF eU~ _R6~=Q&uFl䔧ˠi*M2%Gϩ +&Xaoo0nyMڬ=)3 n!سe-5څN~:HiHp,RӥJҴ@,-#tw,RNI~"_xZ3(yb9TA#p7mmZ ?F2`x.5' b4kwKr0y% ^:},F8,VJr:d9y>dݪK)fc,6D+TOW8n[p1w OSj _wc<-1~G&m7ns~yy;Fд5(jql߰[ȇxl='8BNI1C-uqq!r01`6;&o꼓?V7l$POc ے3 OAPW?1hXҶ=w!a@]x&4;e\pxN|HGdoVM{ B7f*sNsg9a0rgq/ޞ@;X^+{.)U:ֳ<@D@&HXanPY&jcogEyi3=sbmlrZf& ng.jڜi=]O$xcIM.k{'%<-bfbl,ypF)%hîu> *+NRds6Pivt6(^wk/`tAL<~wN@QE(Z Y4ga㮕ɛ J&zM.(n9/9Uu=L7WhoziKUkN.@Q QK*sCj@$Dey : —QF" ߬ H%Q^[fk*'aFI /̣[xnѧ0gUNlI0H J\=q|'@L.Ln" &j@t1PQ1 p6@en^H* 9XUʹo)<6ƎVOtjws14Ne3 745+u2^s$}A!i<ʿﰐ@U}7[׬E4EkUX&3g&]Q "+nyYu +̮JSH ~e~{[b ($ҳV)i&qcNJL~ixqVaiÒ`Z{NxU(त+iW_ %/XPn={{w7O83Kky rnZTbhLG*Nb6&a 7hb18v/$,[%/ִ0&E2J6ڃ+!5Sa愤G7QI'Tf_4aEeKcLQaֵ%-TPv\(A9i4mUN>6+ƟRbb tMˇ\&'['oDI\FIM,*B)@#8OhyK?xo(Ԥ J>_t?`h^KWAA;:k4oxF‚fV%`R5<$ |C0`)ԥn\r8fW蜩,6Qw$ _Y9zڌzd^r0,a :(ogkKIs+elEbXjfKTHk(#}ܐ>^COsCo[J [k~1p▻/`OQy*R0 N`W_`س)pHgҭt(gS=-.!tz-Yt,᪛[s-EyP^4Zh R_ZWvV {-6pdUu7 Pm5N7@zzqElFԘGHsӖ`{խwhL5e{&ڀNc|;ŒDgximtO -~Y~BQ|Ґ8%n=r(8@IMy=Bvۓ_\W>~L/NEx(!Wb8p|"`Y*ц5o2A\|}sU|j)RjW&l&+nYb`ڒ-;TuAʮWO[O'բ hIͯli>|ChubNl]VD=Zf oߦ$:U.&ʛ43uxՒTY2ޜ^Ӭ144ظQ2hVٜz baX.;Z7O75%+[\t`?FG*M:4}{xAV byRGWˇ"ֳa5|ץ,;W킇N]J'm$*j")N咫SWW6<{P2/IJ6HF[TԨ:w4y?ǑJv 1I;R-PWKKʊ*D}iKR"i!O6ӇOW4AiI)C7NHXA3^M'~*b_x|rkl:녕Z68xY% oWX\bQ.o=VU_M[pz_*;H[H|N9v(mc٪$'Oo'^:Ng0h 6=a{S7_ӲJFoL hH)kKȶ(BQ_Y'%"I4-ˆT(j(\b/].oYq;["QمL5JVΩc̴Q[ [~k-Ï`n,/Zy ]1|;;SQM;U\xcμ&'<Ň Q/ WO/7'/׿>vGLb01ڮZ{_C }D?>W]R׷ゥM AoLy!q&z81uK;-"r2fH^=v忉&3.0?4noTM9l:oB5_IJStU:%`g}$͸anzKq8$ G6sҧdN-f tɺY53$;+ǶhՔ\r_gBFo9f]{/Kcvx+VVk/֖IB`ƿebʱt^UoA r')޷FG Us89~Uwٱ1qa 1O%߱ H{[*7:hf'%oKыrL@ѱj-5=xڙ#kq;ۥGLN_=^) kE߰<IybòENSZ#>C魉p cw*5q{<Ҏ ݹus5!KHՄXNsL%эe]&oٶ>>pGya1Com^K,^kev2YDɛ\W6m:{"œ& >Pk(JCyо&E#tNp0؈_1R+~&kN߃-HaƊew{D#~Pkn2<3=gHՇ1E: xUQr;9YՀ;Ɂ͵׸A )}+v\=@& L_ڒRBˇ6E?DȎ3V&rꃣa/O}?=\xv]-/0l JZq6ı*eZgEGM( ;IG;3[%=Fo{m!_xe Ĉ%²T7~4AamHITAHe.wsMj ΢=]v@T]r QFj#(n=?٘P?w%Yܫ_f䳟6(68܄ik˷֖>gqُP.,wdiF t2) l܅NĊ*՝A;Z} qV'l5 -HY Gh6>%jl9V֕~sM= jc'i$P5.c~'O\.}HB`uX;ENQ 푙I;&8@ ͮ@ ViK?rVQ`Ξ9/~h@.Oznt6ٺR`Z,73`)ַqG ikP3nR TJ%Vfd|>:P(Fi?0\w?lnRd2od=E F`h+ʹNYA>Vꪣ F Ϩ7:d~kN b!^]B!& uIp#]a]1F3Ⰲ@``lWKhf* k PWp aQQF`hu@ߗe4<#`{*WhgDi |B`x!|@ B`!tD<11EuC  ]Z yslY/ڿ)pf/s =TM)m7=ؗmL7VeoU*g@#.:9[ 5ej#v3HAn8Μ-~Rt5RfY+G +[('$c=iQMR_!,z4!> ]a<%I]xt,sXwyf";~3U/u* Lz/vժ4VR8TTZi.QM`MVU5 gߺP}\grkRK 6>o.MߔD<@ F3"Y-D@My mݣQp\nL[-,U$n]7) XEB5&iDisY>dx]hC:V}&,rsqgnt] eסްǸM iRx]Lt JNʖ˷)ҖZTE #+5R,mnť-'T[ȏI?DWsW+kv|_񻤶T-eO\@Qsr4{2Z ofix1֗:/ /EZ5X.'7ܬkw5Y!Q"B!&}R>$Gzum x}5΋3͝wRĜGQ8J"LKbjE 3 8;{"xMP24;/Q˩ݬ,KfO`%~0F}q&:><Ðz)`v i7p=vʆ􄤑x:i4ˉ3daa.vX!ru r^w"t=< iZQ!pU7lBr !G˵I ḣ\JJe99AA.Pʁ>-\uw$欣(ő*EC 0J܎36PxY.JQS)vr~~H7A BB`!0=aMV$@#t2! 9t $Ui-(,&4} 0Y^a,^s[.'4^EG`hu놵!j'B^%}de0!W!@ VW6B' r`%.ݸ6 -C)B!0 A)^=,#9FQ5_ CdB? oc$EyG*u 0 M@CH$y`؎0PBq tǁ:*!@ #tyVHRB!@<8PGe"B` t󬐤B!x ]qD A #Y!IB!8@@@ ڤ>rb%w++nlw㟀"$PV?*#,2ғˋ*;L9k? E{u#8K`YBTQG}y̨l= ye9>,tRTcUZ:z7dn5 mI JʱF6.DR7';P˸|=Bkꗟ|DS|gMwu=/0q曀|j1bitCER\X8ön+/4QXv+Uh5,fQ2Z2~(Odx-|͍HHe11@}X?Klfm!nB˝M$}{'\?s9ѐ&M"5B>c,.ʫ[/j7eaX  Ϩuגz}bht|*>>!2ȓǵg(m nQoH[;5Qے Xf5[ߘ>Cr(i+Ťkw}#g㣥P߳A1@IDAT'B!0C2@o 7T `Z8}?}\ĕC!,qJQp| I)@'4\qSnx_^CyS!;W Ӝ;gM)la3+,dU哢z92HI}v b;xR?~R )/&mj/sz'.7NOLݒ9QY8 5꒼qc"&T\]Q :P/#dؤza'([l4aCUe~#"&i?Ǜ=?*,F 5U]Yw<8:+UtBTto)[;+H?^P"J]N+*oۿv\ّVtB3ԨbW۳S~x$wj[rLDU5?a&eʩ h(Q$o$nє5ݖ—sBN5 PQ`ߌJ\>c+5`y joh/9QƿɲIMv9΢: RCmeb R_⨙k6'kLxéSh}q@ghRbX\Grt7_jvfz ®1^aLE}ÜÝ*jNAPp}d&ĶTMutngG'=K$%^17U8Bt>x2hnIzdU?t` xj{|[4˄K+eLVӴoa0J*,~ENC]Q!K|`C]Ι4{jF529ɯ)>6W}p$o: ,lˋ15~uedi"2?OOq95-<+&Xaoo0ny2nK=Y{R(?9ޮ=[XB@=_S]x䧃̞_*n J g( lY, PIi &9,MQ/\<-߼z{FW|ݴ!d;M;~dO]kN hkwKA/y-B`Pa>06V#.RV[X<li;s^]#uO JRŌҠO[Irޮ6x!;%@_'˦́ F :e:a\ -N1`U¾ )ފ2E\86ol?U:Is,jj8":Ʀ!NdwZ}YB}}ZnC,). 4ٽ&G>+J dr/20uRg !]BN:1F}Iç +0pKo5(qWkgxsFb-> .|%—Mh籊Ti!6#Rn_W`WPdt\2PܠEaJ"q&j h_uOEdAٷ~I2'z~q]I2:x W[ᯤ/! X#'&Gl؅QTiGF4֌i#y8-w_-K9#[ӄ@xH <- FA Xں[k{[wkzZ[XB*PD@Pț(@x$@ $$?+$aQ#͜9s|gfΜ94ɒb&l8tj.M6\<:;6jRW8fsC.(겂?''.lP筜&NL{*7.1҄:"9~ dUzqQY@ig! P|!\??%? _ou8v3))p"a'7Ixmy~hDQ9ig3\0LsUomJ\)&w hMd94Q!nki+ȒB)`rE֜M5KjB&JgT_;-D)2D5΋99SYJ=}͏y97 v;4V.a㦳{&;7L)#;nh \&CQIN}~MC$`ƃ9b:u` >]ziCmU'~{ B G䃦^8_XW~#(ۣc3LI%9~x2Q?<-QxM_CP`{`DZTZыDPtjj~]ڛ)pCeH .OP(FDœ}xQo+ŗ/UxFk %_ܥΜP:8 Ĝ1^z~. U!YOʩz#wkͶOKB~e]s Йy}*V.z9cT\0G1Bfyʛ\kL&9r2vO<QVUJHg+w6-RWMxvhfkxk*74jYwP8+| 9?P:fF2^*6,"W:a$v]._I7C+6&"e} {Mg-)kQ$Zzӓ~&:8=~8Fp8:|M1l? N Tg{lFD _zU0V=zطgcBVPae%m@] ?1Y°1^B`6L)Nt5U5m7KtkEZ459jLP]j0+&xu0% ,r}ՙs=H j5&mnpo%BGmZMT|xt jAo`}2CKhU#5褻pmC ԘDa)\)@faGa9{Rd&߱';f;12Vl:{2@:ymQ>8kN"G̊hTLd/~GfQztK =He~{h%YmWiw.XPwWt;;JܐCa"l"Q;eBg -;gC^EDipp6KOl>`'e+ rx26x!H2g/ Lx ^7qKd0eg2+=}n;BY8Z3,TGʎѾ&ps!ʙ`jzayUw"aIL0g ~X.DC-ޘBj۞a4ŰBAlm(]Um|d|N~ٕ<1Gk_KlBrA.`Sy?n+pF"z.R!艰lD1f^Кe rGZFD!cae蠫+_bSuV +y`©+eIgeKVR!BnomIp#IgE=Y[*R.L)yں/x:/5Wj񔱎Dd4dB,P`o4xVSZ؀z˃EpʧtOr*WfTfaQKG:Zs Ϡ[[j*wb3hty>bJ:Mo?e"ig |3edfʮ)OG;p;T2zA_ BjUf_mULSvwOc(QA1)%\=c(% rd#) (g K(qs:>( {@Q9!擸TƗysO\N?G|veh+ g|Z)ڲ L6?J5M*m N W>LJ?@=0^ld7P T)D3S8\wP B>AliЁ_mp~?\-Ư^?}lK0Pqα3/,0n<33S6@ ?Wco߉wѩjaTTL3-묽{: 1/ D 1B,IΦMԝjo ޳hME@f8SW4##7o<M䏡O>o½ o(enF>6E1 ajotmޕZ_ƕl cc9\Qzuxg ežA9'N\1v {uz NDgww z @ ڲ$&;lLϗ/ؽx:J.)#D(@4eM?=ij>ɁtIb>8/d UtDiL[ܚiӊBPҖρ`rwc9Dz4`\9,Dcd{1 q3`ۢqj?N=l#r@;[R]BTHc۪5nfbuJ-]Q&.nA, [5S(Q㇧~#v ?/}94g)|-䨦[lRm_^WK! λvԾ֋w ڵfacTx7]9h'/Xt`+2=\Ca@-YYeg!>[m^{m"6uM𞺢KziTթLHV[z3] ْ3L1pN־1fIwelHVƪwFH3Z:e1GU:[VY6$Lܔ'!K>t<5U}ޤHFqf/AH,: ݂w?Q- v߫)Y2B!-l@#$v_*hDyY$ ڸE?W`namv]T"TnQu)i\oC U!H\Y^?;r III`B$<0YpV&++gbÎ哺f"W5|‰&GId88*_1JpwR?X [K}UVHU:6=qyW; pGufddH|qwbPnF9@r`P[eO[KK݅/5**m<5$@朾m0=BWBA܂+?`.VdXKE6R8k44 p6 *p@WkQЙ\?@KwAXR"mZ(ظ|6¿Xƍܹ:'3`Cjx] mGF0aS tT*jW9zדalyu#@9QE(?)KqxYCP{ ~X4`N0:S"1SB4)ԺERXuh/W[ۃԨNɠ}BmB1'!UW ;u>?ɀ\kA(9y՛"klZKJaBÞL@JPn\T,Iۊ"r>8}{$q"_eSy*뀐 e9Ř<\X@8B [9B Y4HrUI |LY` q\f:?;7#6- &ybh׶{U4vju0jt[G ewЕIRi6Pf)ؘUBO,B>~Ё^-6\yǴ7z$+ 5 #rR<@Ac^l# X ƫBlgg ii6ǮS>tcu7?Bs8Ip=L L'? -z+n2)Ea X.M;c@K{rŅ:jduoqύs`K"+6YKH |\{BPgyh9l87=`\{ޭ)ި"yXNPd(CPLſپmJc24/!fs׼k` ={rj3" ;Y&/6 a{}elE!zR %s*. _N[V_,R-L SL0W<{=|_Eɐ)rf'M0˾ "j]0"ˍ0qʜkk(DOI~, o'y{Y+Y Q)o@vٖ|7D{l҃j3 Lr`Vc{]5è栏 >e'0;ބ.7  o00 A~obyhIV nǍob^eݾ2:d3GYrTQ@pz5?\% ׂVb6R"R" xg텭ر t =ݒ z?z jpg"BRhwig4wS6 i}nq[PE)˅GEް"AqOz?0{),jh$W߾xi~͒c5(3rrkl2O Z Ϡ#Ũvڊ}%<}/ !Ym d; ؃ԴBB%R+](Rv h X޼-$U ݉J@JYQ'{lk+zǬMʦIo!|6íwty XZ.ծ'0LB( tFÐ Lc[ sko}Ak NP4}Ou9TU"U!>Fi8~a(p'QQXeC{1",@8$O tC3ۜ=Iaj4bRQ@Tļ|t J-&(il!hEQQ@;;Ǒ^Tp3&p0$X 1 \' '!i3UBXv*8:|@[؃ḤX~+hs||Yػc !j$X~JT&H4$EYsx"ǎc0 @H_p &Vu|F{iIUĠU]ߙGdheA@$&Exu4[cf;8kB?*J|0By>x\eA jyx[6bj LK+U36rC㬝cp^K0޾NTC0H[ >>ЧxT+ңK 0a&yKu^3K-qu ˼#P@f~%z]fׄAM%woCvNڧv ճ=-ԩR\RFTy&ņQMcXX~KqqucW\2& A#nl%~}jOeP/MlCeqg$J ;1`A9h?W6'yTC$KFH?w:5֙3ZXKFZNKT&,Mr`9`67QVg>i69S߉y+'5zs. "kt0zFU ] *' NQl(Y%ćUQ1ۤLUrc*7.ѷ$ҨtDPsfyxC@|!\??%? _8v3))p"a'7Ixmy~hD_^8M˫$ .hE?HNZfdkW1RWuUW2&*ču 5M|ȒNJDS$⩋<9[oEzK  5i s<LJ'@N ~ ?OSnTѻwD`t f8@JfLfuIUc`kڬՒ]殶Cn7 QYD ^ԪD!RŬ~ؖM̖:k*/l6f] EYC^^&;Ȇf&5֞*HgQ\Ek?o+,A+v=V{@h@uwFH7,wq<46|sjCHh2/~ س8kF0Xr)Jef~F{@/sΒU4{sKF~n9rswcѬJ pgX43cP`C܎!k={l;C & 3G'ܻU2{gWB*S9yKЩ֜aŻU=|"i;⒟^i+:g!Y/!g/*/_ |3gQ}ExBTnX4 ~Ǘy2*<.&&-DjA{*jJ suʄdI%cW|3Ğɰ5/loK5Y"ni({"o?Ws`OV eMvt G(v m٬a~]yb7Qq$ c 2'42Am;.hA]&,Jrip2vOOA: /_*@cr`3vb# UqIÌ^*6,"W:a(&'.rK5P[O01}WlLE>j:E3&júƶ(Je>lgiঀ1NSM]y_yM%~JJǕy%2VdD Y0Ir54P:k:OLU/!piop$uN{ _zU{(k pjM?׊BZTڎC!_m0^Q%$2ܷ\9kZ-F%2j7` f9{WcXfPHdsE`w2jڂ̍ߪ, so a$LOMv:.\l5Z(P7eU.ORe2BKX#dz=) 2kH3܃n ^U܊MSû`8ĭw99×Wl~-8yĬF_;Um7GEmꁀr| {Ol'qhy+єڍ+DD4+:%nHС0]s6b^ΨcDD3nm޳!n62 &`UAZ?߅P¸(ڟ88٠)*vx_v.pVXL`@}xAB_L/52/xY?/YJ{19 fhkͰPᏲd GHrf1h/ߑV_3BS>\''WTЅ'⾞GbpBŌnlk쓦#;\܌$&HO ~X.DCmKxom0bXRh66mb{d6H Y{ o(;#R KgWB@'9}/Q S 3PVv͘X;MiyDo5 +aKcوb̆̋aPDMBƓy@ʗت.,`V!~eC2#=qY5^B04 @gH,J׬jD$C::2z~ dmK)nnjޒSu_1I0ٝBU0hnJ]?QX[!9$kͰXuim3?»+Ťp71?x4ˑmcg$&l<4j~Sm| GO2˹KFE,="}Xq;섁 /V0 3_ \,[s3jIU?@r`fpy5 N S7ZYǦ ~[+F)S) 4fN,Y&DrKj}KpqF2,?9tf"H^p`W5S7+2pa~ }*Ѡ7Hr+<ɁLycz9[}pfK I 9@r33 4$H 9s^p Or$+ 2$H u\ 9@rɁq8@ 0&9@r/8o_p OrHp]RH.:ӡƚ!Uzyl 8'w40Asc[oCsyTN t#6IECR|';z}HW4EO'_f]UN wnXOx/~BC>+$ڿзIH‬`lAO)ϽU\a{k*~{,[Ͷ}ok~<)m'3Ja\Q\ vgfmZgוg(`f܁@߬ZSJ DFrV—ĭ)UZo. @gǫ}K͘'rq raa/<[itAF-üV1<}0ZU}FRzA2\OYxx8㛑 kǬ"( n>K|MEAv|(` T \]'(/mER,]B+e((@'PEA!j(0m ~jT2]a|.鬙..{jan>EM@FSTsF>8T&Wؙ=M%7C8_$~3:ZOٖlG8~Bs}}|9fyqe Eh}vc@چR ar/>xWQFEń:3=?@IDATڻb`OP{4A&.D$ygӁ&Y7 @o*:|x2ۼh鞒璪Q4oxmt&܋0MQq*td58`cStx\FlN]Em\@=6WwlxP6ʈFa]'vM=)z NDgww zȘ!]qGan{R^1jj񛱳OƨK?_`6([g &ދ45DKrS$t@vX2†*w&-ng~4iE_Whc!(i@Q0^]v;w1Мc 0DE 3sn,xbu{/ݿ>W~\58--(@3֪xLJp DuYyj, +'/Xt`mֳyoT&4;]oeIO7mޖX֖0M\܂Xt_@!.^',|8QZ/ީ>6. ծ5[FDhƭTFK\?am5\a]ڐm2 `d,?%k<,5guzy_dTfܿSWtA/:5 jKofWa:Xs8[ux))]7,v0 ]YXUqnCi@A+]V,&z`5 -٫V%uP!Ye<8tJg1 5@AVI,&h'a3x - Ӂ꜒%mH/aaY؅dAK^% 4}!s~C$xw.j$jXp_y98 t  H%Eó :>Mu #2N3Xn!)) ,W ʤP{EcBwر|R׬\6OC8D򞉸(V]Cܡ9 ݲ5Us)4{mQ>ؙQAAEUu`_C=ee܎_s2n00$DŔj c j(MvWQ%OTq>yp?sQ(C-ŕNBf^w6qi֏.V AC#W4A&ѩ,EF^(X9zqTGOt]8~ÐADȊJlXQAШ-[PAQ|o|{̱ xTqo;K`S!x,RTP $j=b1yG.ZUzsK*} bp a|:ޖBd(9p Ϥ5^|ghbFyD5V#T:/;r[K4%*? Xr4G&p y}p[IPRpWhF+o5>$h'^1*>hk=Tl\>_,jUa[%lX(T @yc },(2V aqx ƔG݂B  ,G類Q8Z*>RSPں:%%p Ti+į\w I Z} D"Ts@u7BY {Q}5P Kش3 ͞?g!Vt4Ɓ⺾D?WZ|3Qցルo oZ ~ 6-\u; ̀kϻ T$k>-So\.Ɍo&Qb= D_BLڒ揮)6,6+vF@,ؘ `nw4B^_)[_Q~GȪ{z.~{YMs*. _N[V_,ЄRv썖V|qX؄) HV+ {ohdH9.Bwm E(@"?^ϘErd۷fOCd/Fe[ IYƤbb#bp&0q~X/GQb4ڝ]QE~knx`,khoD ¹G _l=;ބ7 > t6ž*t k(cq&&G- 7G4qsKwQE%z Ata%C:Q Fq@Ȋ: FE F [cA3C0{iUj1K8oZ@۴9daɚѽi~6<5P=Vx JqEAY3|1A.!U*iD0 Tn(I5#_ٴ6IRM2x}pcjGfzL Titem 4 l]ueu!vΛaٜ^>n )Ё'A yFTJEIpB@}c Yҥ%v`;(0F0O+=gǮ^5gH% $<҇U|6=]Y'YwW$BLc:1*(ѯ·'qoGA9(#xq~ U4!3Duy_}V4g7 9:,윴O%@gzcJʔ cѕHma 6u8Gσ}P$~Im5!# I3%ŁMnU}ރ|pIOpzS%ƈ1N-*UA`j([խGN0K*jhjKfE :3]`Dk){P:mJrd:B1)m[X T$n~,I UJMׯ)tb4  2i:0)tmv2uCv RfW6 H!4pi o(q" xm# U!ywF  pӴ?h-Ʃnނşv84Pk1:kӭQFL?X-s&íAg^*TJ|`ݍG6dqg'ge"!905P4F38b/,@p0CH~]HrO_c41Xc 9D(4Nwc;~ګmL1N{g/-pR0cޙSkDj$ {8{f˅m.CŞs9ld(,^ ]glgqn`Sː\JRh;Q^iЈ,md}շr?ͮܜI[X#p4=q<< 2ifƠ m<Cz™wȘL0Y9z6 <ޭ;:PRQ_7OɻX燮NV[Uc'2nܝqNx)(KhE/!>vw`F)%E'LŬ~a{kݍ^WkI\76RΖoD=Bcrnf`{1-f+J}g;N0&j"sB#Cdx%M" $E𧃄ū}gje}UWQs|oZ&ٴlD뮦pT#+M6?N{l2\|ɀ݊u{#I V$Qu#Odp@ޒ`,BE_ dc/![،R_fU\c%Jp KŅȕNɉ ܂zkNP*NXJzwJ;IGyYgp/q|$x"^MA2qU/<v0]%n 5 NS 7::5P=eV)X!Q.cVdD YM@!WPOuPÿʥ19>2 +5%&x?y5U$hI\¥564[459jLP]j0+&xu0EqӋ%,\_eKϡZmMhbD'sj0 HP[Q#熞۾;1ڴ:.><:FC $7)814QW~}e;- IK{Mߙ9@Ȭ5褻pmC XEa)\)@faGa9{Rd&߱';f;mZŭt05@:ymQ>aWl~-ؕ#7?!/qO$+b 7?߅* N7$P9H/gAx ˑS8M !p~7Gd 7{ \hdzA`'e+ Ť޽ME:{s cX/LDJɄǾuCgdmqL*ꗌI \0h%JO}=<%XuyP{$٘䉜2ne4Y9L Q5-6ޡAPΕ(rI8 -1\+\_S^HnFuXj"Uk7r{yA;⍹~ /FS +T FæMJo㋤'6="qve!4r >њ5[;"|PgmehیÛaޔnwHDoVR>l ijȍ{ :ϱ-lt5_Xz\`qjN܊!~eC2#ohXFDhJaUH:{Eb)DP[[.O_-b)Vrf< C>m]<`n O+Xp]!r^jꝞd2O`oL FVWO fkaN-)n=SO?*,,jWGksp!qkKZ厒[_v.g_VYҧ4[GlX3KQ 908 ]( #<ͰRò' Q27zBjUfؑxULSvwOW I-ob9tuh#qfI7)%#9bq`J\g!K`9u)"ˆGľB+nV6Jd5.F@^pPXeޚXHr58ugj"&i"9` nM|x A(ИIVrԜLQYL:4 䖬 o 4<  <]2B5=u#Ҍn/IɁg Lc~ }*р;.NSRWzI 9@r`28vX7O" u#E 9@rɁ^P&9@r$ q q#9@r$H]$H 9@rH]w<$H 9@rH 9@r$ qY {mVsmg%y-[;J3xױ~L(g I~vkЬD_a3BZA6sʃ;4ϥb ޒDN dCP >)幷J9 /cEl@am{t\;-H۸7Nf-Fp7W8-Sk Q}vnoXܦUzוg(`f*)~j1SO)%/KҚJ mqz4T^w )We i6^t d܆a_yo]P{mfDeA 9s:9LgJhUAI9ȴ2%8I$h.6pN/i4r1Xw Q /ПsCE'qɯ.nՁ@>q; ?A\Y(8PϟeKhh˒B\ClHg'wQ G=Lp373ME&ы"(q*xB0 _]AKBaf0|pL!ܷ7s-Zt I3h6-#ixFR9Ǿ|9fyq>Eh}vc@چR v _<};> Zt:kB 0=Q,C!K+*!wk#@F }쥛%Kyv=ٙ2֙{eѪG'#!v7^<3$Mг" `#fA5Q؜ͻ@۸5=+͞pE=yWDً=(칋?8s c?Z'lݜR!hށD^ ctmY-ۿkHĢ]E~ L~u*J5Aូ=Y3uڟ'ދĴ3♩b4=3! }18.I,=lU{'J[ܚie+ʿB AI[v>ʵK޹3nЀqp%* X%Kf-Fb*j>4s4mV:gD{wg|Kc¢{r. ky{] n5r{ANZ󤧁YXoQi,kKW&.nA,#̕"ľ[WN̵ "zs,k59? I{AO5O^OWU)1W$j2'VU~;> 0P]llxzm)x[5S(Q㇧b\a1~lB[L& ? 7Qivd2gX `O,?%k<,5gn`9o/"]zK:NeBқYْ͚{NwvMk F;26$ i~k4C 頕.S3 R;"h_\G/fZ?AU)f :(S70ơS~>DFIWVF8Z d^{2$JV֨+be#?w:PShJH/԰`C(O,Xޛ{(ɚo"m`t]!ϓ%0)>(C}* A>(w5˞v̤Gt$bVCr`]{.0"; Mv(<}IP(~Xvư԰`ǵ74)y̑37-<qI*͇T[.uVve+W܅@!y/}9BĀX~1v}SBbOf X9Pyd9:sB`1uXY] ~BhȋB'R'k%4`E> 4/h[7,,,[f@$ޣGu.pw!p~S.|d)bg|\K}Cqm(-Z"7HIy4G C% f /ysuv9i: 7muЩʇ룅2| %49i6![ɮEKUQ$ҵ3ޓWJpHR[2Z=VZX~t-TM !.k'(eЁ=Odےoyok%&(ӡWAbWst 1Gyu~wv e Y.o 2:]( >2hl:Oxz2G#ֿW D[m%jť7iu[K-/4vý OK'~f% ~fe:'F% :uG#.𭌤]δ |ٹ<[EP'gCJV{ߴ(XVkt\UG^t[`eu!%z%mO[s 'ӛ~L Ś~Dry]H #_OȚ]AIrUi CLs~+rٸaimj#fZ즟kOpc"&) : .U'%ذߙ\^w+>3f Yt܃=)jrtD<>{YzaSR:U!No>m ~RV~=(82z0?Bu%buk(z{Sе(|4(6>mTnAr+h7Y-;IO sERjG/౲@qkYqo#!>BIESf6SKJ;M%T "{+3m2l,qx"LCsj4LJ76@;^A D)TWܣ_-. AġI,Xg7{ljCBmP.&"w%+|uo=8G•[͗|+(~[4s,,ݷMσ0$?|:]*'팡QP1Ѥn+=[@;2( 5wBS%눵N#ξ"kg`x֌U-=/Rrj0b1md(0BY;/4N dAs[쓯BCqD:qOT0r0%~Կe,F3@0rOExZL6} lg d/^P-dE2j]&޼ArW6*ΦI.6`nPPt+cϖ1c(Pw^ j5K?$1D^@! ;+ih[3b`xx(?g tEKԢpUZ gY\']INd 0I_*6O? =B}tPL18IFo=T䄣B}(w]!pڹ3>Oݖx` x@ϧ!דaEvkV^/pdii,>!&Q4֡{k(5 5慌 lGc,#<>מg s/8,M;5Zqd-ԲځO;+xct=#/^e~^so:!kN  7\lٿ2y:ٱrœLʴ0g'ihQN1T,OciF[:a58*v -JMeG,7-/m? '{=[)c, }k~]s@04~ mjҐCh*;031l&4LqJQ,)vtDD#=QӶ `⻳44)*ۼb; ŤWA`Ű"a8 ZEٍ-8 ΁<)24 JH>+7Hʢv46AzCYbqnK|&9r`h Y=J)fvK 02H7H1r6|Mf&Jͬ% &Tc]MPkJIuΜB|-(ݪ4T}>[W_FI ={)$t4'>?=mdbBv 7c[ו=Tz/EǛsH6_!S刔ҌGH4Iah%bP\\greO, [0=Ʌ 8yIWO4d[2K )&Hɧ,/v4`om -#mà KgazQmEqxzERŘYf \*/{ePßj7c2ihiO8Kq)_2,rS]wڤeRH;'y;TZi{eqW\r,`\]kizMZή+Z)8'eŸCD己{{ѫπenZhxK@$mN4`0,٤jH*̶D?|igjn:GV tOo %~yxgL{hFSZsO?9@Kgi"5w`G#Z\&dJp1)#="՘lh(,IDATx@R`d!pZ,z+w䑎3*Ń <;[-=&FR.&_XZwp\h(sd(5?',-7N>KC\h/1ȁKY;-#j5g`KЃ=Pںk'oGϻzgtu %?#7D$R] 99;;/Hbѳ)=^}! ~ś%!+h,:Jڔ&ɩĴAiy/~O7 opW *hْzd~[2n$g {mx|BkCϤ5͈m'*v!tr.\#[!zӋt;΅2#n-q&`Bnj}2 ~zmlo D'YHD8x#^AoB"'aY̛9;qpZ`Rٌ}}`.psrɿf'<`[ᑭ,F#L3"MS~l+L@#`0a0F#l+pA b0F`!mWF#i1F#0ܫs\bF#pA \´F#{`[a9.1F#` m.haZF#=>[IENDB`mustermann-4.0.0/mustermann/000077500000000000000000000000001517364653100161105ustar00rootroot00000000000000mustermann-4.0.0/mustermann/LICENSE000066400000000000000000000020371517364653100171170ustar00rootroot00000000000000Copyright (c) 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. mustermann-4.0.0/mustermann/README.md000066400000000000000000000700651517364653100173770ustar00rootroot00000000000000# The Amazing Mustermann *Make sure you view the correct docs: [latest release](https://rubydoc.info/gems/mustermann/), [master](http://rubydoc.info/github/sinatra/mustermann).* Welcome to [Mustermann](http://en.wikipedia.org/wiki/List_of_placeholder_names_by_language#German). Mustermann is your personal string matching expert. As an expert in the field of strings and patterns, Mustermann keeps its runtime dependencies to a minimum and is fully covered with specs and documentation. Given a string pattern, Mustermann will turn it into an object that behaves like a regular expression and has comparable performance characteristics. ``` ruby if '/foo/bar' =~ Mustermann.new('/foo/*') puts 'it works!' end case 'something.png' when Mustermann.new('foo/*') then puts "prefixed with foo" when Mustermann.new('*.pdf') then puts "it's a PDF" when Mustermann.new('*.png') then puts "it's an image" end pattern = Mustermann.new('/:prefix/*.*') pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] } ``` ## Overview ### Features * **[Pattern Types](#-pattern-types):** Mustermann supports a wide variety of different pattern types, making it compatible with a large variety of existing software. * **[Fine Grained Control](#-available-options):** You can easily adjust matching behavior and add constraints to the placeholders and capture groups. * **[Binary Operators](#-binary-operators) and [Concatenation](#-concatenation):** Patterns can be combined into composite patterns using binary operators. * **[Regexp Look Alike](#-regexp-look-alike):** Mustermann patterns can be used as a replacement for regular expressions. * **[Parameter Parsing](#-parameter-parsing):** Mustermann can parse matched parameters into a Sinatra-style "params" hash, including type casting. * **[Peeking](#-peeking):** Lets you check if the beginning of a string matches a pattern. * **[Expanding](#-expanding):** Besides parsing a parameters from an input string, a pattern object can also be used to generate a string from a set of parameters. * **[Generating Templates](#-generating-templates):** This comes in handy when wanting to hand on patterns rather than fully expanded strings as part of an external API. * **[Proc Look Alike](#-proc-look-alike):** Pass on a pattern instead of a block. * **[Duck Typing](#-duck-typing):** You can create your own pattern-like objects by implementing `to_pattern`. * **[Performance](#-performance):** Patterns are implemented with both performance and a low memory footprint in mind. ### Additional Tooling These features are included in the library, but not loaded by default * **[Pattern Set](#-pattern-set):** A collection of patterns with associated values, designed for building routing tables that dispatch efficiently as the number of routes grows. * **Mustermann::Router:** A very basic rack router built on top of `Mustermann::Set` for demonstration purposes or small-footprint routing in Rack middleware. Simple and fast. ## Pattern Types Mustermann support multiple pattern types. A pattern type defines the syntax, matching semantics and whether certain features, like [expanding](#-expanding) and [generating templates](#-generating-templates), are available. You can create a pattern of a certain type by passing `type` option to `Mustermann.new`: ``` ruby require 'mustermann' pattern = Mustermann.new('/*/**', type: :shell) ``` Note that this will use the type as suggestion: When passing in a string argument, it will create a pattern of the given type, but it might choose a different type for other objects (a regular expression argument will always result in a [regexp](../docs/patterns/regexp.md) pattern, a symbol always in a [sinatra](../docs/patterns/sinatra.md) pattern, etc). Alternatively, you can also load and instantiate the pattern type directly: ``` ruby require 'mustermann/shell' pattern = Mustermann::Shell.new('/*/**') ``` Mustermann itself includes the [sinatra](../docs/patterns/sinatra.md), [identity](../docs/patterns/identity.md) and [regexp](../docs/patterns/regexp.md) pattern types. Other pattern types are available as separate gems. ## Binary Operators Patterns can be combined via binary operators. These are: * `|` (or): Resulting pattern matches if at least one of the input pattern matches. * `&` (and): Resulting pattern matches if all input patterns match. * `^` (xor): Resulting pattern matches if exactly one of the input pattern matches. ``` ruby require 'mustermann' first = Mustermann.new('/foo/:input') second = Mustermann.new('/:input/bar') first | second === "/foo/foo" # => true first | second === "/foo/bar" # => true first & second === "/foo/foo" # => false first & second === "/foo/bar" # => true first ^ second === "/foo/foo" # => true first ^ second === "/foo/bar" # => false ``` These resulting objects are fully functional pattern objects, allowing you to call methods like `params` or `to_proc` on them. Moreover, *or* patterns created solely from expandable patterns will also be expandable. The same logic also applies to generating templates from *or* patterns. ## Concatenation Similar to [Binary Operators](#-binary-operators), two patterns can be concatenated using `+`. ``` ruby require 'mustermann' prefix = Mustermann.new("/:prefix") about = prefix + "/about" about.params("/main/about") # => {"prefix" => "main"} ``` Patterns of different types can be mixed. The availability of `to_templates` and `expand` depends on the patterns being concatenated. ## Regexp Look Alike Pattern objects mimic Ruby's `Regexp` class by implementing `match`, `=~`, `===`, `names` and `named_captures`. ``` ruby require 'mustermann' pattern = Mustermann.new('/:page') pattern.match('/') # => nil pattern.match('/home') # => # pattern =~ '/home' # => 0 pattern === '/home' # => true (this allows using it in case statements) pattern = Mustermann.new('/home', type: :identity) pattern.match('/') # => nil pattern.match('/home') # => # pattern =~ '/home' # => 0 pattern === '/home' # => true (this allows using it in case statements) ``` Moreover, patterns based on regular expressions (all but `identity` and `shell`) automatically convert to regular expressions when needed: ``` ruby require 'mustermann' pattern = Mustermann.new('/:page') union = Regexp.union(pattern, /^$/) union =~ "/foo" # => 0 union =~ "" # => 0 Regexp.try_convert(pattern) # => /.../ ``` This way, unless some code explicitly checks the class for a regular expression, you should be able to pass in a pattern object instead even if the code in question was not written with Mustermann in mind. ## Parameter Parsing Besides being a `Regexp` look-alike, Mustermann also adds a `params` method, that will give you a Sinatra-style hash: ``` ruby require 'mustermann' pattern = Mustermann.new('/:prefix/*.*') pattern.params('/a/b.c') # => { "prefix" => "a", splat => ["b", "c"] } ``` For patterns with typed captures, it will also automatically convert them: ``` ruby require 'mustermann' pattern = Mustermann.new('//', type: :flask) pattern.params('/page/10') # => { "prefix" => "page", "id" => 10 } ``` ## Peeking Peeking gives the option to match a pattern against the beginning of a string rather the full string. Patterns come with four methods for peeking: * `peek` returns the matching substring. * `peek_size` returns the number of characters matching. * `peek_match` will return a `Mustermann::Match` (just like `match` does for the full string) * `peek_params` will return the `params` hash parsed from the substring and the number of characters. All of the above will turn `nil` if there was no match. ``` ruby require 'mustermann' pattern = Mustermann.new('/:prefix') pattern.peek('/foo/bar') # => '/foo' pattern.peek_size('/foo/bar') # => 4 path_info = '/foo/bar' params, size = patter.peek_params(path_info) # params == { "prefix" => "foo" } rest = path_info[size..-1] # => "/bar" ``` ## Expanding Similarly to parsing, it is also possible to generate a string from a pattern by expanding it with a hash. For simple expansions, you can use `Pattern#expand`. ``` ruby pattern = Mustermann.new('/:file(.:ext)?') pattern.expand(file: 'pony') # => "/pony" pattern.expand(file: 'pony', ext: 'jpg') # => "/pony.jpg" pattern.expand(ext: 'jpg') # raises Mustermann::ExpandError ``` Expanding can be useful for instance when implementing link helpers. ### Expander Objects To get fine-grained control over expansion, you can use `Mustermann::Expander` directly. You can create an expander object directly from a string: ``` ruby require 'mustermann/expander' expander = Mustermann::Expander("/:file.jpg") expander.expand(file: 'pony') # => "/pony.jpg" expander = Mustermann::Expander(":file(.:ext)", type: :rails) expander.expand(file: 'pony', ext: 'jpg') # => "/pony.jpg" ``` Or you can pass it a pattern instance: ``` ruby require 'mustermann' pattern = Mustermann.new("/:file") require 'mustermann/expander' expander = Mustermann::Expander.new(pattern) ``` ### Expanding Multiple Patterns You can add patterns to an expander object via `<<`: ``` ruby require 'mustermann' expander = Mustermann::Expander.new expander << "/users/:user_id" expander << "/pages/:page_id" expander.expand(user_id: 15) # => "/users/15" expander.expand(page_id: 58) # => "/pages/58" ``` You can set pattern options when creating the expander: ``` ruby require 'mustermann' expander = Mustermann::Expander.new(type: :template) expander << "/users/{user_id}" expander << "/pages/{page_id}" ``` Additionally, it is possible to combine patterns of different types: ``` ruby require 'mustermann' expander = Mustermann::Expander.new expander << Mustermann.new("/users/{user_id}", type: :template) expander << Mustermann.new("/pages/:page_id", type: :rails) ``` ### Handling Additional Values The handling of additional values passed in to `expand` can be changed by setting the `additional_values` option: ``` ruby require 'mustermann' expander = Mustermann::Expander.new("/:slug", additional_values: :raise) expander.expand(slug: "foo", value: "bar") # raises Mustermann::ExpandError expander = Mustermann::Expander.new("/:slug", additional_values: :ignore) expander.expand(slug: "foo", value: "bar") # => "/foo" expander = Mustermann::Expander.new("/:slug", additional_values: :append) expander.expand(slug: "foo", value: "bar") # => "/foo?value=bar" ``` It is also possible to pass this directly to the `expand` call: ``` ruby require 'mustermann' pattern = Mustermann.new('/:slug') pattern.expand(:append, slug: "foo", value: "bar") # => "/foo?value=bar" ``` ## Generating Templates You can generate a list of URI templates that correspond to a Mustermann pattern (it is a list rather than a single template, as most pattern types are significantly more expressive than URI templates). This comes in quite handy since URI templates are not made for pattern matching. That way you can easily use a more precise template syntax and have it automatically generate hypermedia links for you. Template generation is supported by almost all patterns (notable exceptions are `shell`, `regexp` and `simple` patterns). ``` ruby require 'mustermann' Mustermann.new("/:name").to_templates # => ["/{name}"] Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"] Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"] ``` Union Composite patterns (with the | operator) support template generation if all patterns they are composed of also support it. ``` ruby require 'mustermann' pattern = Mustermann.new('/:name') pattern |= Mustermann.new('/{name}', type: :template) pattern |= Mustermann.new('/example/*nested') pattern.to_templates # => ["/{name}", "/example/{+nested}"] ``` If accepting arbitrary patterns, you can and should use `respond_to?` to check feature availability. ``` ruby if pattern.respond_to? :to_templates pattern.to_templates else warn "does not support template generation" end ``` ## Proc Look Alike Patterns implement `to_proc`: ``` ruby require 'mustermann' pattern = Mustermann.new('/foo') callback = pattern.to_proc # => # callback.call('/foo') # => true callback.call('/bar') # => false ``` They can therefore be easily passed to methods expecting a block: ``` ruby require 'mustermann' list = ["foo", "example@email.com", "bar"] pattern = Mustermann.new(":name@:domain.:tld") email = list.detect(&pattern) # => "example@email.com" ``` ## Pattern Set `Mustermann::Set` is a collection of patterns where each pattern is associated with an arbitrary value — typically a handler or action. A single call to `match` returns both the captured parameters and the value for the first matching pattern, making it straightforward to build a routing table. ``` ruby require 'mustermann/set' set = Mustermann::Set.new set.add('/users/:id', :users_show) set.add('/posts/:id', :posts_show) set.add('/posts', :posts_index) m = set.match('/users/42') m.value # => :users_show m.params['id'] # => '42' set.match('/unknown') # => nil ``` You can supply the initial mapping directly to the constructor: ``` ruby set = Mustermann::Set.new( '/users/:id' => :users_show, '/posts/:id' => :posts_show ) ``` Or use a block for imperative setup: ``` ruby set = Mustermann::Set.new do |s| s.add('/users/:id', :users_show) s.add('/posts/:id', :posts_show) end ``` Pattern options such as `type:` are passed as keyword arguments and apply to all patterns in the set: ``` ruby set = Mustermann::Set.new(type: :rails) set.add('/:controller(/:action(/:id))', :route) ``` ### Values Each pattern can be associated with multiple values. `match` returns the first, while `match_all` returns one match per value: ``` ruby set = Mustermann::Set.new set.add('/users/:id', :admin_handler, :user_handler) set.match('/users/1').value # => :admin_handler set.match_all('/users/1').map(&:value) # => [:admin_handler, :user_handler] ``` When no value is given, a match still succeeds but `value` is `nil`: ``` ruby set = Mustermann::Set.new set.add('/ping') set.match('/ping').value # => nil ``` ### Peeking `peek_match` matches a prefix of the input rather than the full string. The unmatched remainder is available via `post_match`: ``` ruby set = Mustermann::Set.new set.add('/users/:id', :users) m = set.peek_match('/users/42/posts') m.to_s # => '/users/42' m.post_match # => '/posts' m.value # => :users ``` `peek_match_all` returns every pattern that matches a prefix: ``` ruby results = set.peek_match_all('/users/42/posts') results.map(&:value) # => [:users] results.map(&:post_match) # => ['/posts'] ``` ### Expanding A set can generate strings from parameter hashes using the same interface as individual pattern expansion: ``` ruby set = Mustermann::Set.new set.add('/users/:id', :users) set.add('/posts/:id', :posts) set.expand(id: '5') # => '/users/5' (first applicable pattern) set.expand(:posts, id: '5') # => '/posts/5' (patterns for a specific value) ``` ### Match order A set can match patterns and values in loose or strict insertion order. You have the following guarantees without strict ordering: * Patterns with dynamic segments in the same position and equal static parts will always match in the order they were added. * Multiple values for the same pattern will retain their insertion order in regards to that pattern. Trade-offs without strict ordering: * Static segments may be favored over dynamic segments. If you want to guarantee this behavior, enable trie-mode proactively. * When a pattern has multiple values, these will follow each other directly when using `match_all` or `peek_match_all`. Strict ordering comes with both a performance overhead and marginally increased memory usage. How big the performance overhead is depends on the number of patterns that overlap in the strings they successfully match against. It does use Ruby's built-in sorting, which on MRI is based on quicksort. The memory overhead grows linear with the number of pattern and value combinations, but is generally small compared to the memory used by the patterns and values themselves. With strict ordering enabled, patterns and values are guaranteed to occur in insertion order. Without strict ordering, not using a trie: ```ruby set = Mustermann::Set.new(use_trie: false) set.add("/:path", :first) set.add("/static", :second) set.add("/:path", :third) set.match("/static").value # => :first set.match_all("/static").map(&:value) # => [:first, :third, :second] ``` Without strict ordering, using a trie: ```ruby set = Mustermann::Set.new(use_trie: true) set.add("/:path", :first) set.add("/static", :second) set.add("/:path", :third) set.match("/static").value # => :second set.match_all("/static").map(&:value) # => [:second, :first, :third] ``` With strict ordering enabled, regardless of whether a trie is used or not: ```ruby set = Mustermann::Set.new(strict_order: true) set.add("/:path", :first) set.add("/static", :second) set.add("/:path", :third) set.match("/static").value # => :first set.match_all("/static").map(&:value) # => [:first, :second, :third] ``` ## Duck Typing ### `to_pattern` All methods converting string input to pattern objects will also accept any arbitrary object that implements `to_pattern`: ``` ruby require 'mustermann' class MyObject def to_pattern(**options) Mustermann.new("/foo", **options) end end object = MyObject.new Mustermann.new(object, type: :rails) # => # ``` ### `respond_to?` You can and should use `respond_to?` to check if a pattern supports certain features. ``` ruby require 'mustermann' pattern = Mustermann.new("/") puts "supports expanding" if pattern.respond_to? :expand puts "supports generating templates" if pattern.respond_to? :to_templates ``` Alternatively, you can handle a `NotImplementedError` raised from such a method. ``` ruby require 'mustermann' pattern = Mustermann.new("/") begin p pattern.to_templates rescue NotImplementedError puts "does not support generating templates" end ``` This behavior corresponds to what Ruby does, for instance for [`fork`](http://ruby-doc.org/core-2.1.1/NotImplementedError.html). ## Available Options ### `capture` Supported by: All types except `identity`, `shell` and `simple` patterns. Most pattern types support changing the strings named captures will match via the `capture` options. Possible values for a capture: ``` ruby # String: Matches the given string (or any URI encoded version of it) Mustermann.new('/index.:ext', capture: 'png') # Regexp: Matches the Regular expression Mustermann.new('/:id', capture: /\d+/) # Symbol: Matches POSIX character class Mustermann.new('/:id', capture: :digit) # Array of the above: Matches anything in the array Mustermann.new('/:id_or_slug', capture: [/\d+/, :word]) # Hash of the above: Looks up the hash entry by capture name and uses value for matching Mustermann.new('/:id.:ext', capture: { id: /\d+/, ext: ['png', 'jpg'] }) ``` Available POSIX character classes are: `:alnum`, `:alpha`, `:blank`, `:cntrl`, `:digit`, `:graph`, `:lower`, `:print`, `:punct`, `:space`, `:upper`, `:xdigit`, `:word` and `:ascii`. #### Typed Captures Certain Ruby classes and named symbols can be passed as a capture value. They constrain what the capture matches **and** automatically convert the captured string in `params` to the appropriate type. ``` ruby require 'mustermann' require 'date' # Integer: only matches integers, converts to Integer in params pattern = Mustermann.new('/:id', capture: Integer) pattern.match('/42') # matches pattern.match('/foo') # does not match pattern.params('/42') # => { "id" => 42 } # Float: matches integers and decimals, converts to Float in params pattern = Mustermann.new('/:price', capture: Float) pattern.params('/3.14') # => { "price" => 3.14 } pattern.params('/5') # => { "price" => 5.0 } # Symbol: only matches word characters (\w+), converts to Symbol in params pattern = Mustermann.new('/:format', capture: Symbol) pattern.params('/json') # => { "format" => :json } pattern.match('/with-hyphen') # does not match # Date: only matches YYYY-MM-DD dates, converts to Date in params pattern = Mustermann.new('/:date', capture: Date) pattern.params('/2026-04-23') # => { "date" => # } pattern.match('/04-23-2026') # does not match # Gem::Version: matches version strings, converts to Gem::Version in params require 'rubygems/version' pattern = Mustermann.new('/:version', capture: Gem::Version) pattern.params('/1.2.3') # => { "version" => # } ``` Lowercase symbol aliases are also available: `:integer`, `:float`, `:symbol`, `:date`, `:version`. They behave identically to their class counterparts: ``` ruby pattern = Mustermann.new('/:id', capture: :integer) pattern.params('/42') # => { "id" => 42 } ``` These can be mixed with other capture types in a hash: ``` ruby pattern = Mustermann.new('/:id(.:format)?', capture: { id: Integer, format: :slug }) pattern.params('/42') # => { "id" => 42, "format" => nil } pattern.params('/42.json') # => { "id" => 42, "format" => "json" } ``` Like all other capture types, these can also be used in an array: ``` ruby pattern = Mustermann.new('/score/:score', capture: [Integer, Float]) pattern.params('/42') # => { "score" => 42 } pattern.params('/3.14') # => { "score" => 3.14 } ``` #### Other Symbols The following symbols constrain the capture with a regex but do **not** perform any type conversion — `params` still returns a string: | Symbol | Matches | |-----------|---------| | `:locale` | BCP 47 language tags (`en`, `en-US`, `zh-Hans-CN`) | | `:slug` | Lowercase URL slugs (`hello-world`, `foo-bar-baz`) | | `:uuid` | UUIDs (`f47ac10b-58cc-4372-a567-0e02b2c3d479`, case-insensitive) | ``` ruby Mustermann.new('/:lang', capture: :locale).match('/zh-Hans-CN') # matches Mustermann.new('/:slug', capture: :slug).match('/Hello') # does not match Mustermann.new('/:id', capture: :uuid).match('/not-a-uuid') # does not match ``` Again, these can be mixed with other capture types in a hash or array: ```ruby set = Mustermann::Set.new(capture: { id: [Integer, :uuid], locale: :locale }) set.add("(/:locale)?/:id", :show) set.add("/(:locale)?", :index) # without capture constraints, this would match the :show pattern instead match = set.match('/en') match.value # => :index match = set.match('/f47ac10b-58cc-4372-a567-0e02b2c3d479') match.value # => :show match = set.match('/en/12') match.value # => :show ``` ### `except` Supported by: All types except `identity`, `shell` and `simple` patterns. Given you supply a second pattern via the except option. Any string that would match the primary pattern but also matches the except pattern will not result in a successful match. Feel free to read that again. Or just take a look at this example: ``` ruby pattern = Mustermann.new('/auth/*', except: '/auth/login') pattern === '/auth/dunno' # => true pattern === '/auth/login' # => false ``` Now, as said above, `except` treats the value as a pattern: ``` ruby pattern = Mustermann.new('/*anything', type: :rails, except: '/*anything.png') pattern === '/foo.jpg' # => true pattern === '/foo.png' # => false ``` ### `greedy` Supported by: All types except `identity` and `shell` patterns. Default value: `true` **Simple** patterns are greedy, meaning that for the pattern `:foo:bar?`, everything will be captured as `foo`, `bar` will always be `nil`. By setting `greedy` to `false`, `foo` will capture as little as possible (which in this case would only be the first letter), leaving the rest to `bar`. **All other** supported patterns are semi-greedy. This means `:foo(.:bar)?` (`:foo(.:bar)` for Rails patterns) will capture everything before the *last* dot as `foo`. For these two pattern types, you can switch into non-greedy mode by setting the `greedy` option to false. In that case `foo` will only capture the part before the *first* dot. Semi-greedy behavior is not specific to dots, it works with all characters or strings. For instance, `:a(foo:b)` will capture everything before the *last* `foo` as `a`, and `:foo(bar)?` will not capture a `bar` at the end. ``` ruby pattern = Mustermann.new(':a.:b', greedy: true) pattern.match('a.b.c.d') # => # pattern = Mustermann.new(':a.:b', greedy: false) pattern.match('a.b.c.d') # => # ``` ### `space_matches_plus` Supported by: All types except `identity`, `regexp` and `shell` patterns. Default value: `true` Most pattern types will by default also match a plus sign for a space in the pattern: ``` ruby Mustermann.new('a b') === 'a+b' # => true ``` You can disable this behavior via `space_matches_plus`: ``` ruby Mustermann.new('a b', space_matches_plus: false) === 'a+b' # => false ``` **Important:** This setting has no effect on captures, captures will always keep plus signs as plus sings and spaces as spaces: ``` ruby pattern = Mustermann.new(':x') pattern.match('a b')[:x] # => 'a b' pattern.match('a+b')[:x] # => 'a+b' ```` ### `uri_decode` Supported by all pattern types. Default value: `true` Usually, characters in the pattern will also match the URI encoded version of these characters: ``` ruby Mustermann.new('a b') === 'a b' # => true Mustermann.new('a b') === 'a%20b' # => true ``` You can avoid this by setting `uri_decode` to `false`: ``` ruby Mustermann.new('a b', uri_decode: false) === 'a b' # => true Mustermann.new('a b', uri_decode: false) === 'a%20b' # => false ``` ### `ignore_unknown_options` Supported by all patterns. Default value: `false` If you pass an option in that is not supported by the specific pattern type, Mustermann will raise an `ArgumentError`. By setting `ignore_unknown_options` to `true`, it will happily ignore the option. ## Performance Mustermann is designed so that as much work as possible happens at object-creation time, keeping matching and expansion fast at request time. Pattern objects should be treated as immutable; their internals are optimized for both speed and low memory usage. Key points: * **Pattern caching:** `Mustermann.new` may return the same instance for identical arguments while that instance is still alive. Do not rely on object identity. * **Single-pattern matching:** AST-based patterns (sinatra, rails, hybrid, template, flask) use bounded character classes, negative look-ahead, and non-greedy splats to avoid unnecessary backtracking in Ruby's Oniguruma engine. Using a pattern as a `Regexp` replacement adds at most one method-dispatch of overhead. * **Routing with `Mustermann::Set`:** Uses a trie (prefix tree) for large tables. Rather than checking every route in sequence, the trie walks the input one character at a time, sharing prefix traversal across all patterns that start with the same characters. Dispatch time grows far more slowly than a linear scan. A `use_trie:` threshold (default 50) controls when the switch happens, and an optional `ObjectSpace::WeakKeyMap` cache avoids re-matching the same string. * **Expansion:** Most computation is shifted to compile time. Memory grows linearly with the number of optional-key combinations in a pattern. See **[docs/performance.md](../docs/performance.md)** for a detailed explanation of each optimization, the linear vs. trie trade-off, caching, thread-safety, and benchmark guidance. ## Details on Pattern Types - [`identity`](../docs/patterns/identity.md) - [`regexp`](../docs/patterns/regexp.md) - [`sinatra`](../docs/patterns/sinatra.md) - [`rails`](../docs/patterns/rails.md) - [`hybrid`](../docs/patterns/hybrid.md) mustermann-4.0.0/mustermann/lib/000077500000000000000000000000001517364653100166565ustar00rootroot00000000000000mustermann-4.0.0/mustermann/lib/mustermann.rb000066400000000000000000000105201517364653100213720ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/pattern' require 'mustermann/composite' require 'mustermann/concat' require 'thread' # Namespace and main entry point for the Mustermann library. # # Under normal circumstances the only external API entry point you should be using is {Mustermann.new}. module Mustermann # Type to use if no type is given. # @api private DEFAULT_TYPE = :sinatra # Creates a new pattern based on input. # # * From {Mustermann::Pattern}: returns given pattern. # * From String: creates a pattern from the string, depending on type option (defaults to {Mustermann::Sinatra}) # * From Regexp: creates a {Mustermann::Regular} pattern. # * From Symbol: creates a {Mustermann::Sinatra} pattern with a single named capture named after the input. # * From an Array or multiple inputs: creates a new pattern from each element, combines them to a {Mustermann::Composite}. # * From anything else: Will try to call to_pattern on it or raise a TypeError. # # Note that if the input is a {Mustermann::Pattern}, Regexp or Symbol, the type option is ignored and if to_pattern is # called on the object, the type will be handed on but might be ignored by the input object. # # If you want to enforce the pattern type, you should create them via their expected class. # # @example creating patterns # require 'mustermann' # # Mustermann.new("/:name") # => # # Mustermann.new("/{name}", type: :template) # => # # Mustermann.new(/.*/) # => # # Mustermann.new(:name, capture: :word) # => # # Mustermann.new("/", "/*.jpg", type: :shell) # => # # # @example using custom #to_pattern # require 'mustermann' # # class MyObject # def to_pattern(**options) # Mustermann.new("/:name", **options) # end # end # # Mustermann.new(MyObject.new, type: :rails) # => # # # @example enforcing type # require 'mustermann/sinatra' # # Mustermann::Sinatra.new("/:name") # # @param [String, Pattern, Regexp, Symbol, #to_pattern, Array] # input The representation of the pattern # @param [Hash] options The options hash # @return [Mustermann::Pattern] pattern corresponding to string. # @raise (see []) # @raise (see Mustermann::Pattern.new) # @raise [TypeError] if the passed object cannot be converted to a pattern # @see file:README.md#Types_and_Options "Types and Options" in the README def self.new(*input, type: DEFAULT_TYPE, operator: :|, **options) type ||= DEFAULT_TYPE input = input.first if input.size < 2 case input when Pattern then input when Regexp then self[:regexp].new(input, **options) when String then self[type].new(input, **options) when Symbol then self[:sinatra].new(input.inspect, **options) when Array then input.map { |i| new(i, type: type, **options) }.inject(operator) else pattern = input.to_pattern(type: type, **options) if input.respond_to? :to_pattern raise TypeError, "#{input.class} can't be coerced into Mustermann::Pattern" if pattern.nil? pattern end end @mutex = Mutex.new @types = {} # Maps a type to its factory. # # @example # Mustermann[:sinatra] # => Mustermann::Sinatra # # @param [Symbol] name a pattern type identifier # @raise [ArgumentError] if the type is not supported # @return [Class, #new] pattern factory def self.[](name) return name if name.respond_to? :new @types.fetch(normalized = normalized_type(name)) do @mutex.synchronize do error = try_require "mustermann/#{normalized}" @types.fetch(normalized) { raise ArgumentError, "unsupported type %p#{" (#{error.message})" if error}" % name } end end end # @return [LoadError, nil] # @!visibility private def self.try_require(path) require(path) nil rescue LoadError => error raise(error) unless error.path == path error end # @!visibility private def self.register(name, type) @types[normalized_type(name)] = type end # @!visibility private def self.normalized_type(type) type.to_s.gsub('-', '_').downcase end end mustermann-4.0.0/mustermann/lib/mustermann/000077500000000000000000000000001517364653100210475ustar00rootroot00000000000000mustermann-4.0.0/mustermann/lib/mustermann/ast/000077500000000000000000000000001517364653100216365ustar00rootroot00000000000000mustermann-4.0.0/mustermann/lib/mustermann/ast/boundaries.rb000066400000000000000000000026241517364653100243220ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Make sure #start and #stop is set on every node and within its parents #start and #stop. # @!visibility private class Boundaries < Translator # @return [Mustermann::AST::Node] the ast passed as first argument # @!visibility private def self.set_boundaries(ast, string: nil, start: 0, stop: string.length) new.translate(ast, start, stop) ast end translate(:node) do |start, stop| t.set_boundaries(node, start, stop) t(payload, node.start, node.stop) end translate(:with_look_ahead) do |start, stop| t.set_boundaries(node, start, stop) t(head, node.start, node.stop) t(payload, node.start, node.stop) end translate(Array) do |start, stop| each do |subnode| t(subnode, start, stop) start = subnode.stop end end translate(Object) { |*| node } # Checks that a node is within the given boundaries. # @!visibility private def set_boundaries(node, start, stop) node.start = start if node.start.nil? or node.start < start node.stop = node.start + node.min_size if node.stop.nil? or node.stop < node.start node.stop = stop if node.stop > stop end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/compiler.rb000066400000000000000000000243771517364653100240120ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' require 'mustermann/ast/converters' module Mustermann # @see Mustermann::AST::Pattern module AST # Regexp compilation logic. # @!visibility private class Compiler < Translator raises CompileError # Compile an array of AST nodes, detecting which captures can safely use # atomic groups. A capture is safe to atomicize when its very next sibling # is a *path separator* (payload '/'), because every Mustermann capture # character class (Sinatra's [^\/\?#]+, Template's [\w\-\.~%]+, etc.) # excludes '/', so the greedy match naturally stops before '/' and # committing to it atomically never affects correctness. # # More permissive conditions (e.g. end-of-array) are intentionally avoided: # template expressions nest captures inside inner arrays where end-of-array # does NOT mean end-of-pattern, and non-'/' separators (e.g. '.' in # {.a,b,c}) may appear inside the capture character class. # # Splats (.*?) and non-greedy captures are excluded — atomicizing them # would commit to zero or one character and break match resolution. # Strip `atomic:` from incoming options so a parent's value cannot bleed # into siblings; each element's atomicity comes solely from its own context. translate(Array) do |atomic: false, **options| greedy = options.fetch(:greedy, true) each_with_index.map do |element, index| next_sibling = self[index + 1] atomic = greedy && element.is_a?(:capture) && !element.is_a?(:splat) && next_sibling&.is_a?(:separator) && next_sibling.payload == '/' t(element, **options, atomic: atomic) end.join end translate(:node) { |**o| t(payload, **o) } translate(:separator) { |**o| Regexp.escape(payload) } translate(:optional) { |**o| '(?:%s)?' % t(payload, **o) } translate(:char) { |**o| t.encoded(payload, **o) } translate :union do |**options| '(?:%s)' % payload.map { |e| '(?:%s)' % t(e, **options) }.join('|') end translate :expression do |greedy: true, **options| t(payload, allow_reserved: operator.allow_reserved, greedy: greedy && !operator.allow_reserved, parametric: operator.parametric, separator: operator.separator, **options) end translate :with_look_ahead do |atomic: false, **options| greedy = options.fetch(:greedy, true) lookahead = each_leaf.inject('') do |ahead, element| ahead + t(element, skip_optional: true, lookahead: ahead, greedy: false, no_captures: true, **options).to_s end lookahead << (at_end ? '$' : '/') # The look-ahead already constrains what the head capture can match, so # it is safe to make it atomic when greedy. Non-greedy captures rely on # backtracking to extend their match and must not be committed atomically. t(head, **options, lookahead: lookahead, atomic: greedy) + t(payload, **options) end # Capture compilation is complex. :( # @!visibility private class Capture < NodeTranslator register :capture # @!visibility private # When +atomic: true+ is passed (set by the Array translator for captures # that are followed only by a separator or end-of-pattern), the compiled # content is wrapped in an atomic group (?>…). This prevents # Oniguruma from backtracking into characters the capture has already # consumed, giving a measurable speedup on failing matches without # changing the result for any valid input. def translate(atomic: false, **options) return pattern(**options) if options[:no_captures] inner = translate(no_captures: true, **options) # Atomic groups are only safe for pure character-class repetitions. # Captures with an explicit array/hash/string option or a custom # constraint produce alternations that need backtracking to resolve # the correct alternative, so they must not be wrapped atomically. apply_atomic = atomic && options[:capture].nil? && constraint.nil? content = apply_atomic ? "(?>#{inner})" : inner "(?<#{name}>#{content})" end # @return [String] regexp without the named capture # @!visibility private def pattern(capture: nil, **options) case capture when Symbol then from_symbol(capture, **options) when Array then from_array(capture, **options) when Hash then from_hash(capture, **options) when String then from_string(capture, **options) when nil then from_nil(**options) when Regexp then capture when Class then from_class(capture, **options) else raise CompileError, "invalid capture constraint %p for %p" % [capture, name] end end private def qualified(string, greedy: true, **options) "#{string}#{qualifier || "+#{'?' unless greedy}"}" end def with_lookahead(string, lookahead: nil, **options) lookahead ? "(?:(?!#{lookahead})#{string})" : string end def from_hash(hash, **options) pattern(capture: hash[name.to_sym], **options) end def from_array(array, **options) Regexp.union(*array.map { |e| pattern(capture: e, **options) }) end def from_symbol(symbol, **options) capture, _ = CONVERTERS[symbol] return pattern(capture:, **options) if capture qualified(with_lookahead("[[:#{symbol}:]]", **options), **options) end def from_string(string, **options) Regexp.new(string.chars.map { |c| t.encoded(c, **options) }.join) end def from_nil(**options) qualified(with_lookahead(default(**options), **options), **options) end def from_class(klass, **options) capture, _ = CONVERTERS[klass.name] raise CompileError, "no converter for class %p" % klass unless capture pattern(capture:, **options) end def default(**options) = constraint || '[^/\\?#]' end # @!visibility private class Splat < Capture register :splat, :named_splat # splats are always non-greedy # @!visibility private def pattern(**options) constraint || '.*?' end end # @!visibility private class Variable < Capture register :variable # @!visibility private def translate(atomic: false, **options) # Exploded variables expand to `pattern(?:sep pattern)*`. The engine # must be able to backtrack through that repetition when a following # capture (e.g. the 'b' in {/a*,b}) needs to claim the last segment. # Strip `atomic:` so Capture#translate never wraps the repetition. effective_atomic = atomic && !explode return super(atomic: effective_atomic, **options) if explode or !options[:parametric] # Remove this line after fixing broken compatibility between 2.1 and 2.2 options.delete(:parametric) if options.has_key?(:parametric) parametric super(atomic: effective_atomic, parametric: false, **options) end # @!visibility private def pattern(parametric: false, separator: nil, **options) register_param(parametric: parametric, separator: separator, **options) pattern = super(**options) pattern = parametric(pattern) if parametric pattern = "#{pattern}(?:#{Regexp.escape(separator)}#{pattern})*" if explode and separator pattern end # @!visibility private def parametric(string) "#{Regexp.escape(name)}(?:=#{string})?" end # @!visibility private def qualified(string, **options) prefix ? "#{string}{1,#{prefix}}" : super(string, **options) end # @!visibility private def default(allow_reserved: false, **options) allow_reserved ? '[\w\-\.~%\:/\?#\[\]@\!\$\&\'\(\)\*\+,;=]' : '[\w\-\.~%]' end # @!visibility private def register_param(parametric: false, split_params: nil, separator: nil, **options) return unless explode and split_params split_params[name] = { separator: separator, parametric: parametric } end end # @return [Array] all raw string representations of the character (literal + URI-encoded variants) # @!visibility private def self.char_representations(char, uri_decode: true, space_matches_plus: true) if char == ' ' and space_matches_plus @space_and_plus ||= char_representations(' ', space_matches_plus: false) + char_representations('+', space_matches_plus: false) else @char_representations ||= {} @char_representations[char] ||= begin escaped = URI_PARSER.escape(char, /./) [char, escaped.upcase, escaped.downcase].uniq end end end # @return [String] Regular expression for matching the given character in all representations # @!visibility private def encoded(char, uri_decode: true, space_matches_plus: true, **options) return Regexp.escape(char) unless uri_decode '(?:%s)' % self.class.char_representations(char, uri_decode:, space_matches_plus:).map { |c| Regexp.escape(c) }.join('|') end # Compiles an AST to a regular expression. # @param [Mustermann::AST::Node] ast the tree # @return [Regexp] corresponding regular expression. # # @!visibility private def self.compile(ast, **options) new.compile(ast, **options) end # Compiles an AST to a regular expression. # @param [Mustermann::AST::Node] ast the tree # @return [Regexp] corresponding regular expression. # # @!visibility private def compile(ast, except: nil, **options) except &&= "(?!#{translate(except, no_captures: true, **options)}\\Z)" Regexp.new("#{except}#{translate(ast, **options)}") end end private_constant :Compiler end end mustermann-4.0.0/mustermann/lib/mustermann/ast/converters.rb000066400000000000000000000020761517364653100243620ustar00rootroot00000000000000# frozen_string_literal: true require "rubygems/version" require "date" module Mustermann module AST CONVERTERS = { "Integer" => [ /-?\d+/, :to_i ], "Symbol" => [ /\w+/, :to_sym ], "String" => [ nil, :to_s ], "Float" => [ /-?\d+(?:\.\d+)?/, :to_f ], "Date" => [ /\d{4}-\d{2}-\d{2}/, ->(string) { Date.parse(string) } ], "Gem::Version" => [ Regexp.new(Gem::Version::VERSION_PATTERN), ->(string) { Gem::Version.new(string) } ], locale: [ /(?:[A-Za-z]{2,3}|i)(-[A-Za-z0-9]{1,8})*/ ], slug: [ /[a-z0-9]+(?:-[a-z0-9]+)*/ ], uuid: [ /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i ], } CONVERTERS.merge!({ integer: CONVERTERS["Integer"], symbol: CONVERTERS["Symbol"], string: CONVERTERS["String"], float: CONVERTERS["Float"], date: CONVERTERS["Date"], version: CONVERTERS["Gem::Version"], }) CONVERTERS.freeze private_constant :CONVERTERS end end mustermann-4.0.0/mustermann/lib/mustermann/ast/expander.rb000066400000000000000000000104731517364653100237760ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' require 'mustermann/ast/compiler' module Mustermann module AST # Looks at an AST, remembers the important bits of information to do an # ultra fast expansion. # # @!visibility private class Expander < Translator raises ExpandError translate Array do |*args, **options| inject(t.pattern) do |pattern, element| t.add_to(pattern, t(element, *args, **options)) end end translate :capture do |**options| t.for_capture(node, **options) end translate :named_splat, :splat do t.pattern + t.for_capture(node) end translate :expression do t(payload, allow_reserved: operator.allow_reserved) end translate :root, :group do t(payload) end translate :char do t.pattern(t.escape(payload, also_escape: /[\/\?#\&\=%]/).gsub(?%, "%%")) end translate :separator do t.pattern(payload.gsub(?%, "%%")) end translate :with_look_ahead do t.add_to(t(head), t(payload)) end translate :optional do nested = t(payload) nested += t.pattern unless nested.any? { |n| n.first.empty? } nested end translate :union do payload.map { |e| t(e) }.inject(:+) end # helper method for captures # @!visibility private def for_capture(node, **options) name = node.name.to_sym pattern('%s', name, name => /(?!#{pattern_for(node, **options)})./) end # maps sorted key list to sprintf patterns and filters # @!visibility private def mappings @mappings ||= {} end # all the known keys # @!visibility private def keys @keys ||= [] end # add a tree for expansion # @!visibility private def add(ast) translate(ast).each do |keys, pattern, filter| self.keys.concat(keys).uniq! mappings[keys.sort] ||= [keys, pattern, filter] end end # helper method for getting a capture's pattern. # @!visibility private def pattern_for(node, **options) Compiler.new.decorator_for(node).pattern(**options) end # @see Mustermann::Pattern#expand # @!visibility private def expand(values) adjusted = values.each_with_object({}){ |(key, value), new_hash| new_hash[value.instance_of?(Array) ? [key] * value.length : key] = value } keys, pattern, filters = mappings.fetch(adjusted.keys.flatten.sort) { error_for(values) } filters.each { |key, filter| adjusted[key] &&= escape(adjusted[key], also_escape: filter) } pattern % (adjusted[keys] || adjusted.values_at(*keys)) end # @see Mustermann::Pattern#expandable? # @!visibility private def expandable?(values) values = values.keys if values.respond_to? :keys values = values.sort if values.respond_to? :sort mappings.include? values end # @see Mustermann::Expander#with_rest # @!visibility private def expandable_keys(keys) mappings.keys.select { |k| (k - keys).empty? }.max_by(&:size) || keys end # helper method for raising an error for unexpandable values # @!visibility private def error_for(values) expansions = mappings.keys.map(&:inspect).join(" or ") raise error_class, "cannot expand with keys %p, possible expansions: %s" % [values.keys.sort, expansions] end # @see Mustermann::AST::Translator#expand # @!visibility private def escape(string, *args, **options) return super unless string.respond_to?(:=~) # URI::Parser is pretty slow, let's not send every string to it, even if it's unnecessary string =~ /\A\w*\Z/ ? string : super end # Turns a sprintf pattern into our secret internal data structure. # @!visibility private def pattern(string = "", *keys, **filters) [[keys, string, filters]] end # Creates the product of two of our secret internal data structures. # @!visibility private def add_to(list, result) list << [[], ""] if list.empty? list.inject([]) { |l, (k1, p1, f1)| l + result.map { |k2, p2, f2| [k1+k2, p1+p2, f1.merge(f2)] } } end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/fast_pattern.rb000066400000000000000000000076141517364653100246650ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann module AST # Mixin for AST::Pattern subclasses that accelerates compilation and AST # construction for "simple" patterns: only static path segments and # unconstrained full-segment captures (e.g. /foo/:bar/baz/:id). # Patterns with optional groups, constraints, or non-default options fall # through to the full AST pipeline. module FastPattern # Matches patterns that consist only of slashes, static segments, and # simple :name captures — no optional groups, no constraints. SIMPLE = /\A(?:\/(?:[a-zA-Z0-9\-_.~]+|:[a-zA-Z_]\w*))+\z/ # Regexp fragment for each printable ASCII char, matching the same output # as Compiler#encoded with uri_decode: true. ENCODED = (0..127).each_with_object({}) do |byte, h| c = byte.chr pct = '%%%02X' % byte reps = [c, pct, pct.downcase].uniq h[c] = reps.size == 1 ? Regexp.escape(reps.first) : '(?:%s)' % reps.map { |r| Regexp.escape(r) }.join('|') end.freeze SEGMENT_SCAN = %r{(/)|(:[a-zA-Z_]\w*)|([^/:]+)} private_constant :SIMPLE, :ENCODED, :SEGMENT_SCAN # Public override: fast path for simple patterns, falls through to super otherwise. # Must remain public to match AST::Pattern#to_ast visibility. def to_ast return super unless simple_pattern? ast = self.class.ast_cache.fetch(@string) { build_fast_ast } @param_converters ||= {} ast end def params(string = nil) return super unless @fast_match return unless md = @regexp.match(string) result = md.named_captures result.transform_values! { |v| v.include?('%') ? unescape(v) : v } if string.include?('%') result end private def build_match(regexp, string) return super unless @fast_match return unless match = regexp.match(string) params = match.named_captures params.transform_values! { |v| v.include?('%') ? unescape(v) : v } if string.include?('%') Match.new(self, match, params: params) end def simple_pattern? options[:capture].nil? && options[:except].nil? && options.fetch(:greedy, true) != false && uri_decode && @string.match?(SIMPLE) end def compile(**options) return super unless simple_pattern? result = fast_compile @fast_match = true result end def fast_compile tokens = @string.scan(SEGMENT_SCAN) src = String.new tokens.each_with_index do |(sep, cap, chars), i| if sep src << '\\/' elsif cap # Mirror the compiler: wrap in atomic group when the next token is a separator. if tokens[i + 1]&.first src << "(?<#{cap[1..]}>(?>[^/\\?#]+))" else src << "(?<#{cap[1..]}>[^/\\?#]+)" end else chars.each_char { |c| src << ENCODED[c] } end end Regexp.new(src) end def build_fast_ast nodes = [] pos = 0 @string.scan(SEGMENT_SCAN) do |sep, cap, chars| if sep node = Node::Separator.new('/') node.start, node.stop = pos, pos + 1 nodes << node pos += 1 elsif cap node = Node::Capture.new(cap[1..]) node.start, node.stop = pos, pos + cap.length nodes << node pos += cap.length else chars.each_char do |c| node = Node::Char.new(c) node.start, node.stop = pos, pos + 1 nodes << node pos += 1 end end end root = Node::Root.new root.payload = nodes root.pattern = @string root.start, root.stop = 0, @string.length root end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/node.rb000066400000000000000000000130511517364653100231100ustar00rootroot00000000000000module Mustermann # @see Mustermann::AST::Pattern module AST # @!visibility private class Node # @!visibility private attr_accessor :payload, :start, :stop # @!visibility private # @param [Symbol] name of the node # @return [Class] factory for the node def self.[](name) @names ||= {} @names[name] ||= begin const_name = constant_name(name) Object.const_get(const_name) if Object.const_defined?(const_name) end end # Turns a class name into a node identifier. # @!visibility private def self.type name[/[^:]+$/].split(/(?<=.)(?=[A-Z])/).map(&:downcase).join(?_).to_sym end # @!visibility private # @param [Symbol] name of the node # @return [String] qualified name of factory for the node def self.constant_name(name) return self.name if name.to_sym == :node name = name.to_s.split(?_).map(&:capitalize).join "#{self.name}::#{name}" end # Helper for creating a new instance and calling #parse on it. # @return [Mustermann::AST::Node] # @!visibility private def self.parse(payload = nil, **options, &block) new(payload, **options).tap { |n| n.parse(&block) } end # @!visibility private def initialize(payload = nil, **options) options.each { |key, value| public_send("#{key}=", value) } self.payload = payload end # @!visibility private def is_a?(type) type = Node[type] if type.is_a? Symbol super(type) end # Double dispatch helper for reading from the buffer into the payload. # @!visibility private def parse self.payload ||= [] while element = yield payload << element end end # Loop through all nodes that don't have child nodes. # @!visibility private def each_leaf(&block) return enum_for(__method__) unless block_given? called = false Array(payload).each do |entry| next unless entry.respond_to? :each_leaf entry.each_leaf(&block) called = true end yield(self) unless called end # @return [Integer] length of the substring # @!visibility private def length stop - start if start and stop end # @return [Integer] minimum size for a node # @!visibility private def min_size 0 end # Turns a class name into a node identifier. # @!visibility private def type self.class.type end # @!visibility private class Capture < Node # @see Mustermann::AST::Compiler::Capture#default # @!visibility private attr_accessor :constraint # @see Mustermann::AST::Compiler::Capture#qualified # @!visibility private attr_accessor :qualifier # @see Mustermann::AST::Pattern#map_param # @!visibility private attr_accessor :convert # @see Mustermann::AST::Node#parse # @!visibility private def parse self.payload ||= String.new super end # @!visibility private alias_method :name, :payload end # @!visibility private class Char < Node # @return [Integer] minimum size for a node # @!visibility private def min_size 1 end end # AST node for template expressions. # @!visibility private class Expression < Node # @!visibility private attr_accessor :operator end # @!visibility private class Composition < Node # @!visibility private def initialize(payload = nil, **options) super(Array(payload), **options) end end # @!visibility private class Group < Composition end # @!visibility private class Union < Composition end # @!visibility private class Optional < Node end # @!visibility private class Or < Node end # @!visibility private class Root < Node # @!visibility private attr_accessor :pattern # Will trigger transform. # # @see Mustermann::AST::Node.parse # @!visibility private def self.parse(string, &block) root = new root.pattern = string root.parse(&block) root end end # @!visibility private class Separator < Node # @return [Integer] minimum size for a node # @!visibility private def min_size 1 end end # @!visibility private class Splat < Capture # @see Mustermann::AST::Node::Capture#name # @!visibility private def name "splat" end end # @!visibility private class NamedSplat < Splat # @see Mustermann::AST::Node::Capture#name # @!visibility private alias_method :name, :payload end # AST node for template variables. # @!visibility private class Variable < Capture # @!visibility private attr_accessor :prefix, :explode end # @!visibility private class WithLookAhead < Node # @!visibility private attr_accessor :head, :at_end # @!visibility private def initialize(payload, at_end, **options) super(**options) self.head, *self.payload = Array(payload) self.at_end = at_end end end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/param_scanner.rb000066400000000000000000000031771517364653100250040ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/converters' require 'mustermann/ast/translator' module Mustermann module AST # Scans an AST for param converters. # @!visibility private # @see Mustermann::AST::Pattern#to_templates class ParamScanner < Translator # @!visibility private def self.scan_params(ast, options) new.translate(ast, options) end translate(:node) { |o| t(payload, o) } translate(:with_look_ahead) { |o| t(head, o).merge(t(payload, o)) } translate(Array) { |o| map { |e| t(e, o) }.inject(:merge) } translate(Object) { |o| {} } class Capture < NodeTranslator register :capture def translate(options) return { name => convert } if convert _, converter = converter(options[:capture]) converter ? { name => converter } : {} end def converter(capture) case capture when Hash then return converter(capture[name.to_sym]) when Class then regexp, converter = CONVERTERS[capture.name] when Symbol then regexp, converter = CONVERTERS[capture] when Array entries = capture.map { |item| converter(item) }.compact regexp = Regexp.union(entries.map(&:first)) entries.map! { |r, c| [/\A#{r}\Z/, c] } converter = ->(string) do _, c = entries.find { |r, _| r.match?(string) } c&.call(string) || string end end return unless converter [regexp, converter.to_proc] end end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/parser.rb000066400000000000000000000174111517364653100234630ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/node' require 'forwardable' require 'strscan' module Mustermann # @see Mustermann::AST::Pattern module AST # Simple, StringScanner based parser. # @!visibility private class Parser # @param [String] string to be parsed # @return [Mustermann::AST::Node] parse tree for string # @!visibility private def self.parse(string, **options) new(**options).parse(string) end # Defines another grammar rule for first character. # # @see Mustermann::Rails # @see Mustermann::Sinatra # @see Mustermann::Template # @!visibility private def self.on(*chars, &block) chars.each do |char| define_method("read %p" % char, &block) end end # Defines another grammar rule for a suffix. # # @see Mustermann::Sinatra # @!visibility private def self.suffix(pattern = /./, after: :node, &block) @suffix ||= [] @suffix << [pattern, after, block] if block @suffix end # @!visibility private attr_reader :buffer, :string, :pattern extend Forwardable def_delegators :buffer, :eos?, :getch, :pos # @!visibility private def initialize(pattern: nil, **options) @pattern = pattern end # @param [String] string to be parsed # @return [Mustermann::AST::Node] parse tree for string # @!visibility private def parse(string) @string = string @buffer = ::StringScanner.new(string) node(:root, string) { read unless eos? } end # @example # node(:char, 'x').compile =~ 'x' # => true # # @param [Symbol] type node type # @return [Mustermann::AST::Node] # @!visibility private def node(type, *args, **options, &block) type = Node[type] unless type.respond_to? :new start = pos node = block ? type.parse(*args, **options, &block) : type.new(*args, **options) min_size(start, pos, node) end # Create a node for a character we don't have an explicit rule for. # # @param [String] char the character # @return [Mustermann::AST::Node] the node # @!visibility private def default_node(char) char == ?/ ? node(:separator, char) : node(:char, char) end # Reads the next element from the buffer. # @return [Mustermann::AST::Node] next element # @!visibility private def read start = pos char = getch method = "read %p" % char element= respond_to?(method) ? send(method, char) : default_node(char) min_size(start, pos, element) read_suffix(element) end # sets start on node to start if it's not set to a lower value. # sets stop on node to stop if it's not set to a higher value. # @return [Mustermann::AST::Node] the node passed as third argument # @!visibility private def min_size(start, stop, node) stop ||= start start ||= stop node.start = start unless node.start and node.start < start node.stop = stop unless node.stop and node.stop > stop node end # Checks for a potential suffix on the buffer. # @param [Mustermann::AST::Node] element node without suffix # @return [Mustermann::AST::Node] node with suffix # @!visibility private def read_suffix(element) self.class.suffix.inject(element) do |ele, (regexp, after, callback)| next ele unless ele.is_a?(after) and payload = scan(regexp) content = instance_exec(payload, ele, &callback) min_size(element.start, pos, content) end end # Wrapper around {StringScanner#scan} that turns strings into escaped # regular expressions and returns a MatchData if the regexp has any # named captures. # # @param [Regexp, String] regexp # @see StringScanner#scan # @return [String, MatchData, nil] # @!visibility private def scan(regexp) regexp = Regexp.new(Regexp.escape(regexp)) unless regexp.is_a? Regexp string = buffer.scan(regexp) regexp.names.any? ? regexp.match(string) : string end # Asserts a regular expression matches what's next on the buffer. # Will return corresponding MatchData if regexp includes named captures. # # @param [Regexp] regexp expected to match # @return [String, MatchData] the match # @raise [Mustermann::ParseError] if expectation wasn't met # @!visibility private def expect(regexp, char: nil, **options) scan(regexp) || unexpected(char, **options) end # Allows to read a string inside brackets. It does not expect the string # to start with an opening bracket. # # @example # buffer.string = "fo>ba" # read_brackets(?<, ?>) # => "fo" # buffer.rest # => "ba" # # @!visibility private def read_brackets(open, close, char: nil, escape: ?\\, quote: false, **options) result = String.new escape = false if escape.nil? while (current = getch) case current when close then return result when open then result << open << read_brackets(open, close) << close when escape then result << escape << getch else result << current end end unexpected(char, **options) end # Reads an argument string of the format arg1,args2,key:value # # @!visibility private def read_args(key_separator, close, separator: ?,, symbol_keys: true, **options) list, map = [], {} while buffer.peek(1) != close scan(separator) entries = read_list(close, separator, separator: key_separator, **options) case entries.size when 1 then list += entries when 2 then map[symbol_keys ? entries.first.to_sym : entries.first] = entries.last else unexpected(key_separator) end buffer.pos -= 1 end expect(close) [list, map] end # Reads a separated list with the ability to quote, escape and add spaces. # # @!visibility private def read_list(*close, separator: ?,, escape: ?\\, quotes: [?", ?'], ignore: " ", **options) result = [] while current = getch element = result.empty? ? result : result.last case current when *close then return result when ignore then nil # do nothing when separator then result << String.new when escape then element << getch when *quotes then element << read_escaped(current, escape: escape) else element << current end end unexpected(current, **options) end # Read a string until a terminating character, ignoring escaped versions of said character. # # @!visibility private def read_escaped(close, escape: ?\\, **options) result = String.new while current = getch case current when close then return result when escape then result << getch else result << current end end unexpected(current, **options) end # Helper for raising an exception for an unexpected character. # Will read character from buffer if buffer is passed in. # # @param [String, nil] char the unexpected character # @raise [Mustermann::ParseError, Exception] # @!visibility private def unexpected(char = nil, exception: ParseError) char ||= getch char = "space" if char == " " raise exception, "unexpected #{char || "end of string"} while parsing #{string.inspect}" end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/pattern.rb000066400000000000000000000122021517364653100236350ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/parser' require 'mustermann/ast/boundaries' require 'mustermann/ast/compiler' require 'mustermann/ast/transformer' require 'mustermann/ast/validation' require 'mustermann/ast/template_generator' require 'mustermann/ast/param_scanner' require 'mustermann/regexp_based' require 'mustermann/expander' require 'mustermann/equality_map' module Mustermann # @see Mustermann::AST::Pattern module AST # Superclass for pattern styles that parse an AST from the string pattern. # @abstract class Pattern < Mustermann::RegexpBased supported_options :capture, :except, :greedy, :space_matches_plus extend Forwardable, SingleForwardable single_delegate on: :parser, suffix: :parser instance_delegate %i[parser compiler transformer validation template_generator param_scanner boundaries] => 'self.class' instance_delegate parse: :parser, transform: :transformer, validate: :validation, generate_templates: :template_generator, scan_params: :param_scanner, set_boundaries: :boundaries # @api private def self.ast_cache @ast_cache ||= EqualityMap.new end # @api private # @return [#parse] parser object for pattern # @!visibility private def self.parser return Parser if self == AST::Pattern const_set :Parser, Class.new(superclass.parser) unless const_defined? :Parser, false const_get :Parser end # @api private # @return [#compile] compiler object for pattern # @!visibility private def self.compiler Compiler end # @api private # @return [#set_boundaries] translator making sure start and stop is set on all nodes # @!visibility private def self.boundaries Boundaries end # @api private # @return [#transform] transformer object for pattern # @!visibility private def self.transformer Transformer end # @api private # @return [#validate] validation object for pattern # @!visibility private def self.validation Validation end # @api private # @return [#generate_templates] generates URI templates for pattern # @!visibility private def self.template_generator TemplateGenerator end # @api private # @return [#scan_params] param scanner for pattern # @!visibility private def self.param_scanner ParamScanner end # @!visibility private def compile(**options) options[:except] &&= parse options[:except] compiler.compile(to_ast, **options) rescue CompileError => error raise error.class, "#{error.message}: #{@string.inspect}", error.backtrace end # Returns a regexp that matches strings excluded by the +except+ option, # or +nil+ if no +except+ constraint was given. Used by the trie matcher # to filter out excluded strings at leaf nodes. # @return [Regexp, nil] # @!visibility private def except_regexp return unless except_str = options[:except] @except_regexp ||= Regexp.new("\\A#{compiler.new.translate(parse(except_str), no_captures: true, **options.except(:except))}\\z") end # Internal AST representation of pattern. # @!visibility private def to_ast ast = self.class.ast_cache.fetch([@string, options]) do ast = parse(@string, pattern: self) ast &&= transform(ast) ast &&= set_boundaries(ast, string: @string) validate(ast) end @param_converters ||= scan_params(ast, options) if ast ast end # All AST-based pattern implementations support expanding. # # @example (see Mustermann::Pattern#expand) # @param (see Mustermann::Pattern#expand) # @return (see Mustermann::Pattern#expand) # @raise (see Mustermann::Pattern#expand) # @see Mustermann::Pattern#expand # @see Mustermann::Expander def expand(behavior = nil, values = {}) @expander ||= Mustermann::Expander.new(self) @expander.expand(behavior, values) end # All AST-based pattern implementations support generating templates. # # @example (see Mustermann::Pattern#to_templates) # @param (see Mustermann::Pattern#to_templates) # @return (see Mustermann::Pattern#to_templates) # @see Mustermann::Pattern#to_templates def to_templates @to_templates ||= generate_templates(to_ast) end # @!visibility private # @see Mustermann::Pattern#map_param def map_param(key, value) return super unless param_converters.include? key converted = super converted.nil? ? converted : param_converters[key][converted] end # @!visibility private def param_converters @param_converters ||= scan_params(to_ast, options) end # @api private def identity_params?(params) param_converters.empty? && super end private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/template_generator.rb000066400000000000000000000021611517364653100260440ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Turns an AST into an Array of URI templates representing the AST. # @!visibility private # @see Mustermann::AST::Pattern#to_templates class TemplateGenerator < Translator # @!visibility private def self.generate_templates(ast) new.translate(ast).uniq end # translate(:expression) is not needed, since template patterns simply call to_s translate(:root, :group) { t(payload) || [""] } translate(:separator, :char) { t.escape(payload) } translate(:capture) { "{#{name}}" } translate(:optional) { [t(payload), ""] } translate(:named_splat, :splat) { "{+#{name}}" } translate(:with_look_ahead) { t([head, payload]) } translate(:union) { payload.flat_map { |e| t(e) } } translate(Array) do map { |e| Array(t(e)) }.inject { |first, second| first.product(second).map(&:join) } end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/transformer.rb000066400000000000000000000143131517364653100245270ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Takes a tree, turns it into an even better tree. # @!visibility private class Transformer < Translator # Transforms a tree. # @note might mutate handed in tree instead of creating a new one # @param [Mustermann::AST::Node] tree to be transformed # @return [Mustermann::AST::Node] transformed tree # @!visibility private def self.transform(tree) new.translate(tree) end # recursive descent translate(:node) do node.payload = t(payload) node end # eliminate redundant optional nodes - this helps avoid regexp warnings translate(:optional) do return t(payload) if payload.is_a? Node[:optional] node.payload = t(payload) node end # ignore unknown objects on the tree translate(Object) { node } # turn a group containing or nodes into a union # @!visibility private class GroupTransformer < NodeTranslator register :group # @!visibility private def translate payload.flatten! if payload.is_a?(Array) return union if payload.any? { |e| e.is_a? :or } self.payload = t(payload) self end # @!visibility private def union groups = split_payload.map { |g| group(g) } Node[:union].new(groups, start: node.start, stop: node.stop) end # @!visibility private def group(elements) return t(elements.first) if elements.size == 1 start, stop = elements.first.start, elements.last.stop if elements.any? Node[:group].new(t(elements), start: start, stop: stop) end # @!visibility private def split_payload groups = [[]] payload.each { |e| e.is_a?(:or) ? groups << [] : groups.last << e } groups.map! end end # inject a union node right inside the root node if it contains or nodes # @!visibility private class RootTransformer < GroupTransformer register :root # @!visibility private def union self.payload = [super] self end end # URI expression transformations depending on operator # @!visibility private class ExpressionTransform < NodeTranslator register :expression # @!visibility private Operator ||= Struct.new(:separator, :allow_reserved, :prefix, :parametric) # Operators available for expressions. # @!visibility private OPERATORS ||= { nil => Operator.new(?,, false, false, false), ?+ => Operator.new(?,, true, false, false), ?# => Operator.new(?,, true, ?#, false), ?. => Operator.new(?., false, ?., false), ?/ => Operator.new(?/, false, ?/, false), ?; => Operator.new(?;, false, ?;, true), ?? => Operator.new(?&, false, ??, true), ?& => Operator.new(?&, false, ?&, true) } # Sets operator and inserts separators in between variables. # @!visibility private def translate self.operator = OPERATORS.fetch(operator) { raise CompileError, "#{operator} operator not supported" } separator = Node[:separator].new(operator.separator) prefix = Node[:separator].new(operator.prefix) self.payload = Array(payload.inject { |list, element| Array(list) << t(separator.dup) << t(element) }) payload.unshift(prefix) if operator.prefix self end end # Inserts with_look_ahead nodes wherever appropriate # @!visibility private class ArrayTransform < NodeTranslator register Array # the new array # @!visibility private def payload @payload ||= [] end # buffer for potential look ahead # @!visibility private def lookahead_buffer @lookahead_buffer ||= [] end # transform the array # @!visibility private def translate each { |e| track t(e) } payload.concat create_lookahead(lookahead_buffer, true) end # handle a single element from the array # @!visibility private def track(element) return list_for(element) << element if lookahead_buffer.empty? return lookahead_buffer << element if lookahead? element lookahead = lookahead_buffer.dup lookahead = create_lookahead(lookahead, false) if element.is_a? Node[:separator] lookahead_buffer.clear payload.concat(lookahead) << element end # turn look ahead buffer into look ahead node # @!visibility private def create_lookahead(elements, *args) return elements unless elements.size > 1 [Node[:with_look_ahead].new(elements, *args, start: elements.first.start, stop: elements.last.stop)] end # can the given element be used in a look-ahead? # @!visibility private def lookahead?(element, in_lookahead = false) case element when Node[:char] then in_lookahead when Node[:group] then lookahead_payload?(element.payload, in_lookahead) when Node[:optional] then lookahead?(element.payload, true) or expect_lookahead?(element.payload) end end # does the list of elements look look-ahead-ish to you? # @!visibility private def lookahead_payload?(payload, in_lookahead) return unless payload[0..-2].all? { |e| lookahead?(e, in_lookahead) } expect_lookahead?(payload.last) or lookahead?(payload.last, in_lookahead) end # can the current element deal with a look-ahead? # @!visibility private def expect_lookahead?(element) return element.class == Node[:capture] unless element.is_a? Node[:group] element.payload.all? { |e| expect_lookahead?(e) } end # helper method for deciding where to put an element for now # @!visibility private def list_for(element) expect_lookahead?(element) ? lookahead_buffer : payload end end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/translator.rb000066400000000000000000000102171517364653100243550ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/node' require 'mustermann/error' require 'delegate' module Mustermann module AST # Implements translator pattern # # @abstract # @!visibility private class Translator URI_PARSER = defined?(URI::RFC2396_PARSER) ? URI::RFC2396_PARSER : URI::RFC2396_Parser.new # Encapsulates a single node translation # @!visibility private class NodeTranslator < DelegateClass(Node) # @param [Array] types list of types to register for. # @!visibility private def self.register(*types) types.each do |type| type = Node.constant_name(type) if type.is_a? Symbol translator.dispatch_table[type.to_s] = self end end # @param node [Mustermann::AST::Node, Object] # @param translator [Mustermann::AST::Translator] # # @!visibility private def initialize(node, translator) @translator = translator super(node) end # @!visibility private attr_reader :translator # shorthand for translating a nested object # @!visibility private def t(*args, **options, &block) return translator unless args.any? translator.translate(*args, **options, &block) end # @!visibility private alias_method :node, :__getobj__ end # maps types to translations # @!visibility private def self.dispatch_table @dispatch_table ||= {} end # some magic sauce so {NodeTranslator}s know whom to talk to for {#register} # @!visibility private def self.inherited(subclass) node_translator = Class.new(NodeTranslator) node_translator.define_singleton_method(:translator) { subclass } subclass.const_set(:NodeTranslator, node_translator) super end # DSL-ish method for specifying the exception class to use. # @!visibility private def self.raises(error) define_method(:error_class) { error } end # DSL method for defining single method translations. # @!visibility private def self.translate(*types, &block) Class.new(const_get(:NodeTranslator)) do register(*types) define_method(:translate, &block) end end # Enables quick creation of a translator object. # # @example # require 'mustermann' # require 'mustermann/ast/translator' # # translator = Mustermann::AST::Translator.create do # translate(:node) { [type, *t(payload)].flatten.compact } # translate(Array) { map { |e| t(e) } } # translate(Object) { } # end # # ast = Mustermann.new('/:name').to_ast # translator.translate(ast) # => [:root, :separator, :capture] # # @!visibility private def self.create(&block) Class.new(self, &block).new end raises Mustermann::Error # @param [Mustermann::AST::Node, Object] node to translate # @return decorator encapsulating translation # # @!visibility private def self.factory_for(node_class) @factory_for ||= {} @factory_for[node_class] ||= node_class.ancestors.lazy.filter_map { dispatch_table[_1.name] }.first end def decorator_for(node) factory = self.class.factory_for(node.class) or raise error_class, "#{self.class}: Cannot translate #{node.class}" factory.new(node, self) end # Start the translation dance for a (sub)tree. # @!visibility private def translate(node, *args, **options, &block) result = decorator_for(node).translate(*args, **options, &block) result = result.node while result.is_a? NodeTranslator result end # @return [String] escaped character # @!visibility private def escape(char, parser: URI_PARSER, escape: URI_PARSER.regexp[:UNSAFE], also_escape: nil) escape = Regexp.union(also_escape, escape) if also_escape char.to_s =~ escape ? parser.escape(char, Regexp.union(*escape)) : char end end end end mustermann-4.0.0/mustermann/lib/mustermann/ast/validation.rb000066400000000000000000000031761517364653100243240ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann module AST # Checks the AST for certain validations, like correct capture names. # # Internally a poor man's visitor (abusing translator to not have to implement a visitor). # @!visibility private class Validation < Translator # Runs validations. # # @param [Mustermann::AST::Node] ast to be validated # @return [Mustermann::AST::Node] the validated ast # @raise [Mustermann::AST::CompileError] if validation fails # @!visibility private def self.validate(ast) new.translate(ast) ast end translate(Object, :splat) {} translate(:node) { t(payload) } translate(Array) { each { |p| t(p)} } translate(:capture) { t.check_name(name, forbidden: ['captures', 'splat'])} translate(:variable, :named_splat) { t.check_name(name, forbidden: 'captures')} # @raise [Mustermann::CompileError] if name is not acceptable # @!visibility private def check_name(name, forbidden: []) raise CompileError, "capture name can't be empty" if name.nil? or name.empty? raise CompileError, "capture name must start with underscore or lower case letter" unless name =~ /^[a-z_]/ raise CompileError, "capture name can't be #{name}" if Array(forbidden).include? name raise CompileError, "can't use the same capture name twice" if names.include? name names << name end # @return [Array] list of capture names in tree # @!visibility private def names @names ||= [] end end end end mustermann-4.0.0/mustermann/lib/mustermann/caster.rb000066400000000000000000000060661517364653100226650ustar00rootroot00000000000000# frozen_string_literal: true require 'delegate' module Mustermann # Class for defining and running simple Hash transformations. # # @example # caster = Mustermann::Caster.new # caster.register(:foo) { |value| { bar: value.upcase } } # caster.cast(foo: "hello", baz: "world") # => { bar: "HELLO", baz: "world" } # # @see Mustermann::Expander#cast # # @!visibility private class Caster < DelegateClass(Array) # @param (see #register) # @!visibility private def initialize(*types, &block) super([]) register(*types, &block) end # @param [Array] types identifier for cast type (some need block) # @!visibility private def register(*types, &block) return if types.empty? and block.nil? types << Any.new(&block) if types.empty? types.each { |type| self << caster_for(type, &block) } end # @param [Symbol, Regexp, #cast, #===] type identifier for cast type (some need block) # @return [#cast] specific cast operation # @!visibility private def caster_for(type, &block) case type when Symbol, Regexp then Key.new(type, &block) else type.respond_to?(:cast) ? type : Value.new(type, &block) end end # Transforms a Hash. # @param [Hash] hash pre-transform Hash # @return [Hash] post-transform Hash # @!visibility private def cast(hash) return hash if empty? merge = {} hash.delete_if do |key, value| next unless casted = lazy.map { |e| e.cast(key, value) }.detect { |e| e } casted = { key => casted } unless casted.respond_to? :to_hash merge.update(casted.to_hash) end hash.update(merge) end # Class for block based casts that are triggered for every key/value pair. # @!visibility private class Any # @!visibility private def initialize(&block) @block = block end # @see Mustermann::Caster#cast # @!visibility private def cast(key, value) case @block.arity when 0 then @block.call when 1 then @block.call(value) else @block.call(key, value) end end end # Class for block based casts that are triggered for key/value pairs with a matching value. # @!visibility private class Value < Any # @param [#===] type used for matching values # @!visibility private def initialize(type, &block) @type = type super(&block) end # @see Mustermann::Caster#cast # @!visibility private def cast(key, value) super if @type === value end end # Class for block based casts that are triggered for key/value pairs with a matching key. # @!visibility private class Key < Any # @param [#===] type used for matching keys # @!visibility private def initialize(type, &block) @type = type super(&block) end # @see Mustermann::Caster#cast # @!visibility private def cast(key, value) super if @type === key end end end end mustermann-4.0.0/mustermann/lib/mustermann/composite.rb000066400000000000000000000072431517364653100234040ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # Class for pattern objects composed of multiple patterns using binary logic. # @see Mustermann::Pattern#& # @see Mustermann::Pattern#| # @see Mustermann::Pattern#^ class Composite < Pattern attr_reader :patterns, :operator supported_options :operator, :type # @see Mustermann::Pattern.supported? def self.supported?(option, type: nil, **options) return true if super Mustermann[type || Mustermann::DEFAULT_TYPE].supported?(option, **options) end # @return [Mustermann::Pattern] a new composite pattern def self.new(*patterns, **options) patterns = patterns.flatten case patterns.size when 0 then raise ArgumentError, 'cannot create empty composite pattern' when 1 then patterns.first else super(patterns, **options) end end def initialize(patterns, operator: :|, **options) @operator = operator.to_sym @patterns = patterns.flat_map { |p| patterns_from(p, **options) } end # @see Mustermann::Pattern#names def names @names ||= patterns.flat_map { |p| p.respond_to?(:names) ? p.names : [] }.uniq end # @see Mustermann::Pattern#== def ==(pattern) patterns == patterns_from(pattern) end # @see Mustermann::Pattern#eql? def eql?(pattern) patterns.eql? patterns_from(pattern) end # @see Mustermann::Pattern#hash def hash patterns.hash | operator.hash end # @see Mustermann::Pattern#=== def ===(string) patterns.map { |p| p === string }.inject(operator) end # @see Mustermann::Pattern#params def params(string) with_matching(string, :params) end # @see Mustermann::Pattern#match def match(string) with_matching(string, :match) end # @!visibility private def respond_to_special?(method) return false unless operator == :| patterns.all? { |p| p.respond_to?(method) } end # (see Mustermann::Pattern#expand) def expand(behavior = nil, values = {}) raise NotImplementedError, 'expanding not supported' unless respond_to? :expand @expander ||= Mustermann::Expander.new(*patterns) @expander.expand(behavior, values) end # (see Mustermann::Pattern#to_templates) def to_templates raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates patterns.flat_map(&:to_templates).uniq end # @return [String] the string representation of the pattern def to_s = inspect # @!visibility private def inspect "(#{simple_inspect})" end # @!visibility private def simple_inspect patterns.map { |p| p.is_a?(Composite) ? p.inspect : p.simple_inspect }.join(" #{operator} ") end # @!visibility private def pretty_print(q) q.group(1, "(", ")") do patterns.each_with_index do |pattern, index| unless index == 0 q.text " #{operator}" q.breakable " " end if pattern.is_a?(Composite) q.pp pattern else q.text pattern.simple_inspect end end end end # @!visibility private def with_matching(string, method) return unless self === string pattern = patterns.detect { |p| p === string } pattern.public_send(method, string) if pattern end # @!visibility private def patterns_from(pattern, **options) return pattern.patterns if pattern.is_a? Composite and pattern.operator == self.operator [options.empty? && pattern.is_a?(Pattern) ? pattern : Mustermann.new(pattern, **options)] end private :with_matching, :patterns_from end end mustermann-4.0.0/mustermann/lib/mustermann/concat.rb000066400000000000000000000106011517364653100226410ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # Class for pattern objects that are a concatenation of other patterns. # @see Mustermann::Pattern#+ class Concat < Composite # Mixin for patterns to support native concatenation. # @!visibility private module Native # @see Mustermann::Pattern#+ # @!visibility private def +(other) other &&= Mustermann.new(other, type: :identity, **options) if (patterns = look_ahead(other)) && !patterns.empty? concat = (self + patterns.inject(:+)) concat + other.patterns.slice(patterns.length..-1).inject(:+) else native, opts = native_concat(other) native ? self.class.new(native, **options, **opts.to_h) : super end end # @!visibility private def look_ahead(other) return unless other.is_a?(Concat) other.patterns.take_while(&method(:native_concat?)) end # @!visibility private def native_concat(other) "#{self}#{other}" if native_concat?(other) end # @!visibility private def native_concat?(other) other.class == self.class and other.options == options end private :native_concat, :native_concat? end # Should not be used directly. # @!visibility private def initialize(*, **) super AST::Validation.validate(combined_ast) if respond_to? :expand end # @see Mustermann::Composite#operator # @return [Symbol] always :+ def operator :+ end # @see Mustermann::Pattern#=== def ===(string) peek_size(string) == string.size end # @see Mustermann::Pattern#match def match(string) peeked = peek_match(string) peeked if peeked.to_s == string end # @see Mustermann::Pattern#params def params(string) params, size = peek_params(string) params if size == string.size end # @see Mustermann::Pattern#peek_size def peek_size(string) pump(string) { |p,s| p.peek_size(s) } end # @see Mustermann::Pattern#peek_match def peek_match(string) post_match = string params = {} captures = [] named_captures = {} patterns.each do |pattern| return unless part = pattern.peek_match(post_match) params.merge!(part.params) named_captures.merge!(part.named_captures) captures.concat(part.captures) post_match = post_match[part.to_s.size..-1] end matched = string[0, string.size - post_match.size] Match.new(self, string, matched:, params:, captures:, named_captures:, post_match:) end # @see Mustermann::Pattern#peek_params def peek_params(string) pump(string, inject_with: :merge, with_size: true) { |p, s| p.peek_params(s) } end # (see Mustermann::Pattern#expand) def expand(behavior = nil, values = {}) raise NotImplementedError, 'expanding not supported' unless respond_to? :expand @expander ||= Mustermann::Expander.new(self) { combined_ast } @expander.expand(behavior, values) end # (see Mustermann::Pattern#to_templates) def to_templates raise NotImplementedError, 'template generation not supported' unless respond_to? :to_templates @to_templates ||= patterns.inject(['']) { |list, pattern| list.product(pattern.to_templates).map(&:join) }.uniq end # @!visibility private def respond_to_special?(method) method = :to_ast if method.to_sym == :expand patterns.all? { |p| p.respond_to?(method) } end # used to generate results for various methods by scanning through an input string # @!visibility private def pump(string, inject_with: :+, initial: nil, with_size: false) substring = string results = Array(initial) patterns.each do |pattern| result, size = yield(pattern, substring) return unless result results << result size ||= result substring = substring[size..-1] end results = results.inject(inject_with) with_size ? [results, string.size - substring.size] : results end # generates one big AST from all patterns # will not check if patterns support AST generation # @!visibility private def combined_ast payload = patterns.map { |p| AST::Node[:group].new(p.to_ast.payload) } AST::Node[:root].new(payload) end private :combined_ast, :pump end end mustermann-4.0.0/mustermann/lib/mustermann/equality_map.rb000066400000000000000000000036361517364653100240760ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # A simple wrapper around ObjectSpace::WeakMap that allows matching keys by equality rather than identity. # Used for caching. Note that `fetch` is not guaranteed to return the object, even if it has not been # garbage collected yet, especially when used concurrently. Therefore, the block passed to `fetch` has to # be idempotent. # # @example # class ExpensiveComputation # @map = Mustermann::EqualityMap.new # # def self.new(*args) # @map.fetch(args) { super } # end # end # # @see #fetch class EqualityMap attr_reader :map def self.new defined?(ObjectSpace::WeakMap) ? super : {} end def initialize @keys = {} @map = ObjectSpace::WeakMap.new end # @param [#hash] key for caching # @yield block that will be called to populate entry if missing (has to be idempotent) # @return value stored in map or result of block def fetch(key) identity = @keys[key.hash] if identity == key key = identity elsif key.frozen? key = key.dup end # it is ok that this is not thread-safe, worst case it has double cost in # generating, object equality is not guaranteed anyways @map[key] ||= track(key, yield) end # @param [#hash] key for identifying the object # @param [Object] object to be stored # @return [Object] same as the second parameter def track(key, object) object = object.dup if object.frozen? ObjectSpace.define_finalizer(object, finalizer(key.hash)) @keys[key.hash] = key object end # Finalizer proc needs to be generated in different scope so it doesn't keep a reference to the object. # # @param [Integer] hash for key # @return [Proc] finalizer callback def finalizer(hash) proc { @keys.delete(hash) } end private :track, :finalizer end end mustermann-4.0.0/mustermann/lib/mustermann/error.rb000066400000000000000000000011501517364653100225220ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann unless defined?(Mustermann::Error) Error = Class.new(StandardError) # Raised if anything goes wrong while generating a {Pattern}. CompileError = Class.new(Error) # Raised if anything goes wrong while compiling a {Pattern}. ParseError = Class.new(Error) # Raised if anything goes wrong while parsing a {Pattern}. ExpandError = Class.new(Error) # Raised if anything goes wrong while expanding a {Pattern}. TrieError = Class.new(CompileError) # Raised if anything goes wrong while compiling a {Trie}. end end mustermann-4.0.0/mustermann/lib/mustermann/expander.rb000066400000000000000000000217341517364653100232110ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/expander' require 'mustermann/caster' module Mustermann # Allows fine-grained control over pattern expansion. # # @example # expander = Mustermann::Expander.new(additional_values: :append) # expander << "/users/:user_id" # expander << "/pages/:page_id" # # expander.expand(page_id: 58, format: :html5) # => "/pages/58?format=html5" class Expander # @!visibility private ADDITIONAL_VALUES = %i[raise ignore append].freeze attr_reader :patterns, :additional_values, :caster # @param [Array<#to_str, Mustermann::Pattern>] patterns list of patterns to expand, see {#add}. # @param [Symbol] additional_values behavior when encountering additional values, see {#expand}. # @param [Hash] options used when creating/expanding patterns, see {Mustermann.new}. def initialize(*patterns, additional_values: :raise, **options, &block) raise ArgumentError, "Illegal value %p for additional_values" % additional_values unless ADDITIONAL_VALUES.include? additional_values @patterns = [] @api_expander = AST::Expander.new @additional_values = additional_values @options = options @caster = Caster.new add(*patterns, &block) end # Add patterns to expand. # # @example # expander = Mustermann::Expander.new # expander.add("/:a.jpg", "/:b.png") # expander.expand(a: "pony") # => "/pony.jpg" # # @param [Array<#to_str, Mustermann::Pattern>] patterns list of to add for expansion, Strings will be compiled to patterns. # @return [Mustermann::Expander] the expander def add(*patterns) patterns.each do |pattern| if pattern.is_a? Expander add(*pattern.patterns) next end pattern = Mustermann.new(pattern, **@options) if block_given? @api_expander.add(yield(pattern)) else raise NotImplementedError, "expanding not supported for #{pattern.class}" unless pattern.respond_to? :to_ast @api_expander.add(pattern.to_ast) end @patterns << pattern end self end alias_method :<<, :add # Register a block as simple hash transformation that runs before expanding the pattern. # @return [Mustermann::Expander] the expander # # @overload cast # Register a block as simple hash transformation that runs before expanding the pattern for all entries. # # @example casting everything that implements to_param to param # expander.cast { |o| o.to_param if o.respond_to? :to_param } # # @yield every key/value pair # @yieldparam key [Symbol] omitted if block takes less than 2 # @yieldparam value [Object] omitted if block takes no arguments # @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash # @yieldreturn [nil, false] will keep key/value pair in hash # @yieldreturn [Object] will replace value with returned object # # @overload cast(*type_matchers) # Register a block as simple hash transformation that runs before expanding the pattern for certain entries. # # @example convert user to user_id # expander = Mustermann::Expander.new('/users/:user_id') # expand.cast(:user) { |user| { user_id: user.id } } # # expand.expand(user: User.current) # => "/users/42" # # @example convert user, page, image to user_id, page_id, image_id # expander = Mustermann::Expander.new('/users/:user_id', '/pages/:page_id', '/:image_id.jpg') # expand.cast(:user, :page, :image) { |key, value| { "#{key}_id".to_sym => value.id } } # # expand.expand(user: User.current) # => "/users/42" # # @example casting to multiple key/value pairs # expander = Mustermann::Expander.new('/users/:user_id/:image_id.:format') # expander.cast(:image) { |i| { user_id: i.owner.id, image_id: i.id, format: i.format } } # # expander.expander(image: User.current.avatar) # => "/users/42/avatar.jpg" # # @example casting all ActiveRecord objects to param # expander.cast(ActiveRecord::Base, &:to_param) # # @param [Array] type_matchers # To identify key/value pairs to match against. # Regexps and Symbols match against key, everything else matches against value. # # @yield every key/value pair # @yieldparam key [Symbol] omitted if block takes less than 2 # @yieldparam value [Object] omitted if block takes no arguments # @yieldreturn [Hash{Symbol: Object}] will replace key/value pair with returned hash # @yieldreturn [nil, false] will keep key/value pair in hash # @yieldreturn [Object] will replace value with returned object # # @overload cast(*cast_objects) # # @param [Array<#cast>] cast_objects # Before expanding, will call #cast on these objects for each key/value pair. # Return value will be treated same as block return values described above. def cast(*types, &block) caster.register(*types, &block) self end # @example Expanding a pattern # pattern = Mustermann::Expander.new('/:name', '/:name.:ext') # pattern.expand(name: 'hello') # => "/hello" # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png" # # @example Handling additional values # pattern = Mustermann::Expander.new('/:name', '/:name.:ext') # pattern.expand(:ignore, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png" # pattern.expand(:append, name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x" # pattern.expand(:raise, name: 'hello', ext: 'png', scale: '2x') # raises Mustermann::ExpandError # # @example Setting additional values behavior for the expander object # pattern = Mustermann::Expander.new('/:name', '/:name.:ext', additional_values: :append) # pattern.expand(name: 'hello', ext: 'png', scale: '2x') # => "/hello.png?scale=2x" # # @param [Symbol] behavior # What to do with additional key/value pairs not present in the values hash. # Possible options: :raise, :ignore, :append. # # @param [Hash{Symbol: #to_s, Array<#to_s>}] values # Values to use for expansion. # # @return [String] expanded string # @raise [NotImplementedError] raised if expand is not supported. # @raise [Mustermann::ExpandError] raised if a value is missing or unknown def expand(behavior = nil, values = {}) behavior, values = nil, behavior if behavior.is_a? Hash values = map_values(values) case behavior || additional_values when :raise then @api_expander.expand(values) when :ignore then with_rest(values) { |uri, rest| uri } when :append then with_rest(values) { |uri, rest| append(uri, rest) } else raise ArgumentError, "unknown behavior %p" % behavior end end # @!visibility private def inspect return "#<#{self.class.name}>" if @patterns.empty? "#<#{self.class.name}: #{@patterns.map { |p| p.to_s.inspect }.join(", ")}>" end # @!visibility private def pretty_print(q) q.text "#<#{self.class.name}" q.group(1, "", ">") do @patterns.each_with_index do |pattern, index| q.breakable(index == 0 ? " " : ", ") q.pp pattern.to_s end end end # @see Object#== def ==(other) return false unless other.class == self.class other.patterns == patterns and other.additional_values == additional_values end # @see Object#eql? def eql?(other) return false unless other.class == self.class other.patterns.eql? patterns and other.additional_values.eql? additional_values end # @see Object#hash def hash patterns.hash + additional_values.hash end def expandable?(values) return false unless values expandable, _ = split_values(map_values(values)) @api_expander.expandable? expandable end def with_rest(values) expandable, non_expandable = split_values(values) yield expand(:raise, slice(values, expandable)), slice(values, non_expandable) end def split_values(values) expandable = @api_expander.expandable_keys(values.keys) non_expandable = values.keys - expandable [expandable, non_expandable] end def slice(hash, keys) Hash[keys.map { |k| [k, hash[k]] }] end def append(uri, values) return uri unless values and values.any? entries = values.map { |pair| pair.map { |e| @api_expander.escape(e, also_escape: /[\/\?#\&\=%]/) }.join(?=) } "#{ uri }#{ uri[??]??&:?? }#{ entries.join(?&) }" end def map_values(values) values = values.dup @api_expander.keys.each { |key| values[key] ||= values.delete(key.to_s) if values.include? key.to_s } caster.cast(values).delete_if { |k, v| v.nil? } end private :with_rest, :slice, :append, :caster, :map_values, :split_values end end mustermann-4.0.0/mustermann/lib/mustermann/hybrid.rb000066400000000000000000000042321517364653100226560ustar00rootroot00000000000000require 'mustermann/sinatra' module Mustermann # Hybrid pattern type that bridges {Mustermann::Sinatra} and Rails pattern syntax. # # It supports all syntax elements of {Mustermann::Sinatra}, plus URI template-style # placeholders, and changes the semantics of parenthesized groups to match Rails: # # - A group *without* a pipe operator is **implicitly optional**, even without a # trailing `?`. So `/foo(/bar)` matches both `/foo/bar` and `/foo`. # # - A group *with* a pipe operator is **not** implicitly optional, to avoid the # ambiguity of `/scope/(a|b)` also matching `/scope/`. Add a trailing `?` to make # such a group optional explicitly: `/scope/(a|b)?`. # # @example Implicit optional group (no pipe) # require 'mustermann' # pattern = Mustermann.new('/foo(/bar)', type: :hybrid) # pattern === '/foo' # => true # pattern === '/foo/bar' # => true # # @example Non-optional group with pipe # pattern = Mustermann.new('/scope/(a|b)', type: :hybrid) # pattern === '/scope/a' # => true # pattern === '/scope/' # => false # # @example Explicitly optional group with pipe # pattern = Mustermann.new('/scope/(a|b)?', type: :hybrid) # pattern === '/scope/' # => true # # @example Nested implicit optional groups (Rails-style resource routing) # pattern = Mustermann.new('/:controller(/:action(/:id))', type: :hybrid) # pattern.params('/posts') # => { "controller" => "posts" } # pattern.params('/posts/show') # => { "controller" => "posts", "action" => "show" } # pattern.params('/posts/show/1') # => { "controller" => "posts", "action" => "show", "id" => "1" } # # @see Mustermann::Sinatra class Hybrid < Sinatra register :hybrid # Parses a parenthesized group. Groups without a pipe operator are wrapped in an # optional node (implicitly optional, Rails style). Groups that do contain a pipe # operator are left as plain groups; append `?` to make them optional explicitly. on("(") do |c| n = node(:group) { read unless scan(?)) } has_or = n.payload.any? { |e| e.is_a?(:or) } has_or && !scan("?") ? n : node(:optional, n) end end end mustermann-4.0.0/mustermann/lib/mustermann/identity.rb000066400000000000000000000055341517364653100232340ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/pattern' require 'mustermann/ast/node' module Mustermann # Matches strings that are identical to the pattern. # # @example # Mustermann.new('/:foo', type: :identity) === '/bar' # => false # # @see Mustermann::Pattern # @see file:README.md#identity Syntax description in the README class Identity < Pattern include Concat::Native register :identity # @param (see Mustermann::Pattern#===) # @return (see Mustermann::Pattern#===) # @see (see Mustermann::Pattern#===) def ===(string) unescape(string) == @string end # @param (see Mustermann::Pattern#peek_size) # @return (see Mustermann::Pattern#peek_size) # @see (see Mustermann::Pattern#peek_size) def peek_size(string) return unless unescape(string).start_with? @string return @string.size if string.start_with? @string # optimization @string.each_char.with_index.inject(0) do |count, (char, index)| char_size = 1 escaped = @@uri.escape(char, /./) char_size = escaped.size if string[index, escaped.size].downcase == escaped.downcase count + char_size end end # URI templates support generating templates (the logic is quite complex, though). # # @example (see Mustermann::Pattern#to_templates) # @param (see Mustermann::Pattern#to_templates) # @return (see Mustermann::Pattern#to_templates) # @see Mustermann::Pattern#to_templates def to_templates [@@uri.escape(to_s)] end # Generates an AST so it's compatible with {Mustermann::AST::Pattern}. # Not used internally by {Mustermann::Identity}. # @!visibility private def to_ast payload = @string.each_char.with_index.map { |c, i| AST::Node[c == ?/ ? :separator : :char].new(c, start: i, stop: i+1) } AST::Node[:root].new(payload, pattern: @string, start: 0, stop: @string.length) end # Identity patterns support expanding. # # This implementation does not use {Mustermann::Expander} internally to save memory and # compilation time. # # @example (see Mustermann::Pattern#expand) # @param (see Mustermann::Pattern#expand) # @return (see Mustermann::Pattern#expand) # @raise (see Mustermann::Pattern#expand) # @see Mustermann::Pattern#expand # @see Mustermann::Expander def expand(behavior = nil, values = {}) return to_s if values.empty? or behavior == :ignore raise ExpandError, "cannot expand with keys %p" % values.keys.sort if behavior == :raise raise ArgumentError, "unknown behavior %p" % behavior if behavior != :append params = values.map { |key, value| @@uri.escape(key.to_s) + "=" + @@uri.escape(value.to_s, /[^\w]/) } separator = @string.include?(??) ? ?& : ?? @string + separator + params.join(?&) end end end mustermann-4.0.0/mustermann/lib/mustermann/match.rb000066400000000000000000000141351517364653100224740ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # The return value of {Mustermann::Pattern#match}, {Mustermann::Pattern#peek_match}, {Mustermann::Set#match}, and similar methods. # Mimics large parts of the MatchData API, but also provides access to the pattern and params hash. class Match # @return [Mustermann::Pattern] the pattern that produced the match attr_reader :pattern # @return [String] the string that was matched attr_reader :string # @return [Hash] the params hash attr_reader :params # @return [Array] the captures array attr_reader :captures # @return [Hash] the named captures hash, usually identical to {#params} attr_reader :named_captures # @return [String] the post match string attr_reader :post_match # @return [String] the pre match string attr_reader :pre_match # @return [Regexp, nil] the regular expression that produced the match, if available attr_reader :regexp # @overload initialize(pattern, string, **options) # @param pattern [Mustermann::Pattern] the pattern that produced the match # @param string [String] the string that was matched # # @overload initialize(match, **options) # @param match [Mustermann::Match] the match to copy pattern and string from # # @overload initialize(pattern, match, **options) # @param match [Mustermann::Match, MatchData] the match to copy string from # # @option options [Array] :captures the captures array # @option options [Hash] :named_captures the named captures hash # @option options [String] :matched the matched substring (defaults to string for full matches) # @option options [Hash] :params the params hash # @option options [Regexp] :regexp the regular expression that produced the match # @option options [String] :post_match the post match string # @option options [String] :pre_match the pre match string def initialize(pattern_or_match, string_or_match = nil, matched: nil, params: nil, post_match: nil, pre_match: nil, captures: nil, named_captures: nil, regexp: nil) case pattern_or_match when Mustermann::Match, MatchData then match = pattern_or_match when Mustermann::Pattern then pattern = pattern_or_match else raise ArgumentError, "first argument must be a Mustermann::Pattern or a MatchData, not #{pattern_or_match.class}" end case string_or_match when Mustermann::Match, MatchData then match ||= string_or_match when String then string = string_or_match when nil # ignore else raise ArgumentError, "second argument must be a String or a MatchData, not #{string_or_match.class}" end @pattern = pattern || match&.pattern @string = string || match&.string || '' @params = params || match&.params || {} @post_match = post_match || match&.post_match || '' @pre_match = pre_match || match&.pre_match || '' @captures = captures || match&.captures || @params.values @named_captures = named_captures || match&.named_captures || @params @matched = matched || match&.to_s || @string unless @regexp = regexp @regexp = match.regexp if match.respond_to?(:regexp) @regexp ||= pattern.respond_to?(:regexp) ? pattern.regexp : nil end end # @return [Array] the names of the named captures def names = named_captures.keys # @overload [](key) # Access named captures by key. # @param key [String, Symbol] the key to access # @return the value of the named capture, or nil if not found # # @overload [](index) # Access captures by index. # @param index [Integer] the index to access # @return the value of the capture, or nil if not found # # @overload [](start, length) # Access multiple captures by index and length. # @param start [Integer] the starting index to access # @param length [Integer] the number of captures to access # @return [Array] the values of the captures # # @overload [](range) # Access multiple captures by range. # @param range [Range] the range of indices to access # @return [Array] the values of the captures def [](key, length = nil) case key when String then named_captures[key] when Symbol then named_captures[key.to_s] when Integer then length ? captures[key, length] : captures[key] when Range then captures[key] else raise ArgumentError, "key must be a String, Symbol, Integer, or Range, not #{key.class}" end end # Deconstructs the match into a hash of the given keys. Useful for pattern matching. # @param keys [Array] the keys to deconstruct # @return [Hash] a hash of the given keys and their corresponding values # @see https://docs.ruby-lang.org/en/4.0/syntax/pattern_matching_rdoc.html def deconstruct_keys(keys) = keys.to_h { |key| [key, self[key]] } # @see Object#hash def hash = pattern.hash ^ string.hash ^ params.hash # @see Object#eql? def eql?(other) return false unless other.is_a? self.class pattern == other.pattern && string == other.string && params == other.params end # Returns the values of the given keys as an array. # @params keys [Array] the keys to access # @return [Array] the values of the given keys def values_at(*keys) = keys.map { |key| self[key] } # @return [String] the matched substring (like MatchData#to_s) def to_s = @matched alias == eql? alias to_h params # @!visibility private def inspect params_str = params.map { |k, v| " #{k}:#{v.inspect}" }.join "#<#{self.class.name}: #{@matched.inspect}#{params_str}>" end # @!visibility private def pretty_print(q) q.group(1, "#<#{self.class.name}:", ">") do q.breakable q.pp @matched params.each do |key, value| q.breakable q.text("#{key}:") q.pp value end end end end end mustermann-4.0.0/mustermann/lib/mustermann/pattern.rb000066400000000000000000000342171517364653100230600ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/error' require 'mustermann/match' require 'mustermann/equality_map' require 'uri' module Mustermann # Superclass for all pattern implementations. # @abstract class Pattern include Mustermann @@uri ||= URI::RFC2396_Parser.new # List of supported options. # # @overload supported_options # @return [Array] list of supported options # @overload supported_options(*list) # Adds options to the list. # # @api private # @param [Symbol] *list adds options to the list of supported options # @return [Array] list of supported options def self.supported_options(*list) @supported_options ||= [] options = @supported_options.concat(list) options += superclass.supported_options if self < Pattern options end # Registers the pattern with Mustermann. # @see Mustermann.register # @!visibility private def self.register(*names) names.each { |name| Mustermann.register(name, self) } end # @param [Symbol] option The option to check. # @return [Boolean] Whether or not option is supported. def self.supported?(option, **options) supported_options.include? option end # @overload new(string, **options) # @param (see #initialize) # @raise (see #initialize) # @raise [ArgumentError] if some option is not supported # @return [Mustermann::Pattern] a new instance of Mustermann::Pattern # @see #initialize def self.new(string, ignore_unknown_options: false, **options) if ignore_unknown_options options = options.select { |key, value| supported?(key, **options) if key != :ignore_unknown_options } else unsupported = options.keys.detect { |key| not supported?(key, **options) } raise ArgumentError, "unsupported option %p for %p" % [unsupported, self] if unsupported end @map ||= EqualityMap.new @map.fetch([string, options]) { super(string, **options) { options } } end supported_options :uri_decode, :ignore_unknown_options attr_reader :uri_decode # options hash passed to new (with unsupported options removed) # @!visibility private attr_reader :options # @overload initialize(string, **options) # @param [String] string the string representation of the pattern # @param [Hash] options options for fine-tuning the pattern behavior # @raise [Mustermann::Error] if the pattern can't be generated from the string # @see file:README.md#Types_and_Options "Types and Options" in the README # @see Mustermann.new def initialize(string, uri_decode: true, **options) @uri_decode = uri_decode @string = string.to_s.dup @options = yield.freeze if block_given? end # @return [String] the string representation of the pattern def to_s @string.dup end # @param [String] string The string to match against # @return [Mustermann::Match, nil] the match object if the pattern matches. # @see Mustermann::Match def match(string) Match.new(self, string) if self === string end # @return [Array] list of named captures in the pattern def names = [] # @param [String] string The string to match against # @return [Integer, nil] nil if pattern does not match the string, zero if it does. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-7E Regexp#=~ def =~(string) 0 if self === string end # @param [String] string The string to match against # @return [Boolean] Whether or not the pattern matches the given string # @note Needs to be overridden by subclass. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-3D-3D-3D Regexp#=== def ===(string) raise NotImplementedError, 'subclass responsibility' end # Used by Ruby internally for hashing. # @return [Integer] same has value for patterns that are equal def hash self.class.hash ^ @string.hash ^ options.hash end # Two patterns are considered equal if they are of the same type, have the same pattern string # and the same options. # @return [true, false] def ==(other) other.class == self.class and other.to_s == @string and other.options == options end # Two patterns are considered equal if they are of the same type, have the same pattern string # and the same options. # @return [true, false] def eql?(other) other.class.eql?(self.class) and other.to_s.eql?(@string) and other.options.eql?(options) end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return the count of the matching characters if it matches. # # @example # pattern = Mustermann.new('/:name') # pattern.size("/Frank/Sinatra") # => 6 # # @param [String] string The string to match against # @return [Integer, nil] the number of characters that match def peek_size(string) # this is a very naive, unperformant implementation string.size.downto(0).detect { |s| self === string[0, s] } end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return the substring if it matches. # # @example # pattern = Mustermann.new('/:name') # pattern.peek("/Frank/Sinatra") # => "/Frank" # # @param [String] string The string to match against # @return [String, nil] matched subsctring def peek(string) size = peek_size(string) string[0, size] if size end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return a MatchData or similar instance for the matched substring. # # @example # pattern = Mustermann.new('/:name') # pattern.peek("/Frank/Sinatra") # => # # # @param [String] string The string to match against # @return [Mustermann::Match, nil] MatchData or similar object if the pattern matches. # @see #peek_params def peek_match(string) matched = peek(string) Match.new(self, string, matched:, params: {}, post_match: string[matched.size..-1]) if matched end # Tries to match the pattern against the beginning of the string (as opposed to the full string). # Will return a two element Array with the params parsed from the substring as first entry and the length of # the substring as second. # # @example # pattern = Mustermann.new('/:name') # params, _ = pattern.peek_params("/Frank/Sinatra") # # puts "Hello, #{params['name']}!" # Hello, Frank! # # @param [String] string The string to match against # @return [Array, nil] Array with params hash and length of substing if matched, nil otherwise def peek_params(string) match = peek_match(string) match ? [match.params, match.to_s.size] : nil end # @param [String] string the string to match against # @return [Hash{String: String, Array}, nil] Sinatra style params if pattern matches. def params(string = nil) = match(string)&.params # @note This method is only implemented by certain subclasses. # # @example Expanding a pattern # pattern = Mustermann.new('/:name(.:ext)?') # pattern.expand(name: 'hello') # => "/hello" # pattern.expand(name: 'hello', ext: 'png') # => "/hello.png" # # @example Checking if a pattern supports expanding # if pattern.respond_to? :expand # pattern.expand(name: "foo") # else # warn "does not support expanding" # end # # Expanding is supported by almost all patterns (notable exceptions are {Mustermann::Shell}, # {Mustermann::Regular} and {Mustermann::Simple}). # # Union {Mustermann::Composite} patterns (with the | operator) support expanding if all # patterns they are composed of also support it. # # @param (see Mustermann::Expander#expand) # @return [String] expanded string # @raise [NotImplementedError] raised if expand is not supported. # @raise [Mustermann::ExpandError] raised if a value is missing or unknown # @see Mustermann::Expander def expand(behavior = nil, values = {}) raise NotImplementedError, "expanding not supported by #{self.class}" end # @note This method is only implemented by certain subclasses. # # Generates a list of URI template strings representing the pattern. # # Note that this transformation is lossy and the strings matching these # templates might not match the pattern (and vice versa). # # This comes in quite handy since URI templates are not made for pattern matching. # That way you can easily use a more precise template syntax and have it automatically # generate hypermedia links for you. # # @example generating templates # Mustermann.new("/:name").to_templates # => ["/{name}"] # Mustermann.new("/:foo(@:bar)?/*baz").to_templates # => ["/{foo}@{bar}/{+baz}", "/{foo}/{+baz}"] # Mustermann.new("/{name}", type: :template).to_templates # => ["/{name}"] # # @example generating templates from composite patterns # pattern = Mustermann.new('/:name') # pattern |= Mustermann.new('/{name}', type: :template) # pattern |= Mustermann.new('/example/*nested') # pattern.to_templates # => ["/{name}", "/example/{+nested}"] # # Template generation is supported by almost all patterns (notable exceptions are # {Mustermann::Shell}, {Mustermann::Regular} and {Mustermann::Simple}). # Union {Mustermann::Composite} patterns (with the | operator) support template generation # if all patterns they are composed of also support it. # # @example Checking if a pattern supports expanding # if pattern.respond_to? :to_templates # pattern.to_templates # else # warn "does not support template generation" # end # # @return [Array] list of URI templates def to_templates raise NotImplementedError, "template generation not supported by #{self.class}" end # @overload |(other) # Creates a pattern that matches any string matching either one of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => true # pattern === '/fox/bar' # => true # pattern === '/foo' # => false # # @overload &(other) # Creates a pattern that matches any string matching both of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') & Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => true # pattern === '/fox/bar' # => false # pattern === '/foo' # => false # # @overload ^(other) # Creates a pattern that matches any string matching exactly one of the patterns. # If a string is supplied, it is treated as an identity pattern. # # @example # pattern = Mustermann.new('/foo/:name') ^ Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => false # pattern === '/fox/bar' # => true # pattern === '/foo' # => false # # @param [Mustermann::Pattern, String] other the other pattern # @return [Mustermann::Pattern] a composite pattern def |(other) Mustermann::Composite.new(self, other, operator: __callee__, type: :identity) end alias_method :&, :| alias_method :^, :| # @example # require 'mustermann' # prefix = Mustermann.new("/:prefix") # about = prefix + "/about" # about.params("/main/about") # => {"prefix" => "main"} # # Creates a concatenated pattern by combingin self with the other pattern supplied. # Patterns of different types can be mixed. The availability of `to_templates` and # `expand` depends on the patterns being concatenated. # # String input is treated as identity pattern. # # @param [Mustermann::Pattern, String] other pattern to be appended # @return [Mustermann::Pattern] concatenated pattern def +(other) Concat.new(self, other, type: :identity) end # @example # pattern = Mustermann.new('/:a/:b') # strings = ["foo/bar", "/foo/bar", "/foo/bar/"] # strings.detect(&pattern) # => "/foo/bar" # # @return [Proc] proc wrapping {#===} def to_proc @to_proc ||= method(:===).to_proc end # @!visibility private # @return [Boolean] # @see Object#respond_to? def respond_to?(method, *args) return super unless %i[expand to_templates].include? method respond_to_special?(method) end # @!visibility private # @return [Boolean] # @see #respond_to? def respond_to_special?(method) method(method).owner != Mustermann::Pattern end # @!visibility private def pretty_print(q) q.text "#<%p:" % self.class q.pp(@string) q.text ">" end # @!visibility private def inspect "#<%p:%p>" % [self.class, @string] end # @!visibility private def simple_inspect type = self.class.name[/[^:]+$/].downcase "%s:%p" % [type, @string] end # @!visibility private def map_param(key, value) unescape(value, true) end # @!visibility private def unescape(string, decode = uri_decode) return string unless decode and string return string unless string.include?('%') @@uri.unescape(string) end # @!visibility private ALWAYS_ARRAY = %w[splat captures] # @!visibility private def always_array?(key) ALWAYS_ARRAY.include? key end # @api private # Returns true if params can be used as-is without calling map_param. # Used by Set::Trie to skip building a redundant copy of the params hash. def identity_params?(params) !params.any? { |k, v| v.is_a?(Array) || always_array?(k) || (v.respond_to?(:include?) && v.include?('%')) } end private :unescape, :map_param, :respond_to_special? private_constant :ALWAYS_ARRAY end end mustermann-4.0.0/mustermann/lib/mustermann/rails.rb000066400000000000000000000034531517364653100225130ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/ast/pattern' require 'mustermann/ast/fast_pattern' require 'mustermann/versions' module Mustermann # Rails style pattern implementation. # # @example # Mustermann.new('/:foo', type: :rails) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#rails Syntax description in the README class Rails < AST::Pattern include AST::FastPattern extend Versions register :rails # first parser, no optional parts version('2.3') do on(nil) { |c| unexpected(c) } on(?*) { |c| node(:named_splat) { scan(/\w+/) } } on(?:) { |c| node(:capture) { scan(/\w+/) } } end # rack-mount version('3.0', '3.1') do on(?)) { |c| unexpected(c) } on(?() { |c| node(:optional, node(:group) { read unless scan(?)) }) } on(?\\) { |c| node(:char, expect(/./)) } end # stand-alone journey version('3.2') do on(?|) { |c| raise ParseError, "the implementation of | is broken in ActionDispatch, cannot compile compatible pattern" } on(?\\) { |c| node(:char, c) } end # embedded journey, broken (ignored) escapes version('4.0', '4.1') { on(?\\) { |c| read } } # escapes got fixed in 4.2 version('4.2') { on(?\\) { |c| node(:char, expect(/./)) } } # Rails 5.0 fixes | version('5', '6', '7', '8') { on(?|) { |c| node(:or) }} # (see Mustermann::Pattern#|) def |(other) = combine(other, :|) { super } # (see Mustermann::Pattern#+) def +(other) = combine(other, :+) { super } private def combine(other, operator) return yield unless hybrid = Mustermann[:hybrid].try_convert(self, **options) native = hybrid.public_send(operator, other) native.is_a?(Composite) ? yield : native end end end mustermann-4.0.0/mustermann/lib/mustermann/regexp.rb000066400000000000000000000000351517364653100226640ustar00rootroot00000000000000require 'mustermann/regular' mustermann-4.0.0/mustermann/lib/mustermann/regexp_based.rb000066400000000000000000000067731517364653100240410ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/pattern' require 'forwardable' module Mustermann # Superclass for patterns that internally compile to a regular expression. # @see Mustermann::Pattern # @abstract class RegexpBased < Pattern # @return [Regexp] regular expression equivalent to the pattern. attr_reader :regexp alias_method :to_regexp, :regexp # @api private supported_options :cache # @param (see Mustermann::Pattern#initialize) # @return (see Mustermann::Pattern#initialize) # @see (see Mustermann::Pattern#initialize) def initialize(string, **options) cache = options.delete(:cache) { true } super regexp = compile(**options) @peek_regexp = /\A#{regexp}/ @regexp = /\A#{regexp}\Z/ @simple_captures = @regexp.named_captures.none? { |name, positions| positions.size > 1 || always_array?(name) } cache_class = ObjectSpace::WeakKeyMap if defined?(ObjectSpace::WeakKeyMap) case cache when true @match_cache = cache_class&.new || false @peek_cache = cache_class&.new || false when false @match_cache = false @peek_cache = false when Hash @match_cache = cache[:match] || cache_class&.new || false @peek_cache = cache[:peek] || cache_class&.new || false else @match_cache = cache.new @peek_cache = cache.new end end # @param (see Mustermann::Pattern#peek_size) # @return (see Mustermann::Pattern#peek_size) # @see (see Mustermann::Pattern#peek_size) def peek_size(string) return unless match = peek_match(string) match.to_s.size end # @param (see Mustermann::Pattern#peek_match) # @return (see Mustermann::Pattern#peek_match) # @see (see Mustermann::Pattern#peek_match) def peek_match(string) = cache_match(@peek_cache, @peek_regexp, string) # @param (see Mustermann::Pattern#match) # @return (see Mustermann::Pattern#match) # @see (see Mustermann::Pattern#match) def match(string) = cache_match(@match_cache, @regexp, string) # Extracts params directly from the regexp without allocating a Match object or # populating the match cache — significant GC savings when called in hot loops. # @param (see Mustermann::Pattern#params) # @return (see Mustermann::Pattern#params) def params(string = nil) return unless md = @regexp.match(string) build_params(md) end extend Forwardable def_delegators :regexp, :===, :=~, :names private def cache_match(cache, regexp, string) if cache return cache[string] if cache.key?(string) cache[string] = build_match(regexp, string) else build_match(regexp, string) end end def build_match(regexp, string) return unless match = regexp.match(string) Match.new(self, match, params: build_params(match)) end def build_params(match) if @simple_captures params = match.named_captures return params if params.empty? || identity_params?(params) params.each_with_object({}) { |(k, v), h| h[k] = map_param(k, v) } else match.regexp.named_captures.to_h do |name, positions| value = positions.size < 2 && !always_array?(name) ? map_param(name, match[name]) : positions.flat_map { |pos| map_param(name, match[pos]) } [name, value] end end end def compile(**options) = raise NotImplementedError, 'subclass responsibility' end end mustermann-4.0.0/mustermann/lib/mustermann/regular.rb000066400000000000000000000030341517364653100230350ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/regexp_based' require 'strscan' module Mustermann # Regexp pattern implementation. # # @example # Mustermann.new('/.*', type: :regexp) === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#simple Syntax description in the README class Regular < RegexpBased include Concat::Native register :regexp, :regular supported_options :check_anchors # @param (see Mustermann::Pattern#initialize) # @return (see Mustermann::Pattern#initialize) # @see (see Mustermann::Pattern#initialize) def initialize(string, check_anchors: true, **options) string = $1 if string.to_s =~ /\A\(\?\-mix\:(.*)\)\Z/ && string.inspect == "/#$1/" string = string.source.gsub!(/(? "text/plain" }, ["Hello, #{name}!"]] # end # end # # # in config.ru # use Rack::Head # run router # # @example Routing to other applications # router = Mustermann::Router.new do # get "/users", MyApp::Users::Index # get "/users/:id", MyApp::Users::Show # post "/users", MyApp::Users::Create # fallback MyApp::NotFound # end # # router.path_for(MyApp::Users::Show, id: 42) # => "/users/42" # # @example As middleware # use Mustermann::Router do # get("/up") { [200, { "content-type" => "text/plain" }, ["Up!"]] } # end # # run MyApp # # @see Mustermann::Set # @see https://rack.github.io/rack/ class Router NOT_FOUND = [404, { "content-type" => "text/plain", "x-cascade" => "pass" }, ["Not found"]].freeze VERBS = %w[GET POST PUT PATCH DELETE OPTIONS LINK UNLINK].freeze private_constant :VERBS, :NOT_FOUND # Initializes a new router. # @param key [String] The key under which the route match will be stored in the Rack environment hash (default: "mustermann.match"). # @param options [Hash] Options to be passed to the Mustermann patterns. def initialize(fallback = nil, key: "mustermann.match", **options, &block) @key = key @sets = VERBS.to_h { |verb| [verb, Set.new(**options)] } @fallback = fallback || ->(env) { NOT_FOUND.dup } if block_given? instance_exec(&block) @sets.each_value(&:optimize!) end end # @param env [Hash] The Rack environment hash for the request. # @return [Array] The Rack response array (status, headers, body). def call(env) request_method = env["REQUEST_METHOD"] || "GET" request_method = "GET" if request_method == "HEAD" if routes = @sets[request_method] and match = routes.match(env["PATH_INFO"] || "/") env[@key] = match return match.value.call(env) end @fallback.call(env) end def fallback(fallback = nil, &block) = @fallback = fallback || block || @fallback # Adds a route for the given verb and pattern, with the given target. # # @note Shorthand methods, like `get`, `post`, etc. dynamically are defined for all supported verbs. # # @param verb [String] HTTP verb (e.g. "GET", "POST") # @param pattern [String, Mustermann::Pattern] Pattern string or Mustermann pattern (e.g. "/users/:id") # @param target [#call, nil] The Rack application or middleware to call when the route matches. Can be passed a block as well. # @yield [env] Block to be used as the target if no explicit target is given. # @yieldparam env [Hash] The Rack environment hash for the request. # @return [void] def route(verb, pattern, target = nil, **options, &block) raise ArgumentError, "need to provide target, :to or a block" unless target || block raise ArgumentError, "unknown verb: #{verb}" unless VERBS.include?(verb) @sets[verb].add(pattern, target || block) end # Helps generate links # # @param app [#call] The Rack application or middleware for which to generate the path. # @param (see Mustermann::Expander#expand) # @return [String] The generated path. def path_for(app, behavior = nil, params = {}) set = @sets.values.find { |s| s.has_value?(app) } || @sets[VERBS.first] set.expand(app, behavior, params) end VERBS.each do |verb| define_method(verb.downcase) { |*args, **opts, &block| route(verb, *args, **opts, &block) } end end end mustermann-4.0.0/mustermann/lib/mustermann/set.rb000066400000000000000000000422501517364653100221720ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/expander' require 'mustermann/set/cache' require 'mustermann/set/linear' require 'mustermann/set/strict_order' require 'mustermann/set/trie' module Mustermann # A collection of patterns that can be matched against strings efficiently. # # Each pattern in the set may be associated with one or more arbitrary values, # such as handler objects or route actions. A single {#match} call returns a # {Set::Match} that provides both the captured parameters and the associated # value for the matched pattern. When the set contains many patterns, an # internal trie (prefix tree) is used to dispatch requests in sub-linear time. # # @example Building a routing table # require 'mustermann/set' # # set = Mustermann::Set.new # set.add('/users/:id', :users_show) # set.add('/posts/:id', :posts_show) # # m = set.match('/users/42') # m.value # => :users_show # m.params['id'] # => '42' # # @example Constructor shorthand with a hash # set = Mustermann::Set.new('/users/:id' => :users_show, '/posts/:id' => :posts_show) # # @example Block syntax # set = Mustermann::Set.new do |s| # s.add('/users/:id', :users_show) # s.add('/posts/:id', :posts_show) # end # # @note Adding patterns via {#add}, {#update}, or {#[]=} is not thread-safe, but matching and expanding is. class Set # Pattern options forwarded to {Mustermann.new} when patterns are created from strings. # @return [Hash] attr_reader :options # Creates a new set, optionally pre-populated with patterns. # # Patterns can be supplied as a Hash (pattern → value), a plain String or # Pattern, an Array of any of these, or an existing {Set}. The same forms # are accepted by {#update} and {#add}. # # @example Empty set # Mustermann::Set.new # # @example Pre-populated from a hash # Mustermann::Set.new('/users/:id' => :users, '/posts/:id' => :posts) # # @example Imperative block # Mustermann::Set.new do |s| # s.add('/users/:id', :users) # end # # @example Zero-argument block returning a mapping hash # Mustermann::Set.new { { '/users/:id' => :users } } # # @param mapping [Array] initial patterns or mappings to add # # @param additional_values [:raise, :ignore, :append] behavior when extra keys are passed to {#expand}. # Defaults to +:raise+ # # @param use_trie [Boolean, Integer] # whether to use a trie for matching # If an Integer is given, it is the number of patterns at which to switch from linear to trie matching. # Defaults to 50 # # @param use_cache [Boolean] # whether to cache matches not yet garbage collected. Defaults to +true+ # # @param strict_order [Boolean] # whether to match patterns in strict insertion order rather than trie order. Defaults to +false+. # See {#use_strict_order?} for details # # @param options [Hash] # pattern options forwarded to {Mustermann.new} (e.g. +type: :rails+) # # @raise [ArgumentError] if +additional_values+ is not a recognized behavior symbol def initialize(*mapping, additional_values: :raise, use_trie: 50, use_cache: true, strict_order: false, **options, &block) raise ArgumentError, "Illegal value %p for additional_values" % additional_values unless Expander::ADDITIONAL_VALUES.include? additional_values raise ArgumentError, "Illegal value %p for use_trie" % use_trie unless [true, false].include?(use_trie) or use_trie.is_a? Integer @use_trie = use_trie @use_cache = use_cache @matcher = nil @mapping = {} @reverse_mapping = {} @options = {} @expanders = {} @additional_values = additional_values @strict_order = strict_order options.each do |key, value| if key.is_a? Symbol @options[key] = value else mapping << { key => value } end end update(mapping) block.arity == 0 ? update(yield) : yield(self) if block optimize! end # A set can match patterns and values in loose or strict insertion order. # # You have the following guarantees without strict ordering: # - Patterns with dynamic segments in the same position and equal static parts will always match in the order they were added. # - Multiple values for the same pattern will retain their insertion order in regards to that pattern. # # Trade-offs without strict ordering: # - Static segments may be favored over dynamic segments. If you want to guarantee this behavior, enable trie-mode proactively. # - When a pattern has multiple values, these will follow each other directly when using {#match_all} or {#peek_match_all}. # # Strict ordering comes with both a performance overhead and marginally increased memory usage. # How big the performance overhead is depends on the number of patterns that overlap in the strings they successfully match against. # It does use Ruby's built-in sorting, which on MRI is based on quicksort. The memory overhead grows linear with the number # of pattern and value combinations, but is generally small compared to the memory used by the patterns and values themselves. # # With strict ordering enabled, patterns and values are guaranteed to occur in insertion order. # # @example Without strict ordering, not using a trie # set = Mustermann::Set.new(use_trie: false) # # set.add("/:path", :first) # set.add("/static", :second) # set.add("/:path", :third) # # set.match("/static").value # => :first # set.match_all("/static").map(&:value) # => [:first, :third, :second] # # @example Without strict ordering, using a trie # set = Mustermann::Set.new(use_trie: true) # # set.add("/:path", :first) # set.add("/static", :second) # set.add("/:path", :third) # # set.match("/static").value # => :second # set.match_all("/static").map(&:value) # => [:second, :first, :third] # # @example With strict ordering # set = Mustermann::Set.new(strict_order: true) # # set.add("/:path", :first) # set.add("/static", :second) # set.add("/:path", :third) # # set.match("/static").value # => :first # set.match_all("/static").map(&:value) # => [:first, :second, :third] # # @return [Boolean] whether matching happens in strict pattern/value insertion order def strict_order? = @strict_order # @return [Boolean] whether caching is enabled def use_cache? = @use_cache # @return [Boolean] whether trie optimization is enabled def use_trie? = @use_trie == true # Adds a pattern to the set, optionally associated with one or more values. # # If the pattern is given as a String it will be compiled via {Mustermann.new} # using the set's own options. The pattern must be AST-based (Sinatra, Rails, # and similar types). Plain regexp patterns are not supported. # # Calling +add+ more than once for the same pattern appends additional values # without creating duplicates. # # @example # set.add('/users/:id', :users) # set.add('/users/:id', :admin) # same pattern, second value # # @param pattern [String, Pattern] the pattern to add # @param values [Array] zero or more values to associate with the pattern # @return [self] # @raise [ArgumentError] if the pattern is not AST-based, or if a reserved symbol is used as a value def add(pattern, *values) if pattern.is_a? Composite and pattern.operator == :| pattern.patterns.each { |p| add(p, *values) } return self end pattern = Mustermann.new(pattern, **options) raise ArgumentError, "Non-AST patterns are not supported" unless pattern.respond_to? :to_ast if @mapping.key? pattern current = @mapping[pattern] else add_pattern(pattern) current = @mapping[pattern] = [] end values = [nil] if values.empty? values.each do |value| raise ArgumentError, "%p may not be used as a value" % value if Expander::ADDITIONAL_VALUES.include? value raise ArgumentError, "the set itself may not be used as value" if value == self next if current.include? value current << value @reverse_mapping[value] ||= [] @reverse_mapping[value] << pattern unless @reverse_mapping[value].include? pattern @expanders[value]&.add(pattern) @matcher.track(pattern, value) if strict_order? end self end # Adds a pattern associated with a value using hash-assignment syntax. # @see #add alias []= add # Looks up a value by string or retrieves the first value for a known pattern object. # # When given a String, it is matched against the set and the associated value of the # first matching pattern is returned. When given a {Pattern}, the first value # registered for that exact pattern is returned without matching. # # @example String lookup # set['/users/42'] # => :users_show (or nil) # # @example Pattern lookup # pattern = Mustermann.new('/users/:id') # set[pattern] # => :users_show (or nil) # # @param pattern_or_string [String, Pattern] # @return [Object, nil] the associated value, or +nil+ if not found # @raise [ArgumentError] for unsupported argument types def [](pattern_or_string) case pattern_or_string when String then match(pattern_or_string)&.value when Pattern then values_for_pattern(pattern_or_string)&.first else raise ArgumentError, "unsupported pattern type #{pattern_or_string.class}" end end # Matches the string against all patterns in the set and returns the first match. # # @param string [String] the string to match # @return [Set::Match, nil] the first match, or +nil+ if none of the patterns match def match(string) = @matcher&.match(string) # Matches the beginning of the string against all patterns and returns the # first prefix match. The unmatched remainder of the string is available via # {Set::Match#post_match}. # # @param string [String] # @return [Set::Match, nil] the first prefix match, or +nil+ def peek_match(string) = @matcher&.match(string, peek: true) # Matches the string against all patterns and returns every match, one per # (pattern, value) pair, in insertion order. # # @param string [String] # @return [Array] all matches, or an empty array if none def match_all(string) = @matcher&.match(string, all: true) # Matches the beginning of the string against all patterns and returns every # prefix match, one per (pattern, value) pair. The unmatched remainder is # available as {Set::Match#post_match} on each result. # # @param string [String] # @return [Array] all prefix matches, or an empty array if none def peek_match_all(string) = @matcher&.match(string, all: true, peek: true) # Returns a new set that includes all patterns from the receiver plus those # from +mapping+. The receiver is not modified. # # @param mapping [Hash, String, Pattern, Array, Set] patterns to merge in # @return [Set] a new set def merge(mapping) = dup.update(mapping) # @!visibility private def initialize_copy(other) @mapping = other.mapping.transform_values(&:dup) @reverse_mapping = @mapping.each_with_object({}) do |(pattern, values), h| values.each { |value| (h[value] ||= []) << pattern } end @expanders = {} @matcher = nil @mapping.each_key { |pattern| add_pattern(pattern) } end # Adds all patterns from +mapping+ to the set in place and returns +self+. # Aliased as +merge!+. # # Accepts the same argument forms as {#initialize}: a Hash, a String, a # {Pattern}, an Array, or another {Set}. # # @param mapping [Hash, String, Pattern, Array, Set] # @return [self] # @raise [ArgumentError] for unsupported mapping types def update(mapping) case mapping when Set then mapping.mapping.each { |pattern, values| add(pattern, *values) } when Hash then mapping.each { |k, v| add(k, v) } when String, Pattern then add(mapping) when Array then mapping.each { |item| update(item) } else raise ArgumentError, "unsupported mapping type #{mapping.class}" end self end alias merge! update # Returns all patterns that have been added to the set, in insertion order. # @return [Array] def patterns = @mapping.keys # Returns an {Expander} that can generate strings from parameter hashes. # # When called without arguments (or with the set itself as the value) the # expander covers all patterns in the set. Pass a specific value to get an # expander limited to the patterns associated with that value. # # @param value [Object] restricts the expander to patterns associated with # this value; defaults to the set itself (all patterns) # @return [Mustermann::Expander] def expander(value = self) @expanders[value] ||= begin patterns = value == self ? @mapping.keys : @reverse_mapping[value] || [] Mustermann::Expander.new(patterns, additional_values: @additional_values, **options) end end # Generates a string from a parameter hash using the patterns in the set. # # When called with just a parameter hash, the first pattern that can be fully # expanded with those keys is used. Pass a value as the first argument to # restrict expansion to the patterns associated with that value. You may also # pass an +additional_values+ behavior symbol (+:raise+, +:ignore+, or # +:append+) as the first argument to override the set's default behavior for # that call. # # @example Expand using any pattern # set.expand(id: '5') # # @example Expand patterns for a specific value # set.expand(:users, id: '5') # # @example Override additional_values behavior for one call # set.expand(:ignore, id: '5', extra: 'ignored') # # @param value [Object, :raise, :ignore, :append] the value whose patterns # should be used, or an additional_values behavior symbol; defaults to all # patterns # @param behavior [:raise, :ignore, :append, nil] how to handle extra keys; # defaults to the set's +additional_values+ setting # @param values [Hash, nil] the parameters to expand # @return [String] # @raise [Mustermann::ExpandError] if no pattern can be expanded with the given keys def expand(value = self, behavior = nil, values = nil) if Expander::ADDITIONAL_VALUES.include? value if behavior.is_a? Hash values = values ? values.merge(behavior) : behavior behavior = nil elsif behavior and behavior != value raise ArgumentError, "behavior specified multiple times" if behavior end behavior = value value = self elsif value.is_a? Hash and behavior.nil? and values.nil? values = value value = self unless @reverse_mapping.key? values end expander(value).expand(behavior || @additional_values, values || {}) end # @return [Boolean] whether the set contains any pattern associated with the given value def has_value?(value) = @reverse_mapping[value]&.any? # @!visibility private def values_for_pattern(pattern) = @mapping[pattern] # :nodoc: # Runs trie optimizations pro-actively and explicitly rather than at match time. def optimize! = @matcher&.optimize! # @!visibility private def inspect # :nodoc: mapping = @mapping.map do |pattern, values| if values.size > 1 or values.first.is_a? Array "%p => %p" % [pattern.to_s, values] elsif values.first != nil "%p => %p" % [pattern.to_s, values.first] else "%p" % pattern.to_s end end "#<#{self.class.name}: #{mapping.join(", ")}>" end # @!visibility private def pretty_print(q) # :nodoc: q.text "#<#{self.class.name}" q.group(1, "", ">") do @mapping.each_with_index do |(pattern, values), index| q.breakable(index == 0 ? " " : ", ") q.pp(pattern.to_s) if values.size > 1 or values.first.is_a? Array q.text " => " q.pp(values) elsif values.first != nil q.text " => " q.pp(values.first) end end end end protected attr_reader :mapping private def add_pattern(pattern) if @use_trie.is_a? Integer and @mapping.size >= @use_trie @use_trie = true @matcher = build_matcher end @matcher ||= build_matcher @matcher.add(pattern) @expanders[self]&.add(pattern) end def build_matcher factory = use_trie? ? Trie : Linear matcher = factory.new(self, @mapping.keys) matcher = StrictOrder.new(matcher) if strict_order? matcher = Cache.new(matcher) if use_cache? matcher end end end mustermann-4.0.0/mustermann/lib/mustermann/set/000077500000000000000000000000001517364653100216425ustar00rootroot00000000000000mustermann-4.0.0/mustermann/lib/mustermann/set/cache.rb000066400000000000000000000022331517364653100232320ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/equality_map' module Mustermann class Set class Cache PLACEHOLDER = Object.new.freeze EMPTY_ARRAY = [].freeze def self.new(matcher) = defined?(ObjectSpace::WeakKeyMap) ? super : matcher def initialize(matcher) @matcher = matcher reset_cache end def add(pattern) @matcher.add(pattern) reset_cache end def match(string, all: false, peek: false) cache = @match_cache[all][peek] result = cache[string] ||= @matcher.match(string, all: all, peek: peek) || PLACEHOLDER return result unless result.equal? PLACEHOLDER all ? EMPTY_ARRAY : nil end def reset_cache @match_cache = { true => { true => ObjectSpace::WeakKeyMap.new, false => ObjectSpace::WeakKeyMap.new }, false => { true => ObjectSpace::WeakKeyMap.new, false => ObjectSpace::WeakKeyMap.new } } end def optimize! = @matcher.optimize! def track(...) = @matcher.track(...) end private_constant :Cache end end mustermann-4.0.0/mustermann/lib/mustermann/set/linear.rb000066400000000000000000000014541517364653100234450ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/set/match' module Mustermann class Set class Linear def initialize(set, patterns = []) @set = set @patterns = patterns end def add(pattern) @patterns << pattern end def match(string, all: false, peek: false) result = [] if all @patterns.each do |pattern| next unless match = peek ? pattern.peek_match(string) : pattern.match(string) return Match.new(match, value: @set.values_for_pattern(pattern)&.first) unless all values = @set.values_for_pattern(pattern) || [nil] values.each { |value| result << Match.new(match, value:) } end result end def optimize! = nil end private_constant :Linear end end mustermann-4.0.0/mustermann/lib/mustermann/set/match.rb000066400000000000000000000013111517364653100232570ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/match' module Mustermann class Set # Subclass of {Mustermann::Match} that also includes the value associated with the pattern that produced the match. class Match < Mustermann::Match # @return the value associated with the pattern that produced the match, if any attr_reader :value # (see Mustermann::Match#initialize) # @option options [Object] :value the value associated with the pattern that produced the match, if any def initialize(*, value: nil, **) @value = value super(*, **) end # @see Mustermann::Match#eql? def eql?(other) = super && value == other.value end end end mustermann-4.0.0/mustermann/lib/mustermann/set/strict_order.rb000066400000000000000000000012531517364653100246730ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann class Set class StrictOrder def initialize(matcher) @matcher = matcher @order = {} @count = 0 end def add(...) = @matcher.add(...) def optimize! = @matcher.optimize! def match(string, all: false, peek: false) possible = @matcher.match(string, all: true, peek: peek) possible.sort_by! { |m| @order.dig(m.pattern, m.value) } all ? possible : possible.first end def track(pattern, value) @order[pattern] ||= {} @order[pattern][value] = @count += 1 end end private_constant :StrictOrder end end mustermann-4.0.0/mustermann/lib/mustermann/set/trie.rb000066400000000000000000000230431517364653100231340ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann/ast/translator' require 'mustermann/set/match' module Mustermann class Set class Trie class Translator < AST::Translator translate(:node) { |trie, **o| trie[t.compile(node)] } translate(:separator) { |trie, **options| trie[payload] } translate(:root) do |trie, **options| leaves = t(payload, trie, **options) if leaves.is_a? Array leaves.each { |leaf| leaf.patterns << t.pattern } else leaves.patterns << t.pattern end leaves end translate(:char) do |trie, **options| return trie if payload.empty? trie[payload] end translate(:optional) do |trie, **options| [*t(payload, trie, **options), trie] end translate(Array) do |trie, **options| i = 0 while i < size element = self[i] if element.is_a? :char or element.is_a? :separator trie = t(element, trie, **options) i += 1 elsif element.is_a? :splat and self[i + 1]&.is_a? :separator # Compile splat+separator together so the splat is bounded by the separator, # then continue building the trie for the remaining elements. trie = trie[t.compile(self[i..i + 1])] i += 2 elsif element.is_a? :splat or !self[i + 1]&.is_a? :separator return trie[t.compile(self[i..-1])] else trie = t(element, trie, **options) return trie.flat_map { |node| t(self[i + 1..-1], node, **options) } if trie.is_a? Array i += 1 end end trie end attr_reader :pattern def initialize(pattern) @pattern = pattern @compiler = pattern.compiler.new @options = pattern.options super() end # \G anchors to a position passed to String#match, avoiding substring allocation. def compile(node, **options) = /\G#{@compiler.translate(node, **@options, **options)}/ end attr_reader :patterns, :set, :static, :dynamic def initialize(set, patterns = []) @set = set @patterns = [] @dynamic = {} @static = {} @stride = nil @fast_static = nil @byte_lookup = nil @dynamic_entries = nil patterns.each { |pattern| add(pattern) } end def [](key) case key when String then @static[key] ||= Trie.new(@set) when Regexp then @dynamic[key] ||= Trie.new(@set) end end def match(string, all: false, peek: false, position: 0, params: {}) optimize! if @stride.nil? return build_matches(string, params, all:) if position >= string.size result = [] if all if @fast_static stride = @stride if node = @fast_static[string[position, stride]] if nested_result = node.match(string, all:, peek:, position: position + stride, params:) return nested_result unless all result.concat(nested_result) end end elsif @byte_lookup if node = @byte_lookup[string.getbyte(position)] if nested_result = node.match(string, all:, peek:, position: position + 1, params:) return nested_result unless all result.concat(nested_result) end end end unless @dynamic_entries.empty? anchored = nil base_params = all ? params : nil @dynamic_entries.each do |matcher, node, capture_names, fast_name| if fast_name # Fast path: unconstrained single-segment capture — no regex, no MatchData. end_pos = string.index('/', position) || string.size next if end_pos == position edge_params = all ? base_params.dup : params edge_params[fast_name] = string.byteslice(position, end_pos - position) nested_result = node.match(string, all:, params: edge_params, peek:, position: end_pos) return nested_result unless all result.concat(nested_result) next end regexp_match = matcher.match(string, position) # Non-greedy patterns (e.g. splat .*?) can match 0 chars on non-empty input, making # no progress. Retry with an end-of-string anchor so they consume the full remainder. if regexp_match && regexp_match.end(0) == position anchored ||= {} anchored_matcher = anchored[matcher] ||= Regexp.new(matcher.source + '\z') regexp_match = anchored_matcher.match(string, position) end next unless regexp_match edge_params = all ? base_params.dup : params capture_names.each do |name| value = regexp_match[name] next unless value existing = edge_params[name] edge_params[name] = existing ? (existing.is_a?(Array) ? existing << value : [existing, value]) : value end nested_result = node.match(string, all:, params: edge_params, peek:, position: regexp_match.end(0)) return nested_result unless all result.concat(nested_result) end end if peek matches = build_matches(string, params, all:, matched_length: position, post_match: string[position..], pre_match: '') return matches unless all result.concat(matches) end result end NIL_VALUES = [nil].freeze def build_matches(string, params, all: false, matched_length: string.size, post_match: '', pre_match: '') result = [] if all matched = string[0, matched_length] @patterns.each do |pattern| next if pattern.except_regexp&.match?(matched) pattern_params = build_pattern_params(pattern, params) values = @set.values_for_pattern(pattern) || NIL_VALUES values.each do |value| match = Set::Match.new(pattern, string, matched:, params: pattern_params, value:, post_match:, pre_match:) return match unless all result << match end end result end def build_pattern_params(pattern, params) return params if pattern.identity_params?(params) result = {} params.each do |key, raw| if raw.is_a?(Array) val = raw.flat_map { |v| pattern.map_param(key, v) } val = val.first if val.size < 2 && !pattern.always_array?(key) else val = pattern.map_param(key, raw) val = [val] if pattern.always_array?(key) end result[key] = val end result end def add(pattern) @stride = nil @fast_static = nil @byte_lookup = nil @dynamic_entries = nil Translator.new(pattern).translate(pattern.to_ast, self) end # Compacts the trie by replacing sequential single-char static lookups with a # single stride-length hash lookup. The stride is the minimum number of static # steps all paths from this node share before hitting a dynamic edge or branch. def optimize! depth = min_static_depth if depth > 1 @fast_static = build_stride_hash(depth) @byte_lookup = nil @stride = depth @fast_static.each_value(&:optimize!) elsif @static.empty? @fast_static = nil @byte_lookup = nil @stride = 1 # no children to recurse into else @fast_static = nil @byte_lookup = Array.new(256) @static.each { |k, v| @byte_lookup[k.getbyte(0)] = v } @stride = 1 @static.each_value(&:optimize!) end @dynamic.each_value(&:optimize!) @dynamic_entries = @dynamic.map do |matcher, node| names = matcher.names.each(&:freeze) # Detect unconstrained single-segment captures: can use fast string.index instead of regex. # Two conditions: (1) the edge is a bare capture (source starts with \G(?), no leading # static chars) and (2) the capture's character class excludes '/' (PATH_INFO never has '?' or '#'). fast = if names.size == 1 name = names.first matcher.source.start_with?("\\G(?<#{name}>") && !matcher.match?('/') ? name : nil end [matcher, node, names, fast] end end protected # Returns the minimum number of guaranteed static steps from this node across # all possible paths, before encountering a dynamic edge, a terminal pattern, # or an empty node. Branching is allowed; only the minimum depth matters. def min_static_depth return 0 if @dynamic.any? return 0 if @patterns.any? return 0 if @static.empty? 1 + @static.values.map { |node| node.min_static_depth }.min end private # Builds a hash whose keys are +stride+-character strings and whose values are # the trie nodes reached after consuming exactly those characters. def build_stride_hash(stride) stride.times.reduce({ "" => self }) do |frontier, _| frontier.each_with_object({}) do |(prefix, node), nxt| node.static.each { |char, child| nxt[prefix + char] = child } end end end end private_constant :Trie end end mustermann-4.0.0/mustermann/lib/mustermann/sinatra.rb000066400000000000000000000076041517364653100230440ustar00rootroot00000000000000# frozen_string_literal: true require 'mustermann' require 'mustermann/identity' require 'mustermann/ast/pattern' require 'mustermann/ast/fast_pattern' require 'mustermann/sinatra/parser' require 'mustermann/sinatra/safe_renderer' require 'mustermann/sinatra/try_convert' module Mustermann # Sinatra 2.0 style pattern implementation. # # @example # Mustermann.new('/:foo') === '/bar' # => true # # @see Mustermann::Pattern # @see file:README.md#sinatra Syntax description in the README class Sinatra < AST::Pattern include AST::FastPattern include Concat::Native register :sinatra # Takes a string and espaces any characters that have special meaning for Sinatra patterns. # # @example # require 'mustermann/sinatra' # Mustermann::Sinatra.escape("/:name") # => "/\\:name" # # @param [#to_s] string the input string # @return [String] the escaped string def self.escape(string) string.to_s.gsub(/[\?\(\)\*:\\\|\{\}]/) { |c| "\\#{c}" } end # Tries to convert the given input object to a Sinatra pattern with the given options, without # changing its parsing semantics. # @return [Mustermann::Sinatra, nil] the converted pattern, if possible # @!visibility private def self.try_convert(input, **options) TryConvert.convert(self, input, **options) end # Creates a pattern that matches any string matching either one of the patterns. # If a string is supplied, it is treated as a fully escaped Sinatra pattern. # # If the other pattern is also a Sinatra pattern, it might join the two to a third # sinatra pattern instead of generating a composite for efficiency reasons. # # This only happens if the sinatra pattern behaves exactly the same as a composite # would in regards to matching, parsing, expanding and template generation. # # @example # pattern = Mustermann.new('/foo/:name') | Mustermann.new('/:first/:second') # pattern === '/foo/bar' # => true # pattern === '/fox/bar' # => true # pattern === '/foo' # => false # # @param [Mustermann::Pattern, String] other the other pattern # @return [Mustermann::Pattern] a composite pattern # @see Mustermann::Pattern#| def |(other) return super unless converted = try_convert(other) return super if converted.names.any? { |name| names.include?(name) } self.class.new(safe_string + "|" + converted.safe_string, **converted.options) end # Generates a string representation of the pattern that can safely be used for def interpolation # without changing its semantics. # # @example # require 'mustermann' # unsafe = Mustermann.new("/:name") # # Mustermann.new("#{unsafe}bar").params("/foobar") # => { "namebar" => "foobar" } # Mustermann.new("#{unsafe.safe_string}bar").params("/foobar") # => { "name" => "foo" } # # @return [String] string representations of the pattern def safe_string @safe_string ||= SafeRenderer.translate(to_ast) end # @!visibility private def native_concat(other) return unless converted = try_convert(other) [safe_string + converted.safe_string, converted.options] end # @!visibility private def normalize_capture(pattern) case capture = pattern.options[:capture] when Hash then capture.slice(*pattern.names.map(&:to_sym)) when nil then {} else pattern.names.to_h { |name| [name.to_sym, capture] } end end # @!visibility private def try_convert(other) options = self.options if other.is_a? Pattern and other.names.any? capture = normalize_capture(other).merge(normalize_capture(self)) capture = nil if capture.empty? options = options.merge(capture:) end self.class.try_convert(other, names:, **options) end private :native_concat, :normalize_capture, :try_convert end end mustermann-4.0.0/mustermann/lib/mustermann/sinatra/000077500000000000000000000000001517364653100225105ustar00rootroot00000000000000mustermann-4.0.0/mustermann/lib/mustermann/sinatra/parser.rb000066400000000000000000000025351517364653100243360ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann class Sinatra < AST::Pattern # Sinatra syntax definition. # @!visibility private class Parser < AST::Parser on(nil, ??, ?)) { |c| unexpected(c) } on(?*) { |c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) } on(?:) { |c| node(:capture) { scan(/\w+/) } } on(?\\) { |c| node(:char, expect(/./)) } on(?() { |c| node(:group) { read unless scan(?)) } } on(?|) { |c| node(:or) } on ?{ do |char| current_pos = buffer.pos type = scan(?+) ? :named_splat : :capture name = expect(/[\w\.]+/) if type == :capture && scan(?|) buffer.pos = current_pos capture = proc do start = pos match = expect(/(?[^\|}]+)/) node(:capture, match[:capture], start: start) end grouped_captures = node(:group, [capture[]]) do if scan(?|) [min_size(pos - 1, pos, node(:or)), capture[]] end end grouped_captures if expect(?}) else type = :splat if type == :named_splat and name == 'splat' expect(?}) node(type, name) end end suffix ?? do |char, element| node(:optional, element) end end private_constant :Parser end end mustermann-4.0.0/mustermann/lib/mustermann/sinatra/safe_renderer.rb000066400000000000000000000024201517364653100256370ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann class Sinatra < AST::Pattern # Generates a string that can safely be concatenated with other strings # without changing its semantics # @see #safe_string # @!visibility private SafeRenderer = AST::Translator.create do translate(:splat, :named_splat) { "{+#{name}}" } translate(:char, :separator) { Sinatra.escape(payload) } translate(:root) { t(payload) } translate(:group) { "(#{t(payload)})" } translate(:union) { "(#{t(payload, join: ?|)})" } translate(:optional) { "#{t(payload)}?" } translate(:with_look_ahead) { t([head, payload]) } translate(Array) { |join: ""| map { |e| t(e) }.join(join) } translate(:capture) do raise Mustermann::Error, 'cannot render variables' if node.is_a? :variable raise Mustermann::Error, 'cannot translate constraints' if constraint or qualifier or convert prefix = node.is_a?(:splat) ? "+" : "" "{#{prefix}#{name}}" end end private_constant :SafeRenderer end end mustermann-4.0.0/mustermann/lib/mustermann/sinatra/try_convert.rb000066400000000000000000000052561517364653100254230ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann class Sinatra < AST::Pattern # Tries to translate objects to Sinatra patterns. # @!visibility private class TryConvert < AST::Translator # @return [Mustermann::Sinatra, nil] # @!visibility private def self.convert(type, input, **options) new(type, **options).translate(input) end # Reserved variable names. # @!visibility private attr_reader :names # Expected options for the resulting pattern. # @!visibility private attr_reader :options # Expected pattern type for the resulting pattern. # @!visibility private attr_reader :type # @!visibility private def initialize(type, names: [], **options) @names = names @options = options @type = type end # @return [Mustermann::Sinatra] # @!visibility private def new(input, escape: false, **opts) input = Mustermann::Sinatra.escape(input) if escape type.new(input, **opts, **options, ignore_unknown_options: true) end # @return [true, false] whether or not expected pattern should have uri_decode option set # @!visibility private def uri_decode options.fetch(:uri_decode, true) end # @return [true, false] whether or not the given options are compatible with the expected options # @!visibility private def compatible_options?(other_options) other_options.all? do |key, value| case key when :capture then compatible_capture_option?(value) else value == options[key] end end end # @return [true, false] whether or not the given capture option is compatible with the expected capture option # @!visibility private def compatible_capture_option?(capture) return true if names.empty? case capture when Hash then capture.all? { |n, o| !names.include?(n.to_s) and compatible_capture_option?(o) } when Array then capture.all? { |o| compatible_capture_option?(o) } else true end end translate(Object) { nil } translate(String) { t.new(self, escape: true) } translate(Identity) { t.new(self, escape: true) if uri_decode == t.uri_decode } translate(Sinatra) do if node.class == t.type and t.options == options node elsif t.compatible_options? options t.new(to_s, **options) end end translate AST::Pattern do next unless t.compatible_options? options t.new(SafeRenderer.translate(to_ast), **options) rescue nil end end private_constant :TryConvert end end mustermann-4.0.0/mustermann/lib/mustermann/version.rb000066400000000000000000000001121517364653100230530ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann VERSION ||= '4.0.0' end mustermann-4.0.0/mustermann/lib/mustermann/versions.rb000066400000000000000000000024541517364653100232510ustar00rootroot00000000000000# frozen_string_literal: true module Mustermann # Mixin that adds support for multiple versions of the same type. # @see Mustermann::Rails # @!visibility private module Versions # Checks if class has mulitple versions available and picks one that matches the version option. # @!visibility private def new(*args, version: nil, **options) return super(*args, **options) unless versions.any? self[version].new(*args, **options) end # @return [Hash] version to subclass mapping. # @!visibility private def versions @versions ||= {} end # Defines a new version. # @!visibility private def version(*list, inherit_from: nil, &block) superclass = self[inherit_from] || self subclass = Class.new(superclass, &block) list.each { |v| versions[v] = subclass } end # Resolve a subclass for a given version string. # @!visibility private def [](version) return versions.values.last unless version detected = versions.detect { |v,_| version.start_with?(v) } raise ArgumentError, 'unsupported version %p' % version unless detected detected.last end # @!visibility private def name super || superclass.name end # @!visibility private def inspect name end end end mustermann-4.0.0/mustermann/mustermann.gemspec000066400000000000000000000026741517364653100216570ustar00rootroot00000000000000$:.unshift File.expand_path("../lib", __FILE__) require "mustermann/version" github = "https://github.com/sinatra/mustermann" Gem::Specification.new do |s| s.name = "mustermann" s.version = Mustermann::VERSION s.authors = ["Konstantin Haase", "Kunpei Sakai", "Patrik Ragnarsson", "Jordan Owens", "Zachary Scott"] s.email = "sinatrarb@googlegroups.com" s.homepage = github s.summary = %q{Your personal string matching expert.} s.description = %q{A library implementing patterns that behave like regular expressions.} s.license = 'MIT' s.required_ruby_version = '>= 3.3.0' s.files = `git ls-files lib`.split("\n") + ['LICENSE', 'README.md'] s.description = <<~DESC Mustermann is your personal string matching expert. As an expert in the field of strings and patterns, Mustermann keeps its runtime dependencies to a minimum and is fully covered with specs and documentation. Given a string pattern, Mustermann will turn it into an object that behaves like a regular expression and has comparable performance characteristics. DESC s.metadata = { "bug_tracker_uri" => "#{github}/issues", "changelog_uri" => "#{github}/blob/main/CHANGELOG.md", "documentation_uri" => "#{github}/tree/main/mustermann#readme", "source_code_uri" => "#{github}/tree/main/mustermann", } end mustermann-4.0.0/mustermann/spec/000077500000000000000000000000001517364653100170425ustar00rootroot00000000000000mustermann-4.0.0/mustermann/spec/ast_spec.rb000066400000000000000000000007161517364653100211740ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/ast/node' describe Mustermann::AST do describe :type do example { Mustermann::AST::Node[:char].type .should be == :char } example { Mustermann::AST::Node[:char].new.type .should be == :char } end describe :min_size do example { Mustermann::AST::Node[:char].new.min_size.should be == 1 } example { Mustermann::AST::Node[:node].new.min_size.should be == 0 } end end mustermann-4.0.0/mustermann/spec/compiler_spec.rb000066400000000000000000000443261517364653100222240ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/sinatra' require 'mustermann/rails' require 'mustermann/hybrid' # These specs verify the regexp structure emitted by the AST compiler, with a # focus on atomic groups `(?>...)`. An atomic group prevents Oniguruma from # backtracking into characters already consumed by a capture, giving a # measurable speedup on long non-matching inputs without changing the result # on any valid input. # # Safety rule: a capture is made atomic only when its *immediately following # sibling in the AST array is a path separator whose payload is `'/'*. # Every Mustermann capture character class (Sinatra's [^\/\?#]+, Template's # [\w\-\.~%]+, etc.) excludes '/', so the greedy match naturally stops before # '/' — committing atomically never affects correctness. # # More permissive conditions are intentionally avoided: # - End-of-array is NOT used because template expressions nest captures inside # inner arrays where "end of array" does not mean "end of pattern" (e.g. # {name}bar would atomicize :name and prevent backtracking to the literal). # - Non-'/' separators (e.g. '.' in {.a,b,c}) are NOT used because the # character class [\w\-\.~%] includes '.'. # # Exception: with_look_ahead head captures are made atomic (when greedy) # because the look-ahead constraint already limits what the capture can match. # # Note on regex escaping: to match the literal string `(?>` inside a Ruby # regexp, all three characters must be escaped: `/\(\?>/` — `\(` is literal # `(`, `\?` is literal `?`, `>` is literal `>`. Similarly `(?(?>` is # matched by `/\(\?\(\?>/`. Using `/\(\?\(?>/` is WRONG because # `\(?` means "optionally match a literal `(`" (the `?` is a quantifier). describe 'Atomic group compilation' do # Helper: returns the raw Regexp source for a pattern string. def src(pattern_string, type: :sinatra, **opts) Mustermann.new(pattern_string, type: type, **opts).to_regexp.source end # ── Single captures ───────────────────────────────────────────────────────── context 'single named segment at end of pattern (/:name)' do # No '/' follows :name — the rule requires a '/' separator next sibling. it 'does not get an atomic group (no "/" follows)' do expect(src('/:name')).not_to include('(?>') end end context 'single named segment before a "/" separator (/:name/suffix)' do it 'gets an atomic group because "/" follows immediately' do expect(src('/:name/suffix')).to include('(?>') end it 'the named capture surrounds the atomic group' do expect(src('/:name/suffix')).to match(/\(\?\(\?>/) end end context 'two named segments separated by / (/:a/:b)' do # :a is followed by '/' → atomic. :b is at end → NOT atomic. it 'wraps the first capture atomically (before "/")' do expect(src('/:a/:b')).to match(/\(\?\(\?>/) end it 'does not wrap the last capture (nothing follows)' do expect(src('/:a/:b')).not_to match(/\(\?\(\?>/) end end context 'three named segments separated by / (/:a/:b/:c)' do # :a and :b before '/' → atomic. :c at end → NOT atomic. it 'atomicizes :a (before "/")' do expect(src('/:a/:b/:c')).to match(/\(\?\(\?>/) end it 'atomicizes :b (before "/")' do expect(src('/:a/:b/:c')).to match(/\(\?\(\?>/) end it 'does not atomicize :c (at end)' do expect(src('/:a/:b/:c')).not_to match(/\(\?\(\?>/) end end context 'four named segments separated by / (/:a/:b/:c/:d)' do it 'atomicizes :a, :b, :c (each before "/"), not :d (at end)' do r = src('/:a/:b/:c/:d') expect(r).to match(/\(\?\(\?>/) expect(r).to match(/\(\?\(\?>/) expect(r).to match(/\(\?\(\?>/) expect(r).not_to match(/\(\?\(\?>/) end end # ── Splats ────────────────────────────────────────────────────────────────── context 'splat (*) captures' do it 'does not wrap a named splat in an atomic group' do expect(src('/*path')).not_to include('(?>') end it 'does not wrap an unnamed splat in an atomic group' do expect(src('/*')).not_to include('(?>') end it 'does not make a splat atomic even when a "/" follows' do # splats are excluded explicitly — non-greedy .*? must not be atomic r = src('/*path/:name') expect(r).not_to match(/\(\?\(\?>/) end it 'does not make the named segment after a splat atomic (no "/" after :name)' do # :name is at end-of-pattern, so no '/' follows → not atomic expect(src('/*path/:name')).not_to match(/\(\?\(\?>/) end it 'does not produce any atomic groups when all captures are splats' do expect(src('/*a/*b').scan('(?>').size).to eq 0 end end # ── Captures next to literal characters ──────────────────────────────────── context 'capture followed by a literal dot (/:a.:b)' do it 'does not atomicize :a (dot, not "/", follows)' do expect(src('/:a.:b')).not_to match(/\(\?\(\?>/) end it 'does not atomicize :b (at end, no "/" follows)' do expect(src('/:a.:b')).not_to match(/\(\?\(\?>/) end it 'produces no atomic groups at all' do expect(src('/:a.:b').scan('(?>').size).to eq 0 end end context 'capture followed by a literal hyphen (/:a-:b)' do it 'does not atomicize :a (hyphen, not "/", follows)' do expect(src('/:a-:b')).not_to match(/\(\?\(\?>/) end it 'does not atomicize :b (at end, no "/" follows)' do expect(src('/:a-:b')).not_to match(/\(\?\(\?>/) end end context 'capture followed by a literal at-sign (/:user@:host)' do it 'does not atomicize :user (at-sign, not "/", follows)' do expect(src('/:user@:host')).not_to match(/\(\?\(\?>/) end it 'does not atomicize :host (at end)' do expect(src('/:user@:host')).not_to match(/\(\?\(\?>/) end end # ── Optional segments ─────────────────────────────────────────────────────── context 'capture followed by optional group (/:a(/:b)?)' do # :a is followed by the optional node itself (not a bare separator). it 'does not atomicize :a (next sibling is an optional, not "/")' do expect(src('/:a(/:b)?')).not_to match(/\(\?\(\?>/) end # :b is inside the optional array at end-of-inner-array — but the rule # requires a "/" sibling, not just end-of-array. it 'does not atomicize :b inside the optional (no "/" sibling)' do expect(src('/:a(/:b)?')).not_to match(/\(\?\(\?>/) end it 'produces no atomic groups at all' do expect(src('/:a(/:b)?').scan('(?>').size).to eq 0 end end context 'capture followed by optional capture without separator (/:a:b?)' do # The ArrayTransform inserts a with_look_ahead node; the translator forces # atomic:true on the head (when greedy) regardless of siblings. it 'uses look-ahead for :a (no separator between captures)' do expect(src('/:a:b?')).to include('?!') end it 'atomicizes the look-ahead head capture (:a)' do expect(src('/:a:b?')).to match(/\(\?\(\?>/) end end # ── Correctness: atomic groups must not change match results ──────────────── context 'correctness: /:name' do subject(:pattern) { Mustermann.new('/:name') } it 'matches /alice' do expect(pattern).to match('/alice') end it 'matches /alice-smith' do expect(pattern).to match('/alice-smith') end it 'matches /file.tar.gz' do expect(pattern).to match('/file.tar.gz') end it 'matches /hello_world' do expect(pattern).to match('/hello_world') end it 'matches /42' do expect(pattern).to match('/42') end it 'does not match /' do expect(pattern).not_to match('/') end it 'does not match /a/b' do expect(pattern).not_to match('/a/b') end it 'does not match /a?b=c' do expect(pattern).not_to match('/a?b=c') end it 'captures the segment correctly' do expect(pattern.match('/hello')[:name]).to eq 'hello' end end context 'correctness: /:a/:b' do subject(:pattern) { Mustermann.new('/:a/:b') } it 'matches /foo/bar' do expect(pattern).to match('/foo/bar') end it 'matches /users/42' do expect(pattern).to match('/users/42') end it 'does not match /foo' do expect(pattern).not_to match('/foo') end it 'does not match /foo/bar/baz' do expect(pattern).not_to match('/foo/bar/baz') end it 'captures both segments' do m = pattern.match('/hello/world') expect(m[:a]).to eq 'hello' expect(m[:b]).to eq 'world' end it 'handles percent-encoded characters in the input' do m = pattern.match('/hello%20world/foo') expect(m[:a]).to eq 'hello%20world' end end context 'correctness: /:a/:b/:c' do subject(:pattern) { Mustermann.new('/:a/:b/:c') } it 'matches /x/y/z' do expect(pattern).to match('/x/y/z') end it 'does not match /x/y' do expect(pattern).not_to match('/x/y') end it 'captures all three' do m = pattern.match('/one/two/three') expect(m[:a]).to eq 'one' expect(m[:b]).to eq 'two' expect(m[:c]).to eq 'three' end end context 'correctness: /:a/:b/:c/:d' do subject(:pattern) { Mustermann.new('/:a/:b/:c/:d') } it 'captures all four segments' do m = pattern.match('/w/x/y/z') expect(m[:a]).to eq 'w' expect(m[:b]).to eq 'x' expect(m[:c]).to eq 'y' expect(m[:d]).to eq 'z' end end context 'correctness: /:a.:b (dot separator)' do subject(:pattern) { Mustermann.new('/:a.:b') } it 'matches /foo.bar' do expect(pattern).to match('/foo.bar') end it 'matches /index.html' do expect(pattern).to match('/index.html') end it 'does not match /foobar' do expect(pattern).not_to match('/foobar') end it 'does not match /foo/bar' do expect(pattern).not_to match('/foo/bar') end it 'splits correctly on the dot' do m = pattern.match('/file.json') expect(m[:a]).to eq 'file' expect(m[:b]).to eq 'json' end it 'handles greedy ambiguity: last dot wins' do m = pattern.match('/a.b.c') expect(m[:b]).to eq 'c' end end context 'correctness: /:a-:b (hyphen separator)' do subject(:pattern) { Mustermann.new('/:a-:b') } it 'matches /foo-bar' do expect(pattern).to match('/foo-bar') end it 'matches /2024-01' do expect(pattern).to match('/2024-01') end it 'does not match /foo' do expect(pattern).not_to match('/foo') end it 'splits correctly' do m = pattern.match('/first-last') expect(m[:a]).to eq 'first' expect(m[:b]).to eq 'last' end end context 'correctness: /*path/:name' do subject(:pattern) { Mustermann.new('/*path/:name') } it 'matches /a/b' do expect(pattern).to match('/a/b') end it 'matches /a/b/c' do expect(pattern).to match('/a/b/c') end it 'matches /dir/file' do expect(pattern).to match('/dir/file') end it 'does not match /' do expect(pattern).not_to match('/') end it 'captures the last segment as :name' do m = pattern.match('/a/b/last') expect(m[:name]).to eq 'last' expect(m[:path]).to eq 'a/b' end it 'captures single-segment paths' do m = pattern.match('/dir/file.txt') expect(m[:name]).to eq 'file.txt' expect(m[:path]).to eq 'dir' end end context 'correctness: /:a:b? (adjacent optional)' do subject(:pattern) { Mustermann.new('/:a:b?') } it 'matches /hello' do expect(pattern).to match('/hello') end it 'matches /helloworld' do expect(pattern).to match('/helloworld') end it 'does not match /' do expect(pattern).not_to match('/') end it 'gives :a a non-empty value' do m = pattern.match('/hello') expect(m[:a]).to_not be_empty end end context 'correctness: /:a(/:b)? (optional path segment)' do subject(:pattern) { Mustermann.new('/:a(/:b)?') } it 'matches /foo' do expect(pattern).to match('/foo') end it 'matches /foo/bar' do expect(pattern).to match('/foo/bar') end it 'does not match /foo/' do expect(pattern).not_to match('/foo/') end it 'captures only :a when :b absent' do m = pattern.match('/only') expect(m[:a]).to eq 'only' expect(m[:b]).to be_nil end it 'captures both when :b present' do m = pattern.match('/users/42') expect(m[:a]).to eq 'users' expect(m[:b]).to eq '42' end end context 'correctness: /users/:id.:format' do subject(:pattern) { Mustermann.new('/users/:id.:format') } it 'matches /users/42.json' do expect(pattern).to match('/users/42.json') end it 'matches /users/alice.xml' do expect(pattern).to match('/users/alice.xml') end it 'captures id and format' do m = pattern.match('/users/99.xml') expect(m[:id]).to eq '99' expect(m[:format]).to eq 'xml' end end context 'correctness: constant-only patterns' do it 'matches /foo/bar exactly' do expect(Mustermann.new('/foo/bar')).to match('/foo/bar') end it 'does not match /foo/baz' do expect(Mustermann.new('/foo/bar')).not_to match('/foo/baz') end it 'matches /' do expect(Mustermann.new('/')).to match('/') end it 'does not match /x for pattern /' do expect(Mustermann.new('/')).not_to match('/x') end end # ── Atomic group counts ───────────────────────────────────────────────────── # Only captures BEFORE a "/" separator get atomic groups. context 'regexp structure: /:name (single segment at end)' do it 'has no atomic groups (no "/" follows)' do expect(src('/:name').scan('(?>').size).to eq 0 end end context 'regexp structure: /:a/:b' do it 'has exactly one atomic group (only :a, before "/")' do expect(src('/:a/:b').scan('(?>').size).to eq 1 end end context 'regexp structure: /:a/:b/:c' do it 'has exactly two atomic groups (:a and :b, each before "/")' do expect(src('/:a/:b/:c').scan('(?>').size).to eq 2 end end context 'regexp structure: /*path/:name' do it 'has no atomic groups (splat excluded, :name at end)' do expect(src('/*path/:name').scan('(?>').size).to eq 0 end end context 'regexp structure: /*a/*b' do it 'has no atomic groups (both are splats)' do expect(src('/*a/*b').scan('(?>').size).to eq 0 end end context 'regexp structure: /foo/bar (no captures)' do it 'has no atomic groups' do expect(src('/foo/bar').scan('(?>').size).to eq 0 end end context 'regexp structure: /:a/:b/:c/:d' do it 'has exactly three atomic groups (:a, :b, :c — each before "/")' do expect(src('/:a/:b/:c/:d').scan('(?>').size).to eq 3 end end # ── Rails pattern type ────────────────────────────────────────────────────── context 'Rails patterns' do it 'does not atomicize end-of-pattern capture in /:name' do expect(src('/:name', type: :rails)).not_to include('(?>') end it 'has one atomic group in /:a/:b (only :a, before "/")' do r = src('/:a/:b', type: :rails) expect(r.scan('(?>').size).to eq 1 end it 'atomicizes :id (look-ahead head) in /:id(.:format)' do r = src('/:id(.:format)', type: :rails) expect(r).to match(/\(\?\(\?>/) end it 'does not atomicize :format (at end of optional, no "/" follows)' do r = src('/:id(.:format)', type: :rails) expect(r).not_to match(/\(\?\(\?>/) end it 'correctly matches /:id(.:format) with format' do p = Mustermann.new('/:id(.:format)', type: :rails) m = p.match('/42.json') expect(m[:id]).to eq '42' expect(m[:format]).to eq 'json' end it 'correctly matches /:id(.:format) without format' do p = Mustermann.new('/:id(.:format)', type: :rails) m = p.match('/42') expect(m[:id]).to eq '42' expect(m[:format]).to be_nil end it 'correctly matches /:a/:b' do p = Mustermann.new('/:a/:b', type: :rails) m = p.match('/foo/bar') expect(m[:a]).to eq 'foo' expect(m[:b]).to eq 'bar' end it 'works with greedy: false' do p = Mustermann.new('/:file(.:ext)', type: :rails, greedy: false) expect(p).to match('/pony') expect(p).to match('/pony.jpg') expect(p).to match('/pony.png.jpg') end end # ── Hybrid pattern type ───────────────────────────────────────────────────── context 'Hybrid patterns' do it 'has one atomic group in /:a/:b (only :a, before "/")' do r = src('/:a/:b', type: :hybrid) expect(r.scan('(?>').size).to eq 1 end it 'does not atomicize the splat in /*path/:name' do r = src('/*path/:name', type: :hybrid) expect(r).not_to match(/\(\?\(\?>/) end it 'does not atomicize :name in /*path/:name (at end, no "/" follows)' do r = src('/*path/:name', type: :hybrid) expect(r).not_to match(/\(\?\(\?>/) end it 'correctly matches /*path/:name' do p = Mustermann.new('/*path/:name', type: :hybrid) m = p.match('/a/b/file') expect(m[:path]).to eq 'a/b' expect(m[:name]).to eq 'file' end end # ── URI-encoding: literal dots and encoded variants ───────────────────────── context '/:a.:b (URI-decoded literal dot)' do it 'produces no atomic groups (dot is not "/" for either position)' do r = src('/:a.:b') expect(r.scan('(?>').size).to eq 0 end end end mustermann-4.0.0/mustermann/spec/composite_spec.rb000066400000000000000000000204531517364653100224070ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann' describe Mustermann::Composite do describe :new do example 'with no argument' do expect { Mustermann::Composite.new }. to raise_error(ArgumentError, 'cannot create empty composite pattern') end example 'with one argument' do pattern = Mustermann.new('/foo') Mustermann::Composite.new(pattern).should be == pattern end example 'with supported type specific arguments' do Mustermann::Composite.new("/a", "/b", greedy: true) end example 'with unsupported type specific arguments' do expect { Mustermann::Composite.new("/a", "/b", greedy: true, type: :identity) }.to raise_error(ArgumentError) end end context :| do subject(:pattern) { Mustermann::Composite.new(Mustermann.new('/foo/:name'), Mustermann.new('/:first/:second')) } describe :== do example { subject.should be == subject } example { subject.should be == Mustermann::Composite.new(Mustermann.new('/foo/:name'), Mustermann.new('/:first/:second')) } example { subject.should_not be == Mustermann.new('/foo/:name') } example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) } end describe :=== do example { subject.should be === "/foo/bar" } example { subject.should be === "/fox/bar" } example { subject.should_not be === "/foo" } end describe :params do example { subject.params("/foo/bar") .should be == { "name" => "bar" } } example { subject.params("/fox/bar") .should be == { "first" => "fox", "second" => "bar" } } example { subject.params("/foo") .should be_nil } end describe :=== do example { subject.should match("/foo/bar") } example { subject.should match("/fox/bar") } example { subject.should_not match("/foo") } end describe :expand do example { subject.should respond_to(:expand) } example { subject.expand(name: 'bar') .should be == '/foo/bar' } example { subject.expand(first: 'fox', second: 'bar') .should be == '/fox/bar' } context "without expandable patterns" do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', type: :simple) } example { subject.should_not respond_to(:expand) } example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) } end end describe :to_templates do example { should respond_to(:to_templates) } example { should generate_templates('/foo/{name}', '/{first}/{second}') } context "without patterns implementing to_templates" do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', type: :simple) } example { should_not respond_to(:to_templates) } example { expect { subject.to_templates }.to raise_error(NotImplementedError) } end end describe :eql? do example { should be_eql(pattern) } example { should be_eql(Mustermann::Composite.new(Mustermann.new('/foo/:name'), Mustermann.new('/:first/:second'))) } example { should_not be_eql(Mustermann.new('/bar/:name', '/:first/:second', operator: :|)) } example { should_not be_eql(Mustermann.new('/foo/:name', '/:first/:second', operator: :&)) } end end context :& do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', operator: :&) } describe :== do example { subject.should be == subject } example { subject.should be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) } example { subject.should_not be == Mustermann.new('/foo/:name') } example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second') } end describe :=== do example { subject.should be === "/foo/bar" } example { subject.should_not be === "/fox/bar" } example { subject.should_not be === "/foo" } end describe :params do example { subject.params("/foo/bar") .should be == { "name" => "bar" } } example { subject.params("/fox/bar") .should be_nil } example { subject.params("/foo") .should be_nil } end describe :match do example { subject.should match("/foo/bar") } example { subject.should_not match("/fox/bar") } example { subject.should_not match("/foo") } end describe :expand do example { subject.should_not respond_to(:expand) } example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) } end end context :^ do subject(:pattern) { Mustermann.new('/foo/:name', '/:first/:second', operator: :^) } describe :== do example { subject.should be == subject } example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second') } example { subject.should_not be == Mustermann.new('/foo/:name') } example { subject.should_not be == Mustermann.new('/foo/:name', '/:first/:second', operator: :&) } end describe :=== do example { subject.should_not be === "/foo/bar" } example { subject.should be === "/fox/bar" } example { subject.should_not be === "/foo" } end describe :params do example { subject.params("/foo/bar") .should be_nil } example { subject.params("/fox/bar") .should be == { "first" => "fox", "second" => "bar" } } example { subject.params("/foo") .should be_nil } end describe :match do example { subject.should_not match("/foo/bar") } example { subject.should match("/fox/bar") } example { subject.should_not match("/foo") } end describe :expand do example { subject.should_not respond_to(:expand) } example { expect { subject.expand(name: 'bar') }.to raise_error(NotImplementedError) } end end describe :inspect do let(:sinatra) { Mustermann.new('x') } let(:shell) { Mustermann.new('x', type: :shell) } let(:identity) { Mustermann.new('x', type: :identity) } example { (sinatra | shell) .inspect.should be == '(sinatra:"x" | shell:"x")' } example { (sinatra ^ shell) .inspect.should be == '(sinatra:"x" ^ shell:"x")' } example { (sinatra | shell | identity) .inspect.should be == '(sinatra:"x" | shell:"x" | identity:"x")' } example { (sinatra | shell & identity) .inspect.should be == '(sinatra:"x" | (shell:"x" & identity:"x"))' } end describe :simple_inspect do let(:sinatra) { Mustermann.new('x') } let(:shell) { Mustermann.new('x', type: :shell) } let(:identity) { Mustermann.new('x', type: :identity) } example { (sinatra | shell) .simple_inspect.should be == 'sinatra:"x" | shell:"x"' } example { (sinatra | shell | identity) .simple_inspect.should be == 'sinatra:"x" | shell:"x" | identity:"x"' } example { (sinatra | shell & identity) .simple_inspect.should be == 'sinatra:"x" | (shell:"x" & identity:"x")' } end describe :pretty_print do let(:sinatra) { Mustermann.new('x') } let(:shell) { Mustermann.new('x', type: :shell) } let(:identity) { Mustermann.new('x', type: :identity) } example { PP.pp(sinatra | shell, +'').chomp.should be == '(sinatra:"x" | shell:"x")' } example { PP.pp(sinatra ^ shell, +'').chomp.should be == '(sinatra:"x" ^ shell:"x")' } example { PP.pp(sinatra | shell | identity, +'').chomp.should be == '(sinatra:"x" | shell:"x" | identity:"x")' } example { PP.pp(sinatra | shell & identity, +'').chomp.should be == '(sinatra:"x" | (shell:"x" & identity:"x"))' } end end mustermann-4.0.0/mustermann/spec/concat_spec.rb000066400000000000000000000127151517364653100216560ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann' describe Mustermann::Concat do describe Mustermann::Concat::Native do context "sinatra + sinatra" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "/{foo}/{bar}" } end context "sinatra + string" do subject(:pattern) { Mustermann.new("/:foo") + "/:bar" } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "/{foo}/\\:bar" } end context "regular + regular" do subject(:pattern) { Mustermann.new(/foo/) + Mustermann.new(/bar/) } its(:class) { should be == Mustermann::Regular } its(:to_s) { should be == "foobar" } end context "sinatra + rails" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails) } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "/{foo}/{bar}" } end context "sinatra + flask" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/", type: :flask) } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "/{foo}/{bar}" } end context "sinatra + flask (typed)" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/", type: :flask) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/:foo" + flask:"/")' } end context "sinatra + sinatra (different options)" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", uri_decode: false) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/:foo" + sinatra:"/:bar")' } end context "sinatra + rails (different options)" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails, uri_decode: false) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/:foo" + rails:"/:bar")' } end context "sinatra + rails (different options) + sinatra" do subject(:pattern) { Mustermann.new("/:foo") + Mustermann.new("/:bar", type: :rails, uri_decode: false) + Mustermann.new("/:baz") } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/:foo" + rails:"/:bar" + sinatra:"/:baz")' } end context "sinatra + (sinatra + regular)" do subject(:pattern) { Mustermann.new("/foo") + (Mustermann.new("/bar") + Mustermann.new(/baz/)) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/foo/bar" + regular:"baz")' } end context "sinatra + (sinatra + rails (different options) + sinatra)" do subject(:pattern) { Mustermann.new("/foo") + (Mustermann.new("/bar") + Mustermann.new("/baz", type: :rails, uri_decode: false) + Mustermann.new("/boo")) } its(:class) { should be == Mustermann::Concat } its(:to_s) { should be == '(sinatra:"/foo/bar" + hybrid:"/baz/boo")' } end context "rails with Integer capture + sinatra with Symbol capture" do subject(:pattern) { Mustermann.new("/:a", type: :rails, capture: Integer) + Mustermann.new("/:b", capture: Symbol) } its(:class) { should be == Mustermann::Hybrid } its(:to_s) { should be == '/{a}/{b}' } example { subject.params("/42/foo").should be == { "a" => 42, "b" => :foo } } example { subject.params("/foo/bar").should be_nil } end end subject(:pattern) { Mustermann::Concat.new("/:foo", "/:bar") } describe :=== do example { (pattern === "/foo/bar") .should be true } example { (pattern === "/foo/bar/") .should be false } example { (pattern === "/foo") .should be false } end describe :match do it { should match("/foo/bar").capturing(foo: "foo", bar: "bar") } it { should_not match("/foo/bar/") } it { should_not match("/foo/") } end describe :params do example { pattern.params("/foo/bar") .should be == { "foo" => "foo", "bar" => "bar" }} example { pattern.params("/foo/bar/") .should be_nil } example { pattern.params("/foo") .should be_nil } end describe :peek do example { pattern.peek("/foo/bar/baz") .should be == "/foo/bar" } example { pattern.peek("/foo") .should be_nil } end describe :peek_params do example { pattern.peek_params("/foo/bar/baz") .should be == [{ "foo" => "foo", "bar" => "bar" }, 8]} example { pattern.peek_params("/foo") .should be_nil } end describe :peek_match do example { pattern.peek_match("/foo/bar/baz").to_s .should be == "/foo/bar" } example { pattern.peek_match("/foo") .should be_nil } end describe :peek_size do example { pattern.peek_size("/foo/bar/baz") .should be == 8 } example { pattern.peek_size("/foo") .should be_nil } end describe :expand do it { should expand(foo: :bar, bar: :foo) .to('/bar/foo') } it { should expand(:append, foo: :bar, bar: :foo, baz: 42) .to('/bar/foo?baz=42') } it { should_not expand(foo: :bar) } end describe :to_templates do subject(:pattern) { Mustermann::Concat.new("/:foo|:bar", "(/:baz)?") } it { should generate_template("/{foo}/{baz}") } it { should generate_template("{bar}/{baz}") } it { should generate_template("/{foo}") } it { should generate_template("{bar}") } end end mustermann-4.0.0/mustermann/spec/equality_map_spec.rb000066400000000000000000000023301517364653100230710ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/equality_map' RSpec.describe Mustermann::EqualityMap do before { GC.disable } after { GC.enable } describe :fetch do subject { Mustermann::EqualityMap.new } specify 'with existing entry' do next if subject.is_a? Hash subject.fetch("foo") { "foo" } result = subject.fetch("foo") { "bar" } expect(result).to be == "foo" end specify 'with GC-removed entry' do next if subject.is_a? Hash subject.fetch(String.new('foo')) { "foo" } expect(subject.map).to receive(:[]).and_return(nil) result = subject.fetch(String.new('foo')) { "bar" } expect(result).to be == "bar" end specify 'allows a frozen key and value' do next if subject.is_a? Hash key = "foo".freeze value = "bar".freeze subject.fetch(key) { value } result = subject.fetch("foo".dup) { raise "not executed" } expect(result).to be == value expect(result).not_to equal value end specify 'allows only a single argument to be compatible with Hash#fetch' do expect { subject.fetch("foo", "bar", "baz") { "value" } }.to raise_error(ArgumentError) end end end mustermann-4.0.0/mustermann/spec/expander_spec.rb000066400000000000000000000165061517364653100222170ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/expander' describe Mustermann::Expander do it 'expands a pattern' do expander = Mustermann::Expander.new("/:foo.jpg") expander.expand(foo: 42).should be == "/42.jpg" end it 'expands multiple patterns' do expander = Mustermann::Expander.new << "/:foo.:ext" << "/:foo" expander.expand(foo: 42, ext: 'jpg').should be == "/42.jpg" expander.expand(foo: 23).should be == "/23" end it 'supports setting pattern options' do expander = Mustermann::Expander.new(type: :rails) << "/:foo(.:ext)" << "/:bar" expander.expand(foo: 42, ext: 'jpg').should be == "/42.jpg" expander.expand(foo: 42).should be == "/42" end it 'supports combining different pattern styles' do expander = Mustermann::Expander.new << Mustermann.new("/:foo(.:ext)", type: :rails) << Mustermann.new("/:bar", type: :sinatra) expander.expand(foo: 'pony', ext: 'jpg').should be == '/pony.jpg' expander.expand(bar: 23).should be == "/23" end it 'ignores nil values' do expander = Mustermann::Expander.new << Mustermann.new("/:foo(.:ext)?") expander.expand(foo: 'pony', ext: nil).should be == '/pony' end it 'supports splat' do expander = Mustermann::Expander.new << Mustermann.new("/foo/*/baz") expander.expand(splat: 'bar').should be == '/foo/bar/baz' end it 'supports multiple splats' do expander = Mustermann::Expander.new << Mustermann.new("/foo/*/bar/*") expander.expand(splat: [123, 456]).should be == '/foo/123/bar/456' end it 'supports identity patterns' do expander = Mustermann::Expander.new('/:foo', type: :identity) expander.expand.should be == '/:foo' end it 'supports adding an expander to another expander' do inner = Mustermann::Expander.new('/:foo') expander = Mustermann::Expander.new << inner expander.expand(foo: 'bar').should be == '/bar' end describe :additional_values do context "illegal value" do example { expect { Mustermann::Expander.new(additional_values: :foo) }.to raise_error(ArgumentError) } example { expect { Mustermann::Expander.new('/').expand(:foo, a: 10) }.to raise_error(ArgumentError) } end context :raise do subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :raise) } example { expander.expand(a: ?a).should be == '/a' } example { expect { expander.expand(a: ?a, b: ?b) }.to raise_error(Mustermann::ExpandError) } example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) } end context :ignore do subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :ignore) } example { expander.expand(a: ?a).should be == '/a' } example { expander.expand(a: ?a, b: ?b).should be == '/a' } example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) } example { expect { expander.expand(b: ?b, c: []) }.to raise_error(Mustermann::ExpandError) } example { expect { expander.expand(b: ?b, c: [], d: ?d) }.to raise_error(Mustermann::ExpandError) } end context :append do subject(:expander) { Mustermann::Expander.new('/:a', additional_values: :append) } example { expander.expand(a: ?a).should be == '/a' } example { expander.expand(a: ?a, b: ?b).should be == '/a?b=b' } example { expect { expander.expand(b: ?b) }.to raise_error(Mustermann::ExpandError) } end end describe :cast do subject(:expander) { Mustermann::Expander.new('/:a(/:b)?') } example { expander.cast { "FOOBAR" }.expand(a: "foo") .should be == "/FOOBAR" } example { expander.cast { |v| v.upcase }.expand(a: "foo") .should be == "/FOO" } example { expander.cast { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/BAR" } example { expander.cast(:a) { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/bar" } example { expander.cast(:a, :b) { |v| v.upcase }.expand(a: "foo", b: "bar") .should be == "/FOO/BAR" } example { expander.cast(Integer) { |k,v| "#{k}_#{v}" }.expand(a: "foo", b: 42) .should be == "/foo/b_42" } example do expander.cast(:a) { |v| v.upcase } expander.cast(:b) { |v| v.downcase } expander.expand(a: "fOo", b: "bAr").should be == "/FOO/bar" end end describe :== do example { Mustermann::Expander.new('/foo') .should be == Mustermann::Expander.new('/foo') } example { Mustermann::Expander.new('/foo') .should_not be == Mustermann::Expander.new('/bar') } example { Mustermann::Expander.new('/foo', type: :rails) .should be == Mustermann::Expander.new('/foo', type: :rails) } example { Mustermann::Expander.new('/foo', type: :rails) .should_not be == Mustermann::Expander.new('/foo', type: :sinatra) } end describe :hash do example { Mustermann::Expander.new('/foo') .hash.should be == Mustermann::Expander.new('/foo').hash } example { Mustermann::Expander.new('/foo') .hash.should_not be == Mustermann::Expander.new('/bar').hash } example { Mustermann::Expander.new('/foo', type: :rails) .hash.should be == Mustermann::Expander.new('/foo', type: :rails).hash } example { Mustermann::Expander.new('/foo', type: :rails) .hash.should_not be == Mustermann::Expander.new('/foo', type: :sinatra).hash } end describe :eql? do example { Mustermann::Expander.new('/foo') .should be_eql Mustermann::Expander.new('/foo') } example { Mustermann::Expander.new('/foo') .should_not be_eql Mustermann::Expander.new('/bar') } example { Mustermann::Expander.new('/foo', type: :rails) .should be_eql Mustermann::Expander.new('/foo', type: :rails) } example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_eql Mustermann::Expander.new('/foo', type: :sinatra) } end describe :inspect do example('single pattern') do Mustermann::Expander.new('/:name').inspect.should be == '#' end example('multiple patterns') do Mustermann::Expander.new('/:name', '/:name.:ext').inspect.should be == '#' end example('empty expander') do Mustermann::Expander.new.inspect.should be == '#' end end describe :pretty_print do example('single pattern') do PP.pp(Mustermann::Expander.new('/:name'), +'').chomp.should be == '#' end example('multiple patterns') do PP.pp(Mustermann::Expander.new('/:name', '/:name.:ext'), +'').chomp.should be == '#' end example('empty expander') do PP.pp(Mustermann::Expander.new, +'').chomp.should be == '#' end end describe :equal? do example { Mustermann::Expander.new('/foo') .should_not be_equal Mustermann::Expander.new('/foo') } example { Mustermann::Expander.new('/foo') .should_not be_equal Mustermann::Expander.new('/bar') } example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_equal Mustermann::Expander.new('/foo', type: :rails) } example { Mustermann::Expander.new('/foo', type: :rails) .should_not be_equal Mustermann::Expander.new('/foo', type: :sinatra) } end end mustermann-4.0.0/mustermann/spec/hybrid_spec.rb000066400000000000000000000101671517364653100216670ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/hybrid' describe Mustermann::Hybrid do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/:name' do it { should match('/alice') .capturing name: 'alice' } it { should match('/foo.bar') .capturing name: 'foo.bar' } it { should_not match('/foo/bar') } it { should_not match('/') } it { should generate_template('/{name}') } end pattern '/:foo/:bar' do it { should match('/hello/world') .capturing foo: 'hello', bar: 'world' } it { should generate_template('/{foo}/{bar}') } end pattern '/*' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } end pattern '/*foo' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should generate_template('/{+foo}') } end pattern '/{name}' do it { should match('/alice') .capturing name: 'alice' } it { should generate_template('/{name}') } end pattern '/{+path}' do it { should match('/a/b/c') .capturing path: 'a/b/c' } it { should generate_template('/{+path}') } end # Groups without | are implicitly optional (Rails behavior) pattern '/fo(o)' do it { should match('/foo') } it { should match('/fo') } it { should_not match('/') } it { should_not match('/f') } it { should generate_template('/foo') } it { should generate_template('/fo') } end pattern '/scope(/nested)' do it { should match('/scope/nested') } it { should match('/scope') } it { should_not match('/scope/') } end pattern '/:file(.:ext)' do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should generate_template('/{file}') } it { should generate_template('/{file}.{ext}') } end pattern '/:user(@:host)' do it { should match('/alice') .capturing user: 'alice', host: nil } it { should match('/alice@foo') .capturing user: 'alice', host: 'foo' } end pattern '/:controller(/:action(/:id))' do it { should match('/posts') .capturing controller: 'posts', action: nil, id: nil } it { should match('/posts/show') .capturing controller: 'posts', action: 'show', id: nil } it { should match('/posts/show/1') .capturing controller: 'posts', action: 'show', id: '1' } end # Groups with | are NOT implicitly optional pattern '/(foo|bar)' do it { should match('/foo') } it { should match('/bar') } it { should_not match('/') } it { should generate_template('/foo') } it { should generate_template('/bar') } end pattern '/scope/(a|b)' do it { should match('/scope/a') } it { should match('/scope/b') } it { should_not match('/scope/') } it { should_not match('/scope') } end pattern '/(:a/:b|:c)' do it { should match('/foo') .capturing c: 'foo' } it { should match('/foo/bar') .capturing a: 'foo', b: 'bar' } it { should_not match('/') } end # Explicit ? makes groups with | optional pattern '/(foo|bar)?' do it { should match('/foo') } it { should match('/bar') } it { should match('/') } end pattern '/scope/(a|b)?' do it { should match('/scope/a') } it { should match('/scope/b') } it { should match('/scope/') } it { should_not match('/scope') } end end mustermann-4.0.0/mustermann/spec/identity_spec.rb000066400000000000000000000073621517364653100222420ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/identity' describe Mustermann::Identity do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } it { should generate_template('') } it { should expand.to('') } it { should expand(:ignore, a: 10).to('') } it { should expand(:append, a: 10).to('?a=10') } it { should_not expand(:raise, a: 10) } it { should_not expand(a: 10) } example do match = pattern.match('') match.should be_a(Mustermann::Match) match.to_s.should be == '' end end pattern '/' do it { should match('/') } it { should_not match('/foo') } example { pattern.params('/').should be == {} } example { pattern.params('').should be_nil } it { should generate_template('/') } it { should expand.to('/') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should match('/foo%2Fbar') } it { should match('/foo%2fbar') } end pattern '/:foo' do it { should match('/:foo') } it { should match('/%3Afoo') } it { should_not match('/foo') } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/:foo') } it { should expand.to('/:foo') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } it { should generate_template('/path%20with%20spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context "peeking" do subject(:pattern) { Mustermann::Identity.new("foo bar") } describe :peek_size do example { pattern.peek_size("foo bar blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar blah") .should be == "foo%20bar".size } example { pattern.peek_size("foobar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar blah").to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar blah").to_s .should be == "foo%20bar" } example { pattern.peek_match("foobar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar blah") .should be == [{}, "foo bar".size] } example { pattern.peek_params("foo%20bar blah") .should be == [{}, "foo%20bar".size] } example { pattern.peek_params("foobar") .should be_nil } end end end mustermann-4.0.0/mustermann/spec/match_spec.rb000066400000000000000000000067251517364653100215070ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/match' require 'mustermann/sinatra' describe Mustermann::Match do let(:pattern) { Mustermann::Sinatra.new('/:name') } subject(:match) { pattern.match('/foo') } # For a full match, string and to_s are both the matched string. its(:string) { should be == '/foo' } its(:to_s) { should be == '/foo' } its(:params) { should be == { 'name' => 'foo' } } its(:post_match) { should be == '' } its(:pre_match) { should be == '' } its(:to_h) { should be == { 'name' => 'foo' } } its(:captures) { should be == ['foo'] } its(:named_captures) { should be == { 'name' => 'foo' } } its(:names) { should be == ['name'] } # For a peek match, #string is the full searched string and #to_s is the matched portion. describe 'peek match' do subject(:peek) { pattern.peek_match('/foo/bar') } its(:string) { should be == '/foo/bar' } its(:to_s) { should be == '/foo' } end describe :[] do example('symbol key') { match[:name].should be == 'foo' } example('string key') { match['name'].should be == 'foo' } example('integer key') { match[0].should be == 'foo' } example('range key') { match[0..0].should be == ['foo'] } example('invalid key') do expect { match[1.0] }.to raise_error(ArgumentError, /key must be/) end end describe :values_at do example { match.values_at(:name, 'name').should be == ['foo', 'foo'] } end describe :deconstruct_keys do example { match.deconstruct_keys([:name]).should be == { name: 'foo' } } end describe :eql? do example { match.eql?(pattern.match('/foo')).should be true } example { match.eql?(pattern.match('/bar')).should be false } example { match.eql?('not a match').should be false } end describe :hash do example { match.hash.should be == pattern.match('/foo').hash } example { match.hash.should_not be == pattern.match('/bar').hash } end describe :== do example { (match == pattern.match('/foo')).should be true } example { (match == pattern.match('/bar')).should be false } end describe :inspect do example('with a capture') { match.inspect.should be == '#' } example('without captures') do Mustermann::Sinatra.new('/hello').match('/hello').inspect.should be == '#' end example('multiple captures') do Mustermann::Sinatra.new('/:a/:b').match('/x/y').inspect.should be == '#' end example('type-converted capture shows converted value') do Mustermann::Sinatra.new('/:n', capture: Integer).match('/42').inspect.should be == '#' end end describe :pretty_print do example('with a capture') { PP.pp(match, +'').chomp.should be == '#' } example('without captures') do PP.pp(Mustermann::Sinatra.new('/hello').match('/hello'), +'').chomp.should be == '#' end end describe 'invalid constructor arguments' do example('bad first argument') do expect { Mustermann::Match.new('not a pattern', '/foo') }.to raise_error(ArgumentError, /first argument/) end example('bad second argument') do expect { Mustermann::Match.new(pattern, 42) }.to raise_error(ArgumentError, /second argument/) end end end mustermann-4.0.0/mustermann/spec/mustermann_spec.rb000066400000000000000000000072301517364653100225740ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann' describe Mustermann do describe :new do context "string argument" do example { Mustermann.new('') .should be_a(Mustermann::Sinatra) } example { Mustermann.new('', type: :identity) .should be_a(Mustermann::Identity) } example { Mustermann.new('', type: :rails) .should be_a(Mustermann::Rails) } example { Mustermann.new('', type: :shell) .should be_a(Mustermann::Shell) } example { Mustermann.new('', type: :sinatra) .should be_a(Mustermann::Sinatra) } example { Mustermann.new('', type: :simple) .should be_a(Mustermann::Simple) } example { Mustermann.new('', type: :template) .should be_a(Mustermann::Template) } example { expect { Mustermann.new('', foo: :bar) }.to raise_error(ArgumentError, "unsupported option :foo for Mustermann::Sinatra") } example { expect { Mustermann.new('', type: :ast) }.to raise_error(ArgumentError, "unsupported type :ast (cannot load such file -- mustermann/ast)") } end context "pattern argument" do subject(:pattern) { Mustermann.new('') } example { Mustermann.new(pattern).should be == pattern } example { Mustermann.new(pattern, type: :rails).should be_a(Mustermann::Sinatra) } end context "regexp argument" do example { Mustermann.new(//) .should be_a(Mustermann::Regular) } example { Mustermann.new(//, type: :rails) .should be_a(Mustermann::Regular) } end context "argument implementing #to_pattern" do subject(:pattern) { Class.new { def to_pattern(**o) Mustermann.new('foo', **o) end }.new } example { Mustermann.new(pattern) .should be_a(Mustermann::Sinatra) } example { Mustermann.new(pattern, type: :rails) .should be_a(Mustermann::Rails) } example { Mustermann.new(pattern).to_s.should be == 'foo' } end context "multiple arguments" do example { Mustermann.new(':a', ':b/:a') .should be_a(Mustermann::Composite) } example { Mustermann.new(':a', ':b/:a').patterns.first .should be_a(Mustermann::Sinatra) } example { Mustermann.new(':a', ':b/:a').operator .should be == :| } example { Mustermann.new(':a', ':b/:a', operator: :&).operator .should be == :& } example { Mustermann.new(':a', ':b/:a', greedy: true) .should be_a(Mustermann::Composite) } example { Mustermann.new('/foo', ':bar') .should be_a(Mustermann::Sinatra) } example { Mustermann.new('/foo', ':bar').to_s .should be == "/foo|{bar}" } end context "invalid arguments" do it "raise a TypeError for unsupported types" do expect { Mustermann.new(10) }.to raise_error(TypeError, /(Integer|Fixnum) can't be coerced into Mustermann::Pattern/) end end end describe :[] do example { Mustermann[:identity] .should be == Mustermann::Identity } example { Mustermann[:rails] .should be == Mustermann::Rails } example { Mustermann[:shell] .should be == Mustermann::Shell } example { Mustermann[:sinatra] .should be == Mustermann::Sinatra } example { Mustermann[:simple] .should be == Mustermann::Simple } example { Mustermann[:template] .should be == Mustermann::Template } example { expect { Mustermann[:ast] }.to raise_error(ArgumentError, "unsupported type :ast (cannot load such file -- mustermann/ast)") } example { expect { Mustermann[:expander] }.to raise_error(ArgumentError, "unsupported type :expander") } end describe :=== do example { Mustermann.should be === Mustermann.new("") } end end mustermann-4.0.0/mustermann/spec/pattern_spec.rb000066400000000000000000000056161517364653100220660ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/pattern' require 'mustermann/sinatra' require 'mustermann/rails' describe Mustermann::Pattern do describe :=== do it 'raises a NotImplementedError when used directly' do expect { Mustermann::Pattern.new("") === "" }.to raise_error(NotImplementedError) end end describe :initialize do it 'raises an ArgumentError for unknown options' do expect { Mustermann::Pattern.new("", foo: :bar) }.to raise_error(ArgumentError) end it 'does not complain about unknown options if ignore_unknown_options is enabled' do expect { Mustermann::Pattern.new("", foo: :bar, ignore_unknown_options: true) }.not_to raise_error end end describe :respond_to? do subject(:pattern) { Mustermann::Pattern.new("") } it { should_not respond_to(:expand) } it { should_not respond_to(:to_templates) } it { expect { pattern.expand } .to raise_error(NotImplementedError) } it { expect { pattern.to_templates } .to raise_error(NotImplementedError) } end describe :inspect do example { Mustermann::Sinatra.new('/:name').inspect.should be == '#' } example { Mustermann::Sinatra.new('/foo/bar').inspect.should be == '#' } example { Mustermann::Rails.new('/:name').inspect.should be == '#' } end describe :pretty_print do example { PP.pp(Mustermann::Sinatra.new('/:name'), +'').chomp.should be == '#' } example { PP.pp(Mustermann::Rails.new('/foo'), +'').chomp.should be == '#' } end describe :== do example { Mustermann::Pattern.new('/foo') .should be == Mustermann::Pattern.new('/foo') } example { Mustermann::Pattern.new('/foo') .should_not be == Mustermann::Pattern.new('/bar') } example { Mustermann::Sinatra.new('/foo') .should be == Mustermann::Sinatra.new('/foo') } example { Mustermann::Rails.new('/foo') .should_not be == Mustermann::Sinatra.new('/foo') } end describe :eql? do example { Mustermann::Pattern.new('/foo') .should be_eql Mustermann::Pattern.new('/foo') } example { Mustermann::Pattern.new('/foo') .should_not be_eql Mustermann::Pattern.new('/bar') } example { Mustermann::Sinatra.new('/foo') .should be_eql Mustermann::Sinatra.new('/foo') } example { Mustermann::Rails.new('/foo') .should_not be_eql Mustermann::Sinatra.new('/foo') } end describe :equal? do example { Mustermann::Pattern.new('/foo') .should be_equal Mustermann::Pattern.new('/foo') } example { Mustermann::Pattern.new('/foo') .should_not be_equal Mustermann::Pattern.new('/bar') } example { Mustermann::Sinatra.new('/foo') .should be_equal Mustermann::Sinatra.new('/foo') } example { Mustermann::Rails.new('/foo') .should_not be_equal Mustermann::Sinatra.new('/foo') } end end mustermann-4.0.0/mustermann/spec/rails_spec.rb000066400000000000000000000574321517364653100215260ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/rails' describe Mustermann::Rails do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should expand.to('') } it { should_not expand(a: 1) } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } it { should expand.to('/') } it { should_not expand(a: 1) } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } it { should expand.to('/foo') } it { should_not expand(a: 1) } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } it { should expand.to('/foo/bar') } it { should_not expand(a: 1) } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } example { pattern.params('/foo') .should be == {"foo" => "foo"} } example { pattern.params('/f%20o') .should be == {"foo" => "f o"} } example { pattern.params('').should be_nil } it { should expand(foo: 'bar') .to('/bar') } it { should expand(foo: 'b r') .to('/b%20r') } it { should expand(foo: 'foo/bar') .to('/foo%2Fbar') } it { should_not expand(foo: 'foo', bar: 'bar') } it { should_not expand(bar: 'bar') } it { should_not expand } it { should generate_template('/{foo}') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } it { should expand.to("/f%C3%B6%C3%B6") } it { should_not expand(a: 1) } end pattern "/:foo/:bar" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} } example { pattern.params('').should be_nil } it { should expand(foo: 'foo', bar: 'bar').to('/foo/bar') } it { should_not expand(foo: 'foo') } it { should_not expand(bar: 'bar') } it { should generate_template('/{foo}/{bar}') } end pattern '/hello/:person' do it { should match('/hello/Frank').capturing person: 'Frank' } it { should expand(person: 'Frank') .to '/hello/Frank' } it { should expand(person: 'Frank?') .to '/hello/Frank%3F' } it { should generate_template('/hello/{person}') } end pattern '/?:foo?/?:bar?' do it { should match('/?hello?/?world?').capturing foo: 'hello', bar: 'world' } it { should_not match('/hello/world/') } it { should expand(foo: 'hello', bar: 'world').to('/%3Fhello%3F/%3Fworld%3F') } it { should generate_template('/?{foo}?/?{bar}?') } end pattern '/:foo_bar' do it { should match('/hello').capturing foo_bar: 'hello' } it { should expand(foo_bar: 'hello').to('/hello') } it { should generate_template('/{foo_bar}') } end pattern '/*foo' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should expand .to('/') } it { should expand(foo: nil) .to('/') } it { should expand(foo: '') .to('/') } it { should expand(foo: 'foo') .to('/foo') } it { should expand(foo: 'foo/bar') .to('/foo/bar') } it { should expand(foo: 'foo.bar') .to('/foo.bar') } it { should generate_template('/{+foo}') } end pattern '/*splat' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } end pattern '/:foo/*bar' do it { should match("/foo/bar/baz") .capturing foo: 'foo', bar: 'bar/baz' } it { should match("/foo%2Fbar/baz") .capturing foo: 'foo%2Fbar', bar: 'baz' } it { should match("/foo/") .capturing foo: 'foo', bar: '' } it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', bar: 'h%20a%20y' } it { should_not match('/foo') } it { should expand(foo: 'foo') .to('/foo/') } it { should expand(foo: 'foo', bar: 'bar') .to('/foo/bar') } it { should expand(foo: 'foo', bar: 'foo/bar') .to('/foo/foo/bar') } it { should expand(foo: 'foo/bar', bar: 'bar') .to('/foo%2Fbar/bar') } it { should generate_template('/{foo}/{+bar}') } end pattern '/test$/' do it { should match('/test$/') } it { should expand.to('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } it { should expand.to('/te+st/') } end pattern "/path with spaces" do it { should match('/path%20with%20spaces') } it { should match('/path%2Bwith%2Bspaces') } it { should match('/path+with+spaces') } it { should expand.to('/path%20with%20spaces') } it { should generate_template('/path%20with%20spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/*a/:foo/*b/*c' do it { should match('/bar/foo/bling/baz/boom').capturing a: 'bar', foo: 'foo', b: 'bling', c: 'baz/boom' } example { pattern.params('/bar/foo/bling/baz/boom').should be == { "a" => 'bar', "foo" => 'foo', "b" => 'bling', "c" => 'baz/boom' } } it { should expand(a: 'bar', foo: 'foo', b: 'bling', c: 'baz/boom').to('/bar/foo/bling/baz/boom') } it { should generate_template('/{+a}/{foo}/{+b}/{+c}') } end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/:file.:ext' do it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%E6%AD%A3%2Ejpg') .capturing file: 'pony%E6%AD%A3', ext: 'jpg' } it { should match('/pony%e6%ad%a3%2ejpg') .capturing file: 'pony%e6%ad%a3', ext: 'jpg' } it { should match('/pony正%2Ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正%2ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正..jpg') .capturing file: 'pony正.', ext: 'jpg' } it { should_not match('/.jpg') } it { should expand(file: 'pony', ext: 'jpg').to('/pony.jpg') } end pattern '/:a(x)' do it { should match('/a') .capturing a: 'a' } it { should match('/xa') .capturing a: 'xa' } it { should match('/axa') .capturing a: 'axa' } it { should match('/ax') .capturing a: 'a' } it { should match('/axax') .capturing a: 'axa' } it { should match('/axaxx') .capturing a: 'axax' } it { should expand(a: 'x').to('/xx') } it { should expand(a: 'a').to('/ax') } it { should generate_template('/{a}x') } it { should generate_template('/{a}') } end pattern '/:user(@:host)' do it { should match('/foo@bar') .capturing user: 'foo', host: 'bar' } it { should match('/foo.foo@bar') .capturing user: 'foo.foo', host: 'bar' } it { should match('/foo@bar.bar') .capturing user: 'foo', host: 'bar.bar' } it { should expand(user: 'foo') .to('/foo') } it { should expand(user: 'foo', host: 'bar') .to('/foo@bar') } it { should generate_template('/{user}') } it { should generate_template('/{user}@{host}') } end pattern '/:file(.:ext)' do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' } it { should match('/pony.') .capturing file: 'pony.' } it { should_not match('/.jpg') } it { should expand(file: 'pony') .to('/pony') } it { should expand(file: 'pony', ext: 'jpg') .to('/pony.jpg') } it { should generate_template('/{file}') } it { should generate_template('/{file}.{ext}') } end pattern '/:id/test.bar' do it { should match('/3/test.bar') .capturing id: '3' } it { should match('/2/test.bar') .capturing id: '2' } it { should match('/2E/test.bar') .capturing id: '2E' } it { should match('/2e/test.bar') .capturing id: '2e' } it { should match('/%2E/test.bar') .capturing id: '%2E' } end pattern '/10/:id' do it { should match('/10/test') .capturing id: 'test' } it { should match('/10/te.st') .capturing id: 'te.st' } end pattern '/10.1/:id' do it { should match('/10.1/test') .capturing id: 'test' } it { should match('/10.1/te.st') .capturing id: 'te.st' } end pattern '/:foo.:bar/:id' do it { should match('/10.1/te.st') .capturing foo: "10", bar: "1", id: "te.st" } it { should match('/10.1.2/te.st') .capturing foo: "10.1", bar: "2", id: "te.st" } end pattern '/:a/:b(.)(:c)' do it { should match('/a/b') .capturing a: 'a', b: 'b', c: nil } it { should match('/a/b.c') .capturing a: 'a', b: 'b', c: 'c' } it { should match('/a.b/c') .capturing a: 'a.b', b: 'c', c: nil } it { should match('/a.b/c.d') .capturing a: 'a.b', b: 'c', c: 'd' } it { should_not match('/a.b/c.d/e') } it { should expand(a: ?a, b: ?b) .to('/a/b.') } it { should expand(a: ?a, b: ?b, c: ?c) .to('/a/b.c') } it { should generate_template('/{a}/{b}') } it { should generate_template('/{a}/{b}.') } it { should generate_template('/{a}/{b}.{c}') } end pattern '/:a(foo:b)' do it { should match('/barfoobar') .capturing a: 'bar', b: 'bar' } it { should match('/barfoobarfoobar') .capturing a: 'barfoobar', b: 'bar' } it { should match('/bar') .capturing a: 'bar', b: nil } it { should_not match('/') } it { should expand(a: ?a) .to('/a') } it { should expand(a: ?a, b: ?b) .to('/afoob') } it { should generate_template('/{a}foo{b}') } it { should generate_template('/{a}') } it { should_not generate_template('/{a}foo') } end pattern '/fo(o)' do it { should match('/fo') } it { should match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } it { should expand.to('/foo') } end pattern '/foo?' do it { should match('/foo?') } it { should_not match('/foo\?') } it { should_not match('/fo') } it { should_not match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } it { should expand.to('/foo%3F') } end pattern '/:fOO' do it { should match('/a').capturing fOO: 'a' } end pattern '/:_X' do it { should match('/a').capturing _X: 'a' } end pattern '/:f00' do it { should match('/a').capturing f00: 'a' } end pattern '/:foo(/:bar)/:baz' do it { should match('/foo/bar/baz').capturing foo: 'foo', bar: 'bar', baz: 'baz' } it { should expand(foo: ?a, baz: ?b) .to('/a/b') } it { should expand(foo: ?a, baz: ?b, bar: ?x) .to('/a/x/b') } end pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should_not match('/') } it { should_not match('/foo') } end pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should_not match('/') } it { should_not match('/foo') } end pattern '/:foo', capture: '1' do it { should match('/1').capturing foo: '1' } it { should_not match('/') } it { should_not match('/foo') } it { should_not match('/123') } end pattern '/:foo', capture: 'a.b' do it { should match('/a.b') .capturing foo: 'a.b' } it { should match('/a%2Eb') .capturing foo: 'a%2Eb' } it { should match('/a%2eb') .capturing foo: 'a%2eb' } it { should_not match('/ab') } it { should_not match('/afb') } it { should_not match('/a1b') } it { should_not match('/a.bc') } end pattern '/:foo(/:bar)', capture: :alpha do it { should match('/abc') .capturing foo: 'abc', bar: nil } it { should match('/a/b') .capturing foo: 'a', bar: 'b' } it { should match('/a') .capturing foo: 'a', bar: nil } it { should_not match('/1/2') } it { should_not match('/a/2') } it { should_not match('/1/b') } it { should_not match('/1') } it { should_not match('/1/') } it { should_not match('/a/') } it { should_not match('//a') } end pattern '/:foo', capture: ['foo', 'bar', /\d+/] do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/') } it { should_not match('/baz') } it { should_not match('/foo1') } end pattern '/:foo:bar:baz', capture: { foo: :alpha, bar: /\d+/ } do it { should match('/ab123xy-1') .capturing foo: 'ab', bar: '123', baz: 'xy-1' } it { should match('/ab123') .capturing foo: 'ab', bar: '12', baz: '3' } it { should_not match('/123abcxy-1') } it { should_not match('/abcxy-1') } it { should_not match('/abc1') } end pattern '/:foo', capture: { foo: ['foo', 'bar', /\d+/] } do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/') } it { should_not match('/baz') } it { should_not match('/foo1') } end pattern '/:file(.:ext)', capture: { ext: ['jpg', 'png'] } do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png') .capturing file: 'pony', ext: 'png' } it { should match('/pony%2Epng') .capturing file: 'pony', ext: 'png' } it { should match('/pony%2epng') .capturing file: 'pony', ext: 'png' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' } it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: 'png' } it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil } it { should match('/pony.') .capturing file: 'pony.', ext: nil } it { should_not match('.jpg') } end pattern '/:file(:ext)', capture: { ext: ['.jpg', '.png', '.tar.gz'] } do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: '.jpg' } it { should match('/pony.png') .capturing file: 'pony', ext: '.png' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: '.jpg' } it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: '.png' } it { should match('/pony.tar.gz') .capturing file: 'pony', ext: '.tar.gz' } it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil } it { should match('/pony.') .capturing file: 'pony.', ext: nil } it { should_not match('/.jpg') } end pattern '/:a(@:b)', capture: { b: /\d+/ } do it { should match('/a') .capturing a: 'a', b: nil } it { should match('/a@1') .capturing a: 'a', b: '1' } it { should match('/a@b') .capturing a: 'a@b', b: nil } it { should match('/a@1@2') .capturing a: 'a@1', b: '2' } end pattern '/:a(b)', greedy: false do it { should match('/ab').capturing a: 'a' } end pattern '/:file(.:ext)', greedy: false do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png.jpg') .capturing file: 'pony', ext: 'png.jpg' } end pattern '/:controller(/:action(/:id(.:format)))' do it { should match('/content').capturing controller: 'content' } end pattern '/fo(o)', uri_decode: false do it { should match('/foo') } it { should match('/fo') } it { should_not match('/fo(o)') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should match('/path with spaces') } it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern "/path with spaces", space_matches_plus: false do it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context 'invalid syntax' do example 'unexpected closing parenthesis' do expect { Mustermann::Rails.new('foo)bar') }. to raise_error(Mustermann::ParseError, 'unexpected ) while parsing "foo)bar"') end example 'missing closing parenthesis' do expect { Mustermann::Rails.new('foo(bar') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar"') end end context 'invalid capture names' do example 'empty name' do expect { Mustermann::Rails.new('/:/') }. to raise_error(Mustermann::CompileError, "capture name can't be empty: \"/:/\"") end example 'named splat' do expect { Mustermann::Rails.new('/:splat/') }. to raise_error(Mustermann::CompileError, "capture name can't be splat: \"/:splat/\"") end example 'named captures' do expect { Mustermann::Rails.new('/:captures/') }. to raise_error(Mustermann::CompileError, "capture name can't be captures: \"/:captures/\"") end example 'with capital letter' do expect { Mustermann::Rails.new('/:Foo/') }. to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:Foo/\"") end example 'with integer' do expect { Mustermann::Rails.new('/:1a/') }. to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:1a/\"") end example 'same name twice' do expect { Mustermann::Rails.new('/:foo(/:bar)/:bar') }. to raise_error(Mustermann::CompileError, "can't use the same capture name twice: \"/:foo(/:bar)/:bar\"") end end context 'Regexp compatibility' do describe :=== do example('non-matching') { Mustermann::Rails.new("/") .should_not be === '/foo' } example('matching') { Mustermann::Rails.new("/:foo") .should be === '/foo' } end describe :=~ do example('non-matching') { Mustermann::Rails.new("/") .should_not be =~ '/foo' } example('matching') { Mustermann::Rails.new("/:foo") .should be =~ '/foo' } context 'String#=~' do example('non-matching') { "/foo".should_not be =~ Mustermann::Rails.new("/") } example('matching') { "/foo".should be =~ Mustermann::Rails.new("/:foo") } end end describe :to_regexp do example('empty pattern') { Mustermann::Rails.new('').to_regexp.should be == /\A(?-mix:)\Z/ } context 'Regexp.try_convert' do example('empty pattern') { Regexp.try_convert(Mustermann::Rails.new('')).should be == /\A(?-mix:)\Z/ } end end end context 'Proc compatibility' do describe :to_proc do example { Mustermann::Rails.new("/").to_proc.should be_a(Proc) } example('non-matching') { Mustermann::Rails.new("/") .to_proc.call('/foo').should be == false } example('matching') { Mustermann::Rails.new("/:foo") .to_proc.call('/foo').should be == true } end end context "peeking" do subject(:pattern) { Mustermann::Rails.new(":name") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end context 'version compatibility' do context '2.3' do pattern '(foo)', version: '2.3' do it { should_not match("") } it { should_not match("foo") } it { should match("(foo)") } end pattern '\\:name', version: '2.3' do it { should match('%5cfoo').capturing(name: 'foo') } end end context '3.0' do pattern '(foo)', version: '3.0' do it { should match("") } it { should match("foo") } end pattern '\\:name', version: '3.0' do it { should match(':name') } it { should_not match(':foo') } end end context '3.2' do pattern '\\:name', version: '3.2' do it { should match('%5cfoo').capturing(name: 'foo') } end end context '4.0' do pattern '\\:name', version: '4.0' do it { should match('foo').capturing(name: 'foo') } end end context '4.2' do pattern '\\:name', version: '4.2' do it { should match(':name') } it { should_not match(':foo') } end end context '5.0' do pattern 'foo|bar', version: '5.0' do it { should match('foo') } it { should match('bar') } end end context '8.1' do pattern 'foo|bar', version: '8.1' do it { should match('foo') } it { should match('bar') } end end end end mustermann-4.0.0/mustermann/spec/regexp_based_spec.rb000066400000000000000000000004251517364653100230320ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/regexp_based' describe Mustermann::RegexpBased do it 'raises a NotImplementedError when used directly' do expect { Mustermann::RegexpBased.new("") === "" }.to raise_error(NotImplementedError) end end mustermann-4.0.0/mustermann/spec/regular_spec.rb000066400000000000000000000140631517364653100220460ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'timeout' require 'mustermann/regular' describe Mustermann::Regular do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should_not respond_to(:expand) } it { should_not respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern '/(?.*)' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } end context 'with Regexp::EXTENDED' do let(:pattern) { %r{ \/compare\/ # match any URL beginning with \/compare\/ (.+) # extract the full path (including any directories) \/ # match the final slash ([^.]+) # match the first SHA1 \.{2,3} # match .. or ... (.+) # match the second SHA1 }x } example { expect { Timeout.timeout(1){ Mustermann::Regular.new(pattern) }}.not_to raise_error } it { expect(Mustermann::Regular.new(pattern)).to match('/compare/foo/bar..baz') } end describe :check_achnors do context 'raises on anchors' do example { expect { Mustermann::Regular.new('^foo') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('foo$') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('\Afoo') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('foo\Z') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('foo\z') }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/^foo/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/foo$/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/\Afoo/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/foo\Z/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new(/foo\z/) }.to raise_error(Mustermann::CompileError) } example { expect { Mustermann::Regular.new('[^f]') }.not_to raise_error } example { expect { Mustermann::Regular.new('\\\A') }.not_to raise_error } example { expect { Mustermann::Regular.new('[[:digit:]]') }.not_to raise_error } example { expect { Mustermann::Regular.new(/[^f]/) }.not_to raise_error } example { expect { Mustermann::Regular.new(/\\A/) }.not_to raise_error } example { expect { Mustermann::Regular.new(/[[:digit:]]/) }.not_to raise_error } end context 'with check_anchors disabled' do example { expect { Mustermann::Regular.new('^foo', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('foo$', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('\Afoo', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('foo\Z', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('foo\z', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/^foo/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/foo$/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/\Afoo/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/foo\Z/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/foo\z/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('[^f]', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('\\\A', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new('[[:digit:]]', check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/[^f]/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/\\A/, check_anchors: false) }.not_to raise_error } example { expect { Mustermann::Regular.new(/[[:digit:]]/, check_anchors: false) }.not_to raise_error } end end context "peeking" do subject(:pattern) { Mustermann::Regular.new("(?[^/]+)") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end end mustermann-4.0.0/mustermann/spec/router_spec.rb000066400000000000000000000245061517364653100217300ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/router' describe Mustermann::Router do def env(method: 'GET', path: '/') { 'REQUEST_METHOD' => method, 'PATH_INFO' => path } end # ── NOT_FOUND constant ───────────────────────────────────────────────────── describe 'NOT_FOUND' do it 'is a 404 with X-Cascade: pass' do router = described_class.new status, headers, = router.call(env(path: '/missing')) expect(status).to eq 404 expect(headers['x-cascade']).to eq 'pass' end it 'is frozen' do not_found = Mustermann::Router.send(:const_get, :NOT_FOUND) expect(not_found).to be_frozen end end # ── basic routing ────────────────────────────────────────────────────────── context 'basic routing' do subject(:router) do described_class.new do get('/hello') { |_env| [200, {}, ['hello']] } end end it 'routes a matching GET request' do status, = router.call(env(path: '/hello')) expect(status).to eq 200 end it 'returns 404 for a non-matching path' do status, = router.call(env(path: '/goodbye')) expect(status).to eq 404 end it 'returns 404 for the wrong HTTP method' do status, = router.call(env(method: 'POST', path: '/hello')) expect(status).to eq 404 end end # ── match stored in env ──────────────────────────────────────────────────── context 'match stored in env' do it 'stores the match in env["mustermann.match"] by default' do router = described_class.new do get('/:name') { |e| [200, {}, [e['mustermann.match'][:name]]] } end _, _, body = router.call(env(path: '/alice')) expect(body).to eq ['alice'] end it 'exposes params from the match' do captured = nil router = described_class.new do get('/:id') { |e| captured = e['mustermann.match'].params; [200, {}, []] } end router.call(env(path: '/42')) expect(captured).to eq('id' => '42') end it 'uses a custom key when specified' do router = described_class.new(key: 'my.match') do get('/:x') { |e| [200, {}, [e['my.match'][:x]]] } end _, _, body = router.call(env(path: '/world')) expect(body).to eq ['world'] end it 'stores the match in the env hash' do router = described_class.new do get('/foo') { |_e| [200, {}, []] } end original = env(path: '/foo') router.call(original) expect(original).to have_key('mustermann.match') end end # ── fallback ─────────────────────────────────────────────────────────────── context 'fallback' do it 'uses the default NOT_FOUND when no fallback is given' do router = described_class.new status, headers, = router.call(env) expect(status).to eq 404 expect(headers['x-cascade']).to eq 'pass' end it 'returns a mutable response from the default fallback so middleware can modify it' do router = described_class.new response = router.call(env(path: '/missing')) expect(response).not_to be_frozen end it 'accepts a callable as the first positional argument' do custom = ->(_env) { [503, {}, ['down']] } router = described_class.new(custom) status, _, body = router.call(env) expect(status).to eq 503 expect(body).to eq ['down'] end it 'accepts a callable via #fallback' do router = described_class.new router.fallback ->(_env) { [410, {}, ['gone']] } status, = router.call(env) expect(status).to eq 410 end it 'accepts a block via #fallback' do router = described_class.new router.fallback { |_env| [418, {}, ["I'm a teapot"]] } status, = router.call(env) expect(status).to eq 418 end end # ── HTTP verb shorthand methods ──────────────────────────────────────────── context 'HTTP verb shorthand methods' do %w[GET POST PUT PATCH DELETE OPTIONS LINK UNLINK].each do |verb| it "defines ##{verb.downcase} routing" do router = described_class.new router.send(verb.downcase, '/test') { |_env| [200, {}, [verb]] } status, _, body = router.call(env(method: verb, path: '/test')) expect(status).to eq 200 expect(body).to eq [verb] end end end # ── #route method ────────────────────────────────────────────────────────── context '#route' do it 'adds a route with a block' do router = described_class.new router.route('GET', '/ping') { |_env| [200, {}, ['pong']] } _, _, body = router.call(env(path: '/ping')) expect(body).to eq ['pong'] end it 'adds a route with a callable target' do app = ->(_env) { [200, {}, ['app']] } router = described_class.new router.route('POST', '/submit', app) _, _, body = router.call(env(method: 'POST', path: '/submit')) expect(body).to eq ['app'] end it 'raises ArgumentError for an unknown verb' do router = described_class.new expect { router.route('BREW', '/coffee') { } }.to raise_error(ArgumentError, /unknown verb/) end it 'raises ArgumentError when neither target nor block is given' do router = described_class.new expect { router.route('GET', '/foo') }.to raise_error(ArgumentError, /target/) end end # ── DSL block at construction ────────────────────────────────────────────── context 'DSL block at construction' do it 'evaluates the block in router context' do router = described_class.new do get('/a') { |_| [200, {}, ['a']] } post('/b') { |_| [201, {}, ['b']] } end expect(router.call(env(path: '/a')).first).to eq 200 expect(router.call(env(method: 'POST', path: '/b')).first).to eq 201 end end # ── pattern options ──────────────────────────────────────────────────────── context 'pattern options' do it 'forwards options to Mustermann (e.g. type: :rails)' do router = described_class.new(type: :rails) do get('/:id(.:format)') { |e| [200, {}, [e['mustermann.match'][:id]]] } end status, _, body = router.call(env(path: '/42.json')) expect(status).to eq 200 expect(body).to eq ['42'] end end # ── #path_for URL generation ─────────────────────────────────────────────── context '#path_for' do it 'generates a path for a registered handler' do target = ->(_env) { [200, {}, []] } router = described_class.new do get '/users/:id', target end expect(router.path_for(target, id: '5')).to eq '/users/5' end it 'generates a path searching across all verbs' do target = ->(_env) { [200, {}, []] } router = described_class.new do delete '/posts/:slug', target end expect(router.path_for(target, slug: 'hello')).to eq '/posts/hello' end end # ── middleware usage ─────────────────────────────────────────────────────── context 'middleware usage' do it 'passes unmatched requests to the inner app (first positional arg)' do inner = ->(_env) { [200, {}, ['inner']] } router = described_class.new(inner) do get('/handled') { |_| [200, {}, ['handled']] } end _, _, body = router.call(env(path: '/unhandled')) expect(body).to eq ['inner'] end it 'does not call the inner app for matched routes' do inner = ->(_env) { raise 'should not be called' } router = described_class.new(inner) do get('/handled') { |_| [200, {}, ['handled']] } end expect { router.call(env(path: '/handled')) }.not_to raise_error end end # ── strict_order option ──────────────────────────────────────────────────── context 'strict_order option' do it 'forwards strict_order: true to each underlying set' do router = described_class.new(strict_order: true) sets = router.instance_variable_get(:@sets) expect(sets.values).to all(satisfy(&:strict_order?)) end it 'defaults to strict_order: false on each underlying set' do router = described_class.new sets = router.instance_variable_get(:@sets) expect(sets.values).to all(satisfy { |s| !s.strict_order? }) end it 'dispatches in insertion order when strict_order: true' do matched = nil dynamic_handler = ->(_env) { matched = :dynamic; [200, {}, []] } static_handler = ->(_env) { matched = :static; [200, {}, []] } # use_trie: true forces the trie, which would normally prefer the static # route over the dynamic one — strict_order must override that. router = described_class.new(strict_order: true, use_trie: true) do get('/:path', dynamic_handler) get('/static', static_handler) end router.call(env(path: '/static')) expect(matched).to eq :dynamic end it 'prefers static routes over dynamic without strict_order (trie default behavior)' do matched = nil dynamic_handler = ->(_env) { matched = :dynamic; [200, {}, []] } static_handler = ->(_env) { matched = :static; [200, {}, []] } router = described_class.new(use_trie: true) do get('/:path', dynamic_handler) get('/static', static_handler) end router.call(env(path: '/static')) expect(matched).to eq :static end end end mustermann-4.0.0/mustermann/spec/set_spec.rb000066400000000000000000000610211517364653100211740ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/set' describe Mustermann::Set do # Run every example against both matching strategies to ensure they agree. # `use_trie: true` forces the trie; `use_trie: false` forces linear. shared_examples 'a set' do |use_trie:| subject(:set) { described_class.new(use_trie:, use_cache: false) } # ── basic matching ────────────────────────────────────────────────────── context 'basic matching' do before { set.add('/:name') } it('matches a string to a pattern') { expect(set.match('/foo')).not_to be_nil } it('returns nil for no match') { expect(set.match('/foo/bar')).to be_nil } it('returns a Set::Match') { expect(set.match('/foo')).to be_a(Mustermann::Set::Match) } it('exposes the matched string') { expect(set.match('/foo').to_s).to eq '/foo' } it('exposes params') { expect(set.match('/foo').params).to eq('name' => 'foo') } it('supports symbol key access') { expect(set.match('/foo')[:name]).to eq 'foo' } it('supports string key access') { expect(set.match('/foo')['name']).to eq 'foo' } end # ── captures and named_captures ───────────────────────────────────────── context 'captures and named_captures' do before { set.add('/a/:a/b/:b', :handler) } let(:m) { set.match('/a/foo/b/bar') } it('exposes captures as an ordered array') { expect(m.captures).to eq ['foo', 'bar'] } it('exposes named_captures as a hash') { expect(m.named_captures).to eq('a' => 'foo', 'b' => 'bar') } it('supports integer index access') { expect(m[0]).to eq 'foo' } it('supports integer index access (second)') { expect(m[1]).to eq 'bar' } it('supports integer + length access') { expect(m[0, 2]).to eq ['foo', 'bar'] } it('supports range access') { expect(m[0..1]).to eq ['foo', 'bar'] } end # ── values ────────────────────────────────────────────────────────────── context 'values' do it 'returns nil value when none given' do set.add('/foo') expect(set.match('/foo').value).to be_nil end it 'stores and returns a value' do set.add('/foo', :handler) expect(set.match('/foo').value).to eq :handler end it 'stores first value on single match' do set.add('/foo', :a, :b) expect(set.match('/foo').value).to eq :a end it 'de-duplicates values for the same pattern' do set.add('/foo', :a) set.add('/foo', :a) expect(set.match_all('/foo').map(&:value)).to eq [:a] end it 'returns all values via match_all' do set.add('/foo', :a, :b) expect(set.match_all('/foo').map(&:value)).to eq [:a, :b] end it 'stores values per-pattern independently' do set.add('/foo', :foo_handler) set.add('/bar', :bar_handler) expect(set.match('/foo').value).to eq :foo_handler expect(set.match('/bar').value).to eq :bar_handler end end # ── match_all ──────────────────────────────────────────────────────────── context 'match_all' do it 'returns [] when nothing matches' do set.add('/foo') expect(set.match_all('/bar')).to eq [] end it 'returns all matching patterns' do set.add('/:a') set.add('/:b') results = set.match_all('/foo') expect(results.size).to eq 2 end it 'preserves insertion order' do set.add('/:first', :first) set.add('/:second', :second) values = set.match_all('/foo').map(&:value) expect(values).to eq [:first, :second] end end # ── peek_match ─────────────────────────────────────────────────────────── context 'peek_match' do it 'matches a prefix' do set.add('/foo') m = set.peek_match('/foo/extra') expect(m).not_to be_nil expect(m.to_s).to eq '/foo' expect(m.post_match).to eq '/extra' end it 'returns nil when nothing can match' do set.add('/foo') expect(set.peek_match('/bar/extra')).to be_nil end end # ── peek_match_all ─────────────────────────────────────────────────────── context 'peek_match_all' do it 'returns all patterns that match a prefix' do set.add('/:a', :first) set.add('/:b', :second) results = set.peek_match_all('/foo/extra') expect(results.size).to eq 2 expect(results.map(&:to_s)).to all(eq '/foo') expect(results.map(&:post_match)).to all(eq '/extra') expect(results.map(&:value)).to eq [:first, :second] end it 'returns [] when nothing matches as a prefix' do set.add('/foo') expect(set.peek_match_all('/bar/extra')).to eq [] end end # ── [] accessor ────────────────────────────────────────────────────────── context '[]' do it 'looks up by string (matches against patterns)' do set.add('/foo', :result) expect(set['/foo']).to eq :result end it 'looks up by Pattern object' do pat = Mustermann.new('/foo') set.add(pat, :result) expect(set[pat]).to eq :result end it 'returns nil for no match on string lookup' do set.add('/foo') expect(set['/bar']).to be_nil end it 'raises for unsupported type' do expect { set[42] }.to raise_error(ArgumentError) end end # ── update / merge ─────────────────────────────────────────────────────── context 'update' do it 'merges from a Hash' do set.update('/foo' => :a, '/bar' => :b) expect(set['/foo']).to eq :a expect(set['/bar']).to eq :b end it 'merges from an Array' do set.update(['/foo', '/bar']) expect(set.match('/foo')).not_to be_nil expect(set.match('/bar')).not_to be_nil end it 'merges from a String' do set.update('/foo') expect(set.match('/foo')).not_to be_nil end it 'merges from another Set' do other = described_class.new(use_trie:, use_cache: false) other.add('/foo', :from_other) set.update(other) expect(set['/foo']).to eq :from_other end it 'combines values when merging overlapping patterns from another Set' do other = described_class.new(use_trie:, use_cache: false) set.add('/foo', :a) other.add('/foo', :b) set.update(other) expect(set.match_all('/foo').map(&:value)).to eq [:a, :b] end end context 'merge' do it 'returns a new set without modifying the original' do set.add('/foo', :a) merged = set.merge('/bar' => :b) expect(set.match('/bar')).to be_nil expect(merged['/foo']).to eq :a expect(merged['/bar']).to eq :b end end # ── patterns ───────────────────────────────────────────────────────────── context 'patterns' do it 'lists all added patterns' do set.add('/foo') set.add('/bar') expect(set.patterns.map(&:to_s)).to contain_exactly('/foo', '/bar') end end # ── except option ──────────────────────────────────────────────────────── context 'except option' do it 'respects the except constraint on a pattern' do set.add(Mustermann.new('/:foo', except: '/bar'), :handler) expect(set.match('/foo').value).to eq :handler expect(set.match('/bar')).to be_nil end it 'falls through to an unrestricted pattern when except excludes a match' do set.add(Mustermann.new('/:foo', except: '/bar'), :restricted) set.add('/:foo', :unrestricted) expect(set.match('/foo').value).to eq :restricted expect(set.match('/bar').value).to eq :unrestricted end end # ── conflict resolution (ordering) ─────────────────────────────────────── context 'conflict resolution' do it 'returns the first-added pattern on conflict' do set.add('/foo', :static) set.add('/:var', :dynamic) expect(set.match('/foo').value).to eq :static end it 'a dynamic pattern matches what the static one does not' do set.add('/foo', :static) set.add('/:var', :dynamic) expect(set.match('/bar').value).to eq :dynamic end it 'two dynamic patterns: insertion order wins' do set.add('/:a', :first) set.add('/:b', :second) expect(set.match('/foo').value).to eq :first end it 'match_all returns both when two patterns match' do set.add('/foo', :static) set.add('/:var', :dynamic) values = set.match_all('/foo').map(&:value) expect(values).to contain_exactly(:static, :dynamic) end it 'match_all works with constrained captures (regex path)' do s = described_class.new(use_trie:, use_cache: false, capture: { id: /\d+/ }) s.add('/:id', :handler) results = s.match_all('/42') expect(results.map(&:value)).to eq [:handler] expect(results.first.params['id']).to eq '42' end end # ── strict ordering ────────────────────────────────────────────────────── context 'strict ordering' do it 'tracks insertion order if strict_order is enabled' do s = described_class.new(use_trie:, use_cache: false, strict_order: true) s.add('/:a', :first) s.add('/b', :second) s.add('/:c', :third) expect(s.match('/b').value).to eq :first results = s.match_all('/b').map(&:value) expect(results).to eq [:first, :second, :third] end it 'vaguely maintains insertion order even if strict_order is disabled' do set.add('/:a', :first) set.add('/b', :second) set.add('/:c', :third) expect(set.match('/b').value).to eq (set.use_trie? ? :second : :first) expect(set.match('/a').value).to eq :first end end # ── complex patterns ───────────────────────────────────────────────────── context 'complex patterns' do it 'handles nested segments' do set.add('/users/:id/posts/:post_id', :posts) m = set.match('/users/42/posts/7') expect(m.value).to eq :posts expect(m.params).to eq('id' => '42', 'post_id' => '7') end it 'handles splat captures' do set.add('/files/*path', :files) m = set.match('/files/a/b/c') expect(m).not_to be_nil # named splat (not 'splat') returns the full captured string expect(m.params['path']).to eq 'a/b/c' end it 'handles unnamed splat (always an array)' do set.add('/files/*', :files) m = set.match('/files/a/b/c') expect(m).not_to be_nil expect(m.params['splat']).to eq ['a/b/c'] end it 'handles optional segments (rails-style)' do s = described_class.new(type: :rails, use_trie:, use_cache: false) s.add('/:controller(/:action)', :route) expect(s.match('/users').value).to eq :route expect(s.match('/users/show').value).to eq :route expect(s.match('/users').params['action']).to be_nil expect(s.match('/users/show').params['action']).to eq 'show' end it 'distinguishes prefix-ambiguous patterns by length' do set.add('/users', :list) set.add('/users/:id', :show) expect(set.match('/users').value).to eq :list expect(set.match('/users/1').value).to eq :show end it 'deep nesting with multiple params' do set.add('/:a/:b/:c', :deep) m = set.match('/x/y/z') expect(m.params).to eq('a' => 'x', 'b' => 'y', 'c' => 'z') end it 'handles URI-encoded params' do set.add('/:name', :cap) m = set.match('/hello%20world') expect(m.params['name']).to eq 'hello world' end it 'handles overlapping static and parameterized prefixes' do set.add('/users/new', :new_form) set.add('/users/:id', :show) expect(set.match('/users/new').value).to eq :new_form expect(set.match('/users/42').value).to eq :show end it 'handles multiple splats' do set.add('/*/*', :two_splats) m = set.match('/foo/bar') expect(m).not_to be_nil expect(m.params['splat']).to eq ['foo', 'bar'] end it 'handles optional prefix before required segment (sinatra-style)' do set.add('(/:slug)?/bar', :route) expect(set.match('/bar').value).to eq :route expect(set.match('/foo/bar').value).to eq :route expect(set.match('/bar').params['slug']).to be_nil expect(set.match('/foo/bar').params['slug']).to eq 'foo' expect(set.match('/baz')).to be_nil end it 'handles optional prefix before required segment (rails-style)' do s = described_class.new(type: :rails, use_trie:, use_cache: false) s.add('(/:slug)/bar', :route) expect(s.match('/bar').value).to eq :route expect(s.match('/foo/bar').value).to eq :route expect(s.match('/bar').params['slug']).to be_nil expect(s.match('/foo/bar').params['slug']).to eq 'foo' expect(s.match('/baz')).to be_nil end it 'resolves conflict between exact static and optional-prefix pattern' do set.add('/foo/bar', :exact) set.add('(/:slug)?/bar', :optional) expect(set.match('/foo/bar').value).to eq :exact expect(set.match('/baz/bar').value).to eq :optional expect(set.match('/bar').value).to eq :optional end end # ── error handling ─────────────────────────────────────────────────────── context 'error handling' do it 'rejects illegal additional_values' do expect { described_class.new(additional_values: :foo, use_trie:) } .to raise_error(ArgumentError) end it 'rejects illegal use_trie value' do expect { described_class.new(use_trie: :maybe) } .to raise_error(ArgumentError) end it 'rejects non-AST patterns' do expect { set.add(Mustermann.new('/foo', type: :regular)) } .to raise_error(ArgumentError, /Non-AST/) end it 'rejects reserved values' do expect { set.add('/foo', :raise) } .to raise_error(ArgumentError) end it 'rejects the set itself as a value' do expect { set.add('/foo', set) } .to raise_error(ArgumentError, /set itself/) end it 'rejects unsupported mapping types in update' do expect { set.update(42) } .to raise_error(ArgumentError, /unsupported mapping type/) end end # ── composite patterns ─────────────────────────────────────────────────── context 'composite patterns' do let(:users) { Mustermann.new('/users/:id') } let(:posts) { Mustermann.new('/posts/:id') } it 'unpacks a | composite and adds each component pattern' do set.add(users | posts, :handler) expect(set.patterns.map(&:to_s)).to contain_exactly('/users/:id', '/posts/:id') end it 'matches each component pattern independently' do set.add(users | posts, :handler) expect(set.match('/users/42')).not_to be_nil expect(set.match('/posts/hello')).not_to be_nil expect(set.match('/other')).to be_nil end it 'associates the value with each component pattern' do set.add(users | posts, :handler) expect(set.match('/users/42').value).to eq :handler expect(set.match('/posts/hello').value).to eq :handler end it 'exposes params from each component pattern' do set.add(users | posts, :handler) expect(set.match('/users/42').params).to eq('id' => '42') expect(set.match('/posts/hello').params).to eq('id' => 'hello') end it 'flattens nested | composites' do pages = Mustermann.new('/pages/:title') set.add((users | posts) | pages, :handler) expect(set.patterns.map(&:to_s)).to contain_exactly('/users/:id', '/posts/:id', '/pages/:title') end it 'rejects a & composite' do expect { set.add(users & posts, :handler) } .to raise_error(ArgumentError, /Non-AST/) end end # ── initialization shortcuts ───────────────────────────────────────────── context 'initialization' do it 'accepts patterns in the constructor' do s = described_class.new('/foo' => :a, '/bar' => :b, use_trie:, use_cache: false) expect(s['/foo']).to eq :a expect(s['/bar']).to eq :b end it 'accepts a zero-arity block returning a mapping' do s = described_class.new(use_trie:, use_cache: false) { { '/foo' => :block } } expect(s['/foo']).to eq :block end it 'accepts a one-argument block for imperative building' do s = described_class.new(use_trie:, use_cache: false) { |set| set.add('/foo', :imperative) } expect(s['/foo']).to eq :imperative end end # ── expand ─────────────────────────────────────────────────────────────── context 'expand' do it 'expands using the first matching pattern' do set.add('/:name', :a) expect(set.expand(name: 'foo')).to include '/foo' end it 'expands for a specific value' do set.add('/users/:id', :users) set.add('/posts/:id', :posts) expect(set.expand(:users, id: '5')).to include '/users/5' expect(set.expand(:posts, id: '5')).to include '/posts/5' end it 'expands with additional_values behavior passed as value' do set.add('/:name', :handler) expect(set.expand(:ignore, name: 'foo')).to include '/foo' end it 'raises when conflicting behavior is specified twice' do set.add('/:name') expect { set.expand(:ignore, :raise, { name: 'foo' }) } .to raise_error(ArgumentError, /behavior specified multiple times/) end end end context 'with linear matching (use_trie: false)' do include_examples 'a set', use_trie: false end context 'with trie matching (use_trie: true)' do include_examples 'a set', use_trie: true end # ── trie fast-path for single-segment captures ─────────────────────────── context 'trie fast-path for single-segment captures' do subject(:set) { described_class.new(use_trie: true, use_cache: false) } it 'does not match when the capture position holds a slash (empty segment)' do set.add('/:id', :show) # After consuming '/', the next char is another '/' — the segment is empty, # so the fast path skips this dynamic edge and the overall match fails. expect(set.match('//foo')).to be_nil end it 'returns all matches through the regex path for non-fast-path dynamic edges' do set.add('/files/*path', :files) # Named splat compiles to \G(?.*?) which can match '/', so fast_name is nil # and the regex path is taken. With all: true, result.concat(nested_result) is hit. results = set.match_all('/files/a/b/c') expect(results.size).to eq 1 expect(results.first.params['path']).to eq 'a/b/c' end end # ── strategy-specific / trie threshold ─────────────────────────────────── context 'trie threshold' do it 'switches to trie when pattern count reaches threshold' do set = described_class.new(use_trie: 2, use_cache: false) set.add('/a', :a) set.add('/b', :b) # triggers switch set.add('/c', :c) expect(set.match('/a').value).to eq :a expect(set.match('/c').value).to eq :c end end # ── caching ─────────────────────────────────────────────────────────────── context 'with cache' do it 'returns consistent results on repeated lookups' do set = described_class.new(use_trie: false, use_cache: true) set.add('/:name', :handler) 2.times { expect(set.match('/foo').value).to eq :handler } end it 'invalidates cache when a pattern is added' do set = described_class.new(use_trie: false, use_cache: true) set.add('/foo', :first) set.match('/bar') # populate cache with nil result set.add('/bar', :second) expect(set.match('/bar').value).to eq :second end it 'supports match_all with cache enabled' do set = described_class.new(use_trie: false, use_cache: true) set.add('/:name', :handler) expect(set.match_all('/foo').map(&:value)).to eq [:handler] end it 'supports peek_match with cache enabled' do set = described_class.new(use_trie: false, use_cache: true) set.add('/:name', :handler) expect(set.peek_match('/foo/extra').value).to eq :handler end end # ── inspect and pretty_print ───────────────────────────────────────────── describe :inspect do example('single pattern with value') do set = Mustermann::Set.new set.add('/:name', :handler) set.inspect.should be == '# :handler>' end example('multiple values for one pattern') do set = Mustermann::Set.new set.add('/:name', :a) set.add('/:name', :b) set.inspect.should be == '# [:a, :b]>' end example('pattern with no value') do set = Mustermann::Set.new set.add('/:name') set.inspect.should be == '#' end example('array value') do set = Mustermann::Set.new set.add('/:name', [:x, :y]) set.inspect.should be == '# [[:x, :y]]>' end example('multiple patterns') do set = Mustermann::Set.new set.add('/foo', :a) set.add('/bar', :b) set.inspect.should be == '# :a, "/bar" => :b>' end end describe :pretty_print do example('single pattern with value') do set = Mustermann::Set.new set.add('/:name', :handler) PP.pp(set, +'').chomp.should be == '# :handler>' end example('multiple patterns') do set = Mustermann::Set.new set.add('/foo', :a) set.add('/bar', :b) PP.pp(set, +'').chomp.should be == '# :a, "/bar" => :b>' end example('pattern with no value') do set = Mustermann::Set.new set.add('/foo') PP.pp(set, +'').chomp.should be == '#' end example('multiple values for one pattern') do set = Mustermann::Set.new set.add('/:name', :a) set.add('/:name', :b) PP.pp(set, +'').chomp.should be == '# [:a, :b]>' end end end mustermann-4.0.0/mustermann/spec/sinatra_spec.rb000066400000000000000000001203141517364653100220430ustar00rootroot00000000000000# frozen_string_literal: true require 'support' require 'mustermann/sinatra' require 'date' require 'rubygems/version' describe Mustermann::Sinatra do extend Support::Pattern pattern '' do it { should match('') } it { should_not match('/') } it { should generate_template('') } it { should respond_to(:expand) } it { should respond_to(:to_templates) } end pattern '/' do it { should match('/') } it { should_not match('/foo') } end pattern '/foo' do it { should match('/foo') } it { should_not match('/bar') } it { should_not match('/foo.bar') } end pattern '/foo/bar' do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern '/foo\/bar' do it { should match('/foo/bar') } it { should match('/foo%2Fbar') } it { should match('/foo%2fbar') } end pattern '/:foo' do it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should match('/foo.bar') .capturing foo: 'foo.bar' } it { should match('/%0Afoo') .capturing foo: '%0Afoo' } it { should match('/foo%2Fbar') .capturing foo: 'foo%2Fbar' } it { should_not match('/foo?') } it { should_not match('/foo/bar') } it { should_not match('/') } it { should_not match('/foo/') } it { should generate_template('/{foo}') } end pattern '/föö' do it { should match("/f%C3%B6%C3%B6") } end pattern "/:foo/:bar" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} } example { pattern.params('').should be_nil } it { should generate_template('/{foo}/{bar}') } end pattern "/{foo}/{bar}" do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should match('/foo.bar/bar.foo') .capturing foo: 'foo.bar', bar: 'bar.foo' } it { should match('/user@example.com/name') .capturing foo: 'user@example.com', bar: 'name' } it { should match('/10.1/te.st') .capturing foo: '10.1', bar: 'te.st' } it { should match('/10.1.2/te.st') .capturing foo: '10.1.2', bar: 'te.st' } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } example { pattern.params('/bar/foo').should be == {"foo" => "bar", "bar" => "foo"} } example { pattern.params('').should be_nil } it { should generate_template('/{foo}/{bar}') } end pattern '/hello/:person' do it { should match('/hello/Frank').capturing person: 'Frank' } it { should generate_template('/hello/{person}') } end pattern '/hello/{person}' do it { should match('/hello/Frank').capturing person: 'Frank' } it { should generate_template('/hello/{person}') } end pattern '/?:foo?/?:bar?' do it { should match('/hello/world') .capturing foo: 'hello', bar: 'world' } it { should match('/hello') .capturing foo: 'hello', bar: nil } it { should match('/') .capturing foo: nil, bar: nil } it { should match('') .capturing foo: nil, bar: nil } it { should expand(foo: 'hello') .to('/hello/') } it { should expand(foo: 'hello', bar: 'world') .to('/hello/world') } it { should expand(bar: 'world') .to('//world') } it { should expand .to('//') } it { should_not expand(baz: '') } it { should_not match('/hello/world/') } it { should generate_templates("", "/", "//", "//{bar}", "/{bar}", "/{foo}", "/{foo}/", "/{foo}/{bar}", "/{foo}{bar}", "{bar}", "{foo}", "{foo}/", "{foo}/{bar}", "{foo}{bar}") } end pattern '/:foo_bar' do it { should match('/hello').capturing foo_bar: 'hello' } it { should generate_template('/{foo_bar}') } end pattern '/{foo.bar}' do it { should match('/hello').capturing :"foo.bar" => 'hello' } it { should generate_template('/{foo.bar}') } end pattern '/*' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } example { pattern.params('/foo').should be == {"splat" => ["foo"]} } end pattern '/{+splat}' do it { should match('/') .capturing splat: '' } it { should match('/foo') .capturing splat: 'foo' } it { should match('/foo/bar') .capturing splat: 'foo/bar' } it { should generate_template('/{+splat}') } example { pattern.params('/foo').should be == {"splat" => ["foo"]} } end pattern '/*foo' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should generate_template('/{+foo}') } example { pattern.params('/foo') .should be == {"foo" => "foo" } } example { pattern.params('/foo/bar') .should be == {"foo" => "foo/bar" } } end pattern '/{+foo}' do it { should match('/') .capturing foo: '' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/foo/bar') .capturing foo: 'foo/bar' } it { should generate_template('/{+foo}') } example { pattern.params('/foo') .should be == {"foo" => "foo" } } example { pattern.params('/foo/bar') .should be == {"foo" => "foo/bar" } } end pattern '/*foo/*bar' do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should generate_template('/{+foo}/{+bar}') } end pattern '/{+foo}/{+bar}' do it { should match('/foo/bar') .capturing foo: 'foo', bar: 'bar' } it { should generate_template('/{+foo}/{+bar}') } end pattern '/:foo/*' do it { should match("/foo/bar/baz") .capturing foo: 'foo', splat: 'bar/baz' } it { should match("/foo/") .capturing foo: 'foo', splat: '' } it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', splat: 'h%20a%20y' } it { should_not match('/foo') } it { should generate_template('/{foo}/{+splat}') } example { pattern.params('/bar/foo').should be == {"splat" => ["foo"], "foo" => "bar"} } example { pattern.params('/bar/foo/f%20o').should be == {"splat" => ["foo/f o"], "foo" => "bar"} } end pattern '/{foo}/*' do it { should match("/foo/bar/baz") .capturing foo: 'foo', splat: 'bar/baz' } it { should match("/foo/") .capturing foo: 'foo', splat: '' } it { should match('/h%20w/h%20a%20y') .capturing foo: 'h%20w', splat: 'h%20a%20y' } it { should_not match('/foo') } it { should generate_template('/{foo}/{+splat}') } example { pattern.params('/bar/foo').should be == {"splat" => ["foo"], "foo" => "bar"} } example { pattern.params('/bar/foo/f%20o').should be == {"splat" => ["foo/f o"], "foo" => "bar"} } end pattern '/test$/' do it { should match('/test$/') } end pattern '/te+st/' do it { should match('/te+st/') } it { should_not match('/test/') } it { should_not match('/teest/') } end pattern "/path with spaces" do it { should match('/path with spaces') } it { should match('/path%20with%20spaces') } it { should match('/path%2Bwith%2Bspaces') } it { should match('/path+with+spaces') } it { should generate_template('/path%20with%20spaces') } end pattern '/foo&bar' do it { should match('/foo&bar') } end pattern '/foo\{bar' do it { should match('/foo%7Bbar') } end pattern '/*/:foo/*/*' do it { should match('/bar/foo/bling/baz/boom') } it 'should map to proper params' do pattern.params('/bar/foo/bling/baz/boom').should be == { "foo" => "foo", "splat" => ['bar', 'bling', 'baz/boom'] } end end pattern '/{+splat}/{foo}/{+splat}/{+splat}' do it { should match('/bar/foo/bling/baz/boom') } it 'should map to proper params' do pattern.params('/bar/foo/bling/baz/boom').should be == { "foo" => "foo", "splat" => ['bar', 'bling', 'baz/boom'] } end end pattern '/test.bar' do it { should match('/test.bar') } it { should_not match('/test0bar') } end pattern '/:file.:ext' do it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%E6%AD%A3%2Ejpg') .capturing file: 'pony%E6%AD%A3', ext: 'jpg' } it { should match('/pony%e6%ad%a3%2ejpg') .capturing file: 'pony%e6%ad%a3', ext: 'jpg' } it { should match('/pony正%2Ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正%2ejpg') .capturing file: 'pony正', ext: 'jpg' } it { should match('/pony正..jpg') .capturing file: 'pony正.', ext: 'jpg' } it { should_not match('/.jpg') } end pattern '/(:a)x?' do it { should match('/a') .capturing a: 'a' } it { should match('/xa') .capturing a: 'xa' } it { should match('/axa') .capturing a: 'axa' } it { should match('/ax') .capturing a: 'a' } it { should match('/axax') .capturing a: 'axa' } it { should match('/axaxx') .capturing a: 'axax' } it { should generate_template('/{a}x') } it { should generate_template('/{a}') } end pattern '/:user(@:host)?' do it { should match('/foo@bar') .capturing user: 'foo', host: 'bar' } it { should match('/foo.foo@bar') .capturing user: 'foo.foo', host: 'bar' } it { should match('/foo@bar.bar') .capturing user: 'foo', host: 'bar.bar' } it { should generate_template('/{user}') } it { should generate_template('/{user}@{host}') } end pattern '/:file(.:ext)?' do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' } it { should match('/pony.') .capturing file: 'pony.' } it { should_not match('/.jpg') } it { should generate_template('/{file}') } it { should generate_template('/{file}.{ext}') } it { should_not generate_template('/{file}.') } end pattern '/:id/test.bar' do it { should match('/3/test.bar') .capturing id: '3' } it { should match('/2/test.bar') .capturing id: '2' } it { should match('/2E/test.bar') .capturing id: '2E' } it { should match('/2e/test.bar') .capturing id: '2e' } it { should match('/%2E/test.bar') .capturing id: '%2E' } end pattern '/10/:id' do it { should match('/10/test') .capturing id: 'test' } it { should match('/10/te.st') .capturing id: 'te.st' } end pattern '/10.1/:id' do it { should match('/10.1/test') .capturing id: 'test' } it { should match('/10.1/te.st') .capturing id: 'te.st' } end pattern '/:foo.:bar/:id' do it { should match('/10.1/te.st') .capturing foo: "10", bar: "1", id: "te.st" } it { should match('/10.1.2/te.st') .capturing foo: "10.1", bar: "2", id: "te.st" } end pattern '/:a/:b.?:c?' do it { should match('/a/b') .capturing a: 'a', b: 'b', c: nil } it { should match('/a/b.c') .capturing a: 'a', b: 'b', c: 'c' } it { should match('/a.b/c') .capturing a: 'a.b', b: 'c', c: nil } it { should match('/a.b/c.d') .capturing a: 'a.b', b: 'c', c: 'd' } it { should_not match('/a.b/c.d/e') } end pattern '/:a(foo:b)?' do it { should match('/barfoobar') .capturing a: 'bar', b: 'bar' } it { should match('/barfoobarfoobar') .capturing a: 'barfoobar', b: 'bar' } it { should match('/bar') .capturing a: 'bar', b: nil } it { should_not match('/') } end pattern '/foo?' do it { should match('/fo') } it { should match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } end pattern '/foo\?' do it { should match('/foo?') } it { should_not match('/foo\?') } it { should_not match('/fo') } it { should_not match('/foo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } end pattern '/foo\\\?' do it { should match('/foo%5c') } it { should match('/foo') } it { should_not match('/foo\?') } it { should_not match('/fo') } it { should_not match('') } it { should_not match('/') } it { should_not match('/f') } it { should_not match('/fooo') } end pattern '/\(' do it { should match('/(') } end pattern '/\(?' do it { should match('/(') } it { should match('/') } end pattern '/(\()?' do it { should match('/(') } it { should match('/') } end pattern '/(\(\))?' do it { should match('/') } it { should match('/()') } it { should_not match('/(') } end pattern '/\(\)?' do it { should match('/(') } it { should match('/()') } it { should_not match('/') } end pattern '/\*' do it { should match('/*') } it { should_not match('/a') } end pattern '/\*/*' do it { should match('/*/b/c') } it { should_not match('/a/b/c') } end pattern '/\:foo' do it { should match('/:foo') } it { should_not match('/foo') } end pattern '/:fOO' do it { should match('/a').capturing fOO: 'a' } end pattern '/:_X' do it { should match('/a').capturing _X: 'a' } end pattern '/:f00' do it { should match('/a').capturing f00: 'a' } end pattern '/:foo(/:bar)?/:baz?' do it { should match('/foo/bar/baz').capturing foo: 'foo', bar: 'bar', baz: 'baz' } end pattern "/(foo|bar)" do it { should match("/foo") } it { should match("/bar") } it { should generate_template('/foo') } it { should generate_template('/bar') } end pattern "/(foo\\|bar)" do it { should match "/foo%7Cbar" } it { should generate_template "/foo%7Cbar" } it { should_not match("/foo") } it { should_not match("/bar") } it { should_not generate_template('/foo') } it { should_not generate_template('/bar') } end pattern "/(:a/:b|:c)" do it { should match("/foo") .capturing c: 'foo' } it { should match("/foo/bar") .capturing a: 'foo', b: 'bar' } it { should generate_template('/{a}/{b}') } it { should generate_template('/{c}') } it { should expand(a: 'foo', b: 'bar') .to('/foo/bar') } it { should expand(c: 'foo') .to('/foo') } it { should_not expand(a: 'foo', b: 'bar', c: 'baz') } end pattern "/:a/:b|:c" do it { should match("foo") .capturing c: 'foo' } it { should match("/foo/bar") .capturing a: 'foo', b: 'bar' } it { should generate_template('/{a}/{b}') } it { should generate_template('{c}') } it { should expand(a: 'foo', b: 'bar') .to('/foo/bar') } it { should expand(c: 'foo') .to('foo') } it { should_not expand(a: 'foo', b: 'bar', c: 'baz') } end pattern "/({foo}|{bar})", capture: { foo: /\d+/, bar: /\w+/ } do it { should match("/a") .capturing foo: nil, bar: 'a' } it { should match("/1234") .capturing foo: "1234", bar: nil } it { should_not match("/a/b") } end pattern "/{foo|bar}", capture: { foo: /\d+/, bar: /\w+/ } do it { should match("/a") .capturing foo: nil, bar: 'a' } it { should match("/1234") .capturing foo: "1234", bar: nil } it { should_not match("/a/b") } end pattern "/{foo|bar|baz}", capture: { foo: /\d+/, bar: /[ab]+/, baz: /[cde]+/ } do it { should match("/ab") .capturing foo: nil, bar: 'ab', baz: nil } it { should match("/1234") .capturing foo: "1234", bar: nil, baz: nil } it { should match("/ccddee") .capturing foo: nil, bar: nil, baz: "ccddee" } it { should_not match("/a/b") } end pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should_not match('/') } it { should_not match('/foo') } end pattern '/:foo', capture: /\d+/ do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should_not match('/') } it { should_not match('/foo') } end pattern '/:foo', capture: '1' do it { should match('/1').capturing foo: '1' } it { should_not match('/') } it { should_not match('/foo') } it { should_not match('/123') } end pattern '/:foo', capture: 'a.b' do it { should match('/a.b') .capturing foo: 'a.b' } it { should match('/a%2Eb') .capturing foo: 'a%2Eb' } it { should match('/a%2eb') .capturing foo: 'a%2eb' } it { should_not match('/ab') } it { should_not match('/afb') } it { should_not match('/a1b') } it { should_not match('/a.bc') } end pattern '/:foo(/:bar)?', capture: :alpha do it { should match('/abc') .capturing foo: 'abc', bar: nil } it { should match('/a/b') .capturing foo: 'a', bar: 'b' } it { should match('/a') .capturing foo: 'a', bar: nil } it { should_not match('/1/2') } it { should_not match('/a/2') } it { should_not match('/1/b') } it { should_not match('/1') } it { should_not match('/1/') } it { should_not match('/a/') } it { should_not match('//a') } end pattern '/:foo', capture: ['foo', 'bar', /\d+/] do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/') } it { should_not match('/baz') } it { should_not match('/foo1') } end pattern '/:foo:bar:baz', capture: { foo: :alpha, bar: /\d+/ } do it { should match('/ab123xy-1') .capturing foo: 'ab', bar: '123', baz: 'xy-1' } it { should match('/ab123') .capturing foo: 'ab', bar: '12', baz: '3' } it { should_not match('/123abcxy-1') } it { should_not match('/abcxy-1') } it { should_not match('/abc1') } end pattern '/:foo', capture: { foo: ['foo', 'bar', /\d+/] } do it { should match('/1') .capturing foo: '1' } it { should match('/123') .capturing foo: '123' } it { should match('/foo') .capturing foo: 'foo' } it { should match('/bar') .capturing foo: 'bar' } it { should_not match('/') } it { should_not match('/baz') } it { should_not match('/foo1') } end pattern '/:file(.:ext)?', capture: { ext: ['jpg', 'png'] } do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2Ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony%2ejpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png') .capturing file: 'pony', ext: 'png' } it { should match('/pony%2Epng') .capturing file: 'pony', ext: 'png' } it { should match('/pony%2epng') .capturing file: 'pony', ext: 'png' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: 'jpg' } it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: 'png' } it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil } it { should match('/pony.') .capturing file: 'pony.', ext: nil } it { should_not match('.jpg') } end pattern '/:file:ext?', capture: { ext: ['.jpg', '.png', '.tar.gz'] } do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: '.jpg' } it { should match('/pony.png') .capturing file: 'pony', ext: '.png' } it { should match('/pony.png.jpg') .capturing file: 'pony.png', ext: '.jpg' } it { should match('/pony.jpg.png') .capturing file: 'pony.jpg', ext: '.png' } it { should match('/pony.tar.gz') .capturing file: 'pony', ext: '.tar.gz' } it { should match('/pony.gif') .capturing file: 'pony.gif', ext: nil } it { should match('/pony.') .capturing file: 'pony.', ext: nil } it { should_not match('/.jpg') } end pattern '/:a(@:b)?', capture: { b: /\d+/ } do it { should match('/a') .capturing a: 'a', b: nil } it { should match('/a@1') .capturing a: 'a', b: '1' } it { should match('/a@b') .capturing a: 'a@b', b: nil } it { should match('/a@1@2') .capturing a: 'a@1', b: '2' } end context 'capture: class and symbol converters' do pattern '/:n', capture: Integer do it { should match('/42') .capturing n: '42' } it { should match('/-5') .capturing n: '-5' } it { should match('/0') .capturing n: '0' } it { should_not match('/') } it { should_not match('/foo') } it { should_not match('/3.14') } example { pattern.params('/42').should be == { 'n' => 42 } } example { pattern.params('/-5').should be == { 'n' => -5 } } end pattern '/:n', capture: :integer do it { should match('/42') .capturing n: '42' } it { should match('/-5') .capturing n: '-5' } it { should_not match('/foo') } example { pattern.params('/42').should be == { 'n' => 42 } } end pattern '/:name', capture: Symbol do it { should match('/hello') .capturing name: 'hello' } it { should match('/foo_bar') .capturing name: 'foo_bar' } it { should_not match('/') } it { should_not match('/hello-world') } example { pattern.params('/hello').should be == { 'name' => :hello } } end pattern '/:name', capture: :symbol do it { should match('/hello') .capturing name: 'hello' } it { should match('/foo_bar') .capturing name: 'foo_bar' } it { should_not match('/hello-world') } example { pattern.params('/hello').should be == { 'name' => :hello } } end pattern '/:f', capture: Float do it { should match('/3.14') .capturing f: '3.14' } it { should match('/-2.5') .capturing f: '-2.5' } it { should match('/5') .capturing f: '5' } it { should_not match('/') } it { should_not match('/foo') } example { pattern.params('/3.14').should be == { 'f' => 3.14 } } example { pattern.params('/5').should be == { 'f' => 5.0 } } end pattern '/:f', capture: :float do it { should match('/3.14') .capturing f: '3.14' } it { should match('/5') .capturing f: '5' } it { should_not match('/foo') } example { pattern.params('/3.14').should be == { 'f' => 3.14 } } end pattern '/:d', capture: Date do it { should match('/2026-04-23') .capturing d: '2026-04-23' } it { should_not match('/') } it { should_not match('/foo') } it { should_not match('/04-23-2026') } it { should_not match('/2026/04/23') } example { pattern.params('/2026-04-23').should be == { 'd' => Date.new(2026, 4, 23) } } end pattern '/:d', capture: :date do it { should match('/2026-04-23') .capturing d: '2026-04-23' } it { should_not match('/foo') } example { pattern.params('/2026-04-23').should be == { 'd' => Date.new(2026, 4, 23) } } end pattern '/:v', capture: Gem::Version do it { should match('/1.0') .capturing v: '1.0' } it { should match('/1.2.3') .capturing v: '1.2.3' } it { should match('/0.9.0.alpha.1') .capturing v: '0.9.0.alpha.1' } it { should_not match('/') } it { should_not match('/foo') } example { pattern.params('/1.2.3').should be == { 'v' => Gem::Version.new('1.2.3') } } end pattern '/:v', capture: :version do it { should match('/1.2.3') .capturing v: '1.2.3' } it { should_not match('/foo') } example { pattern.params('/1.2.3').should be == { 'v' => Gem::Version.new('1.2.3') } } end pattern '/:lang', capture: :locale do it { should match('/en') .capturing lang: 'en' } it { should match('/en-US') .capturing lang: 'en-US' } it { should match('/zh-Hans-CN') .capturing lang: 'zh-Hans-CN' } it { should match('/i') .capturing lang: 'i' } it { should_not match('/') } it { should_not match('/e') } end pattern '/:s', capture: :slug do it { should match('/hello') .capturing s: 'hello' } it { should match('/hello-world') .capturing s: 'hello-world' } it { should_not match('/') } it { should_not match('/Hello') } it { should_not match('/hello-') } end pattern '/:id', capture: :uuid do it { should match('/f47ac10b-58cc-4372-a567-0e02b2c3d479') .capturing id: 'f47ac10b-58cc-4372-a567-0e02b2c3d479' } it { should match('/F47AC10B-58CC-4372-A567-0E02B2C3D479') .capturing id: 'F47AC10B-58CC-4372-A567-0E02B2C3D479' } it { should_not match('/') } it { should_not match('/foo') } it { should_not match('/f47ac10b-58cc-4372-a567') } end example 'capture: String raises CompileError (nil pattern cannot constrain)' do expect { Mustermann::Sinatra.new('/:x', capture: String) }. to raise_error(Mustermann::CompileError, /no converter for class String/) end example 'capture: unsupported class raises CompileError' do expect { Mustermann::Sinatra.new('/:x', capture: Range) }. to raise_error(Mustermann::CompileError, /no converter for class Range/) end example 'capture: unsupported value raises CompileError' do expect { Mustermann::Sinatra.new('/:x', capture: Object.new) }. to raise_error(Mustermann::CompileError, /invalid capture constraint/) end context 'capture: array of types tries each in order' do pattern '/:n', capture: [Integer, Float] do it { should match('/42') .capturing n: '42' } it { should match('/-5') .capturing n: '-5' } it { should match('/3.14') .capturing n: '3.14' } it { should match('/-2.5') .capturing n: '-2.5' } it { should_not match('/') } it { should_not match('/foo') } example { pattern.params('/42').should be == { 'n' => 42 } } example { pattern.params('/-5').should be == { 'n' => -5 } } example { pattern.params('/3.14').should be == { 'n' => 3.14 } } example { pattern.params('/-2.5').should be == { 'n' => -2.5 } } end pattern '/:n', capture: [Integer, :slug] do it { should match('/42') .capturing n: '42' } it { should match('/hello-world') .capturing n: 'hello-world' } it { should_not match('/') } it { should_not match('/Hello') } example { pattern.params('/42').should be == { 'n' => 42 } } example { pattern.params('/hello-world').should be == { 'n' => 'hello-world' } } end end context 'converters apply when capture is the head of a look-ahead node' do pattern '/:id(.:format)?', capture: { id: Integer } do it { should match('/42') .capturing id: '42' } it { should match('/42.json') .capturing id: '42' } it { should_not match('/foo') } it { should_not match('/foo.json') } example { pattern.params('/42').should be == { 'id' => 42, 'format' => nil } } example { pattern.params('/42.json').should be == { 'id' => 42, 'format' => 'json' } } end pattern '/:id(.:format)?', capture: { id: Integer, format: :slug } do it { should match('/42') .capturing id: '42' } it { should match('/42.json') .capturing id: '42' } it { should_not match('/foo.json') } example { pattern.params('/42').should be == { 'id' => 42, 'format' => nil } } example { pattern.params('/42.json').should be == { 'id' => 42, 'format' => 'json' } } end end end pattern '/(:a)b?', greedy: false do it { should match('/ab').capturing a: 'a' } end pattern '/:file(.:ext)?', greedy: false do it { should match('/pony') .capturing file: 'pony', ext: nil } it { should match('/pony.jpg') .capturing file: 'pony', ext: 'jpg' } it { should match('/pony.png.jpg') .capturing file: 'pony', ext: 'png.jpg' } end pattern '/auth/*', except: '/auth/login' do it { should match('/auth/admin') } it { should match('/auth/foobar') } it { should_not match('/auth/login') } end pattern '/:foo/:bar', except: '/:bar/20' do it { should match('/foo/bar').capturing foo: 'foo', bar: 'bar' } it { should_not match('/20/20') } end pattern '/foo?', uri_decode: false do it { should match('/foo') } it { should match('/fo') } it { should_not match('/foo?') } end pattern '/foo/bar', uri_decode: false do it { should match('/foo/bar') } it { should_not match('/foo%2Fbar') } it { should_not match('/foo%2fbar') } end pattern "/path with spaces", uri_decode: false do it { should match('/path with spaces') } it { should_not match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end pattern "/path with spaces", space_matches_plus: false do it { should match('/path with spaces') } it { should match('/path%20with%20spaces') } it { should_not match('/path%2Bwith%2Bspaces') } it { should_not match('/path+with+spaces') } end context 'invalid syntax' do example 'unexpected closing parenthesis' do expect { Mustermann::Sinatra.new('foo)bar') }. to raise_error(Mustermann::ParseError, 'unexpected ) while parsing "foo)bar"') end example 'missing closing parenthesis' do expect { Mustermann::Sinatra.new('foo(bar') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar"') end example 'missing unescaped closing parenthesis' do expect { Mustermann::Sinatra.new('foo(bar\)') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo(bar\\\\)"') end example '? at beginning of route' do expect { Mustermann::Sinatra.new('?foobar') }. to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "?foobar"') end example 'double ?' do expect { Mustermann::Sinatra.new('foo??bar') }. to raise_error(Mustermann::ParseError, 'unexpected ? while parsing "foo??bar"') end example 'dangling escape' do expect { Mustermann::Sinatra.new('foo\\') }. to raise_error(Mustermann::ParseError, 'unexpected end of string while parsing "foo\\\\"') end end context 'invalid capture names' do example 'empty name' do expect { Mustermann::Sinatra.new('/:/') }. to raise_error(Mustermann::CompileError, "capture name can't be empty: \"/:/\"") end example 'named splat' do expect { Mustermann::Sinatra.new('/:splat/') }. to raise_error(Mustermann::CompileError, "capture name can't be splat: \"/:splat/\"") end example 'named captures' do expect { Mustermann::Sinatra.new('/:captures/') }. to raise_error(Mustermann::CompileError, "capture name can't be captures: \"/:captures/\"") end example 'with capital letter' do expect { Mustermann::Sinatra.new('/:Foo/') }. to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:Foo/\"") end example 'with integer' do expect { Mustermann::Sinatra.new('/:1a/') }. to raise_error(Mustermann::CompileError, "capture name must start with underscore or lower case letter: \"/:1a/\"") end example 'same name twice' do expect { Mustermann::Sinatra.new('/:foo(/:bar)?/:bar?') }. to raise_error(Mustermann::CompileError, "can't use the same capture name twice: \"/:foo(/:bar)?/:bar?\"") end end context 'Regexp compatibility' do describe :=== do example('non-matching') { Mustermann::Sinatra.new("/") .should_not be === '/foo' } example('matching') { Mustermann::Sinatra.new("/:foo") .should be === '/foo' } end describe :=~ do example('non-matching') { Mustermann::Sinatra.new("/") .should_not be =~ '/foo' } example('matching') { Mustermann::Sinatra.new("/:foo") .should be =~ '/foo' } context 'String#=~' do example('non-matching') { "/foo".should_not be =~ Mustermann::Sinatra.new("/") } example('matching') { "/foo".should be =~ Mustermann::Sinatra.new("/:foo") } end end describe :to_regexp do example('empty pattern') { Mustermann::Sinatra.new('').to_regexp.should be == /\A(?-mix:)\Z/ } context 'Regexp.try_convert' do example('empty pattern') { Regexp.try_convert(Mustermann::Sinatra.new('')).should be == /\A(?-mix:)\Z/ } end end end context 'Proc compatibility' do describe :to_proc do example { Mustermann::Sinatra.new("/").to_proc.should be_a(Proc) } example('non-matching') { Mustermann::Sinatra.new("/") .to_proc.call('/foo').should be == false } example('matching') { Mustermann::Sinatra.new("/:foo") .to_proc.call('/foo').should be == true } end end context "peeking" do subject(:pattern) { Mustermann::Sinatra.new(":name") } describe :peek_size do example { pattern.peek_size("foo bar/blah") .should be == "foo bar".size } example { pattern.peek_size("foo%20bar/blah") .should be == "foo%20bar".size } example { pattern.peek_size("/foo bar") .should be_nil } end describe :peek_match do example { pattern.peek_match("foo bar/blah") .to_s .should be == "foo bar" } example { pattern.peek_match("foo%20bar/blah") .to_s .should be == "foo%20bar" } example { pattern.peek_match("/foo bar") .should be_nil } end describe :peek_params do example { pattern.peek_params("foo bar/blah") .should be == [{"name" => "foo bar"}, "foo bar".size] } example { pattern.peek_params("foo%20bar/blah") .should be == [{"name" => "foo bar"}, "foo%20bar".size] } example { pattern.peek_params("/foo bar") .should be_nil } end end describe :| do let(:first) { Mustermann.new("a") } let(:second) { Mustermann.new("b") } subject(:composite) { first | second } context "with no capture names" do its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "a|b" } end context "only first has captures" do let(:first) { Mustermann.new(":a") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "{a}|b" } end context "only second has captures" do let(:second) { Mustermann.new(":b") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "a|{b}" } end context "both have captures, non-overlapping names" do let(:first) { Mustermann.new(":a") } let(:second) { Mustermann.new(":b") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "{a}|{b}" } end context "both have captures, overlapping names" do let(:first) { Mustermann.new(":a") } let(:second) { Mustermann.new(":a") } its(:class) { should be == Mustermann::Composite } end context "options mismatch" do let(:second) { Mustermann.new(":b", greedy: false) } its(:class) { should be == Mustermann::Composite } end context "argument is a string" do let(:second) { ":b" } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "a|\\:b" } end context "argument is an identity pattern" do let(:second) { Mustermann::Identity.new(":b") } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "a|\\:b" } end context "argument is an identity pattern, but options mismatch" do let(:second) { Mustermann::Identity.new(":b", uri_decode: false) } its(:class) { should be == Mustermann::Composite } end context "rails pattern with Integer capture | sinatra pattern with Symbol capture" do subject { Mustermann.new("/:a", type: :rails, capture: Integer) | Mustermann.new("/:b", capture: Symbol) } its(:class) { should be == Mustermann::Hybrid } its(:to_s) { should be == "/{a}|/{b}" } example { subject.params("/42") .should be == { "a" => 42, "b" => nil } } example { subject.params("/foo").should be == { "a" => nil, "b" => :foo } } end context "with Hash capture on second pattern" do let(:first) { Mustermann.new(":a", capture: Integer) } let(:second) { Mustermann.new(":b", capture: {b: Float}) } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "{a}|{b}" } end context "with Array-in-Hash capture on second pattern" do let(:first) { Mustermann.new(":a", capture: Integer) } let(:second) { Mustermann.new(":b", capture: {b: [Integer, Float]}) } its(:class) { should be == Mustermann::Sinatra } its(:to_s) { should be == "{a}|{b}" } end end describe "native concatination" do subject { Mustermann.new(prefix) + Mustermann.new(pattern) } let(:prefix) { "/a" } let(:pattern) { "/:b(.:c)?" } it { should match("/a/b.json") } end describe "match caching" do if defined?(ObjectSpace::WeakKeyMap) describe "default caching behavior" do subject(:pattern) { Mustermann.new("/foo/:bar") } it "returns the same Match object for the same string object" do string = "/foo/bar" expect(pattern.match(string)).to be_equal(pattern.match(string)) end it "returns nil for a non-matching string without caching errors" do expect(pattern.match("/nope")).to be_nil expect(pattern.match("/nope")).to be_nil end end end describe "with cache enabled" do subject(:pattern) { Mustermann.new("/foo/:bar", cache: Hash) } it "returns the same Match object for the same string object" do string = "/foo/bar" expect(pattern.match(string)).to be_equal(pattern.match(string)) end it "returns nil for a non-matching string without caching errors" do expect(pattern.match("/nope")).to be_nil pattern.match("/foo/bar") expect(pattern.match("/nope")).to be_nil expect(pattern.params("/nope")).to be_nil end it "extracts params correctly" do expect(pattern.params("/foo/baz")).to eq("bar" => "baz") end it "decodes percent-encoded params" do expect(pattern.params("/foo/f%20o")).to eq("bar" => "f o") end it "does not populate the match cache when calling params" do cache = {} pattern = Mustermann.new("/foo/:bar", cache: { match: cache }) string = "/foo/baz" pattern.params(string) expect(cache.key?(string)).to be false end end describe "with cache disabled" do subject(:pattern) { Mustermann.new("/foo/:bar", cache: false) } it "returns a match for a matching string" do expect(pattern.match("/foo/bar")).not_to be_nil end it "returns nil for a non-matching string" do expect(pattern.match("/nope")).to be_nil end end end end mustermann-4.0.0/support/000077500000000000000000000000001517364653100154335ustar00rootroot00000000000000mustermann-4.0.0/support/lib/000077500000000000000000000000001517364653100162015ustar00rootroot00000000000000mustermann-4.0.0/support/lib/support.rb000066400000000000000000000003601517364653100202410ustar00rootroot00000000000000require 'support/env' require 'support/coverage' if RUBY_ENGINE == 'ruby' require 'support/expand_matcher' require 'support/generate_template_matcher' require 'support/match_matcher' require 'support/pattern' require 'support/scan_matcher' mustermann-4.0.0/support/lib/support/000077500000000000000000000000001517364653100177155ustar00rootroot00000000000000mustermann-4.0.0/support/lib/support/coverage.rb000066400000000000000000000007361517364653100220430ustar00rootroot00000000000000require 'simplecov' require 'support/projects' require 'coveralls' SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new( [ SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter ] ) SimpleCov.start do project_name 'mustermann' minimum_coverage 100 coverage_dir '.coverage' add_filter "/spec/" add_filter "/support/" Support::Projects.each do |project| add_group project.sub('mustermann-', ''), "/#{project}/lib" end end mustermann-4.0.0/support/lib/support/env.rb000066400000000000000000000005421517364653100210330ustar00rootroot00000000000000if RUBY_VERSION < '2.1.0' $stderr.puts "needs Ruby 2.1.0, you're running #{RUBY_VERSION}" exit 1 end RUBY_ENGINE ||= 'ruby' ENV['RACK_ENV'] = 'test' require 'tool/warning_filter' $-w = true require 'pp' require 'rspec' require 'rspec/its' RSpec.configure do |config| config.expect_with :rspec do |c| c.syntax = [:should, :expect] end end mustermann-4.0.0/support/lib/support/expand_matcher.rb000066400000000000000000000013541517364653100232270ustar00rootroot00000000000000RSpec::Matchers.define :expand do |behavior = nil, **values| match do |pattern| @string ||= nil begin expanded = pattern.expand(behavior, values) rescue Exception false else @string ? @string == expanded : !!expanded end end chain :to do |string| @string = string end failure_message do |pattern| message = "expected %p to be expandable with %p" % [pattern, values] message << " (%p behavior)" % behavior if behavior expanded = pattern.expand(behavior, values) message << " and result in %p, but got %p" % [@string, expanded] if @string message end failure_message_when_negated do |pattern| "expected %p not to be expandable with %p" % [pattern, values] end endmustermann-4.0.0/support/lib/support/generate_template_matcher.rb000066400000000000000000000015151517364653100254340ustar00rootroot00000000000000RSpec::Matchers.define :generate_template do |template| match { |pattern| pattern.to_templates.include? template } failure_message do |pattern| "expected %p to generate template %p, but only generated %s" % [ pattern, template, pattern.to_templates.map(&:inspect).join(', ') ] end failure_message_when_negated do |pattern| "expected %p not to generate template %p" % [ pattern, template ] end end RSpec::Matchers.define :generate_templates do |*templates| match { |pattern| pattern.to_templates.sort == templates.sort } failure_message do |pattern| "expected %p to generate templates %p, but generated %p" % [ pattern, templates.sort, pattern.to_templates.sort ] end failure_message_when_negated do |pattern| "expected %p not to generate templates %p" % [ pattern, templates ] end endmustermann-4.0.0/support/lib/support/match_matcher.rb000066400000000000000000000020211517364653100230340ustar00rootroot00000000000000RSpec::Matchers.define :match do |expected| match do |actual| @captures ||= false match = actual.match(expected) match &&= @captures.all? { |k, v| match[k] == v } if @captures match end chain :capturing do |captures| @captures = captures end failure_message do |actual| require 'pp' match = actual.match(expected) if match key, value = @captures.detect { |k, v| match[k] != v } message = "expected %p to capture %p as %p when matching %p, but got %p\n\nRegular Expression:\n%p" % [ actual.to_s, key, value, expected, match[key], actual.regexp ] if actual.respond_to? :to_ast require 'mustermann/visualizer/tree_renderer' tree = Mustermann::Visualizer::TreeRenderer.render(actual) message << "\n\nAST:\n#{tree}" end message else "expected %p to match %p" % [ actual, expected ] end end failure_message_when_negated do |actual| "expected %p not to match %p" % [ actual.to_s, expected ] end end mustermann-4.0.0/support/lib/support/pattern.rb000066400000000000000000000023361517364653100217230ustar00rootroot00000000000000require 'timeout' module Support module Pattern extend RSpec::Matchers::DSL def pattern(pattern, options = nil, &block) description = "pattern %p" % pattern if options description << " with options %p" % [options] instance = subject_for(pattern, **options) else instance = subject_for(pattern) end context description do subject(:pattern, &instance) its(:to_s) { should be == pattern } its(:inspect) { should be == "#<#{described_class}:#{pattern.inspect}>" } its(:to_templates) { should be == [pattern] } if described_class.name == "Mustermann::Template" example 'string should be immune to external change' do subject.to_s.replace "NOT THE PATTERN" subject.to_s.should be == pattern end instance_eval(&block) end end def subject_for(pattern, *args, **options) instance = Timeout.timeout(1) { described_class.new(pattern, *args, **options) } proc { instance } rescue Timeout::Error => error proc { raise Timeout::Error, "could not compile #{pattern.inspect} in time", error.backtrace } rescue Exception => error proc { raise error } end end endmustermann-4.0.0/support/lib/support/projects.rb000066400000000000000000000005441517364653100220760ustar00rootroot00000000000000module Support module Projects include Enumerable extend self def base File.expand_path('../../../..', __FILE__) end def projects @@projects ||= Dir.chdir(base) do Dir['mustermann*/*.gemspec'].map { |f| File.dirname(f) }.sort end end def each(&block) projects.each(&block) end end end mustermann-4.0.0/support/lib/support/scan_matcher.rb000066400000000000000000000035701517364653100226760ustar00rootroot00000000000000module Support module ScanMatcher extend RSpec::Matchers::DSL def scan(pattern, **options) give_scan_result(:scan, pattern, **options) end def check(pattern, **options) give_scan_result(:check, pattern, **options) end def scan_until(pattern, **options) give_scan_result(:scan_until, pattern, **options) end def check_until(pattern, **options) give_scan_result(:check_until, pattern, **options) end matcher :give_scan_result do |method_name, pattern, **options| def result_expectations @result_expectations ||= [] end def expect_result(description, expected, &block) result_expectations << Proc.new do |result| if !block.call(result) "expected %p to %s %p matching %s" % [ result.scanner, method_name, pattern, description ] end end end match do |scanner| scanned = scanner.public_send(method_name, pattern, **options) scanned and result_expectations.all? { |e| !e.call(scanned) } end chain(:matching_substring) do |substring| expected_result("the substring %p" % substring) { |r| r.to_s == substring } end chain(:matching_length) do |length| expected_result("%d characters" % length) { |r| r.length == length } end chain(:matching_params) do |params| expected_result("with params %p" % [params]) { |r| r.params == params } end failure_message do |scanner| if scanned = scanner.public_send(method_name, pattern, **options) message = result_expectations.inject(nil) { |m,e| m || e.call(scanned) } end message || "expected %p to %s %p" % [ scanner, method_name, pattern ] end failure_message_when_negated do |scanner| "expected %p not to %s %p" % [ scanner, method_name, pattern ] end end end endmustermann-4.0.0/support/support.gemspec000066400000000000000000000014421517364653100205150ustar00rootroot00000000000000$:.unshift File.expand_path("../../mustermann/lib", __FILE__) require "mustermann/version" Gem::Specification.new do |s| s.name = "support" s.version = "0.0.1" s.author = "Konstantin Haase" s.email = "konstantin.mailinglists@googlemail.com" s.homepage = "https://github.com/rkh/mustermann" s.summary = %q{support for mustermann development} s.require_path = 'lib' s.files = `git ls-files lib`.split("\n") s.add_dependency 'tool', '~> 0.2' s.add_dependency 'rspec' s.add_dependency 'rspec-its' s.add_dependency 'addressable' s.add_dependency 'sinatra' s.add_dependency 'rack-test' s.add_dependency 'rake' s.add_dependency 'yard' s.add_dependency 'simplecov' s.add_dependency 'coveralls_reborn' s.add_dependency 'irb' end