mustermann-1.0.0/0000755000175000017500000000000013124654676012743 5ustar pravipravimustermann-1.0.0/README.md0000644000175000017500000007151613124654676014234 0ustar pravipravi# The Amazing Mustermann *Make sure you view the correct docs: [latest release](http://rubydoc.info/gems/mustermann/frames), [master](http://rubydoc.info/github/rkh/mustermann/master/frames).* 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 * **[Mapper](#-mapper):** A simple tool for mapping one string to another based on patterns. * **[Sinatra Integration](#-sinatra-integration):** Mustermann can be used as a [Sinatra](http://www.sinatrarb.com/) extension. Sinatra 2.0 and beyond will use Mustermann by default. ## 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](#-pattern-details-regexp) pattern, a symbol always in a [sinatra](#-pattern-details-sinatra) 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](#-sinatra-pattern), [identity](#-identity-pattern) and [regexp](#-regexp-pattern) 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.names # => ['page'] pattern.names # => {"page"=>[1]} 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) pattern.names # => [] pattern.names # => {} ``` 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 `MatchData` or `Mustermann::SimpleMatch` (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" ``` ## Mapper You can use a mapper to transform strings according to two or more mappings: ``` 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" ``` ## Sinatra Integration All patterns implement `match`, which means they can be dropped into Sinatra and other Rack routers: ``` ruby require 'sinatra' require 'mustermann' get Mustermann.new('/:foo') do params[:foo] end ``` In fact, since using this with Sinatra is the main use case, it comes with a build-in extension for **Sinatra 1.x**. ``` ruby require 'sinatra' require 'mustermann' register Mustermann # this will use Mustermann rather than the built-in pattern matching get '/:slug(.ext)?' do params[:slug] end ``` ### Configuration You can change what pattern type you want to use for your app via the `pattern` option: ``` ruby require 'sinatra/base' require 'mustermann' class MyApp < Sinatra::Base register Mustermann set :pattern, type: :shell get '/images/*.png' do send_file request.path_info end get '/index{.htm,.html,}' do erb :index end end ``` You can use the same setting for options: ``` ruby require 'sinatra' require 'mustermann' register Mustermann set :pattern, capture: { ext: %w[png jpg html txt] } get '/:slug(.:ext)?' do # slug will be 'foo' for '/foo.png' # slug will be 'foo.bar' for '/foo.bar' # slug will be 'foo.bar' for '/foo.bar.html' params[:slug] end ``` It is also possible to pass in options to a specific route: ``` ruby require 'sinatra' require 'mustermann' register Mustermann get '/:slug(.:ext)?', pattern: { greedy: false } do # slug will be 'foo' for '/foo.png' # slug will be 'foo' for '/foo.bar' # slug will be 'foo' for '/foo.bar.html' params[:slug] end ``` Of course, all of the above can be combined. Moreover, the `capture` and the `except` option can be passed to route directly. And yes, this also works with `before` and `after` filters. ``` ruby require 'sinatra/base' require 'sinatra/respond_with' require 'mustermann' class MyApp < Sinatra::Base register Mustermann, Sinatra::RespondWith set :pattern, capture: { id: /\d+/ } # id will only match digits # only capture extensions known to Rack before '*:ext', capture: Rack::Mime::MIME_TYPES.keys do content_type params[:ext] # set Content-Type request.path_info = params[:splat].first # drop the extension end get '/:id' do not_found unless page = Page.find params[:id] respond_with(page) end end ``` ### Why would I want this? * It gives you fine grained control over the pattern matching * Allows you to use different pattern styles in your app * The default is more robust and powerful than the built-in patterns * Sinatra 2.0 will use Mustermann internally * Better exceptions for broken route syntax ### Why not include this in Sinatra 1.x? * It would introduce breaking changes, even though these would be minor * Like Sinatra 2.0, Mustermann requires Ruby 2.0 or newer ## 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) # => # ``` It might also be that you want to call `to_pattern` yourself instead of `Mustermann.new`. You can load `mustermann/to_pattern` to implement this method for strings, regular expressions and pattern objects: ``` ruby require 'mustermann/to_pattern' "/foo".to_pattern # => # "/foo".to_pattern(type: :rails) # => # %r{/foo}.to_pattern # => # "/foo".to_pattern.to_pattern # => # ``` You can also use the `Mustermann::ToPattern` mixin to easily add `to_pattern` to your own objects: ``` ruby require 'mustermann/to_pattern' class MyObject include Mustermann::ToPattern def to_s "/foo" end end MyObject.new.to_pattern # => # ``` ### `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`. ### `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 It's generally a good idea to reuse pattern objects, since as much computation as possible is happening during object creation, so that the actual matching or expanding is quite fast. Pattern objects should be treated as immutable. Their internals have been designed for both performance and low memory usage. To reduce pattern compilation, `Mustermann.new` and `Mustermann::Pattern.new` might return the same instance when given the same arguments, if that instance has not yet been garbage collected. However, this is not guaranteed, so do not rely on object identity. ### String Matching When using a pattern instead of a regular expression for string matching, performance will usually be comparable. In certain cases, Mustermann might outperform naive, equivalent regular expressions. It achieves this by using look-ahead and atomic groups in ways that work well with a backtracking, NFA-based regular expression engine (such as the Oniguruma/Onigmo engine used by Ruby). It can be difficult and error prone to construct complex regular expressions using these techniques by hand. This only applies to patterns generating an AST internally (all but [identity](#-pattern-details-identity), [shell](#-pattern-details-shell), [simple](#-pattern-details-simple) and [regexp](#-pattern-details-regexp) patterns). When using a Mustermann pattern as a direct Regexp replacement (ie, via methods like `=~`, `match` or `===`), the overhead will be a single method dispatch, which some Ruby implementations might even eliminate with method inlining. This only applies to patterns using a regular expression internally (all but [identity](#-pattern-details-identity) and [shell](#-pattern-details-shell) patterns). ### Expanding Pattern expansion significantly outperforms other, widely used Ruby tools for generating URLs from URL patterns in most use cases. This comes with a few trade-offs: * As with pattern compilation, as much computation as possible has been shifted to compiling expansion rules. This will add compilation overhead, which is why patterns only generate these rules on the first invocation to `Mustermann::Pattern#expand`. Create a `Mustermann::Expander` instance yourself to get better control over the point in time this computation should happen. * Memory is sacrificed in favor of performance: The size of the expander object will grow linear with the number of possible combination for expansion keys ("/:foo/:bar" has one such combination, but "/(:foo/)?:bar?" has four) * Parsing a params hash from a string generated from another params hash might not result in two identical hashes, and vice versa. Specifically, expanding ignores capture constraints, type casting and greediness. * Partial expansion is (currently) not supported. ## Details on Pattern Types ### `identity` **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.
### `regexp` **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.
### `sinatra` **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).
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.
mustermann-1.0.0/bench/0000755000175000017500000000000013124654676014022 5ustar pravipravimustermann-1.0.0/bench/template_vs_addressable.rb0000644000175000017500000000176213124654676021231 0ustar pravipravi$:.unshift File.expand_path('../lib', __dir__) 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") { 1_000.times { no_capture.match('/simple') } } x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } } simple = klass.new("/{match}") x.report("simple, match") { 1_000.times { simple.match('/simple').captures } } x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } } explode = klass.new("{/segments*}") x.report("explode, match") { 1_000.times { explode.match("/a/b/c").captures } } x.report("explode, miss") { 1_000.times { explode.match("/a/b/c.miss") } } expand = klass.new("/prefix/{foo}/something/{bar}") x.report("expand") { 100.times { expand.expand(foo: 'foo', bar: 'bar').to_s } } end puts end mustermann-1.0.0/bench/simple_vs_sinatra.rb0000644000175000017500000000150713124654676020074 0ustar pravipravi$:.unshift File.expand_path('../lib', __dir__) require 'benchmark' require 'mustermann/simple' require 'mustermann/sinatra' [Mustermann::Simple, Mustermann::Sinatra].each do |klass| puts "", " #{klass} ".center(64, '=') Benchmark.bmbm do |x| no_capture = klass.new("/simple") x.report("no captures, match") { 1_000.times { no_capture.match('/simple') } } x.report("no captures, miss") { 1_000.times { no_capture.match('/miss') } } simple = klass.new("/:name") x.report("simple, match") { 1_000.times { simple.match('/simple').captures } } x.report("simple, miss") { 1_000.times { simple.match('/mi/ss') } } splat = klass.new("/*") x.report("splat, match") { 1_000.times { splat.match("/a/b/c").captures } } x.report("splat, miss") { 1_000.times { splat.match("/a/b/c.miss") } } end puts end mustermann-1.0.0/bench/regexp.rb0000644000175000017500000000077413124654676015651 0ustar pravipravirequire '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-1.0.0/bench/capturing.rb0000644000175000017500000000256613124654676016354 0ustar pravipravi$:.unshift File.expand_path('../lib', __dir__) require 'benchmark' require 'mustermann' require 'mustermann/regexp_based' require 'addressable/template' Mustermann.register(:regexp, Class.new(Mustermann::RegexpBased) { def compile(**options) Regexp.new(@string) end }, load: false) Mustermann.register(:addressable, Class.new(Mustermann::RegexpBased) { def compile(**options) Addressable::Template.new(@string) end }, load: false) list = [ [:sinatra, '/*/:name' ], [:rails, '/*prefix/:name' ], [:simple, '/*/:name' ], [:template, '{/prefix*}/{name}' ], [:regexp, '\A\/(?.*?)\/(?[^\/\?#]+)\Z' ], [:addressable, '{/prefix*}/{name}' ] ] def self.assert(value) fail unless value end string = '/a/b/c/d' name = 'd' GC.disable puts "Compilation:" Benchmark.bmbm do |x| list.each do |type, pattern| x.report(type) { 1_000.times { Mustermann.new(pattern, type: type) } } end end puts "", "Matching with two captures (one splat, one normal):" Benchmark.bmbm do |x| list.each do |type, pattern| pattern = Mustermann.new(pattern, type: type) x.report type do 10_000.times do match = pattern.match(string) assert match[:name] == name end end end endmustermann-1.0.0/lib/0000755000175000017500000000000013124654676013511 5ustar pravipravimustermann-1.0.0/lib/mustermann.rb0000644000175000017500000001153713124654676016236 0ustar pravipravi# 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 # @!visibility private def self.extend_object(object) return super unless defined? ::Sinatra::Base and object.is_a? Class and object < ::Sinatra::Base require 'mustermann/extension' object.register Extension end end # :nocov: begin require 'mustermann/visualizer' if defined?(Pry) or defined?(IRB) rescue LoadError => error raise error unless error.path == 'mustermann/visualizer' $stderr.puts(error.message) if caller_locations[1].absolute_path =~ %r{/lib/pry/|/irb/|^\((?:irb|pry)\)$} end # :nocov: mustermann-1.0.0/lib/mustermann/0000755000175000017500000000000013124654676015702 5ustar pravipravimustermann-1.0.0/lib/mustermann/caster.rb0000644000175000017500000000606613124654676017520 0ustar pravipravi# 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-1.0.0/lib/mustermann/concat.rb0000644000175000017500000000720213124654676017477 0ustar pravipravi# 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) return super unless native = native_concat(other) self.class.new(native, **options) 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) pump(string, initial: SimpleMatch.new) do |pattern, substring| return unless match = pattern.peek_match(substring) [match, match.to_s.size] end 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-1.0.0/lib/mustermann/to_pattern.rb0000644000175000017500000000273713124654676020417 0ustar pravipravi# 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-1.0.0/lib/mustermann/sinatra.rb0000644000175000017500000000622213124654676017672 0ustar pravipravi# frozen_string_literal: true require 'mustermann' require 'mustermann/identity' require 'mustermann/ast/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 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(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 Sintara pattern, it might join the two to a third # sinatra pattern instead of generating a composite for efficency 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 = self.class.try_convert(other, **options) return super unless converted.names.empty? or names.empty? self.class.new(safe_string + "|" + converted.safe_string, **options) end # Generates a string represenation 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" => "bar" } # # @return [String] string representatin of the pattern def safe_string @safe_string ||= SafeRenderer.translate(to_ast) end # @!visibility private def native_concat(other) return unless converted = self.class.try_convert(other, **options) safe_string + converted.safe_string end private :native_concat end end mustermann-1.0.0/lib/mustermann/extension.rb0000644000175000017500000000300413124654676020240 0ustar pravipravi# frozen_string_literal: true require 'sinatra/version' fail "no need to load the Mustermann extension for #{::Sinatra::VERSION}" if ::Sinatra::VERSION >= '2.0.0' require 'mustermann' module Mustermann # Sinatra 1.x extension switching default pattern parsing over to Mustermann. # # @example With classic Sinatra application # require 'sinatra' # require 'mustermann' # # register Mustermann # get('/:id', capture: /\d+/) { ... } # # @example With modular Sinatra application # require 'sinatra/base' # require 'mustermann' # # class MyApp < Sinatra::Base # register Mustermann # get('/:id', capture: /\d+/) { ... } # end # # @see file:README.md#Sinatra_Integration "Sinatra Integration" in the README module Extension def compile!(verb, path, block, except: nil, capture: nil, pattern: { }, **options) if path.respond_to? :to_str pattern[:except] = except if except pattern[:capture] = capture if capture if settings.respond_to? :pattern and settings.pattern? pattern.merge! settings.pattern do |key, local, global| next local unless local.is_a? Hash next global.merge(local) if global.is_a? Hash Hash.new(global).merge! local end end path = Mustermann.new(path, **pattern) condition { params.merge! path.params(captures: Array(params[:captures]), offset: -1) } end super(verb, path, block, options) end private :compile! end end mustermann-1.0.0/lib/mustermann/regular.rb0000644000175000017500000000250613124654676017673 0ustar pravipravi# 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/" @check_anchors = check_anchors super(string, **options) end def compile(**options) if @check_anchors scanner = ::StringScanner.new(@string) check_anchors(scanner) until scanner.eos? end /#{@string}/ end def check_anchors(scanner) return scanner.scan_until(/\]/) if scanner.scan(/\[/) return scanner.scan(/\\?./) unless illegal = scanner.scan(/\\[AzZ]|[\^\$]/) raise CompileError, "regular expression should not contain %s: %p" % [illegal.to_s, @string] end private :compile, :check_anchors end end mustermann-1.0.0/lib/mustermann/mapper.rb0000644000175000017500000000631513124654676017520 0ustar pravipravi# frozen_string_literal: true require 'mustermann' require 'mustermann/expander' 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) # # @example map after options # require 'mustermann/mapper' # Mustermann::Mapper.new(type: :rails, "/:foo" => "/:foo.html") def initialize(map = {}, additional_values: :ignore, **options, &block) @map = [] @options = options @additional_values = additional_values 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| input = Mustermann.new(input, **@options) output = Expander.new(*output, additional_values: @additional_values, **@options) unless output.is_a? Expander @map << [input, output] end end # @return [Hash{Patttern: Expander}] Hash version of the mapper. def to_h Hash[@map] end # 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 = {}) @map.inject(input) do |current, (pattern, expander)| params = pattern.params(current) params &&= Hash[values.merge(params).map { |k,v| [k.to_s, v] }] expander.expandable?(params) ? expander.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-1.0.0/lib/mustermann/identity.rb0000644000175000017500000000553413124654676020067 0ustar pravipravi# 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-1.0.0/lib/mustermann/pattern_cache.rb0000644000175000017500000000273313124654676021034 0ustar pravipravi# 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-1.0.0/lib/mustermann/expander.rb0000644000175000017500000002063113124654676020037 0ustar pravipravi# frozen_string_literal: true require 'mustermann/ast/expander' require 'mustermann/caster' require 'mustermann' 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 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) unless additional_values == :raise or additional_values == :ignore or additional_values == :append raise ArgumentError, "Illegal value %p for additional_values" % additional_values end @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| 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 # @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-1.0.0/lib/mustermann/simple_match.rb0000644000175000017500000000232013124654676020671 0ustar pravipravi# frozen_string_literal: true module Mustermann # Fakes MatchData for patterns that do not support capturing. # @see http://ruby-doc.org/core-2.0/MatchData.html MatchData class SimpleMatch # @api private def initialize(string = "", names: [], captures: []) @string = string.dup @names = names @captures = captures end # @return [String] the string that was matched against def to_s @string.dup end # @return [Array] empty array for imitating MatchData interface def names @names.dup end # @return [Array] empty array for imitating MatchData interface def captures @captures.dup end # @return [nil] imitates MatchData interface def [](*args) args.map! do |arg| next arg unless arg.is_a? Symbol or arg.is_a? String names.index(arg.to_s) end @captures[*args] end # @!visibility private def +(other) SimpleMatch.new(@string + other.to_s, names: @names + other.names, captures: @captures + other.captures) end # @return [String] string representation def inspect "#<%p %p>" % [self.class, @string] end end end mustermann-1.0.0/lib/mustermann/equality_map.rb0000644000175000017500000000356113124654676020726 0ustar pravipravi# 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 [Array<#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] key = identity == key ? identity : key # 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-1.0.0/lib/mustermann/regexp.rb0000644000175000017500000000003513124654676017517 0ustar pravipravirequire 'mustermann/regular' mustermann-1.0.0/lib/mustermann/error.rb0000644000175000017500000000072713124654676017366 0ustar pravipravi# frozen_string_literal: true module Mustermann 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}. end mustermann-1.0.0/lib/mustermann/ast/0000755000175000017500000000000013124654676016471 5ustar pravipravimustermann-1.0.0/lib/mustermann/ast/parser.rb0000644000175000017500000001734613124654676020325 0ustar pravipravi# 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, &block) type = Node[type] unless type.respond_to? :new start = pos node = block ? type.parse(*args, &block) : type.new(*args) 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-1.0.0/lib/mustermann/ast/node.rb0000644000175000017500000001300113124654676017736 0ustar pravipravimodule 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(*args, &block) new(*args).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 ||= "" 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-1.0.0/lib/mustermann/ast/translator.rb0000644000175000017500000000757713124654676021227 0ustar pravipravi# 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 # 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, &block) return translator unless args.any? translator.translate(*args, &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 decorator_for(node) factory = node.class.ancestors.inject(nil) { |d,a| d || self.class.dispatch_table[a.name] } raise error_class, "#{self.class}: Cannot translate #{node.class}" unless factory factory.new(node, self) end # Start the translation dance for a (sub)tree. # @!visibility private def translate(node, *args, &block) result = decorator_for(node).translate(*args, &block) result = result.node while result.is_a? NodeTranslator result end # @return [String] escaped character # @!visibility private def escape(char, parser: URI::DEFAULT_PARSER, escape: parser.regexp[:UNSAFE], also_escape: nil) escape = Regexp.union(also_escape, escape) if also_escape char =~ escape ? parser.escape(char, Regexp.union(*escape)) : char end end end end mustermann-1.0.0/lib/mustermann/ast/compiler.rb0000644000175000017500000001444013124654676020633 0ustar pravipravi# frozen_string_literal: true require 'mustermann/ast/translator' module Mustermann # @see Mustermann::AST::Pattern module AST # Regexp compilation logic. # @!visibility private class Compiler < Translator raises CompileError # Trivial compilations translate(Array) { |**o| map { |e| t(e, **o) }.join } 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 |**options| 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 ? '$' : '/') t(head, lookahead: lookahead, **options) + t(payload, **options) end # Capture compilation is complex. :( # @!visibility private class Capture < NodeTranslator register :capture # @!visibility private def translate(**options) return pattern(options) if options[:no_captures] "(?<#{name}>#{translate(no_captures: true, **options)})" 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) else capture 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) 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 default(**options) constraint || "[^/\\?#]" end 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(**options) return super(**options) if explode or not 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(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 [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 encoded = escape(char, escape: /./) list = [escape(char), encoded.downcase, encoded.upcase].uniq.map { |c| Regexp.escape(c) } if char == " " list << encoded('+') if space_matches_plus list << " " end "(?:%s)" % list.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-1.0.0/lib/mustermann/ast/template_generator.rb0000644000175000017500000000216113124654676022677 0ustar pravipravi# 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-1.0.0/lib/mustermann/ast/validation.rb0000644000175000017500000000317613124654676021157 0ustar pravipravi# 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-1.0.0/lib/mustermann/ast/expander.rb0000644000175000017500000001032713124654676020627 0ustar pravipravi# 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| inject(t.pattern) do |pattern, element| t.add_to(pattern, t(element, *args)) 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) values = values.each_with_object({}){ |(key, value), new_hash| new_hash[value.instance_of?(Array) ? [key] * value.length : key] = value } keys, pattern, filters = mappings.fetch(values.keys.flatten.sort) { error_for(values) } filters.each { |key, filter| values[key] &&= escape(values[key], also_escape: filter) } pattern % (values[keys] || values.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) # 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, **f2] } } end end end end mustermann-1.0.0/lib/mustermann/ast/param_scanner.rb0000644000175000017500000000107113124654676021626 0ustar pravipravi# frozen_string_literal: true 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) new.translate(ast) end translate(:node) { t(payload) } translate(Array) { map { |e| t(e) }.inject(:merge) } translate(Object) { {} } translate(:capture) { convert ? { name => convert } : {} } end end end mustermann-1.0.0/lib/mustermann/ast/boundaries.rb0000644000175000017500000000262413124654676021155 0ustar pravipravi# 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-1.0.0/lib/mustermann/ast/transformer.rb0000644000175000017500000001375213124654676021370 0ustar pravipravi# 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 # 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-1.0.0/lib/mustermann/ast/pattern.rb0000644000175000017500000001047513124654676020502 0ustar pravipravi# 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 # @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 # Internal AST representation of pattern. # @!visibility private def to_ast @ast_cache ||= EqualityMap.new @ast_cache.fetch(@string) do ast = parse(@string, pattern: self) ast &&= transform(ast) ast &&= set_boundaries(ast, string: @string) validate(ast) end 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 param_converters[key][super] end # @!visibility private def param_converters @param_converters ||= scan_params(to_ast) end private :compile, :parse, :transform, :validate, :generate_templates, :param_converters, :scan_params, :set_boundaries end end end mustermann-1.0.0/lib/mustermann/version.rb0000644000175000017500000000011213124654676017706 0ustar pravipravi# frozen_string_literal: true module Mustermann VERSION ||= '1.0.0' end mustermann-1.0.0/lib/mustermann/composite.rb0000644000175000017500000000622713124654676020240 0ustar pravipravi# 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#== 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 simple_inspect end # @!visibility private def inspect "#<%p:%s>" % [self.class, simple_inspect] end # @!visibility private def simple_inspect pattern_strings = patterns.map { |p| p.simple_inspect } "(#{pattern_strings.join(" #{operator} ")})" 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 = nil) return pattern.patterns if pattern.is_a? Composite and pattern.operator == self.operator [options ? Mustermann.new(pattern, **options) : pattern] end private :with_matching, :patterns_from end end mustermann-1.0.0/lib/mustermann/sinatra/0000755000175000017500000000000013124654676017343 5ustar pravipravimustermann-1.0.0/lib/mustermann/sinatra/parser.rb0000644000175000017500000000253513124654676021171 0ustar pravipravi# 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-1.0.0/lib/mustermann/sinatra/safe_renderer.rb0000644000175000017500000000227613124654676022503 0ustar pravipravi# frozen_string_literal: true module Mustermann class Sinatra < AST::Pattern # Generates a string that can safely be concatenated with other strings # without chaning 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(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-1.0.0/lib/mustermann/sinatra/try_convert.rb0000644000175000017500000000263013124654676022247 0ustar pravipravi# 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(input, **options) new(options).translate(input) end # Expected options for the resulting pattern. # @!visibility private attr_reader :options # @!visibility private def initialize(options) @options = options end # @return [Mustermann::Sinatra] # @!visibility private def new(input, escape = false) input = Mustermann::Sinatra.escape(input) if escape Mustermann::Sinatra.new(input, **options) 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 translate(Object) { nil } translate(String) { t.new(self, true) } translate(Identity) { t.new(self, true) if uri_decode == t.uri_decode } translate(Sinatra) { node if options == t.options } translate AST::Pattern do next unless options == t.options t.new(SafeRenderer.translate(to_ast)) rescue nil end end private_constant :TryConvert end end mustermann-1.0.0/lib/mustermann/regexp_based.rb0000644000175000017500000000255613124654676020667 0ustar pravipravi# 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 # @param (see Mustermann::Pattern#initialize) # @return (see Mustermann::Pattern#initialize) # @see (see Mustermann::Pattern#initialize) def initialize(string, **options) super regexp = compile(**options) @peek_regexp = /\A#{regexp}/ @regexp = /\A#{regexp}\Z/ 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) @peek_regexp.match(string) end extend Forwardable def_delegators :regexp, :===, :=~, :match, :names, :named_captures def compile(**options) raise NotImplementedError, 'subclass responsibility' end private :compile end end mustermann-1.0.0/lib/mustermann/pattern.rb0000644000175000017500000003460513124654676017714 0ustar pravipravi# frozen_string_literal: true require 'mustermann/error' require 'mustermann/simple_match' require 'mustermann/equality_map' require 'uri' module Mustermann # Superclass for all pattern implementations. # @abstract class Pattern include Mustermann @@uri ||= URI::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 [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-match Regexp#match # @see http://ruby-doc.org/core-2.0/MatchData.html MatchData # @see Mustermann::SimpleMatch def match(string) SimpleMatch.new(string) if self === string end # @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 [MatchData, Mustermann::SimpleMatch, nil] MatchData or similar object if the pattern matches. # @see #peek_params def peek_match(string) matched = peek(string) match(matched) 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) [params(captures: match), match.to_s.size] if match end # @return [Hash{String: Array}] capture names mapped to capture index. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-named_captures Regexp#named_captures def named_captures {} end # @return [Array] capture names. # @see http://ruby-doc.org/core-2.0/Regexp.html#method-i-names Regexp#names def names [] 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, captures: nil, offset: 0) return unless captures ||= match(string) params = named_captures.map do |name, positions| values = positions.map { |pos| map_param(name, captures[pos + offset]) }.flatten values = values.first if values.size < 2 and not always_array? name [name, values] end Hash[params] end # @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 execptions 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 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 @@uri.unescape(string) end # @!visibility private ALWAYS_ARRAY = %w[splat captures] # @!visibility private def always_array?(key) ALWAYS_ARRAY.include? key end private :unescape, :map_param, :respond_to_special? private_constant :ALWAYS_ARRAY end end mustermann-1.0.0/LICENSE0000644000175000017500000000211713124654676013751 0ustar pravipraviCopyright (c) 2013-2017 Konstantin Haase Copyright (c) 2016-2017 Zachary Scott 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-1.0.0/mustermann.gemspec0000644000175000017500000000153613124654676016506 0ustar pravipravi$:.unshift File.expand_path("../lib", __FILE__) require "mustermann/version" Gem::Specification.new do |s| s.name = "mustermann" s.version = Mustermann::VERSION s.authors = ["Konstantin Haase", "Zachary Scott"] s.email = "sinatrarb@googlegroups.com" s.homepage = "https://github.com/sinatra/mustermann" s.summary = %q{use patterns like regular expressions} s.description = %q{library implementing patterns that behave like regular expressions} s.license = 'MIT' s.required_ruby_version = '>= 2.2.0' s.files = `git ls-files`.split("\n") s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } end mustermann-1.0.0/spec/0000755000175000017500000000000013124654676013675 5ustar pravipravimustermann-1.0.0/spec/to_pattern_spec.rb0000644000175000017500000000462513124654676017422 0ustar pravipravi# 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::Composite) } example { [:foo, :bar].to_pattern(type: :rails) .should be_a(Mustermann::Composite) } 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::Composite) } example { example_class.new([:foo, :bar]).to_pattern(type: :rails) .should be_a(Mustermann::Composite) } 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::Composite) } example { example_class.new([:foo, :bar]).to_pattern(type: :rails) .should be_a(Mustermann::Composite) } end end mustermann-1.0.0/spec/expander_spec.rb0000644000175000017500000001374613124654676017055 0ustar pravipravi# 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 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) } 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 :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-1.0.0/spec/identity_spec.rb0000644000175000017500000000732313124654676017072 0ustar pravipravi# 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 pattern.match('').inspect.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-1.0.0/spec/sinatra_spec.rb0000644000175000017500000007336013124654676016706 0ustar pravipravi# frozen_string_literal: true require 'support' require 'mustermann/sinatra' 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%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 capture all splat parts" do match = pattern.match('/bar/foo/bling/baz/boom') match.captures.should be == ['bar', 'foo', 'bling', 'baz/boom'] match.names.should be == ['splat', 'foo'] end 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 capture all splat parts" do match = pattern.match('/bar/foo/bling/baz/boom') match.captures.should be == ['bar', 'foo', 'bling', 'baz/boom'] match.names.should be == ['splat', 'foo'] end 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 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%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" do let(:first) { Mustermann.new(":a") } let(:second) { Mustermann.new(":b") } 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 end end mustermann-1.0.0/spec/ast_spec.rb0000644000175000017500000000071613124654676016027 0ustar pravipravi# 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-1.0.0/spec/concat_spec.rb0000644000175000017500000001055513124654676016511 0ustar pravipravi# 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 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-1.0.0/spec/mustermann_spec.rb0000644000175000017500000001050313124654676017424 0ustar pravipravi# frozen_string_literal: true require 'support' require 'mustermann' require 'mustermann/extension' require 'sinatra/base' 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 :extend_object do context 'special behavior for Sinatra only' do example { Object .new.extend(Mustermann).should be_a(Mustermann) } example { Object .new.extend(Mustermann).should_not be_a(Mustermann::Extension) } example { Class .new.extend(Mustermann).should be_a(Mustermann) } example { Class .new.extend(Mustermann).should_not be_a(Mustermann::Extension) } example { Sinatra .new.extend(Mustermann).should_not be_a(Mustermann) } example { Sinatra .new.extend(Mustermann).should be_a(Mustermann::Extension) } end end describe :=== do example { Mustermann.should be === Mustermann.new("") } end end mustermann-1.0.0/spec/regular_spec.rb0000644000175000017500000001274613124654676016707 0ustar pravipravi# frozen_string_literal: true require 'support' 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 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-1.0.0/spec/pattern_spec.rb0000644000175000017500000000446413124654676016721 0ustar pravipravi# 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 :== 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-1.0.0/spec/extension_spec.rb0000644000175000017500000002214113124654676017250 0ustar pravipravi# frozen_string_literal: true require 'support' require 'mustermann/extension' require 'sinatra/base' require 'rack/test' describe Mustermann::Extension do include Rack::Test::Methods subject :app do Sinatra.new do set :environment, :test register Mustermann end end it 'sets up the extension' do app.should be_a(Mustermann::Extension) end context 'uses Sinatra-style patterns by default' do before { app.get('/:slug(.:extension)?') { params[:slug] } } example { get('/foo') .body.should be == 'foo' } example { get('/foo.') .body.should be == 'foo.' } example { get('/foo.bar') .body.should be == 'foo' } example { get('/a%20b') .body.should be == 'a b' } end describe :except do before { app.get('/auth/*', except: '/auth/login') { 'ok' } } example { get('/auth/dunno').should be_ok } example { get('/auth/login').should_not be_ok } end describe :capture do context 'global' do before do app.set(:pattern, capture: { ext: %w[png jpg gif] }) app.get('/:slug(.:ext)?') { params[:slug] } end example { get('/foo.bar').body.should be == 'foo.bar' } example { get('/foo.png').body.should be == 'foo' } end context 'route local' do before do app.set(:pattern, nil) app.get('/:id', capture: /\d+/) { 'ok' } end example { get('/42').should be_ok } example { get('/foo').should_not be_ok } end context 'global and route local' do context 'global is a hash' do before do app.set(:pattern, capture: { id: /\d+/ }) app.get('/:id(.:ext)?', capture: { ext: 'png' }) { ?a } app.get('/:id', capture: { id: 'foo' }) { ?b } app.get('/:id', capture: :alpha) { ?c } end example { get('/20') .body.should be == ?a } example { get('/20.png') .body.should be == ?a } example { get('/foo') .body.should be == ?b } example { get('/bar') .body.should be == ?c } end context 'global is not a hash' do before do app.set(:pattern, capture: /\d+/) app.get('/:slug(.:ext)?', capture: { ext: 'png' }) { params[:slug] } app.get('/:slug', capture: :alpha) { 'ok' } end example { get('/20.png').should be_ok } example { get('/foo.png').should_not be_ok } example { get('/foo').should be_ok } example { get('/20.png') .body.should be == '20' } example { get('/42') .body.should be == '42' } example { get('/foo') .body.should be == 'ok' } end end end describe :pattern do describe :except do before { app.get('/auth/*', pattern: { except: '/auth/login' }) { 'ok' } } example { get('/auth/dunno').should be_ok } example { get('/auth/login').should_not be_ok } end describe :capture do context 'route local' do before do app.set(:pattern, nil) app.get('/:id', pattern: { capture: /\d+/ }) { 'ok' } end example { get('/42').should be_ok } example { get('/foo').should_not be_ok } end context 'global and route local' do context 'global is a hash' do before do app.set(:pattern, capture: { id: /\d+/ }) app.get('/:id(.:ext)?', pattern: { capture: { ext: 'png' }}) { ?a } app.get('/:id', pattern: { capture: { id: 'foo' }}) { ?b } app.get('/:id', pattern: { capture: :alpha }) { ?c } end example { get('/20') .body.should be == ?a } example { get('/20.png') .body.should be == ?a } example { get('/foo') .body.should be == ?b } example { get('/bar') .body.should be == ?c } end context 'global is not a hash' do before do app.set(:pattern, capture: /\d+/) app.get('/:slug(.:ext)?', pattern: { capture: { ext: 'png' }}) { params[:slug] } app.get('/:slug', pattern: { capture: :alpha }) { 'ok' } end example { get('/20.png').should be_ok } example { get('/foo.png').should_not be_ok } example { get('/foo').should be_ok } example { get('/20.png') .body.should be == '20' } example { get('/42') .body.should be == '42' } example { get('/foo') .body.should be == 'ok' } end end end describe :greedy do context 'default' do before { app.get('/:name.:ext') { params[:name] }} example { get('/foo.bar') .body.should be == 'foo' } example { get('/foo.bar.baz') .body.should be == 'foo.bar' } end context 'enabled' do before { app.get('/:name.:ext', pattern: { greedy: true }) { params[:name] }} example { get('/foo.bar') .body.should be == 'foo' } example { get('/foo.bar.baz') .body.should be == 'foo.bar' } end context 'disabled' do before { app.get('/:name.:ext', pattern: { greedy: false }) { params[:name] }} example { get('/foo.bar') .body.should be == 'foo' } example { get('/foo.bar.baz') .body.should be == 'foo' } end context 'global' do before do app.set(:pattern, greedy: false) app.get('/:name.:ext') { params[:name] } end example { get('/foo.bar') .body.should be == 'foo' } example { get('/foo.bar.baz') .body.should be == 'foo' } end end describe :space_matches_plus do context 'default' do before { app.get('/foo bar') { 'ok' }} example { get('/foo%20bar') .should be_ok } example { get('/foo+bar') .should be_ok } end context 'enabled' do before { app.get('/foo bar', pattern: { space_matches_plus: true }) { 'ok' }} example { get('/foo%20bar') .should be_ok } example { get('/foo+bar') .should be_ok } end context 'disabled' do before { app.get('/foo bar', pattern: { space_matches_plus: false }) { 'ok' }} example { get('/foo%20bar') .should be_ok } example { get('/foo+bar') .should_not be_ok } end context 'global' do before do app.set(:pattern, space_matches_plus: false) app.get('/foo bar') { 'ok' } end example { get('/foo%20bar') .should be_ok } example { get('/foo+bar') .should_not be_ok } end end describe :uri_decode do context 'default' do before { app.get('/&') { 'ok' }} example { get('/&') .should be_ok } example { get('/%26') .should be_ok } end context 'enabled' do before { app.get('/&', pattern: { uri_decode: true }) { 'ok' }} example { get('/&') .should be_ok } example { get('/%26') .should be_ok } end context 'disabled' do before { app.get('/&', pattern: { uri_decode: false }) { 'ok' }} example { get('/&') .should be_ok } example { get('/%26') .should_not be_ok } end context 'global' do before do app.set(:pattern, uri_decode: false) app.get('/&') { 'ok' } end example { get('/&') .should be_ok } example { get('/%26') .should_not be_ok } end end end describe :type do describe :identity do before do app.set(:pattern, type: :identity) app.get('/:foo') { 'ok' } end example { get('/:foo').should be_ok } example { get('/foo').should_not be_ok } end describe :rails do before do app.set(:pattern, type: :rails) app.get('/:slug(.:extension)') { params[:slug] } end example { get('/foo') .body.should be == 'foo' } example { get('/foo.') .body.should be == 'foo.' } example { get('/foo.bar') .body.should be == 'foo' } example { get('/a%20b') .body.should be == 'a b' } end describe :shell do before do app.set(:pattern, type: :shell) app.get('/{foo,bar}') { 'ok' } end example { get('/foo').should be_ok } example { get('/bar').should be_ok } end describe :simple do before do app.set(:pattern, type: :simple) app.get('/(a)') { 'ok' } end example { get('/(a)').should be_ok } example { get('/a').should_not be_ok } end describe :simple do before do app.set(:pattern, type: :template) app.get('/foo{/segments*}{.ext}') { "%p %p" % [params[:segments], params[:ext]] } end example { get('/foo/a.png').should be_ok } example { get('/foo/a').should_not be_ok } example { get('/foo/a.png').body.should be == '["a"] "png"' } example { get('/foo/a/b.png').body.should be == '["a", "b"] "png"' } end end context 'works with filters' do before do app.before('/auth/*', except: '/auth/login') { halt 'auth required' } app.get('/auth/login') { 'please log in' } end example { get('/auth/dunno').body.should be == 'auth required' } example { get('/auth/login').body.should be == 'please log in' } end end mustermann-1.0.0/spec/equality_map_spec.rb0000644000175000017500000000132613124654676017730 0ustar pravipravi# 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 end end mustermann-1.0.0/spec/composite_spec.rb0000644000175000017500000001557013124654676017246 0ustar pravipravi# 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.new('/foo/:name', '/:first/:second') } describe :== do example { subject.should be == subject } example { subject.should 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 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.new('/foo/:name', '/:first/:second', operator: :|)) } 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 include('(sinatra:"x" | shell:"x")') } example { (sinatra ^ shell) .inspect.should include('(sinatra:"x" ^ shell:"x")') } example { (sinatra | shell | identity) .inspect.should include('(sinatra:"x" | shell:"x" | identity:"x")') } example { (sinatra | shell & identity) .inspect.should include('(sinatra:"x" | (shell:"x" & identity:"x"))') } end end mustermann-1.0.0/spec/mapper_spec.rb0000644000175000017500000000713513124654676016526 0ustar pravipravi# frozen_string_literal: true require 'support' require 'mustermann/mapper' describe Mustermann::Mapper do describe :initialize do context 'accepts a block with no arguments, using the return value' do subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise) {{ "/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 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 'accepts options followed by mappings' do subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise, "/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 'allows specifying type' do subject(:mapper) { Mustermann::Mapper.new(additional_values: :raise, type: :rails, "/foo" => "/bar") } 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-1.0.0/spec/simple_match_spec.rb0000644000175000017500000000051113124654676017676 0ustar pravipravi# frozen_string_literal: true require 'support' require 'mustermann/simple_match' describe Mustermann::SimpleMatch do subject { Mustermann::SimpleMatch.new('example') } its(:to_s) { should be == 'example' } its(:names) { should be == [] } its(:captures) { should be == [] } example { subject[1].should be == nil } end mustermann-1.0.0/spec/regexp_based_spec.rb0000644000175000017500000000042513124654676017665 0ustar pravipravi# 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