roda 3.26.0 → 3.27.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 441b09ae88ce4427866fe7c773ce8476f60e837b7b4477bfc5106c162e8b0a06
4
- data.tar.gz: aca50917cc14429a77c16461352c997dff592e52cc9a44d1f4f64688b8b50693
3
+ metadata.gz: 1aaf8be3e1a6fa8d822436561e48f5a5978e94e3430c0287b32be3a0a292d351
4
+ data.tar.gz: 84ed6cd43e59574cc1264889578296d30c96c91d4b53a45bb2b839619f2d1786
5
5
  SHA512:
6
- metadata.gz: bb56cb6dbcb70fbc182d10bf161db009162d6887e9117823b9fab26554670644c3eec1c2e2977008310826fee799d1213ff8d7a85ed261e5f257ed949b9baa2b
7
- data.tar.gz: 8d7b4ee8b669e7a3a5f3f4ac79f7d51b879cf9700e9634bd5b52b9c219c1158d0f07f03ce5aeab983bbddd3038febd996738b45047fa1e8e49c6e1dc7e07687d
6
+ metadata.gz: 7e339e3062178e6e47bf5b7fb59b32076f5e48131ce14480e3c685f5eb4d0d59903fa635ac790410ef88a2d5cdc46a5e3792d98c511c21acee7931ec5d15d16a
7
+ data.tar.gz: 38f9c91a1b9356d07854c364a10a901c2a126162e42e25e112cdca59b9b065e587f17c2019f4c785ed6610fc0b98011f0ab7adb8cfab0da51acb13e1a0477c34
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ = 3.27.0 (2019-12-13)
2
+
3
+ * Allow json_parser return correct result for invalid JSON if the params_capturing plugin is used (jeremyevans) (#180)
4
+
5
+ * Add multibyte_string_matcher plugin for matching multibyte characters (jeremyevans)
6
+
7
+ * Split roda.rb into separate files (janko) (#177)
8
+
1
9
  = 3.26.0 (2019-11-18)
2
10
 
3
11
  * Combine multiple asset files with a newline when compiling them, avoiding corner cases with comments (ameuret) (#176)
@@ -0,0 +1,15 @@
1
+ = New Features
2
+
3
+ * A multibyte_string_matcher plugin has been added that supports
4
+ multibyte characters in strings used as matchers. It uses a slower
5
+ string matching implementation that supports multibyte characters.
6
+ As multibyte strings in paths must be escaped, this also loads the
7
+ unescape_path plugin.
8
+
9
+ = Other Improvements
10
+
11
+ * The json_parser plugin now returns expected results for invalid JSON
12
+ if the params_capturing plugin is used.
13
+
14
+ * lib/roda.rb has been split into multiple files for easier code
15
+ navigation.
@@ -1,7 +1,10 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- require "rack"
4
3
  require "thread"
4
+ require_relative "roda/request"
5
+ require_relative "roda/response"
6
+ require_relative "roda/plugins"
7
+ require_relative "roda/cache"
5
8
  require_relative "roda/version"
6
9
 
7
10
  # The main class for Roda. Roda is built completely out of plugins, with the
@@ -11,51 +14,6 @@ class Roda
11
14
  # Error class raised by Roda
12
15
  class RodaError < StandardError; end
13
16
 
14
- # A thread safe cache class, offering only #[] and #[]= methods,
15
- # each protected by a mutex.
16
- class RodaCache
17
- # Create a new thread safe cache.
18
- def initialize
19
- @mutex = Mutex.new
20
- @hash = {}
21
- end
22
-
23
- # Make getting value from underlying hash thread safe.
24
- def [](key)
25
- @mutex.synchronize{@hash[key]}
26
- end
27
-
28
- # Make setting value in underlying hash thread safe.
29
- def []=(key, value)
30
- @mutex.synchronize{@hash[key] = value}
31
- end
32
-
33
- private
34
-
35
- # Create a copy of the cache with a separate mutex.
36
- def initialize_copy(other)
37
- @mutex = Mutex.new
38
- other.instance_variable_get(:@mutex).synchronize do
39
- @hash = other.instance_variable_get(:@hash).dup
40
- end
41
- end
42
- end
43
-
44
- # Base class used for Roda requests. The instance methods for this
45
- # class are added by Roda::RodaPlugins::Base::RequestMethods, the
46
- # class methods are added by Roda::RodaPlugins::Base::RequestClassMethods.
47
- class RodaRequest < ::Rack::Request
48
- @roda_class = ::Roda
49
- @match_pattern_cache = ::Roda::RodaCache.new
50
- end
51
-
52
- # Base class used for Roda responses. The instance methods for this
53
- # class are added by Roda::RodaPlugins::Base::ResponseMethods, the class
54
- # methods are added by Roda::RodaPlugins::Base::ResponseClassMethods.
55
- class RodaResponse
56
- @roda_class = ::Roda
57
- end
58
-
59
17
  @app = nil
60
18
  @inherit_middleware = true
61
19
  @middleware = []
@@ -64,53 +22,7 @@ class Roda
64
22
  @route_block = nil
65
23
  @rack_app_route_block = nil
66
24
 
67
- # Module in which all Roda plugins should be stored. Also contains logic for
68
- # registering and loading plugins.
69
25
  module RodaPlugins
70
- OPTS = {}.freeze
71
- EMPTY_ARRAY = [].freeze
72
-
73
- # Stores registered plugins
74
- @plugins = RodaCache.new
75
-
76
- class << self
77
- # Make warn a public method, as it is used for deprecation warnings.
78
- # Roda::RodaPlugins.warn can be overridden for custom handling of
79
- # deprecation warnings.
80
- public :warn
81
- end
82
-
83
- # If the registered plugin already exists, use it. Otherwise,
84
- # require it and return it. This raises a LoadError if such a
85
- # plugin doesn't exist, or a RodaError if it exists but it does
86
- # not register itself correctly.
87
- def self.load_plugin(name)
88
- h = @plugins
89
- unless plugin = h[name]
90
- require "roda/plugins/#{name}"
91
- raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = h[name]
92
- end
93
- plugin
94
- end
95
-
96
- # Register the given plugin with Roda, so that it can be loaded using #plugin
97
- # with a symbol. Should be used by plugin files. Example:
98
- #
99
- # Roda::RodaPlugins.register_plugin(:plugin_name, PluginModule)
100
- def self.register_plugin(name, mod)
101
- @plugins[name] = mod
102
- end
103
-
104
- # Deprecate the constant with the given name in the given module,
105
- # if the ruby version supports it.
106
- def self.deprecate_constant(mod, name)
107
- # :nocov:
108
- if RUBY_VERSION >= '2.3'
109
- mod.deprecate_constant(name)
110
- end
111
- # :nocov:
112
- end
113
-
114
26
  # The base plugin for Roda, implementing all default functionality.
115
27
  # Methods are put into a plugin so future plugins can easily override
116
28
  # them and call super to get the default behavior.
@@ -628,771 +540,6 @@ WARNING
628
540
  @_request.session
629
541
  end
630
542
  end
631
-
632
- # Class methods for RodaRequest
633
- module RequestClassMethods
634
- # Reference to the Roda class related to this request class.
635
- attr_accessor :roda_class
636
-
637
- # The cache to use for match patterns for this request class.
638
- attr_accessor :match_pattern_cache
639
-
640
- # Return the cached pattern for the given object. If the object is
641
- # not already cached, yield to get the basic pattern, and convert the
642
- # basic pattern to a pattern that does not partial segments.
643
- def cached_matcher(obj)
644
- cache = @match_pattern_cache
645
-
646
- unless pattern = cache[obj]
647
- pattern = cache[obj] = consume_pattern(yield)
648
- end
649
-
650
- pattern
651
- end
652
-
653
- # Since RodaRequest is anonymously subclassed when Roda is subclassed,
654
- # and then assigned to a constant of the Roda subclass, make inspect
655
- # reflect the likely name for the class.
656
- def inspect
657
- "#{roda_class.inspect}::RodaRequest"
658
- end
659
-
660
- private
661
-
662
- # The pattern to use for consuming, based on the given argument. The returned
663
- # pattern requires the path starts with a string and does not match partial
664
- # segments.
665
- def consume_pattern(pattern)
666
- /\A\/(?:#{pattern})(?=\/|\z)/
667
- end
668
- end
669
-
670
- # Instance methods for RodaRequest, mostly related to handling routing
671
- # for the request.
672
- module RequestMethods
673
- TERM = Object.new
674
- def TERM.inspect
675
- "TERM"
676
- end
677
- TERM.freeze
678
-
679
- # The current captures for the request. This gets modified as routing
680
- # occurs.
681
- attr_reader :captures
682
-
683
- # The Roda instance related to this request object. Useful if routing
684
- # methods need access to the scope of the Roda route block.
685
- attr_reader :scope
686
-
687
- # Store the roda instance and environment.
688
- def initialize(scope, env)
689
- @scope = scope
690
- @captures = []
691
- @remaining_path = _remaining_path(env)
692
- @env = env
693
- end
694
-
695
- # Handle match block return values. By default, if a string is given
696
- # and the response is empty, use the string as the response body.
697
- def block_result(result)
698
- res = response
699
- if res.empty? && (body = block_result_body(result))
700
- res.write(body)
701
- end
702
- end
703
-
704
- # Match GET requests. If no arguments are provided, matches all GET
705
- # requests, otherwise, matches only GET requests where the arguments
706
- # given fully consume the path.
707
- def get(*args, &block)
708
- _verb(args, &block) if is_get?
709
- end
710
-
711
- # Immediately stop execution of the route block and return the given
712
- # rack response array of status, headers, and body. If no argument
713
- # is given, uses the current response.
714
- #
715
- # r.halt [200, {'Content-Type'=>'text/html'}, ['Hello World!']]
716
- #
717
- # response.status = 200
718
- # response['Content-Type'] = 'text/html'
719
- # response.write 'Hello World!'
720
- # r.halt
721
- def halt(res=response.finish)
722
- throw :halt, res
723
- end
724
-
725
- # Show information about current request, including request class,
726
- # request method and full path.
727
- #
728
- # r.inspect
729
- # # => '#<Roda::RodaRequest GET /foo/bar>'
730
- def inspect
731
- "#<#{self.class.inspect} #{@env["REQUEST_METHOD"]} #{path}>"
732
- end
733
-
734
- # Does a terminal match on the current path, matching only if the arguments
735
- # have fully matched the path. If it matches, the match block is
736
- # executed, and when the match block returns, the rack response is
737
- # returned.
738
- #
739
- # r.remaining_path
740
- # # => "/foo/bar"
741
- #
742
- # r.is 'foo' do
743
- # # does not match, as path isn't fully matched (/bar remaining)
744
- # end
745
- #
746
- # r.is 'foo/bar' do
747
- # # matches as path is empty after matching
748
- # end
749
- #
750
- # If no arguments are given, matches if the path is already fully matched.
751
- #
752
- # r.on 'foo/bar' do
753
- # r.is do
754
- # # matches as path is already empty
755
- # end
756
- # end
757
- #
758
- # Note that this matches only if the path after matching the arguments
759
- # is empty, not if it still contains a trailing slash:
760
- #
761
- # r.remaining_path
762
- # # => "/foo/bar/"
763
- #
764
- # r.is 'foo/bar' do
765
- # # does not match, as path isn't fully matched (/ remaining)
766
- # end
767
- #
768
- # r.is 'foo/bar/' do
769
- # # matches as path is empty after matching
770
- # end
771
- #
772
- # r.on 'foo/bar' do
773
- # r.is "" do
774
- # # matches as path is empty after matching
775
- # end
776
- # end
777
- def is(*args, &block)
778
- if args.empty?
779
- if empty_path?
780
- always(&block)
781
- end
782
- else
783
- args << TERM
784
- if_match(args, &block)
785
- end
786
- end
787
-
788
- # Optimized method for whether this request is a +GET+ request.
789
- # Similar to the default Rack::Request get? method, but can be
790
- # overridden without changing rack's behavior.
791
- def is_get?
792
- @env["REQUEST_METHOD"] == 'GET'
793
- end
794
-
795
- # Does a match on the path, matching only if the arguments
796
- # have matched the path. Because this doesn't fully match the
797
- # path, this is usually used to setup branches of the routing tree,
798
- # not for final handling of the request.
799
- #
800
- # r.remaining_path
801
- # # => "/foo/bar"
802
- #
803
- # r.on 'foo' do
804
- # # matches, path is /bar after matching
805
- # end
806
- #
807
- # r.on 'bar' do
808
- # # does not match
809
- # end
810
- #
811
- # Like other routing methods, If it matches, the match block is
812
- # executed, and when the match block returns, the rack response is
813
- # returned. However, in general you will call another routing method
814
- # inside the match block that fully matches the path and does the
815
- # final handling for the request:
816
- #
817
- # r.on 'foo' do
818
- # r.is 'bar' do
819
- # # handle /foo/bar request
820
- # end
821
- # end
822
- def on(*args, &block)
823
- if args.empty?
824
- always(&block)
825
- else
826
- if_match(args, &block)
827
- end
828
- end
829
-
830
- # The already matched part of the path, including the original SCRIPT_NAME.
831
- def matched_path
832
- e = @env
833
- e["SCRIPT_NAME"] + e["PATH_INFO"].chomp(@remaining_path)
834
- end
835
-
836
- # This an an optimized version of Rack::Request#path.
837
- #
838
- # r.env['SCRIPT_NAME'] = '/foo'
839
- # r.env['PATH_INFO'] = '/bar'
840
- # r.path
841
- # # => '/foo/bar'
842
- def path
843
- e = @env
844
- "#{e["SCRIPT_NAME"]}#{e["PATH_INFO"]}"
845
- end
846
-
847
- # The current path to match requests against.
848
- attr_reader :remaining_path
849
-
850
- # An alias of remaining_path. If a plugin changes remaining_path then
851
- # it should override this method to return the untouched original.
852
- def real_remaining_path
853
- remaining_path
854
- end
855
-
856
- # Match POST requests. If no arguments are provided, matches all POST
857
- # requests, otherwise, matches only POST requests where the arguments
858
- # given fully consume the path.
859
- def post(*args, &block)
860
- _verb(args, &block) if post?
861
- end
862
-
863
- # Immediately redirect to the path using the status code. This ends
864
- # the processing of the request:
865
- #
866
- # r.redirect '/page1', 301 if r['param'] == 'value1'
867
- # r.redirect '/page2' # uses 302 status code
868
- # response.status = 404 # not reached
869
- #
870
- # If you do not provide a path, by default it will redirect to the same
871
- # path if the request is not a +GET+ request. This is designed to make
872
- # it easy to use where a +POST+ request to a URL changes state, +GET+
873
- # returns the current state, and you want to show the current state
874
- # after changing:
875
- #
876
- # r.is "foo" do
877
- # r.get do
878
- # # show state
879
- # end
880
- #
881
- # r.post do
882
- # # change state
883
- # r.redirect
884
- # end
885
- # end
886
- def redirect(path=default_redirect_path, status=default_redirect_status)
887
- response.redirect(path, status)
888
- throw :halt, response.finish
889
- end
890
-
891
- # The response related to the current request. See ResponseMethods for
892
- # instance methods for the response, but in general the most common usage
893
- # is to override the response status and headers:
894
- #
895
- # response.status = 200
896
- # response['Header-Name'] = 'Header value'
897
- def response
898
- @scope.response
899
- end
900
-
901
- # Return the Roda class related to this request.
902
- def roda_class
903
- self.class.roda_class
904
- end
905
-
906
- # Match method that only matches +GET+ requests where the current
907
- # path is +/+. If it matches, the match block is executed, and when
908
- # the match block returns, the rack response is returned.
909
- #
910
- # [r.request_method, r.remaining_path]
911
- # # => ['GET', '/']
912
- #
913
- # r.root do
914
- # # matches
915
- # end
916
- #
917
- # This is usuable inside other match blocks:
918
- #
919
- # [r.request_method, r.remaining_path]
920
- # # => ['GET', '/foo/']
921
- #
922
- # r.on 'foo' do
923
- # r.root do
924
- # # matches
925
- # end
926
- # end
927
- #
928
- # Note that this does not match non-+GET+ requests:
929
- #
930
- # [r.request_method, r.remaining_path]
931
- # # => ['POST', '/']
932
- #
933
- # r.root do
934
- # # does not match
935
- # end
936
- #
937
- # Use <tt>r.post ""</tt> for +POST+ requests where the current path
938
- # is +/+.
939
- #
940
- # Nor does it match empty paths:
941
- #
942
- # [r.request_method, r.remaining_path]
943
- # # => ['GET', '/foo']
944
- #
945
- # r.on 'foo' do
946
- # r.root do
947
- # # does not match
948
- # end
949
- # end
950
- #
951
- # Use <tt>r.get true</tt> to handle +GET+ requests where the current
952
- # path is empty.
953
- def root(&block)
954
- if remaining_path == "/" && is_get?
955
- always(&block)
956
- end
957
- end
958
-
959
- # Call the given rack app with the environment and return the response
960
- # from the rack app as the response for this request. This ends
961
- # the processing of the request:
962
- #
963
- # r.run(proc{[403, {}, []]}) unless r['letmein'] == '1'
964
- # r.run(proc{[404, {}, []]})
965
- # response.status = 404 # not reached
966
- #
967
- # This updates SCRIPT_NAME/PATH_INFO based on the current remaining_path
968
- # before dispatching to another rack app, so the app still works as
969
- # a URL mapper.
970
- def run(app)
971
- e = @env
972
- path = real_remaining_path
973
- sn = "SCRIPT_NAME"
974
- pi = "PATH_INFO"
975
- script_name = e[sn]
976
- path_info = e[pi]
977
- begin
978
- e[sn] += path_info.chomp(path)
979
- e[pi] = path
980
- throw :halt, app.call(e)
981
- ensure
982
- e[sn] = script_name
983
- e[pi] = path_info
984
- end
985
- end
986
-
987
- # The session for the current request. Raises a RodaError if
988
- # a session handler has not been loaded.
989
- def session
990
- @env['rack.session'] || raise(RodaError, "You're missing a session handler, try using the sessions plugin.")
991
- end
992
-
993
- private
994
-
995
- # Match any of the elements in the given array. Return at the
996
- # first match without evaluating future matches. Returns false
997
- # if no elements in the array match.
998
- def _match_array(matcher)
999
- matcher.any? do |m|
1000
- if matched = match(m)
1001
- if m.is_a?(String)
1002
- @captures.push(m)
1003
- end
1004
- end
1005
-
1006
- matched
1007
- end
1008
- end
1009
-
1010
- # Match the given class. Currently, the following classes
1011
- # are supported by default:
1012
- # Integer :: Match an integer segment, yielding result to block as an integer
1013
- # String :: Match any non-empty segment, yielding result to block as a string
1014
- def _match_class(klass)
1015
- meth = :"_match_class_#{klass}"
1016
- if respond_to?(meth, true)
1017
- # Allow calling private methods, as match methods are generally private
1018
- send(meth)
1019
- else
1020
- unsupported_matcher(klass)
1021
- end
1022
- end
1023
-
1024
- # Match the given hash if all hash matchers match.
1025
- def _match_hash(hash)
1026
- # Allow calling private methods, as match methods are generally private
1027
- hash.all?{|k,v| send("match_#{k}", v)}
1028
- end
1029
-
1030
- # Match integer segment, and yield resulting value as an
1031
- # integer.
1032
- def _match_class_Integer
1033
- consume(/\A\/(\d+)(?=\/|\z)/){|i| [i.to_i]}
1034
- end
1035
-
1036
- # Match only if all of the arguments in the given array match.
1037
- # Match the given regexp exactly if it matches a full segment.
1038
- def _match_regexp(re)
1039
- consume(self.class.cached_matcher(re){re})
1040
- end
1041
-
1042
- # Match the given string to the request path. Matches only if the
1043
- # request path ends with the string or if the next character in the
1044
- # request path is a slash (indicating a new segment).
1045
- def _match_string(str)
1046
- rp = @remaining_path
1047
- length = str.length
1048
-
1049
- match = case rp.rindex(str, length)
1050
- when nil
1051
- # segment does not match, most common case
1052
- return
1053
- when 1
1054
- # segment matches, check first character is /
1055
- rp.getbyte(0) == 47
1056
- else # must be 0
1057
- # segment matches at first character, only a match if
1058
- # empty string given and first character is /
1059
- length == 0 && rp.getbyte(0) == 47
1060
- end
1061
-
1062
- if match
1063
- length += 1
1064
- case rp.getbyte(length)
1065
- when 47
1066
- # next character is /, update remaining path to rest of string
1067
- @remaining_path = rp[length, 100000000]
1068
- when nil
1069
- # end of string, so remaining path is empty
1070
- @remaining_path = ""
1071
- # else
1072
- # Any other value means this was partial segment match,
1073
- # so we return nil in that case without updating the
1074
- # remaining_path. No need for explicit else clause.
1075
- end
1076
- end
1077
- end
1078
-
1079
- # Match the given symbol if any segment matches.
1080
- def _match_symbol(sym=nil)
1081
- rp = @remaining_path
1082
- if rp.getbyte(0) == 47
1083
- if last = rp.index('/', 1)
1084
- if last > 1
1085
- @captures << rp[1, last-1]
1086
- @remaining_path = rp[last, rp.length]
1087
- end
1088
- elsif rp.length > 1
1089
- @captures << rp[1,rp.length]
1090
- @remaining_path = ""
1091
- end
1092
- end
1093
- end
1094
-
1095
- # Match any nonempty segment. This should be called without an argument.
1096
- alias _match_class_String _match_symbol
1097
-
1098
- # The base remaining path to use.
1099
- def _remaining_path(env)
1100
- env["PATH_INFO"]
1101
- end
1102
-
1103
- # Backbone of the verb method support, using a terminal match if
1104
- # args is not empty, or a regular match if it is empty.
1105
- def _verb(args, &block)
1106
- if args.empty?
1107
- always(&block)
1108
- else
1109
- args << TERM
1110
- if_match(args, &block)
1111
- end
1112
- end
1113
-
1114
- # Yield to the match block and return rack response after the block returns.
1115
- def always
1116
- block_result(yield)
1117
- throw :halt, response.finish
1118
- end
1119
-
1120
- # The body to use for the response if the response does not already have
1121
- # a body. By default, a String is returned directly, and nil is
1122
- # returned otherwise.
1123
- def block_result_body(result)
1124
- case result
1125
- when String
1126
- result
1127
- when nil, false
1128
- # nothing
1129
- else
1130
- raise RodaError, "unsupported block result: #{result.inspect}"
1131
- end
1132
- end
1133
-
1134
- # Attempts to match the pattern to the current path. If there is no
1135
- # match, returns false without changes. Otherwise, modifies
1136
- # SCRIPT_NAME to include the matched path, removes the matched
1137
- # path from PATH_INFO, and updates captures with any regex captures.
1138
- def consume(pattern)
1139
- if matchdata = remaining_path.match(pattern)
1140
- @remaining_path = matchdata.post_match
1141
- captures = matchdata.captures
1142
- captures = yield(*captures) if block_given?
1143
- @captures.concat(captures)
1144
- end
1145
- end
1146
-
1147
- # The default path to use for redirects when a path is not given.
1148
- # For non-GET requests, redirects to the current path, which will
1149
- # trigger a GET request. This is to make the common case where
1150
- # a POST request will redirect to a GET request at the same location
1151
- # will work fine.
1152
- #
1153
- # If the current request is a GET request, raise an error, as otherwise
1154
- # it is easy to create an infinite redirect.
1155
- def default_redirect_path
1156
- raise RodaError, "must provide path argument to redirect for get requests" if is_get?
1157
- path
1158
- end
1159
-
1160
- # The default status to use for redirects if a status is not provided,
1161
- # 302 by default.
1162
- def default_redirect_status
1163
- 302
1164
- end
1165
-
1166
- # Whether the current path is considered empty.
1167
- def empty_path?
1168
- remaining_path.empty?
1169
- end
1170
-
1171
- # If all of the arguments match, yields to the match block and
1172
- # returns the rack response when the block returns. If any of
1173
- # the match arguments doesn't match, does nothing.
1174
- def if_match(args)
1175
- path = @remaining_path
1176
- # For every block, we make sure to reset captures so that
1177
- # nesting matchers won't mess with each other's captures.
1178
- captures = @captures.clear
1179
-
1180
- if match_all(args)
1181
- block_result(yield(*captures))
1182
- throw :halt, response.finish
1183
- else
1184
- @remaining_path = path
1185
- false
1186
- end
1187
- end
1188
-
1189
- # Attempt to match the argument to the given request, handling
1190
- # common ruby types.
1191
- def match(matcher)
1192
- case matcher
1193
- when String
1194
- _match_string(matcher)
1195
- when Class
1196
- _match_class(matcher)
1197
- when TERM
1198
- empty_path?
1199
- when Regexp
1200
- _match_regexp(matcher)
1201
- when true
1202
- matcher
1203
- when Array
1204
- _match_array(matcher)
1205
- when Hash
1206
- _match_hash(matcher)
1207
- when Symbol
1208
- _match_symbol(matcher)
1209
- when false, nil
1210
- matcher
1211
- when Proc
1212
- matcher.call
1213
- else
1214
- unsupported_matcher(matcher)
1215
- end
1216
- end
1217
-
1218
- # Match only if all of the arguments in the given array match.
1219
- def match_all(args)
1220
- args.all?{|arg| match(arg)}
1221
- end
1222
-
1223
- # Match by request method. This can be an array if you want
1224
- # to match on multiple methods.
1225
- def match_method(type)
1226
- if type.is_a?(Array)
1227
- type.any?{|t| match_method(t)}
1228
- else
1229
- type.to_s.upcase == @env["REQUEST_METHOD"]
1230
- end
1231
- end
1232
-
1233
- # Handle an unsupported matcher.
1234
- def unsupported_matcher(matcher)
1235
- raise RodaError, "unsupported matcher: #{matcher.inspect}"
1236
- end
1237
- end
1238
-
1239
- # Class methods for RodaResponse
1240
- module ResponseClassMethods
1241
- # Reference to the Roda class related to this response class.
1242
- attr_accessor :roda_class
1243
-
1244
- # Since RodaResponse is anonymously subclassed when Roda is subclassed,
1245
- # and then assigned to a constant of the Roda subclass, make inspect
1246
- # reflect the likely name for the class.
1247
- def inspect
1248
- "#{roda_class.inspect}::RodaResponse"
1249
- end
1250
- end
1251
-
1252
- # Instance methods for RodaResponse
1253
- module ResponseMethods
1254
- DEFAULT_HEADERS = {"Content-Type" => "text/html".freeze}.freeze
1255
-
1256
- # The body for the current response.
1257
- attr_reader :body
1258
-
1259
- # The hash of response headers for the current response.
1260
- attr_reader :headers
1261
-
1262
- # The status code to use for the response. If none is given, will use 200
1263
- # code for non-empty responses and a 404 code for empty responses.
1264
- attr_accessor :status
1265
-
1266
- # Set the default headers when creating a response.
1267
- def initialize
1268
- @headers = {}
1269
- @body = []
1270
- @length = 0
1271
- end
1272
-
1273
- # Return the response header with the given key. Example:
1274
- #
1275
- # response['Content-Type'] # => 'text/html'
1276
- def [](key)
1277
- @headers[key]
1278
- end
1279
-
1280
- # Set the response header with the given key to the given value.
1281
- #
1282
- # response['Content-Type'] = 'application/json'
1283
- def []=(key, value)
1284
- @headers[key] = value
1285
- end
1286
-
1287
- # The default headers to use for responses.
1288
- def default_headers
1289
- DEFAULT_HEADERS
1290
- end
1291
-
1292
- # Whether the response body has been written to yet. Note
1293
- # that writing an empty string to the response body marks
1294
- # the response as not empty. Example:
1295
- #
1296
- # response.empty? # => true
1297
- # response.write('a')
1298
- # response.empty? # => false
1299
- def empty?
1300
- @body.empty?
1301
- end
1302
-
1303
- # Return the rack response array of status, headers, and body
1304
- # for the current response. If the status has not been set,
1305
- # uses the return value of default_status if the body has
1306
- # been written to, otherwise uses a 404 status.
1307
- # Adds the Content-Length header to the size of the response body.
1308
- #
1309
- # Example:
1310
- #
1311
- # response.finish
1312
- # # => [200,
1313
- # # {'Content-Type'=>'text/html', 'Content-Length'=>'0'},
1314
- # # []]
1315
- def finish
1316
- b = @body
1317
- set_default_headers
1318
- h = @headers
1319
-
1320
- if b.empty?
1321
- s = @status || 404
1322
- if (s == 304 || s == 204 || (s >= 100 && s <= 199))
1323
- h.delete("Content-Type")
1324
- elsif s == 205
1325
- h.delete("Content-Type")
1326
- h["Content-Length"] = '0'
1327
- else
1328
- h["Content-Length"] ||= '0'
1329
- end
1330
- else
1331
- s = @status || default_status
1332
- h["Content-Length"] ||= @length.to_s
1333
- end
1334
-
1335
- [s, h, b]
1336
- end
1337
-
1338
- # Return the rack response array using a given body. Assumes a
1339
- # 200 response status unless status has been explicitly set,
1340
- # and doesn't add the Content-Length header or use the existing
1341
- # body.
1342
- def finish_with_body(body)
1343
- set_default_headers
1344
- [@status || default_status, @headers, body]
1345
- end
1346
-
1347
- # Return the default response status to be used when the body
1348
- # has been written to. This is split out to make overriding
1349
- # easier in plugins.
1350
- def default_status
1351
- 200
1352
- end
1353
-
1354
- # Show response class, status code, response headers, and response body
1355
- def inspect
1356
- "#<#{self.class.inspect} #{@status.inspect} #{@headers.inspect} #{@body.inspect}>"
1357
- end
1358
-
1359
- # Set the Location header to the given path, and the status
1360
- # to the given status. Example:
1361
- #
1362
- # response.redirect('foo', 301)
1363
- # response.redirect('bar')
1364
- def redirect(path, status = 302)
1365
- @headers["Location"] = path
1366
- @status = status
1367
- nil
1368
- end
1369
-
1370
- # Return the Roda class related to this response.
1371
- def roda_class
1372
- self.class.roda_class
1373
- end
1374
-
1375
- # Write to the response body. Returns nil.
1376
- #
1377
- # response.write('foo')
1378
- def write(str)
1379
- s = str.to_s
1380
- @length += s.bytesize
1381
- @body << s
1382
- nil
1383
- end
1384
-
1385
- private
1386
-
1387
- # For each default header, if a header has not already been set for the
1388
- # response, set the header in the response.
1389
- def set_default_headers
1390
- h = @headers
1391
- default_headers.each do |k,v|
1392
- h[k] ||= v
1393
- end
1394
- end
1395
- end
1396
543
  end
1397
544
  end
1398
545