addressable 2.3.2 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
- # encoding:utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  #--
3
- # Copyright (C) 2006-2011 Bob Aman
4
+ # Copyright (C) Bob Aman
4
5
  #
5
6
  # Licensed under the Apache License, Version 2.0 (the "License");
6
7
  # you may not use this file except in compliance with the License.
@@ -1,6 +1,7 @@
1
- # encoding:utf-8
1
+ # frozen_string_literal: true
2
+
2
3
  #--
3
- # Copyright (C) 2006-2011 Bob Aman
4
+ # Copyright (C) Bob Aman
4
5
  #
5
6
  # Licensed under the Apache License, Version 2.0 (the "License");
6
7
  # you may not use this file except in compliance with the License.
@@ -35,9 +36,11 @@ module Addressable
35
36
  Addressable::URI::CharacterClasses::DIGIT + '_'
36
37
 
37
38
  var_char =
38
- "(?:(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
39
+ "(?>(?:[#{variable_char_class}]|%[a-fA-F0-9][a-fA-F0-9])+)"
39
40
  RESERVED =
40
41
  "(?:[#{anything}]|%[a-fA-F0-9][a-fA-F0-9])"
42
+ RESERVED_NO_COMMA =
43
+ "(?:[#{anything.delete(',')}]|%[a-fA-F0-9][a-fA-F0-9])"
41
44
  UNRESERVED =
42
45
  "(?:[#{
43
46
  Addressable::URI::CharacterClasses::UNRESERVED
@@ -132,6 +135,7 @@ module Addressable
132
135
  self.template.variables
133
136
  end
134
137
  alias_method :keys, :variables
138
+ alias_method :names, :variables
135
139
 
136
140
  ##
137
141
  # @return [Array]
@@ -146,6 +150,64 @@ module Addressable
146
150
  end
147
151
  alias_method :captures, :values
148
152
 
153
+ ##
154
+ # Accesses captured values by name or by index.
155
+ #
156
+ # @param [String, Symbol, Fixnum] key
157
+ # Capture index or name. Note that when accessing by with index
158
+ # of 0, the full URI will be returned. The intention is to mimic
159
+ # the ::MatchData#[] behavior.
160
+ #
161
+ # @param [#to_int, nil] len
162
+ # If provided, an array of values will be returned with the given
163
+ # parameter used as length.
164
+ #
165
+ # @return [Array, String, nil]
166
+ # The captured value corresponding to the index or name. If the
167
+ # value was not provided or the key is unknown, nil will be
168
+ # returned.
169
+ #
170
+ # If the second parameter is provided, an array of that length will
171
+ # be returned instead.
172
+ def [](key, len = nil)
173
+ if len
174
+ to_a[key, len]
175
+ elsif String === key or Symbol === key
176
+ mapping[key.to_s]
177
+ else
178
+ to_a[key]
179
+ end
180
+ end
181
+
182
+ ##
183
+ # @return [Array]
184
+ # Array with the matched URI as first element followed by the captured
185
+ # values.
186
+ def to_a
187
+ [to_s, *values]
188
+ end
189
+
190
+ ##
191
+ # @return [String]
192
+ # The matched URI as String.
193
+ def to_s
194
+ uri.to_s
195
+ end
196
+ alias_method :string, :to_s
197
+
198
+ # Returns multiple captured values at once.
199
+ #
200
+ # @param [String, Symbol, Fixnum] *indexes
201
+ # Indices of the captures to be returned
202
+ #
203
+ # @return [Array]
204
+ # Values corresponding to given indices.
205
+ #
206
+ # @see Addressable::Template::MatchData#[]
207
+ def values_at(*indexes)
208
+ indexes.map { |i| self[i] }
209
+ end
210
+
149
211
  ##
150
212
  # Returns a <tt>String</tt> representation of the MatchData's state.
151
213
  #
@@ -154,6 +216,15 @@ module Addressable
154
216
  sprintf("#<%s:%#0x RESULT:%s>",
155
217
  self.class.to_s, self.object_id, self.mapping.inspect)
156
218
  end
