Class: Mustermann::Set

Inherits:
Object
  • Object
show all
Defined in:
lib/mustermann/set.rb,
lib/mustermann/set/trie.rb,
lib/mustermann/set/cache.rb,
lib/mustermann/set/match.rb,
lib/mustermann/set/linear.rb

Overview

Note:

Adding patterns via #add, #update, or #[]= is not thread-safe, but matching and expanding is.

A collection of patterns that can be matched against strings efficiently.

Each pattern in the set may be associated with one or more arbitrary values, such as handler objects or route actions. A single #match call returns a Match that provides both the captured parameters and the associated value for the matched pattern. When the set contains many patterns, an internal trie (prefix tree) is used to dispatch requests in sub-linear time.

Examples:

Building a routing table

require 'mustermann/set'

set = Mustermann::Set.new
set.add('/users/:id',  :users_show)
set.add('/posts/:id',  :posts_show)

m = set.match('/users/42')
m.value          # => :users_show
m.params['id']   # => '42'

Constructor shorthand with a hash

set = Mustermann::Set.new('/users/:id' => :users_show, '/posts/:id' => :posts_show)

Block syntax

set = Mustermann::Set.new do |s|
  s.add('/users/:id', :users_show)
  s.add('/posts/:id', :posts_show)
end

Defined Under Namespace

Classes: Match

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*mapping, additional_values: :raise, use_trie: 50, use_cache: true, **options, &block) ⇒ Set

Creates a new set, optionally pre-populated with patterns.

Patterns can be supplied as a Hash (pattern → value), a plain String or Pattern, an Array of any of these, or an existing Mustermann::Set. The same forms are accepted by #update and #add.

Examples:

Empty set

Mustermann::Set.new

Pre-populated from a hash

Mustermann::Set.new('/users/:id' => :users, '/posts/:id' => :posts)

Imperative block

Mustermann::Set.new do |s|
  s.add('/users/:id', :users)
end

Zero-argument block returning a mapping hash

Mustermann::Set.new { { '/users/:id' => :users } }

Parameters:

  • mapping (Array)

    initial patterns or mappings to add

  • additional_values (:raise, :ignore, :append) (defaults to: :raise)

    behavior when extra keys are passed to #expand; defaults to :raise

  • options (Hash)

    pattern options forwarded to Mustermann.new (e.g. type: :rails)

Raises:

  • (ArgumentError)

    if additional_values is not a recognized behavior symbol



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/mustermann/set.rb', line 68

def initialize(*mapping, additional_values: :raise, use_trie: 50, use_cache: true, **options, &block)
  raise ArgumentError, "Illegal value %p for additional_values" % additional_values unless Expander::ADDITIONAL_VALUES.include? additional_values
  raise ArgumentError, "Illegal value %p for use_trie" % use_trie unless [true, false].include?(use_trie) or use_trie.is_a? Integer

  @use_trie          = use_trie
  @use_cache         = use_cache
  @matcher           = nil
  @mapping           = {}
  @reverse_mapping   = {}
  @options           = {}
  @expanders         = {}
  @additional_values = additional_values

  options.each do |key, value|
    if key.is_a? Symbol
      @options[key] = value
    else
      mapping << { key => value }
    end
  end

  update(mapping)

  block.arity == 0 ? update(yield) : yield(self) if block
end

Instance Attribute Details

#optionsHash (readonly)

Pattern options forwarded to Mustermann.new when patterns are created from strings.

Returns:

  • (Hash)


41
42
43
# File 'lib/mustermann/set.rb', line 41

def options
  @options
end

Instance Method Details

#[](pattern_or_string) ⇒ Object?

Looks up a value by string or retrieves the first value for a known pattern object.

When given a String, it is matched against the set and the associated value of the first matching pattern is returned. When given a Pattern, the first value registered for that exact pattern is returned without matching.

Examples:

String lookup

set['/users/42']  # => :users_show (or nil)

Pattern lookup

