rutter 0.1.2 → 0.2.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 +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
|