219
+
220
+ ##
221
+ # Dummy method for code expecting a ::MatchData instance
222
+ #
223
+ # @return [String] An empty string.
224
+ def pre_match
225
+ ""
226
+ end
227
+ alias_method :post_match, :pre_match
157
228
  end
158
229
 
159
230
  ##
@@ -166,7 +237,18 @@ module Addressable
166
237
  if !pattern.respond_to?(:to_str)
167
238
  raise TypeError, "Can't convert #{pattern.class} into String."
168
239
  end
169
- @pattern = pattern.to_str.freeze
240
+ @pattern = pattern.to_str.dup.freeze
241
+ end
242
+
243
+ ##
244
+ # Freeze URI, initializing instance variables.
245
+ #
246
+ # @return [Addressable::URI] The frozen URI object.
247
+ def freeze
248
+ self.variables
249
+ self.variable_defaults
250
+ self.named_captures
251
+ super
170
252
  end
171
253
 
172
254
  ##
@@ -182,6 +264,26 @@ module Addressable
182
264
  self.class.to_s, self.object_id, self.pattern)
183
265
  end
184
266
 
267
+ ##
268
+ # Returns <code>true</code> if the Template objects are equal. This method
269
+ # does NOT normalize either Template before doing the comparison.
270
+ #
271
+ # @param [Object] template The Template to compare.
272
+ #
273
+ # @return [TrueClass, FalseClass]
274
+ # <code>true</code> if the Templates are equivalent, <code>false</code>
275
+ # otherwise.
276
+ def ==(template)
277
+ return false unless template.kind_of?(Template)
278
+ return self.pattern == template.pattern
279
+ end
280
+
281
+ ##
282
+ # Addressable::Template makes no distinction between `==` and `eql?`.
283
+ #
284
+ # @see #==
285
+ alias_method :eql?, :==
286
+
185
287
  ##
186
288
  # Extracts a mapping from the URI using a URI Template pattern.
187
289
  #
@@ -311,12 +413,14 @@ module Addressable
311
413
  # match.captures
312
414
  # #=> ["a", ["b", "c"]]
313
415
  def match(uri, processor=nil)
314
- uri = Addressable::URI.parse(uri)
416
+ uri = Addressable::URI.parse(uri) unless uri.is_a?(Addressable::URI)
315
417
  mapping = {}
316
418
 
317
419
  # First, we need to process the pattern, and extract the values.
318
420
  expansions, expansion_regexp =
319
421
  parse_template_pattern(pattern, processor)
422
+
423
+ return nil unless uri.to_str.match(expansion_regexp)
320
424
  unparsed_values = uri.to_str.scan(expansion_regexp).flatten
321
425
 
322
426
  if uri.to_str == pattern
@@ -327,6 +431,7 @@ module Addressable
327
431
  _, operator, varlist = *expansion.match(EXPRESSION)
328
432
  varlist.split(',').each do |varspec|
329
433
  _, name, modifier = *varspec.match(VARSPEC)
434
+ mapping[name] ||= nil
330
435
  case operator
331
436
  when nil, '+', '#', '/', '.'
332
437
  unparsed_value = unparsed_values[index]
@@ -335,12 +440,14 @@ module Addressable
335
440
  value = value.split(JOINERS[operator]) if value && modifier == '*'
336
441
  when ';', '?', '&'
337
442
  if modifier == '*'
338
- value = unparsed_values[index].split(JOINERS[operator])
339
- value = value.inject({}) do |acc, v|
340
- key, val = v.split('=')
341
- val = "" if val.nil?
342
- acc[key] = val
343
- acc
443
+ if unparsed_values[index]
444
+ value = unparsed_values[index].split(JOINERS[operator])
445
+ value = value.inject({}) do |acc, v|
446
+ key, val = v.split('=')
447
+ val = "" if val.nil?
448
+ acc[key] = val
449
+ acc
450
+ end
344
451
  end
345
452
  else
346
453
  if (unparsed_values[index])
@@ -365,10 +472,9 @@ module Addressable
365
472
  value = Addressable::URI.unencode_component(value)
366
473
  end
367
474
  end