pat = Mustermann.new('/users/:id')
set[pat]          # => :users_show (or nil)

Parameters:

  • pattern_or_string (String, Pattern)

Returns:

  • (Object, nil)

    the associated value, or nil if not found

Raises:

  • (ArgumentError)

    for unsupported argument types



157
158
159
160
161
162
163
# File 'lib/mustermann/set.rb', line 157

def [](pattern_or_string)
  case pattern_or_string
  when String  then match(pattern_or_string)&.value
  when Pattern then values_for_pattern(pattern_or_string)&.first
  else raise ArgumentError, "unsupported pattern type #{pattern_or_string.class}"
  end
end

#add(pattern, *values) ⇒ self Also known as: []=

Adds a pattern to the set, optionally associated with one or more values.

If the pattern is given as a String it will be compiled via Mustermann.new using the set’s own options. The pattern must be AST-based (Sinatra, Rails, and similar types). Plain regexp patterns are not supported.

Calling add more than once for the same pattern appends additional values without creating duplicates.

Examples:

set.add('/users/:id', :users)
set.add('/users/:id', :admin)   # same pattern, second value

Parameters:

  • pattern (String, Pattern)

    the pattern to add

  • values (Array)

    zero or more values to associate with the pattern

Returns:

  • (self)

Raises:

  • (ArgumentError)

    if the pattern is not AST-based, or if a reserved symbol is used as a value



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/mustermann/set.rb', line 111

def add(pattern, *values)
  pattern = Mustermann.new(pattern, **options)
  raise ArgumentError, "Non-AST patterns are not supported" unless pattern.respond_to? :to_ast

  if @mapping.key? pattern
    current = @mapping[pattern]
  else
    add_pattern(pattern)
    current = @mapping[pattern] = []
  end

  values = [nil] if values.empty?

  values.each do |value|
    raise ArgumentError, "%p may not be used as a value" % value if Expander::ADDITIONAL_VALUES.include? value
    raise ArgumentError, "the set itself may not be used as value" if value == self
    next if current.include? value
    current << value
    @reverse_mapping[value] ||= []
    @reverse_mapping[value] << pattern unless @reverse_mapping[value].include? pattern
    @expanders[value]&.add(pattern)
  end

  self
end

#expand(value = self, behavior = nil, values = nil) ⇒ String

Generates a string from a parameter hash using the patterns in the set.

When called with just a parameter hash, the first pattern that can be fully expanded with those keys is used. Pass a value as the first argument to restrict expansion to the patterns associated with that value. You may also pass an additional_values behavior symbol (:raise, :ignore, or :append) as the first argument to override the set’s default behavior for that call.

Examples:

Expand using any pattern

set.expand(id: '5')

Expand patterns for a specific value

set.expand(:users, id: '5')

Override additional_values behavior for one call

set.expand(:ignore, id: '5', extra: 'ignored')

Parameters:

  • value (Object, :raise, :ignore, :append) (defaults to: self)

    the value whose patterns should be used, or an additional_values behavior symbol; defaults to all patterns

  • behavior (:raise, :ignore, :append, nil) (defaults to: nil)

    how to handle extra keys; defaults to the set’s additional_values setting

  • values (Hash, nil) (defaults to: nil)

    the parameters to expand

Returns:

  • (String)

Raises:



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/mustermann/set.rb', line 280

def expand(value = self, behavior = nil, values = nil)
  if Expander::ADDITIONAL_VALUES.include? value
    if behavior.is_a? Hash
      values   = values ? values.merge(behavior) : behavior
      behavior = nil
    elsif behavior and behavior != value
      raise ArgumentError, "behavior specified multiple times" if behavior
    end
    behavior = value
    value    = self
  elsif value.is_a? Hash and behavior.nil? and values.nil?
    values = value
    value  = self unless @reverse_mapping.key? values
  end
  expander(value).expand(behavior || @additional_values, values || {})
end

#expander(value = self) ⇒ Mustermann::Expander

