rutter 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +14 -4
- data/.travis.yml +12 -2
- data/Gemfile +4 -1
- data/LICENSE.txt +1 -1
- data/README.md +17 -102
- data/Rakefile +7 -1
- data/bench/config.ru +10 -9
- data/lib/rutter.rb +10 -7
- data/lib/rutter/builder.rb +180 -221
- data/lib/rutter/mount.rb +21 -0
- data/lib/rutter/naming.rb +95 -0
- data/lib/rutter/route.rb +69 -126
- data/lib/rutter/routes.rb +36 -9
- data/lib/rutter/scope.rb +39 -40
- data/lib/rutter/verbs.rb +8 -0
- data/lib/rutter/version.rb +1 -1
- data/rutter.gemspec +4 -3
- data/spec/integration/rack_spec.rb +21 -13
- data/spec/spec_helper.rb +6 -48
- data/spec/support/rack.rb +20 -0
- data/spec/unit/builder_spec.rb +86 -74
- data/spec/unit/namespace_spec.rb +26 -0
- data/spec/unit/route_spec.rb +33 -50
- data/spec/unit/routes_spec.rb +20 -11
- data/spec/unit/rutter_spec.rb +3 -2
- data/spec/unit/scope_spec.rb +49 -56
- metadata +25 -17
- data/bench/dynamic_routes +0 -20
- data/bench/expand +0 -19
- data/bench/helper.rb +0 -19
- data/bench/mount +0 -32
- data/bench/routes_helper +0 -24
- data/bench/static_routes +0 -20
- data/spec/integration/mount_spec.rb +0 -18
- data/spec/integration/params_spec.rb +0 -28
- data/spec/integration/redirect_spec.rb +0 -36
data/lib/rutter/mount.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rutter
|
4
|
+
# Represents a mounted app route.
|
5
|
+
#
|
6
|
+
# @see Rutter::Route
|
7
|
+
#
|
8
|
+
# @private
|
9
|
+
class Mount < Route
|
10
|
+
# Matches the app pattern against environment.
|
11
|
+
#
|
12
|
+
# @param env [Hash]
|
13
|
+
# Rack environment hash.
|
14
|
+
#
|
15
|
+
# @return [nil, String]
|
16
|
+
# Returns the matching substring or nil on no match.
|
17
|
+
def match?(env)
|
18
|
+
@pattern.peek(env["PATH_INFO"])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rutter
|
4
|
+
# Conveniences for inflecting and working with names in Rutter.
|
5
|
+
module Naming
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# Return a downcased and underscore separated version of the string.
|
9
|
+
#
|
10
|
+
# Revised version of `Hanami::Utils::String.underscore` implementation.
|
11
|
+
#
|
12
|
+
# @param string [String]
|
13
|
+
# String to be transformed.
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
# The transformed string.
|
17
|
+
#
|
18
|
+
# @example
|
19
|
+
# string = "RutterNaming"
|
20
|
+
# Rutter::Naming.underscore(string) # => 'rutter_naming'
|
21
|
+
def underscore(string)
|
22
|
+
string = +string.to_s
|
23
|
+
string.gsub!("::", "/")
|
24
|
+
string.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
25
|
+
string.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
26
|
+
string.gsub!(/[[:space:]]|\-/, '\1_\2')
|
27
|
+
string.downcase!
|
28
|
+
string
|
29
|
+
end
|
30
|
+
|
31
|
+
# Return a CamelCase version of the string.
|
32
|
+
#
|
33
|
+
# Revised version of `Hanami::Utils::String.classify` implementation.
|
34
|
+
#
|
35
|
+
# @param string [String]
|
36
|
+
# String to be transformed.
|
37
|
+
#
|
38
|
+
# @return [String]
|
39
|
+
# The transformed string
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# string = "rutter_naming"
|
43
|
+
# Rutter::Naming.classify(string) # => 'RutterNaming'
|
44
|
+
def classify(string)
|
45
|
+
words = underscore(string).split(%r{_|::|\/|\-}).map!(&:capitalize)
|
46
|
+
delimiters = underscore(string).scan(%r{_|::|\/|\-})
|
47
|
+
delimiters.map! { |delimiter| delimiter == "_" ? "" : "::" }
|
48
|
+
words.zip(delimiters).join
|
49
|
+
end
|
50
|
+
|
51
|
+
# Normalize the given string/symbol to a valid route name.
|
52
|
+
#
|
53
|
+
# @param string [String, Symbol]
|
54
|
+
# Name to be normalized.
|
55
|
+
#
|
56
|
+
# @return [Symbol]
|
57
|
+
# Normalized route name.
|
58
|
+
def route_name(string)
|
59
|
+
string = underscore(string)
|
60
|
+
string.tr!("/", "_")
|
61
|
+
string.gsub!(/[_]{2,}/, "_")
|
62
|
+
string.gsub!(/\A_|_\z/, "")
|
63
|
+
string.to_sym
|
64
|
+
end
|
65
|
+
|
66
|
+
# Normalize the given path.
|
67
|
+
#
|
68
|
+
# @param path [String]
|
69
|
+
# Path to be normalized.
|
70
|
+
#
|
71
|
+
# @return [String]
|
72
|
+
# Normalized path.
|
73
|
+
def cleanpath(path)
|
74
|
+
Pathname.new("/#{path}").cleanpath.to_s
|
75
|
+
end
|
76
|
+
|
77
|
+
# Join the given arguments with the separator.
|
78
|
+
#
|
79
|
+
# @overload join(part)
|
80
|
+
# @param part [String, nil]
|
81
|
+
# First part to join.
|
82
|
+
# @overload join(part)
|
83
|
+
# @param ... [String, nil]
|
84
|
+
# Another part to join.
|
85
|
+
#
|
86
|
+
# @param sep [String]
|
87
|
+
# Part separator.
|
88
|
+
#
|
89
|
+
# @return [String]
|
90
|
+
def join(*part, sep: "/")
|
91
|
+
part.reject { |s| s.nil? || s == "" }
|
92
|
+
.join(sep)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/rutter/route.rb
CHANGED
@@ -1,168 +1,111 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "
|
3
|
+
require "mustermann"
|
4
4
|
|
5
5
|
module Rutter
|
6
6
|
# Represents a single route.
|
7
7
|
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
8
|
+
# @!attribute [r] path
|
9
|
+
# @return [String] Raw path template.
|
10
|
+
# @!attribute [r] endpoint
|
11
|
+
# @return [Hash] Route endpoint.
|
12
|
+
#
|
13
|
+
# @private
|
14
14
|
class Route
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
attr_reader :method, :path, :endpoint
|
15
|
+
attr_reader :path
|
16
|
+
attr_reader :endpoint
|
19
17
|
|
20
18
|
# Initializes the route.
|
21
19
|
#
|
22
|
-
# @param method [Symbol, String]
|
23
|
-
# Requets method to match.
|
24
20
|
# @param path [String]
|
25
|
-
# Path template.
|
26
|
-
# @param endpoint [
|
27
|
-
#
|
28
|
-
# @param
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# @return [self]
|
21
|
+
# Path template to match.
|
22
|
+
# @param endpoint [#call]
|
23
|
+
# Rack endpoint.
|
24
|
+
# @param constraints [Hash]
|
25
|
+
# Route segment constraints.
|
32
26
|
#
|
33
|
-
# @
|
34
|
-
# If request method is unknown.
|
27
|
+
# @return [void]
|
35
28
|
#
|
36
29
|
# @private
|
37
|
-
def initialize(
|
38
|
-
@
|
39
|
-
|
40
|
-
|
41
|
-
raise ArgumentError, "invalid verb: '#{method}'"
|
42
|
-
end
|
43
|
-
|
44
|
-
@controller_suffix = controller_suffix || ""
|
45
|
-
@path = normalize_path(path)
|
46
|
-
@dynamic = false
|
47
|
-
@pattern = path_to_pattern(@path).freeze
|
48
|
-
@endpoint = build_endpoint_hash(endpoint).freeze
|
49
|
-
|
30
|
+
def initialize(path, endpoint, constraints = nil)
|
31
|
+
@path = Naming.cleanpath(path)
|
32
|
+
@endpoint = endpoint_to_hash(endpoint)
|
33
|
+
@pattern = ::Mustermann.new(@path, capture: constraints)
|
50
34
|
freeze
|
51
35
|
end
|
52
36
|
|
53
|
-
# Matches the
|
37
|
+
# Matches the route pattern against environment.
|
54
38
|
#
|
55
|
-
# @param
|
56
|
-
#
|
39
|
+
# @param env [Hash]
|
40
|
+
# Rack environment hash.
|
57
41
|
#
|
58
42
|
# @return [Boolean]
|
59
|
-
def match?(
|
60
|
-
@
|
43
|
+
def match?(env)
|
44
|
+
@pattern === env["PATH_INFO"] # rubocop:disable Style/CaseEquality
|
61
45
|
end
|
62
46
|
|
63
|
-
#
|
47
|
+
# Generates a path from the given arguments.
|
64
48
|
#
|
65
|
-
#
|
49
|
+
# @overload expand(key: value)
|
50
|
+
# @param key [String, Integer, Array]
|
51
|
+
# Key value.
|
52
|
+
# @overload expand(key: value, key2: value2)
|
53
|
+
# @param key2 [String, Integer, Array]
|
54
|
+
# Key value.
|
66
55
|
#
|
67
|
-
# @
|
68
|
-
#
|
56
|
+
# @return [String]
|
57
|
+
# Generated path.
|
69
58
|
#
|
70
|
-
# @
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
59
|
+
# @raise [ArguemntError]
|
60
|
+
# If the path cannot be generated. Mostly due to missing key(s).
|
61
|
+
def expand(**args)
|
62
|
+
@pattern.expand(:append, **args)
|
63
|
+
rescue ::Mustermann::ExpandError => e
|
64
|
+
raise ArgumentError, e.message
|
75
65
|
end
|
76
66
|
|
77
|
-
#
|
78
|
-
#
|
79
|
-
# @param params [Hash]
|
80
|
-
# Expand parameters.
|
81
|
-
#
|
82
|
-
# @return [String] Expanded string.
|
83
|
-
#
|
84
|
-
# @example basic example
|
85
|
-
# route = Route.new("GET", "/books/:id/:title", -> {})
|
86
|
-
#
|
87
|
-
# route.expand(id: 54, title: "eloquent-ruby")
|
88
|
-
# # => "/books/54/eloquent-ruby"
|
67
|
+
# Extract params from the given path.
|
89
68
|
#
|
90
|
-
# @
|
91
|
-
#
|
69
|
+
# @param path [String]
|
70
|
+
# Path used to extract params from.
|
92
71
|
#
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
string = if @dynamic
|
97
|
-
new_path = path.gsub(SEGMENT_MATCH) do |match|
|
98
|
-
params.delete(match[1..-1].to_sym)
|
99
|
-
end
|
100
|
-
new_path.gsub(/\(|\)/, "")
|
101
|
-
else
|
102
|
-
path.dup
|
103
|
-
end
|
104
|
-
|
105
|
-
string = normalize_path(string)
|
106
|
-
return string if params.empty?
|
107
|
-
|
108
|
-
"#{string}?#{Rack::Utils.build_nested_query(params)}"
|
72
|
+
# @return [Hash]
|
73
|
+
def params(path)
|
74
|
+
@pattern.params(path)
|
109
75
|
end
|
110
76
|
|
111
|
-
|
112
|
-
|
113
|
-
# Segment for dynamic parts.
|
77
|
+
# Calls the endpoint.
|
114
78
|
#
|
115
|
-
# @
|
116
|
-
|
117
|
-
|
118
|
-
# Segment for wildcards.
|
79
|
+
# @param env [Hash]
|
80
|
+
# Rack's environment hash.
|
119
81
|
#
|
120
|
-
# @
|
121
|
-
|
122
|
-
|
123
|
-
# Match segment regexp.
|
82
|
+
# @return [Array]
|
83
|
+
# Rack response array.
|
124
84
|
#
|
125
85
|
# @private
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
if
|
133
|
-
|
134
|
-
pattern = pattern.gsub("(", "(?:").gsub(")", ")?")
|
135
|
-
end
|
136
|
-
|
137
|
-
pattern = pattern.gsub(SEGMENT_MATCH) do |match|
|
138
|
-
@dynamic = true
|
139
|
-
segment = match[0] == ":" ? DYNAMIC_SEGMENT : WILDCARD_SEGMENT
|
140
|
-
"(?<#{match[1..-1]}>#{segment})"
|
141
|
-
end
|
142
|
-
|
143
|
-
/\A#{pattern}\z/
|
86
|
+
def call(env)
|
87
|
+
env["rutter.params"] ||= {}
|
88
|
+
env["rutter.params"].merge!(params(env["PATH_INFO"]))
|
89
|
+
env["rutter.action"] = @endpoint[:action]
|
90
|
+
|
91
|
+
ctrl = @endpoint[:controller]
|
92
|
+
ctrl = ::Object.const_get(ctrl) if ctrl.is_a?(String)
|
93
|
+
ctrl.call(env)
|
144
94
|
end
|
145
95
|
|
146
|
-
|
147
|
-
def normalize_path(path)
|
148
|
-
path = path.gsub(%r{[\/]{2,}}, "/")
|
149
|
-
.gsub(%r{\A[\/]+|[\/]+\z}, "")
|
150
|
-
.downcase
|
151
|
-
|
152
|
-
"/#{path}"
|
153
|
-
end
|
96
|
+
private
|
154
97
|
|
155
98
|
# @private
|
156
|
-
def
|
157
|
-
if endpoint.is_a?(String)
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
99
|
+
def endpoint_to_hash(endpoint)
|
100
|
+
ctrl, action = if endpoint.is_a?(String)
|
101
|
+
ctrl, action = endpoint.split("#")
|
102
|
+
ctrl = Naming.classify(ctrl)
|
103
|
+
[ctrl, action]
|
104
|
+
else
|
105
|
+
[endpoint, nil]
|
106
|
+
end
|
107
|
+
|
108
|
+
{ controller: ctrl, action: action }
|
166
109
|
end
|
167
110
|
end
|
168
111
|
end
|
data/lib/rutter/routes.rb
CHANGED
@@ -3,24 +3,51 @@
|
|
3
3
|
require "forwardable"
|
4
4
|
|
5
5
|
module Rutter
|
6
|
-
# Routes
|
6
|
+
# Routes URL builder.
|
7
|
+
#
|
8
|
+
# @see Rutter::Builder#path
|
9
|
+
# @see Rutter::Builder#url
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# router = Rutter.new(base: "http://rutter.org") do
|
13
|
+
# get "/login", to: "sessions#new", as: :login
|
14
|
+
# get "/books/:id", to: "books#show", as: :book
|
15
|
+
# end.freeze
|
16
|
+
#
|
17
|
+
# routes = Rutter::Routes.new(router)
|
18
|
+
#
|
19
|
+
# routes.login_path
|
20
|
+
# # => "/login"
|
21
|
+
# routes.login_path(return_to: "/")
|
22
|
+
# # => "/login?return_to=/"
|
23
|
+
# routes.book_path(id: 82)
|
24
|
+
# # => "/books/82"
|
25
|
+
#
|
26
|
+
# routes.login_url
|
27
|
+
# # => "http://rutter.org/login"
|
28
|
+
# routes.login_url(return_to: "/")
|
29
|
+
# # => "http://rutter.org/login?return_to=/"
|
30
|
+
# routes.book_url(id: 82)
|
31
|
+
# # => "http://rutter.org/books/82"
|
7
32
|
class Routes
|
8
33
|
extend Forwardable
|
9
34
|
|
10
|
-
# Delegate path and url method to the
|
35
|
+
# Delegate path and url method to the router.
|
11
36
|
#
|
12
37
|
# @see Rutter::Builder#path
|
13
38
|
# @see Rutter::Builder#url
|
14
|
-
def_delegators :@
|
39
|
+
def_delegators :@router, :path, :url
|
15
40
|
|
16
41
|
# Initializes the helper.
|
17
42
|
#
|
18
|
-
# @param
|
19
|
-
# Route
|
43
|
+
# @param router [Rutter::Builder]
|
44
|
+
# Route router object.
|
20
45
|
#
|
21
|
-
# @return [
|
22
|
-
|
23
|
-
|
46
|
+
# @return [void]
|
47
|
+
#
|
48
|
+
# @private
|
49
|
+
def initialize(router)
|
50
|
+
@router = router
|
24
51
|
end
|
25
52
|
|
26
53
|
protected
|
@@ -29,7 +56,7 @@ module Rutter
|
|
29
56
|
def method_missing(method_name, *args)
|
30
57
|
named_route, type = method_name.to_s.split(/\_(path|url)\z/)
|
31
58
|
return super unless type
|
32
|
-
@
|
59
|
+
@router.public_send(type, named_route.to_sym, *args)
|
33
60
|
end
|
34
61
|
|
35
62
|
# @private
|
data/lib/rutter/scope.rb
CHANGED
@@ -1,67 +1,66 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Rutter
|
4
|
-
#
|
4
|
+
# Represents a scoped set.
|
5
|
+
#
|
6
|
+
# @see Rutter::Builder#scope
|
7
|
+
#
|
8
|
+
# @private
|
5
9
|
class Scope
|
6
10
|
# Initializes the scope.
|
7
11
|
#
|
8
12
|
# @param router [Rutter::Builder]
|
9
|
-
#
|
10
|
-
# @
|
11
|
-
#
|
12
|
-
# @
|
13
|
-
#
|
14
|
-
# @
|
15
|
-
#
|
13
|
+
# Router object.
|
14
|
+
# @param path [String]
|
15
|
+
# Scope path prefix.
|
16
|
+
# @param namespace [String, Symbol]
|
17
|
+
# Scope namespace.
|
18
|
+
# @param as [Symbol]
|
19
|
+
# Scope name prefix.
|
20
|
+
#
|
16
21
|
# @yield
|
17
|
-
#
|
22
|
+
# Block is evaluated inside the created scope context.
|
18
23
|
#
|
19
|
-
# @return [
|
24
|
+
# @return [void]
|
20
25
|
#
|
21
26
|
# @private
|
22
|
-
def initialize(router,
|
27
|
+
def initialize(router, path: nil, namespace: nil, as: nil, &block)
|
23
28
|
@router = router
|
24
|
-
@
|
25
|
-
@
|
26
|
-
@
|
29
|
+
@path = path
|
30
|
+
@namespace = namespace
|
31
|
+
@as = as
|
27
32
|
|
28
33
|
instance_eval(&block) if block_given?
|
29
34
|
end
|
30
35
|
|
31
|
-
# @see Rutter::Builder#
|
32
|
-
def
|
33
|
-
|
34
|
-
opts[:path] = "#{@path_prefix}/#{opts[:path]}" if @path_prefix
|
35
|
-
|
36
|
-
if @namespace_prefix
|
37
|
-
opts[:namespace] = "#{@namespace_prefix}::#{opts[:namespace]}"
|
38
|
-
end
|
39
|
-
|
40
|
-
Scope.new(@router, **opts, &block)
|
36
|
+
# @see Rutter::Builder#mount
|
37
|
+
def mount(app, at:)
|
38
|
+
@router.mount app, at: Naming.join(@path, at)
|
41
39
|
end
|
42
40
|
|
43
|
-
# @see Rutter::Builder#
|
44
|
-
def
|
45
|
-
|
46
|
-
@router.mount app, at: at, **opts
|
41
|
+
# @see Rutter::Builder#scope
|
42
|
+
def scope(path: nil, namespace: nil, as: nil, &block)
|
43
|
+
Scope.new(self, path: path, namespace: namespace, as: as, &block)
|
47
44
|
end
|
48
45
|
|
49
|
-
# @see Rutter::Builder#
|
50
|
-
def
|
51
|
-
|
46
|
+
# @see Rutter::Builder#namespace
|
47
|
+
def namespace(name, &block)
|
48
|
+
scope path: name, namespace: name, as: name, &block
|
52
49
|
end
|
53
50
|
|
54
51
|
# @see Rutter::Builder#add
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
opts[:as] = "#{@name_prefix}_#{opts[:as]}" if opts[:as]
|
60
|
-
if @namespace_prefix && opts[:to].is_a?(String)
|
61
|
-
opts[:to] = "#{@namespace_prefix}::#{opts[:to]}"
|
62
|
-
end
|
52
|
+
def add(verb, path, to:, as: nil, constraints: nil)
|
53
|
+
path = Naming.join(@path, path)
|
54
|
+
to = Naming.join(@namespace, to) if to.is_a?(String)
|
55
|
+
as = Naming.join(@as, as) if as
|
63
56
|
|
64
|
-
|
57
|
+
@router.add verb, path, to: to, as: as, constraints: constraints
|
58
|
+
end
|
59
|
+
|
60
|
+
# @see Rutter::Builder#add
|
61
|
+
VERBS.each do |verb|
|
62
|
+
define_method verb.downcase do |path, to:, as: nil, constraints: nil|
|
63
|
+
add verb, path, to: to, as: as, constraints: constraints
|
65
64
|
end
|
66
65
|
end
|
67
66
|
end
|