368
- if mapping[name] == nil || mapping[name] == value
475
+ if !mapping.has_key?(name) || mapping[name].nil?
476
+ # Doesn't exist, set to value (even if value is nil)
369
477
  mapping[name] = value
370
- else
371
- return nil
372
478
  end
373
479
  index = index + 1
374
480
  end
@@ -385,6 +491,8 @@ module Addressable
385
491
  # @param [Hash] mapping The mapping that corresponds to the pattern.
386
492
  # @param [#validate, #transform] processor
387
493
  # An optional processor object may be supplied.
494
+ # @param [Boolean] normalize_values
495
+ # Optional flag to enable/disable unicode normalization. Default: true
388
496
  #
389
497
  # The object should respond to either the <tt>validate</tt> or
390
498
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -415,10 +523,11 @@ module Addressable
415
523
  # "http://example.com/{?one,two,three}/"
416
524
  # ).partial_expand({"one" => "1", "three" => 3}).pattern
417
525
  # #=> "http://example.com/?one=1{&two}&three=3"
418
- def partial_expand(mapping, processor=nil)
526
+ def partial_expand(mapping, processor=nil, normalize_values=true)
419
527
  result = self.pattern.dup
528
+ mapping = normalize_keys(mapping)
420
529
  result.gsub!( EXPRESSION ) do |capture|
421
- transform_partial_capture(mapping, capture, processor)
530
+ transform_partial_capture(mapping, capture, processor, normalize_values)
422
531
  end
423
532
  return Addressable::Template.new(result)
424
533
  end
@@ -429,6 +538,8 @@ module Addressable
429
538
  # @param [Hash] mapping The mapping that corresponds to the pattern.
430
539
  # @param [#validate, #transform] processor
431
540
  # An optional processor object may be supplied.
541
+ # @param [Boolean] normalize_values
542
+ # Optional flag to enable/disable unicode normalization. Default: true
432
543
  #
433
544
  # The object should respond to either the <tt>validate</tt> or
434
545
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -479,11 +590,11 @@ module Addressable
479
590
  # ExampleProcessor
480
591
  # ).to_str
481
592
  # #=> Addressable::Template::InvalidTemplateValueError
482
- def expand(mapping, processor=nil)
593
+ def expand(mapping, processor=nil, normalize_values=true)
483
594
  result = self.pattern.dup
484
595
  mapping = normalize_keys(mapping)
485
596
  result.gsub!( EXPRESSION ) do |capture|
486
- transform_capture(mapping, capture, processor)
597
+ transform_capture(mapping, capture, processor, normalize_values)
487
598
  end
488
599
  return Addressable::URI.parse(result)
489
600
  end
@@ -499,6 +610,7 @@ module Addressable
499
610
  @variables ||= ordered_variable_defaults.map { |var, val| var }.uniq
500
611
  end
501
612
  alias_method :keys, :variables
613
+ alias_method :names, :variables
502
614
 
503
615
  ##
504
616
  # Returns a mapping of variables to their default values specified
@@ -510,17 +622,49 @@ module Addressable
510
622
  Hash[*ordered_variable_defaults.reject { |k, v| v.nil? }.flatten]
511
623
  end
512
624
 
625
+ ##
626
+ # Coerces a template into a `Regexp` object. This regular expression will
627
+ # behave very similarly to the actual template, and should match the same
628
+ # URI values, but it cannot fully handle, for example, values that would
629
+ # extract to an `Array`.
630
+ #
631
+ # @return [Regexp] A regular expression which should match the template.
632
+ def to_regexp
633
+ _, source = parse_template_pattern(pattern)
634
+ Regexp.new(source)
635
+ end
636
+
637
+ ##
638
+ # Returns the source of the coerced `Regexp`.
639
+ #
640
+ # @return [String] The source of the `Regexp` given by {#to_regexp}.
641
+ #
642
+ # @api private
643
+ def source
644
+ self.to_regexp.source
645
+ end
646
+
647
+ ##
648
+ # Returns the named captures of the coerced `Regexp`.
649
+ #
650
+ # @return [Hash] The named captures of the `Regexp` given by {#to_regexp}.
651
+ #
652
+ # @api private
653
+ def named_captures
654
+ self.to_regexp.named_captures
655
+ end
656
+
513
657
  private
