roda 3.26.0 → 3.27.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.
- checksums.yaml +4 -4
- data/CHANGELOG +8 -0
- data/doc/release_notes/3.27.0.txt +15 -0
- data/lib/roda.rb +4 -857
- data/lib/roda/cache.rb +35 -0
- data/lib/roda/plugins.rb +53 -0
- data/lib/roda/plugins/halt.rb +8 -3
- data/lib/roda/plugins/multibyte_string_matcher.rb +57 -0
- data/lib/roda/plugins/params_capturing.rb +5 -3
- data/lib/roda/request.rb +625 -0
- data/lib/roda/response.rb +172 -0
- data/lib/roda/version.rb +1 -1
- data/spec/define_roda_method_spec.rb +1 -1
- data/spec/plugin/json_parser_spec.rb +5 -0
- data/spec/plugin/multibyte_string_matcher_spec.rb +44 -0
- data/spec/plugin/sinatra_helpers_spec.rb +2 -2
- metadata +10 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1aaf8be3e1a6fa8d822436561e48f5a5978e94e3430c0287b32be3a0a292d351
|
4
|
+
data.tar.gz: 84ed6cd43e59574cc1264889578296d30c96c91d4b53a45bb2b839619f2d1786
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/lib/roda.rb
CHANGED
@@ -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
|
|