lennarb 0.1.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 +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENCE +24 -0
- data/README.md +31 -0
- data/lib/lenna/base.rb +52 -0
- data/lib/lenna/middleware/app.rb +103 -0
- data/lib/lenna/middleware/default/error_handler.rb +205 -0
- data/lib/lenna/middleware/default/logging.rb +95 -0
- data/lib/lenna/router/builder.rb +99 -0
- data/lib/lenna/router/cache.rb +38 -0
- data/lib/lenna/router/namespace_stack.rb +73 -0
- data/lib/lenna/router/request.rb +77 -0
- data/lib/lenna/router/response.rb +357 -0
- data/lib/lenna/router/route_matcher.rb +69 -0
- data/lib/lenna/router.rb +173 -0
- data/lib/lennarb.rb +3 -0
- metadata +172 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lenna
|
4
|
+
class Router
|
5
|
+
# @api public
|
6
|
+
# @note This class is used to cache the routes.
|
7
|
+
class Cache
|
8
|
+
def initialize = @cache = {}
|
9
|
+
|
10
|
+
# @api public
|
11
|
+
# @param [String] method
|
12
|
+
# @param [String] path
|
13
|
+
# @return [String]
|
14
|
+
# @note This method is used to generate a key for the cache.
|
15
|
+
def cache_key(method, path) = "#{method} #{path}"
|
16
|
+
|
17
|
+
# @api public
|
18
|
+
# @param route_key [String] The key for the route.
|
19
|
+
# @param node [Lenna::Route::Node] The node for the route.
|
20
|
+
# @return [Lenna::Route::Node]
|
21
|
+
# @note This method is used to add a route to the cache.
|
22
|
+
def add(route_key, node) = @cache[route_key] = node
|
23
|
+
|
24
|
+
# @api public
|
25
|
+
# @param route_key [String] The key for the route.
|
26
|
+
# @return [Lenna::Route::Node]
|
27
|
+
# @note This method is used to get a route from the cache.
|
28
|
+
def get(route_key) = @cache[route_key]
|
29
|
+
|
30
|
+
# @api public
|
31
|
+
# @param route_key [String] The key for the route.
|
32
|
+
# @return [Boolean]
|
33
|
+
# @note This method is used to check if a route exists
|
34
|
+
# in the cache.
|
35
|
+
def exist?(route_key) = @cache.key?(route_key)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lenna
|
4
|
+
class Router
|
5
|
+
# This class is used to manage the namespaces.
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class NamespaceStack
|
9
|
+
# @return [Array] The stack of namespaces
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
attr_reader :stack
|
13
|
+
|
14
|
+
# @return [void]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
def initialize = @stack = ['']
|
18
|
+
|
19
|
+
# This method is used to push a prefix to the stack.
|
20
|
+
#
|
21
|
+
# @param prefix [String] The prefix to be pushed
|
22
|
+
# @return [void]
|
23
|
+
#
|
24
|
+
# @example:
|
25
|
+
#
|
26
|
+
# stack = NamespaceStack.new
|
27
|
+
# stack.push('/users')
|
28
|
+
# stack.current_prefix # => '/users'
|
29
|
+
#
|
30
|
+
# @see #resolve_prefix
|
31
|
+
#
|
32
|
+
# @api private
|
33
|
+
def push(prefix)
|
34
|
+
@stack.push(resolve_prefix(prefix))
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [String] The popped prefix
|
38
|
+
#
|
39
|
+
# @api private
|
40
|
+
def pop
|
41
|
+
@stack.pop unless @stack.size == 1
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [String] The current prefix
|
45
|
+
#
|
46
|
+
# @api private
|
47
|
+
#
|
48
|
+
# @since 0.1.0
|
49
|
+
def current_prefix = @stack.last
|
50
|
+
|
51
|
+
# The to_s method is used to return the current prefix.
|
52
|
+
#
|
53
|
+
# @return [String] The current prefix
|
54
|
+
#
|
55
|
+
# @api private
|
56
|
+
def to_s = current_prefix
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# The resolve_prefix method is used to resolve the prefix.
|
61
|
+
#
|
62
|
+
# @param prefix [String] The prefix to be resolved
|
63
|
+
# @return [String] The resolved prefix
|
64
|
+
#
|
65
|
+
# @see #current_prefix
|
66
|
+
#
|
67
|
+
# @since 0.1.0
|
68
|
+
def resolve_prefix(prefix)
|
69
|
+
current_prefix + prefix
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lenna
|
4
|
+
class Router
|
5
|
+
# The Request class is responsible for managing the request.
|
6
|
+
#
|
7
|
+
# @attr headers [Hash] the request headers
|
8
|
+
# @attr body [Hash] the request body
|
9
|
+
# @attr params [Hash] the request params
|
10
|
+
class Request < ::Rack::Request
|
11
|
+
# This method is used to parse the body params.
|
12
|
+
#
|
13
|
+
# @return [Hash] the request params
|
14
|
+
#
|
15
|
+
# @public
|
16
|
+
def params = super.merge(parse_body_params)
|
17
|
+
|
18
|
+
# This method rewinds the body
|
19
|
+
#
|
20
|
+
# @return [String] the request body content
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
#
|
24
|
+
# @since 0.1.0
|
25
|
+
def body_content
|
26
|
+
body.rewind
|
27
|
+
body.read
|
28
|
+
end
|
29
|
+
|
30
|
+
# This method returns the headers in a normalized way.
|
31
|
+
#
|
32
|
+
# @return [Hash] the request headers
|
33
|
+
#
|
34
|
+
# @api public
|
35
|
+
#
|
36
|
+
# @example:
|
37
|
+
# Turn this:
|
38
|
+
# HTTP_FOO=bar Foo=bar
|
39
|
+
def headers
|
40
|
+
@headers ||= env.select { |k, _| k.start_with?('HTTP_') }
|
41
|
+
.transform_keys { |k| format_header_name(k) }
|
42
|
+
end
|
43
|
+
|
44
|
+
# This method returns the request body in a normalized way.
|
45
|
+
#
|
46
|
+
# @return [Hash] the request body
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def json_body = @json_body ||= parse_body_params
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def json_request? = media_type == 'application/json'
|
54
|
+
|
55
|
+
def parse_json_body
|
56
|
+
@parsed_json_body ||= ::JSON.parse(body_content) if json_request?
|
57
|
+
rescue ::JSON::ParserError
|
58
|
+
{}
|
59
|
+
end
|
60
|
+
|
61
|
+
def parse_body_params
|
62
|
+
case media_type
|
63
|
+
when 'application/json'
|
64
|
+
parse_json_body
|
65
|
+
when 'application/x-www-form-urlencoded', 'multipart/form-data'
|
66
|
+
post_params
|
67
|
+
else
|
68
|
+
{}
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def format_header_name(name)
|
73
|
+
name.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,357 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lenna
|
4
|
+
class Router
|
5
|
+
# The Response class is responsible for managing the response.
|
6
|
+
#
|
7
|
+
# @attr _headers [Hash] the response headers
|
8
|
+
# @attr _body [Array(String)] the response body
|
9
|
+
# @attr _status [Integer] the response status
|
10
|
+
# @attr params [Hash] the response params
|
11
|
+
class Response
|
12
|
+
public attr_reader :params
|
13
|
+
private attr_accessor :_headers, :_body, :_status
|
14
|
+
|
15
|
+
# Initialize the Response
|
16
|
+
def initialize(headers = {}, status = 200, body = [])
|
17
|
+
self._headers = headers
|
18
|
+
self._status = status
|
19
|
+
self._body = body
|
20
|
+
@params = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api public
|
24
|
+
# @return [Integer] the response status
|
25
|
+
def status = fetch_status
|
26
|
+
|
27
|
+
# @api public
|
28
|
+
# @param status [Integer] the response status
|
29
|
+
# @return [void]
|
30
|
+
def put_status(value) = status!(value)
|
31
|
+
|
32
|
+
# @api public
|
33
|
+
# @return [Array(String)] the body value
|
34
|
+
def body = fetch_body
|
35
|
+
|
36
|
+
# @api public
|
37
|
+
# @param value [Array(String)] the body value
|
38
|
+
# @return [void]
|
39
|
+
def put_body(value) = body!(value)
|
40
|
+
|
41
|
+
# @api public
|
42
|
+
# @param header [String] the header name
|
43
|
+
# @return [String] the header value
|
44
|
+
# @note This method will get the header value.
|
45
|
+
def header(key) = fetch_header(key)
|
46
|
+
|
47
|
+
# @api public
|
48
|
+
# @return [Hash] the response headers
|
49
|
+
def headers = fetch_headers
|
50
|
+
|
51
|
+
# @api public
|
52
|
+
# @param header [String] the header name
|
53
|
+
# @param value [String] the header value
|
54
|
+
# @return [void]
|
55
|
+
# @note This method will set the header value.
|
56
|
+
# If the header already exists, then the value will
|
57
|
+
# be appended to the header.
|
58
|
+
#
|
59
|
+
# @example
|
60
|
+
# put_header('X-Request-Id', '123')
|
61
|
+
# # => '123'
|
62
|
+
#
|
63
|
+
# put_header('X-Request-Id', '456')
|
64
|
+
# # => ['123', '456']
|
65
|
+
#
|
66
|
+
# put_header('X-Request-Id', ['456', '789'])
|
67
|
+
# # => ['123', '456', '789']
|
68
|
+
def put_header(key, value) = header!(key, value)
|
69
|
+
|
70
|
+
# Add multiple headers.
|
71
|
+
# @param headers [Hash] the headers
|
72
|
+
# @return [void]
|
73
|
+
# @note This method will add the headers.
|
74
|
+
# The headers are a hash where the key is the
|
75
|
+
# header name and the value is the header value.
|
76
|
+
#
|
77
|
+
# @example
|
78
|
+
# headers = {
|
79
|
+
# 'Content-Type' => 'application/json',
|
80
|
+
# 'X-Request-Id' => '123'
|
81
|
+
# }
|
82
|
+
#
|
83
|
+
def put_headers(headers)
|
84
|
+
headers => ::Hash
|
85
|
+
|
86
|
+
headers.each { |key, value| put_header(key, value) }
|
87
|
+
end
|
88
|
+
|
89
|
+
# @api public
|
90
|
+
# @param header [String] the header name
|
91
|
+
# @return [void]
|
92
|
+
# @note This method will delete the header.
|
93
|
+
def remove_header(key) = delete_header(key)
|
94
|
+
|
95
|
+
# @api public
|
96
|
+
# @param value [String] the key of the cookie
|
97
|
+
# @return [String] the cookie
|
98
|
+
# @note This method will get the cookie.
|
99
|
+
def cookie(value)
|
100
|
+
value => ::String
|
101
|
+
|
102
|
+
fetch_header('Set-Cookie')
|
103
|
+
.then { |cookie| cookie.split('; ') }
|
104
|
+
.then { |cookie| cookie.find { |c| c.start_with?("#{value}=") } }
|
105
|
+
.then { |cookie| cookie.split('=').last }
|
106
|
+
end
|
107
|
+
|
108
|
+
# @api public
|
109
|
+
# @param key [String] the key of the cookie
|
110
|
+
# @param value [String] the value of the cookie
|
111
|
+
# @return [void]
|
112
|
+
# @note This method will set the cookie.
|
113
|
+
def put_cookie(key, value)
|
114
|
+
key => ::String
|
115
|
+
value => ::String
|
116
|
+
|
117
|
+
cookie = "#{key}=#{value}"
|
118
|
+
|
119
|
+
header!('Set-Cookie', cookie)
|
120
|
+
end
|
121
|
+
|
122
|
+
# @api public
|
123
|
+
# @return [Hash] the cookies
|
124
|
+
def cookies
|
125
|
+
fetch_header('Set-Cookie')
|
126
|
+
.then { |cookie| cookie.split('; ') }
|
127
|
+
.each_with_object({}) do |cookie, acc|
|
128
|
+
key, value = cookie.split('=')
|
129
|
+
|
130
|
+
acc[key] = value
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# @api public
|
135
|
+
# @param location [String] the redirect location
|
136
|
+
# @param status [Integer] the redirect status
|
137
|
+
# @return [void]
|
138
|
+
# @note This method will set the redirect location and
|
139
|
+
# status and finish the response.
|
140
|
+
def redirect(location, status: 302)
|
141
|
+
location => ::String
|
142
|
+
|
143
|
+
header!('Location', location)
|
144
|
+
status!(status)
|
145
|
+
|
146
|
+
finish!
|
147
|
+
rescue ::NoMatchingPatternError
|
148
|
+
raise ::ArgumentError, 'location must be a string'
|
149
|
+
end
|
150
|
+
|
151
|
+
# @api public
|
152
|
+
# @return [void]
|
153
|
+
# @note This method will finish the response.
|
154
|
+
def finish = finish!
|
155
|
+
|
156
|
+
# @api public
|
157
|
+
# @return [String] the response content type
|
158
|
+
# @note This method will set
|
159
|
+
# the response content type.
|
160
|
+
def content_type = header('Content-Type')
|
161
|
+
|
162
|
+
# @api public
|
163
|
+
# @param type [String] the response content type
|
164
|
+
# @param charset [Hash] the response charset
|
165
|
+
# @return [void]
|
166
|
+
def put_content_type(type, charset: nil)
|
167
|
+
type => ::String
|
168
|
+
|
169
|
+
case charset
|
170
|
+
in ::String then header!('Content-Type', "#{type}; charset=#{charset}")
|
171
|
+
else header!('Content-Type', type)
|
172
|
+
end
|
173
|
+
rescue ::NoMatchingPatternError
|
174
|
+
raise ::ArgumentError, 'type must be a string'
|
175
|
+
end
|
176
|
+
|
177
|
+
# @api public
|
178
|
+
# @param data [Hash, Array] the response data
|
179
|
+
# @return [void]
|
180
|
+
# @note This method will set the response data and
|
181
|
+
# finish the response.
|
182
|
+
def json(data:, status: 200)
|
183
|
+
data => ::Array | ::Hash
|
184
|
+
|
185
|
+
status!(status)
|
186
|
+
header!('Content-Type', 'application/json')
|
187
|
+
body!(data.to_json)
|
188
|
+
|
189
|
+
finish!
|
190
|
+
end
|
191
|
+
|
192
|
+
# Set the response content type to text/html.
|
193
|
+
# @param str [String] the response body
|
194
|
+
# @return [void]
|
195
|
+
def html(str = nil, status: 200)
|
196
|
+
status!(status)
|
197
|
+
header!('Content-Type', 'text/html')
|
198
|
+
body!(str)
|
199
|
+
|
200
|
+
finish!
|
201
|
+
end
|
202
|
+
|
203
|
+
# @param template_nam [String] the template name
|
204
|
+
# @param path [String] the template path, default is 'views'
|
205
|
+
# @param locals [Hash] the template locals
|
206
|
+
# @return [void | Exception]
|
207
|
+
# @note This method will render the template.
|
208
|
+
# The template engine is determined by the
|
209
|
+
# file extension.
|
210
|
+
#
|
211
|
+
# @example
|
212
|
+
# render('index')
|
213
|
+
# # => Render the template `views/index.html.erb`
|
214
|
+
#
|
215
|
+
# render('users/index')
|
216
|
+
# # => Render the template `views/users/index.html.erb`
|
217
|
+
#
|
218
|
+
# render('index', path: 'app/views/users')
|
219
|
+
# # => Render the template `app/views/users/index.html.erb`
|
220
|
+
#
|
221
|
+
# render('index', locals: { name: 'John' })
|
222
|
+
# # => Render the template `views/index.html.erb` with the local
|
223
|
+
# # variable `name` set to 'John'
|
224
|
+
def render(template_name, path: 'views', locals: {}, status: 200)
|
225
|
+
template_path = ::File.join(path, "#{template_name}.html.erb")
|
226
|
+
|
227
|
+
# Check if the template exists
|
228
|
+
unless File.exist?(template_path)
|
229
|
+
msg = "Template not found: #{template_path} 🤷♂️."
|
230
|
+
|
231
|
+
# Oops! The template doesn't exist or the path is wrong.
|
232
|
+
#
|
233
|
+
# The template exists? 🤔
|
234
|
+
# If you want to render a template from a custom path, then you
|
235
|
+
# can pass the full path though the path: keyword argument instead
|
236
|
+
# of just the name. For example:
|
237
|
+
# render('index', path: 'app/views/users')
|
238
|
+
raise msg
|
239
|
+
end
|
240
|
+
|
241
|
+
::File
|
242
|
+
.read(template_path)
|
243
|
+
.then { |template| ::ERB.new(template).result_with_hash(locals) }
|
244
|
+
.then { |erb_template| html(erb_template, status:) }
|
245
|
+
end
|
246
|
+
|
247
|
+
# Helper methods for the response.
|
248
|
+
# @api public
|
249
|
+
# @return [void]
|
250
|
+
# @note This method will finish the response with a 404 status.
|
251
|
+
def not_found
|
252
|
+
body!(['Not Found'])
|
253
|
+
status!(404)
|
254
|
+
finish!
|
255
|
+
end
|
256
|
+
|
257
|
+
private
|
258
|
+
|
259
|
+
# @api private
|
260
|
+
# @return [Integer] the response status
|
261
|
+
# @note This method will get the response status.
|
262
|
+
def fetch_status = _status
|
263
|
+
|
264
|
+
# @api private
|
265
|
+
# @return [Integer] the response status
|
266
|
+
# @note This method will get the response status.
|
267
|
+
def status!(value)
|
268
|
+
value => ::Integer
|
269
|
+
|
270
|
+
self._status = value
|
271
|
+
rescue ::NoMatchingPatternError
|
272
|
+
raise ::ArgumentError, 'status must be an integer'
|
273
|
+
end
|
274
|
+
|
275
|
+
# @api private
|
276
|
+
# @return [Array(String)] the body value
|
277
|
+
def fetch_body = _body
|
278
|
+
|
279
|
+
# @api private
|
280
|
+
# @param body [Array(String)] the body to be used
|
281
|
+
# @return [void]
|
282
|
+
# @note This method will set the body.
|
283
|
+
def body!(value)
|
284
|
+
body => ::String | ::Array
|
285
|
+
|
286
|
+
case value
|
287
|
+
in ::String then self._body = [value]
|
288
|
+
in ::Array then self._body = value
|
289
|
+
end
|
290
|
+
rescue ::NoMatchingPatternError
|
291
|
+
raise ::ArgumentError, 'body must be a string or an array'
|
292
|
+
end
|
293
|
+
|
294
|
+
# @api private
|
295
|
+
# @param header [String] the header name
|
296
|
+
# @return [String] the header value
|
297
|
+
# @note This method will get the header value.
|
298
|
+
def fetch_header(header) = _headers[header]
|
299
|
+
|
300
|
+
# @api private
|
301
|
+
# @return [Hash] the response headers
|
302
|
+
def fetch_headers = _headers
|
303
|
+
|
304
|
+
# @api private
|
305
|
+
# @param key [String] the header name
|
306
|
+
# @param value [String] the value to be used
|
307
|
+
# @return [void]
|
308
|
+
# @note This method will set the header value.
|
309
|
+
# If the header already exists, then the value will
|
310
|
+
# be appended to the header.
|
311
|
+
def header!(key, value)
|
312
|
+
key => ::String
|
313
|
+
value => ::String | ::Array
|
314
|
+
|
315
|
+
header_value = fetch_header(key)
|
316
|
+
|
317
|
+
case value
|
318
|
+
in ::String then _headers[key] = [*header_value, value].uniq.join(', ')
|
319
|
+
in ::Array then _headers[key] = [*header_value, *value].uniq.join(', ')
|
320
|
+
end
|
321
|
+
rescue ::NoMatchingPatternError
|
322
|
+
raise ::ArgumentError, 'header must be a string or an array'
|
323
|
+
end
|
324
|
+
|
325
|
+
# @api private
|
326
|
+
# @param key [String] the header name
|
327
|
+
# @return [void]
|
328
|
+
# @note This method will delete the header.
|
329
|
+
def delete_header(key) = _headers.delete(key)
|
330
|
+
|
331
|
+
# @api private
|
332
|
+
# @param value [String] the redirect location
|
333
|
+
# @return [void]
|
334
|
+
def location!(value)
|
335
|
+
value => ::String
|
336
|
+
|
337
|
+
header!('Location', value)
|
338
|
+
end
|
339
|
+
|
340
|
+
# @api private
|
341
|
+
# @param value [String] the content value
|
342
|
+
# @return [String] the size of the content
|
343
|
+
# @note This method will get the size of the content.
|
344
|
+
def content_length!(value) = header!('Content-Length', value)
|
345
|
+
|
346
|
+
# @api private
|
347
|
+
# @return [void]
|
348
|
+
# @note This method will finish the response.
|
349
|
+
def finish!
|
350
|
+
put_content_type('text/html') unless header('Content-Type')
|
351
|
+
content_length!(body.join.size.to_s) unless header('Content-Length')
|
352
|
+
|
353
|
+
[_status, _headers, _body]
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lenna
|
4
|
+
class Router
|
5
|
+
# @note This class is responsible for matching the request path
|
6
|
+
# to an endpoint and executing the endpoint action.
|
7
|
+
#
|
8
|
+
# @api private
|
9
|
+
#
|
10
|
+
# @since 0.1.0
|
11
|
+
#
|
12
|
+
# This will match the request path to an endpoint and execute
|
13
|
+
# the endpoint action.
|
14
|
+
class RouteMatcher
|
15
|
+
# @param root_node [Lenna::Node] The root node
|
16
|
+
def initialize(root_node) = @root_node = root_node
|
17
|
+
|
18
|
+
# This method will match the request path to an endpoint and execute
|
19
|
+
# the endpoint action.
|
20
|
+
#
|
21
|
+
# @param req [Lenna::Request] The request object
|
22
|
+
# @param res [Lenna::Response] The response object
|
23
|
+
# @return [Lenna::Response] The response object
|
24
|
+
#
|
25
|
+
# @see #split_path
|
26
|
+
# @see #find_endpoint
|
27
|
+
# @see Lenna::Response#not_found
|
28
|
+
def match_and_execute_route(req, res)
|
29
|
+
params = {}
|
30
|
+
path_parts = split_path(req.path_info)
|
31
|
+
endpoint = find_endpoint(@root_node, path_parts, params)
|
32
|
+
|
33
|
+
if endpoint && (action = endpoint[req.request_method])
|
34
|
+
req.params.merge!(params)
|
35
|
+
action.call(req, res)
|
36
|
+
else
|
37
|
+
res.not_found
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @todo: Refactor this method to a module.
|
44
|
+
def split_path(path) = path.split('/').reject(&:empty?)
|
45
|
+
|
46
|
+
# @param node [Lenna::Node] The node to search
|
47
|
+
# @param parts [Array] The path parts
|
48
|
+
# @param params [Hash] The params hash
|
49
|
+
# @return [Lenna::Node] The node that matches the path
|
50
|
+
#
|
51
|
+
# @note This method is recursive.
|
52
|
+
#
|
53
|
+
# @since 0.1.0
|
54
|
+
def find_endpoint(node, parts, params)
|
55
|
+
return node.endpoint if parts.empty?
|
56
|
+
|
57
|
+
part = parts.shift
|
58
|
+
child_node = node.children[part]
|
59
|
+
|
60
|
+
if child_node.nil? && (placeholder_node = node.children[:placeholder])
|
61
|
+
params[placeholder_node.placeholder_name] = part
|
62
|
+
child_node = placeholder_node
|
63
|
+
end
|
64
|
+
|
65
|
+
find_endpoint(child_node, parts, params) if child_node
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|