514
658
  def ordered_variable_defaults
515
- @ordered_variable_defaults ||= (
516
- expansions, expansion_regexp = parse_template_pattern(pattern)
517
- expansions.map do |capture|
518
- _, operator, varlist = *capture.match(EXPRESSION)
659
+ @ordered_variable_defaults ||= begin
660
+ expansions, _ = parse_template_pattern(pattern)
661
+ expansions.flat_map do |capture|
662
+ _, _, varlist = *capture.match(EXPRESSION)
519
663
  varlist.split(',').map do |varspec|
520
- name = varspec[VARSPEC, 1]
664
+ varspec[VARSPEC, 1]
521
665
  end
522
- end.flatten
523
- )
666
+ end
667
+ end
524
668
  end
525
669
 
526
670
 
@@ -533,6 +677,8 @@ module Addressable
533
677
  # The expression to expand
534
678
  # @param [#validate, #transform] processor
535
679
  # An optional processor object may be supplied.
680
+ # @param [Boolean] normalize_values
681
+ # Optional flag to enable/disable unicode normalization. Default: true
536
682
  #
537
683
  # The object should respond to either the <tt>validate</tt> or
538
684
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -547,21 +693,36 @@ module Addressable
547
693
  # after sending the value to the transform method.
548
694
  #
549
695
  # @return [String] The expanded expression
550
- def transform_partial_capture(mapping, capture, processor = nil)
696
+ def transform_partial_capture(mapping, capture, processor = nil,
697
+ normalize_values = true)
551
698
  _, operator, varlist = *capture.match(EXPRESSION)
552
- is_first = true
553
- varlist.split(',').inject('') do |acc, varspec|
554
- _, name, modifier = *varspec.match(VARSPEC)
555
- value = mapping[name]
556
- if value
557
- operator = '&' if !is_first && operator == '?'
558
- acc << transform_capture(mapping, "{#{operator}#{varspec}}", processor)
559
- else
560
- operator = '&' if !is_first && operator == '?'
561
- acc << "{#{operator}#{varspec}}"
562
- end
563
- is_first = false
564
- acc
699
+
700
+ vars = varlist.split(",")
701
+
702
+ if operator == "?"
703
+ # partial expansion of form style query variables sometimes requires a
704
+ # slight reordering of the variables to produce a valid url.
705
+ first_to_expand = vars.find { |varspec|
706
+ _, name, _ = *varspec.match(VARSPEC)
707
+ mapping.key?(name) && !mapping[name].nil?
708
+ }
709
+
710
+ vars = [first_to_expand] + vars.reject {|varspec| varspec == first_to_expand} if first_to_expand
711
+ end
712
+
713
+ vars.
714
+ inject("".dup) do |acc, varspec|
715
+ _, name, _ = *varspec.match(VARSPEC)
716
+ next_val = if mapping.key? name
717
+ transform_capture(mapping, "{#{operator}#{varspec}}",
718
+ processor, normalize_values)
719
+ else
720
+ "{#{operator}#{varspec}}"
721
+ end
722
+ # If we've already expanded at least one '?' operator with non-empty
723
+ # value, change to '&'
724
+ operator = "&" if (operator == "?") && (next_val != "")
725
+ acc << next_val
565
726
  end
566
727
  end
567
728
 
@@ -574,6 +735,9 @@ module Addressable
574
735
  # The expression to replace
575
736
  # @param [#validate, #transform] processor
576
737
  # An optional processor object may be supplied.
738
+ # @param [Boolean] normalize_values
739
+ # Optional flag to enable/disable unicode normalization. Default: true
740
+ #
577
741
  #
578
742
  # The object should respond to either the <tt>validate</tt> or
579
743
  # <tt>transform</tt> messages or both. Both the <tt>validate</tt> and
@@ -588,14 +752,19 @@ module Addressable
588
752
  # after sending the value to the transform method.
589
753
  #
590
754
  # @return [String] The expanded expression
