lennarb 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|