File: pattern.rb

package info (click to toggle)
ruby-mustermann19 0.4.3%2Bgit20160621-1
  • links: PTS, VCS
  • area: main
  • in suites: stretch
  • size: 756 kB
  • ctags: 445
  • sloc: ruby: 7,197; makefile: 3
file content (363 lines) | stat: -rw-r--r-- 13,406 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
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

    PATTERN_METHODS = %w[expand to_templates].map(&:to_sym)
    # List of supported options.
    #
    # @overload supported_options
    #   @return [Array<Symbol>] 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<Symbol>] 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, options = {})
      ignore_unknown_options = options.fetch(:ignore_unknown_options, false)
      options.delete(:ignore_unknown_options)
      unless ignore_unknown_options
        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) }
    end

    supported_options :uri_decode, :ignore_unknown_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, options = {})
      uri_decode = options.fetch(:uri_decode, true)
      @uri_decode = uri_decode
      @string     = string.to_s.dup
    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

    # 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") # => #<MatchData "/Frank" name:"Frank">
    #
    # @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<Hash, Integer>, nil] Array with params hash and length of substing if matched, nil otherwise
    def peek_params(string)
      match = peek_match(string)
      [params(nil, :captures => match), match.to_s.size] if match
    end

    # @return [Hash{String: Array<Integer>}] 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<String>] 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<String>}, nil] Sinatra style params if pattern matches.
    def params(string = nil, options = {})
      options, string = string, nil if string.is_a?(Hash)
      captures = options.fetch(:captures, nil)
      offset   = options.fetch(: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 {Mustermann::Sinatra}, {Mustermann::Rails},
    # {Mustermann::Template} and {Mustermann::Identity} patterns.  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<String>] 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.new(self, other, :operator => :|, :type => :identity)
    end

    def &(other)
      Mustermann.new(self, other, :operator => :&, :type => :identity)
    end

    def ^(other)
      Mustermann.new(self, other, :operator => :^, :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 PATTERN_METHODS.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