591
- def transform_capture(mapping, capture, processor=nil)
755
+ def transform_capture(mapping, capture, processor=nil,
756
+ normalize_values=true)
592
757
  _, operator, varlist = *capture.match(EXPRESSION)
593
758
  return_value = varlist.split(',').inject([]) do |acc, varspec|
594
759
  _, name, modifier = *varspec.match(VARSPEC)
595
760
  value = mapping[name]
596
761
  unless value == nil || value == {}
597
762
  allow_reserved = %w(+ #).include?(operator)
598
- value = value.to_s if Numeric === value || Symbol === value
763
+ # Common primitives where the .to_s output is well-defined
764
+ if Numeric === value || Symbol === value ||
765
+ value == true || value == false
766
+ value = value.to_s
767
+ end
599
768
  length = modifier.gsub(':', '').to_i if modifier =~ /^:\d+/
600
769
 
601
770
  unless (Hash === value) ||
@@ -604,7 +773,7 @@ module Addressable
604
773
  "Can't convert #{value.class} into String or Array."
605
774
  end
606
775
 
607
- value = normalize_value(value)
776
+ value = normalize_value(value) if normalize_values
608
777
 
609
778
  if processor == nil || !processor.respond_to?(:transform)
610
779
  # Handle percent escaping
@@ -667,7 +836,9 @@ module Addressable
667
836
  end
668
837
  if processor.respond_to?(:transform)
669
838
  transformed_value = processor.transform(name, value)
670
- transformed_value = normalize_value(transformed_value)
839
+ if normalize_values
840
+ transformed_value = normalize_value(transformed_value)
841
+ end
671
842
  end
672
843
  end
673
844
  acc << [name, transformed_value]
@@ -698,7 +869,7 @@ module Addressable
698
869
  if v.is_a?(Array) && v.first =~ /=/
699
870
  v.join(joiner)
700
871
  elsif v.is_a?(Array)
701
- v.map{|v| "#{k}=#{v}"}.join(joiner)
872
+ v.map{|inner_value| "#{k}=#{inner_value}"}.join(joiner)
702
873
  else
703
874
  "#{k}=#{v}"
704
875
  end
@@ -708,7 +879,7 @@ module Addressable
708
879
  if v.is_a?(Array) && v.first =~ /=/
709
880
  ';' + v.join(";")
710
881
  elsif v.is_a?(Array)
711
- ';' + v.map{|v| "#{k}=#{v}"}.join(";")
882
+ ';' + v.map{|inner_value| "#{k}=#{inner_value}"}.join(";")
712
883
  else
713
884
  v && v != '' ? ";#{k}=#{v}" : ";#{k}"
714
885
  end
@@ -723,25 +894,24 @@ module Addressable
723
894
  # operator.
724
895
  #
725
896
  # @param [Hash, Array, String] value
726
- # Normalizes keys and values with IDNA#unicode_normalize_kc
897
+ # Normalizes unicode keys and values with String#unicode_normalize (NFC)
727
898
  #
728
899
  # @return [Hash, Array, String] The normalized values
729
900
  def normalize_value(value)
730
- unless value.is_a?(Hash)
731
- value = value.respond_to?(:to_ary) ? value.to_ary : value.to_str
732
- end
733
-
734
901
  # Handle unicode normalization
735
- if value.kind_of?(Array)
736
- value.map! { |val| Addressable::IDNA.unicode_normalize_kc(val) }
902
+ if value.respond_to?(:to_ary)
903
+ value.to_ary.map! { |val| normalize_value(val) }
737
904
  elsif value.kind_of?(Hash)
738
905
  value = value.inject({}) { |acc, (k, v)|
739
- acc[Addressable::IDNA.unicode_normalize_kc(k)] =
740
- Addressable::IDNA.unicode_normalize_kc(v)
906
+ acc[normalize_value(k)] = normalize_value(v)
741
907
  acc
742
908
  }
743
909
  else
744
- value = Addressable::IDNA.unicode_normalize_kc(value)
910
+ value = value.to_s if !value.kind_of?(String)
911
+ if value.encoding != Encoding::UTF_8
912
+ value = value.dup.force_encoding(Encoding::UTF_8)
913
+ end
914
+ value = value.unicode_normalize(:nfc)
745
915
  end
746
916
  value
747
917
  end
@@ -769,15 +939,35 @@ module Addressable
769
939
  end
770
940
  end
771
941
 
942
+ ##
943
+ # Generates the <tt>Regexp</tt> that parses a template pattern. Memoizes the
944
+ # value if template processor not set (processors may not be deterministic)
945
+ #
946
+ # @param [String] pattern The URI template pattern.
947
+ # @param [#match] processor The template processor to use.
948
+ #
949
+ # @return [Array, Regexp]
950
+ # An array of expansion variables nad a regular expression which may be
951
+ # used to parse a template pattern
952
+ def parse_template_pattern(pattern, processor = nil)
953
+ if processor.nil? && pattern == @pattern
954
+ @cached_template_parse ||=
955
+ parse_new_template_pattern(pattern, processor)
956
+ else
957
+ parse_new_template_pattern(pattern, processor)
958
+ end
959
+ end
960
+
772
961
  ##
773
962
  # Generates the <tt>Regexp</tt> that parses a template pattern.
774
963
  #
775
964
  # @param [String] pattern The URI template pattern.
776
965
  # @param [#match] processor The template processor to use.
777
966
  #
778
- # @return [Regexp]
779
- # A regular expression which may be used to parse a template pattern.
780
- def parse_template_pattern(pattern, processor=nil)
967
+ # @return [Array, Regexp]
968
+ # An array of expansion variables nad a regular expression which may be
969
+ # used to parse a template pattern
970
+ def parse_new_template_pattern(pattern, processor = nil)
781
971
  # Escape the pattern. The two gsubs restore the escaped curly braces
782
972
  # back to their original form. Basically, escape everything that isn't
783
973
  # within an expansion.
@@ -797,10 +987,13 @@ module Addressable
797
987
  _, operator, varlist = *expansion.match(EXPRESSION)
798
988
  leader = Regexp.escape(LEADERS.fetch(operator, ''))
799
989
  joiner = Regexp.escape(JOINERS.fetch(operator, ','))
800
- leader + varlist.split(',').map do |varspec|
990
+ varspecs = varlist.split(',')
991
+ combined = varspecs.map do |varspec|
801
992
  _, name, modifier = *varspec.match(VARSPEC)
802
- if processor != nil && processor.respond_to?(:match)
803
- "(#{ processor.match(name) })"
993
+
994
+ result = processor && processor.respond_to?(:match) ? processor.match(name) : nil
995
+ if result
996
+ "(?<#{name}>#{ result })"
804
997
  else
805
998
  group = case operator
806
999
  when '+'
@@ -821,16 +1014,25 @@ module Addressable
821
1014
  "#{ UNRESERVED }*?"
822
1015
  end
823
1016
  if modifier == '*'
824
- "(#{group}(?:#{joiner}?#{group})*)?"
1017
+ seg = case operator
1018
+ when '+', '#' then "#{RESERVED_NO_COMMA}*+"
1019
+ else group
1020
+ end
1021
+ joiner_pattern = operator.nil? ? joiner : "#{joiner}?"
1022
+ "(?<#{name}>#{seg}(?:#{joiner_pattern}#{seg})*)?"
1023
+ elsif varspecs.size > 1 && (operator == '+' || operator == '#') &&
1024
+ varspec != varspecs.last
1025
+ "(?<#{name}>#{RESERVED_NO_COMMA}*+)?"
825
1026
  else
826
- "(#{group})?"
1027
+ "(?<#{name}>#{group})?"
827
1028
  end
828
1029
  end
829
1030
  end.join("#{joiner}?")
1031
+ "(?:|#{leader}#{combined})"
830
1032
  end
831
1033
 
832
1034
  # Ensure that the regular expression matches the whole URI.
833
- regexp_string = "^#{regexp_string}$"
1035
+ regexp_string = "\\A#{regexp_string}\\z"
834
1036
  return expansions, Regexp.new(regexp_string)
835
1037
  end
836
1038