Returns an Expander that can generate strings from parameter hashes.

When called without arguments (or with the set itself as the value) the expander covers all patterns in the set. Pass a specific value to get an expander limited to the patterns associated with that value.

Parameters:

  • value (Object) (defaults to: self)

    restricts the expander to patterns associated with this value; defaults to the set itself (all patterns)

Returns:



247
248
249
250
251
252
# File 'lib/mustermann/set.rb', line 247

def expander(value = self)
  @expanders[value] ||= begin
    patterns = value == self ? @mapping.keys : @reverse_mapping[value] || []
    Mustermann::Expander.new(patterns, additional_values: @additional_values, **options)
  end
end

#has_value?(value) ⇒ Boolean

Returns whether the set contains any pattern associated with the given value.

Returns:

  • (Boolean)

    whether the set contains any pattern associated with the given value



298
# File 'lib/mustermann/set.rb', line 298

def has_value?(value) = @reverse_mapping[value]&.any?

#match(string) ⇒ Set::Match?

Matches the string against all patterns in the set and returns the first match.

Parameters:

  • string (String)

    the string to match

Returns:

  • (Set::Match, nil)

    the first match, or nil if none of the patterns match



169
# File 'lib/mustermann/set.rb', line 169

def match(string) = @matcher&.match(string)

#match_all(string) ⇒ Array<Set::Match>

Matches the string against all patterns and returns every match, one per (pattern, value) pair, in insertion order.

Parameters:

  • string (String)

Returns:

  • (Array<Set::Match>)

    all matches, or an empty array if none



184
# File 'lib/mustermann/set.rb', line 184

def match_all(string) = @matcher&.match(string, all: true)

#merge(mapping) ⇒ Set

Returns a new set that includes all patterns from the receiver plus those from mapping. The receiver is not modified.

Parameters:

  • mapping (Hash, String, Pattern, Array, Set)

    patterns to merge in

Returns:

  • (Set)

    a new set



199
# File 'lib/mustermann/set.rb', line 199

def merge(mapping) = dup.update(mapping)

#patternsArray<Pattern>

Returns all patterns that have been added to the set, in insertion order.

Returns:



236
# File 'lib/mustermann/set.rb', line 236

def patterns = @mapping.keys

#peek_match(string) ⇒ Set::Match?

Matches the beginning of the string against all patterns and returns the first prefix match. The unmatched remainder of the string is available via Match#post_match.

Parameters:

  • string (String)

Returns:

  • (Set::Match, nil)

    the first prefix match, or nil



177
# File 'lib/mustermann/set.rb', line 177

def peek_match(string) = @matcher&.match(string, peek: true)

#peek_match_all(string) ⇒ Array<Set::Match>

Matches the beginning of the string against all patterns and returns every prefix match, one per (pattern, value) pair. The unmatched remainder is available as Match#post_match on each result.

Parameters:

  • string (String)

Returns:

  • (Array<Set::Match>)

    all prefix matches, or an empty array if none



192
# File 'lib/mustermann/set.rb', line 192

def peek_match_all(string) = @matcher&.match(string, all: true, peek: true)

#update(mapping) ⇒ self Also known as: merge!

Adds all patterns from mapping to the set in place and returns self. Aliased as merge!.

Accepts the same argument forms as #initialize: a Hash, a String, a Pattern, an Array, or another Mustermann::Set.

Parameters:

Returns:

  • (self)

Raises:

  • (ArgumentError)

    for unsupported mapping types



221
222
223
224
225
226
227
228
229
230
# File 'lib/mustermann/set.rb', line 221

def update(mapping)
  case mapping
  when Set             then mapping.mapping.each { |pattern, values| add(pattern, *values) }
  when Hash            then mapping.each { |k, v| add(k, v) }
  when String, Pattern then add(mapping)
  when Array           then mapping.each { |item| update(item) }
  else raise ArgumentError, "unsupported mapping type #{mapping.class}"
  end
  self
end