aris 1.3.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/LICENSE.txt +21 -0
- data/README.md +342 -0
- data/lib/aris/adapters/base.rb +25 -0
- data/lib/aris/adapters/joys_integration.rb +94 -0
- data/lib/aris/adapters/mock/adapter.rb +141 -0
- data/lib/aris/adapters/mock/request.rb +81 -0
- data/lib/aris/adapters/mock/response.rb +17 -0
- data/lib/aris/adapters/rack/adapter.rb +117 -0
- data/lib/aris/adapters/rack/request.rb +66 -0
- data/lib/aris/adapters/rack/response.rb +16 -0
- data/lib/aris/core.rb +931 -0
- data/lib/aris/discovery.rb +312 -0
- data/lib/aris/locale_injector.rb +39 -0
- data/lib/aris/pipeline_runner.rb +100 -0
- data/lib/aris/plugins/api_key_auth.rb +61 -0
- data/lib/aris/plugins/basic_auth.rb +68 -0
- data/lib/aris/plugins/bearer_auth.rb +64 -0
- data/lib/aris/plugins/cache.rb +120 -0
- data/lib/aris/plugins/compression.rb +96 -0
- data/lib/aris/plugins/cookies.rb +46 -0
- data/lib/aris/plugins/cors.rb +81 -0
- data/lib/aris/plugins/csrf.rb +48 -0
- data/lib/aris/plugins/etag.rb +90 -0
- data/lib/aris/plugins/flash.rb +124 -0
- data/lib/aris/plugins/form_parser.rb +46 -0
- data/lib/aris/plugins/health_check.rb +62 -0
- data/lib/aris/plugins/json.rb +32 -0
- data/lib/aris/plugins/multipart.rb +160 -0
- data/lib/aris/plugins/rate_limiter.rb +60 -0
- data/lib/aris/plugins/request_id.rb +38 -0
- data/lib/aris/plugins/request_logger.rb +43 -0
- data/lib/aris/plugins/security_headers.rb +99 -0
- data/lib/aris/plugins/session.rb +175 -0
- data/lib/aris/plugins.rb +23 -0
- data/lib/aris/response_helpers.rb +156 -0
- data/lib/aris/route_helpers.rb +141 -0
- data/lib/aris/utils/redirects.rb +44 -0
- data/lib/aris/utils/sitemap.rb +84 -0
- data/lib/aris/version.rb +3 -0
- data/lib/aris.rb +35 -0
- metadata +151 -0
data/lib/aris/core.rb
ADDED
|
@@ -0,0 +1,931 @@
|
|
|
1
|
+
require 'uri'
|
|
2
|
+
require 'set'
|
|
3
|
+
|
|
4
|
+
module Aris
|
|
5
|
+
@@default_not_found_handler = nil
|
|
6
|
+
@@default_error_handler = nil
|
|
7
|
+
module Config
|
|
8
|
+
class << self
|
|
9
|
+
attr_accessor :trailing_slash, :trailing_slash_status
|
|
10
|
+
attr_accessor :secret_key_base, :cookie_options
|
|
11
|
+
def reset!
|
|
12
|
+
@trailing_slash = :strict
|
|
13
|
+
@trailing_slash_status = 301
|
|
14
|
+
@secret_key_base = nil
|
|
15
|
+
@cookie_options = {
|
|
16
|
+
httponly: true,
|
|
17
|
+
secure: false, # Default to false for development
|
|
18
|
+
same_site: :lax,
|
|
19
|
+
path: '/'
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
reset!
|
|
25
|
+
end
|
|
26
|
+
def self.not_found(request, response = nil)
|
|
27
|
+
handler = @@default_not_found_handler
|
|
28
|
+
handler ? handler.call(request, {}) : [404, {'content-type' => 'text/plain'}, ['Not Found']]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.error(request, exception, response = nil)
|
|
32
|
+
handler = @@default_error_handler
|
|
33
|
+
return handler.call(request, exception) if handler
|
|
34
|
+
[500, {'content-type' => 'text/plain'}, ['Internal Server Error']]
|
|
35
|
+
rescue => e
|
|
36
|
+
# Handler itself failed - return fallback
|
|
37
|
+
[500, {'content-type' => 'text/plain'}, ['Internal Server Error']]
|
|
38
|
+
end
|
|
39
|
+
def self.configure
|
|
40
|
+
yield Config
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
module Router
|
|
45
|
+
extend self
|
|
46
|
+
HTTP_METHODS = Set[:get, :post, :put, :patch, :delete, :options].freeze
|
|
47
|
+
|
|
48
|
+
@@default_domain = nil
|
|
49
|
+
@@default_404_handler = nil
|
|
50
|
+
@@default_500_handler = nil
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def default_domain=(domain)
|
|
54
|
+
@@default_domain = domain
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def default_domain
|
|
58
|
+
@@default_domain
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def set_defaults(config)
|
|
62
|
+
@@default_domain = config[:default_host] if config.key?(:default_host)
|
|
63
|
+
Aris.class_variable_set(:@@default_not_found_handler, config[:not_found]) if config.key?(:not_found)
|
|
64
|
+
Aris.class_variable_set(:@@default_error_handler, config[:error]) if config.key?(:error)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def define(config)
|
|
68
|
+
reset!
|
|
69
|
+
compile!(config)
|
|
70
|
+
self
|
|
71
|
+
end
|
|
72
|
+
def domain_config(domain)
|
|
73
|
+
@domain_configs ||= {}
|
|
74
|
+
@domain_configs[domain.to_s]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def store_domain_config(domain, config)
|
|
78
|
+
@domain_configs ||= {}
|
|
79
|
+
@domain_configs[domain.to_s] = config
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_named_path(route_name, domain:, **params)
|
|
83
|
+
route_name = route_name.to_sym
|
|
84
|
+
locale = params.delete(:locale)
|
|
85
|
+
domain_config = self.domain_config(domain)
|
|
86
|
+
|
|
87
|
+
# If no locale specified but domain has default locale, use it
|
|
88
|
+
if !locale && domain_config && domain_config[:default_locale]
|
|
89
|
+
locale = domain_config[:default_locale]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# For localized routes, always use the base route name and let build_path handle it
|
|
93
|
+
found_metadata = @metadata.values.find do |meta|
|
|
94
|
+
meta[:name] == route_name && meta[:domain] == domain
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
unless found_metadata
|
|
98
|
+
found_metadata = @metadata.values.find do |meta|
|
|
99
|
+
meta[:name] == route_name && meta[:domain] == '*'
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
unless found_metadata
|
|
104
|
+
raise RouteNotFoundError.new(route_name, domain)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# If this is a localized route and we have a locale, use locale-aware path building
|
|
108
|
+
if found_metadata[:localized] && locale && domain_config
|
|
109
|
+
build_path_with_locale(found_metadata, params, locale, domain_config)
|
|
110
|
+
else
|
|
111
|
+
build_path_without_locale(found_metadata, params)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
def match(domain:, method:, path:)
|
|
115
|
+
|
|
116
|
+
return nil if path.start_with?('//') && path != '//'
|
|
117
|
+
normalized_domain = domain.to_s.downcase
|
|
118
|
+
normalized_path = normalize_path(path)
|
|
119
|
+
normalized_method = method.to_sym
|
|
120
|
+
segments = get_path_segments(normalized_path)
|
|
121
|
+
|
|
122
|
+
match_result = nil
|
|
123
|
+
|
|
124
|
+
# 1. Try exact domain match first
|
|
125
|
+
domain_trie = @tries[normalized_domain]
|
|
126
|
+
if domain_trie
|
|
127
|
+
match_result = traverse_trie(domain_trie, segments)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# 2. Try subdomain wildcard match
|
|
131
|
+
unless match_result
|
|
132
|
+
wildcard_domain = find_wildcard_domain_match(normalized_domain)
|
|
133
|
+
|
|
134
|
+
if wildcard_domain
|
|
135
|
+
domain_trie = @tries[wildcard_domain]
|
|
136
|
+
if domain_trie
|
|
137
|
+
match_result = traverse_trie(domain_trie, segments)
|
|
138
|
+
|
|
139
|
+
if match_result
|
|
140
|
+
subdomain = extract_subdomain_from_wildcard(normalized_domain, wildcard_domain)
|
|
141
|
+
# Store subdomain in match result
|
|
142
|
+
match_result = [match_result[0], match_result[1], subdomain]
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# 3. Try global wildcard match (existing functionality)
|
|
149
|
+
unless match_result
|
|
150
|
+
wildcard_trie = @tries['*']
|
|
151
|
+
if wildcard_trie
|
|
152
|
+
match_result = traverse_trie(wildcard_trie, segments)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
return nil unless match_result
|
|
157
|
+
|
|
158
|
+
node, params, subdomain = match_result
|
|
159
|
+
metadata_key = node[:handlers][normalized_method]
|
|
160
|
+
return nil unless metadata_key
|
|
161
|
+
|
|
162
|
+
metadata = @metadata[metadata_key]
|
|
163
|
+
|
|
164
|
+
unless enforce_constraints(params, metadata[:constraints]); return nil; end
|
|
165
|
+
|
|
166
|
+
route = {
|
|
167
|
+
name: metadata[:name],
|
|
168
|
+
handler: metadata[:handler],
|
|
169
|
+
use: metadata[:use].dup,
|
|
170
|
+
params: params,
|
|
171
|
+
locale: metadata[:locale],
|
|
172
|
+
domain: normalized_domain
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Add subdomain to route if we matched a wildcard domain
|
|
176
|
+
route[:subdomain] = subdomain if subdomain
|
|
177
|
+
|
|
178
|
+
route
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
private
|
|
182
|
+
|
|
183
|
+
def find_wildcard_domain_match(domain)
|
|
184
|
+
# Look through all registered domains for wildcard patterns
|
|
185
|
+
@tries.keys.each do |registered_domain|
|
|
186
|
+
if registered_domain.start_with?('*.')
|
|
187
|
+
base_domain = registered_domain[2..] # Remove '*.'
|
|
188
|
+
|
|
189
|
+
# Check if the requested domain matches the wildcard pattern
|
|
190
|
+
# Examples:
|
|
191
|
+
# - domain: "acme.example.com" matches "*.example.com"
|
|
192
|
+
# - domain: "example.com" matches "*.example.com" (no subdomain case)
|
|
193
|
+
if domain == base_domain || domain.end_with?(".#{base_domain}")
|
|
194
|
+
return registered_domain
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def extract_subdomain_from_wildcard(full_domain, wildcard_domain)
|
|
202
|
+
base_domain = wildcard_domain[2..] # Remove '*.'
|
|
203
|
+
|
|
204
|
+
if full_domain == base_domain
|
|
205
|
+
# No subdomain (user visited example.com directly)
|
|
206
|
+
nil
|
|
207
|
+
else
|
|
208
|
+
# Extract subdomain by removing the base domain
|
|
209
|
+
# "acme.example.com" -> "acme"
|
|
210
|
+
# "app.staging.example.com" -> "app.staging"
|
|
211
|
+
full_domain.gsub(".#{base_domain}", '')
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
def self.find_domain_config(domain)
|
|
215
|
+
# Exact match first
|
|
216
|
+
exact_match = @domains[domain]
|
|
217
|
+
return exact_match if exact_match
|
|
218
|
+
|
|
219
|
+
# Then wildcard matches
|
|
220
|
+
@domains.each do |config_domain, config|
|
|
221
|
+
if config_domain.start_with?('*.')
|
|
222
|
+
base_domain = config_domain[2..] # Remove '*.'
|
|
223
|
+
if domain.end_with?(".#{base_domain}") || domain == base_domain
|
|
224
|
+
return config.merge(wildcard: true, base_domain: base_domain)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
nil
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def build_path(metadata, params)
|
|
233
|
+
build_path_without_locale(metadata, params)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def build_path_without_locale(metadata, params)
|
|
237
|
+
segments = metadata[:segments].dup
|
|
238
|
+
required_params = metadata[:params]
|
|
239
|
+
provided_params = params.dup
|
|
240
|
+
missing_params = required_params - provided_params.keys
|
|
241
|
+
unless missing_params.empty?
|
|
242
|
+
raise ArgumentError, "Missing required param(s) #{missing_params.map { |p| "'#{p}'" }.join(', ')} for route :#{metadata[:name]}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
path_parts = segments.map do |segment|
|
|
246
|
+
if segment.start_with?(':')
|
|
247
|
+
param_name = segment[1..].to_sym
|
|
248
|
+
value = provided_params.delete(param_name)
|
|
249
|
+
URI.encode_www_form_component(value.to_s)
|
|
250
|
+
elsif segment.start_with?('*')
|
|
251
|
+
param_name = segment[1..].to_s
|
|
252
|
+
param_name = 'path' if param_name.empty?
|
|
253
|
+
value = provided_params.delete(param_name.to_sym)
|
|
254
|
+
value.to_s
|
|
255
|
+
else
|
|
256
|
+
segment
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
path = path_parts.empty? ? '/' : '/' + path_parts.join('/')
|
|
261
|
+
unless provided_params.empty?
|
|
262
|
+
query_string = URI.encode_www_form(provided_params)
|
|
263
|
+
path += "?#{query_string}"
|
|
264
|
+
end
|
|
265
|
+
path
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def build_path_with_locale(metadata, params, locale, domain_config)
|
|
269
|
+
# Validate locale is available for this domain
|
|
270
|
+
unless domain_config[:locales].include?(locale)
|
|
271
|
+
raise LocaleError, "Locale :#{locale} not available for domain '#{metadata[:domain]}'. Available locales: #{domain_config[:locales].inspect}"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# For localized routes, we need to find the actual locale-specific route metadata
|
|
275
|
+
localized_name = "#{metadata[:name]}_#{locale}".to_sym
|
|
276
|
+
# Find the locale-specific route
|
|
277
|
+
localized_metadata = @metadata.values.find do |meta|
|
|
278
|
+
meta[:name] == localized_name && meta[:domain] == metadata[:domain]
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
if localized_metadata
|
|
282
|
+
# Use the locale-specific route's segments
|
|
283
|
+
build_path_without_locale(localized_metadata, params)
|
|
284
|
+
else
|
|
285
|
+
# Fallback: build path manually with locale prefix
|
|
286
|
+
base_path = build_path_without_locale(metadata, params)
|
|
287
|
+
if base_path == '/'
|
|
288
|
+
"/#{locale}"
|
|
289
|
+
else
|
|
290
|
+
"/#{locale}#{base_path}"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def self.not_found(request, response = nil)
|
|
296
|
+
handler = @@default_not_found_handler
|
|
297
|
+
handler ? handler.call(request, {}) : [404, {'content-type' => 'text/plain'}, ['Not Found']]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def self.error(request, exception)
|
|
301
|
+
handler = @@default_error_handler
|
|
302
|
+
handler ? handler.call(request, exception) : [500, {'content-type' => 'text/plain'}, ['Internal Server Error']]
|
|
303
|
+
end
|
|
304
|
+
def self.error_response(request, exception)
|
|
305
|
+
handler = @@default_error_handler
|
|
306
|
+
return handler.call(request, exception) if handler
|
|
307
|
+
[500, {'content-type' => 'text/plain'}, ['Internal Server Error']]
|
|
308
|
+
end
|
|
309
|
+
private
|
|
310
|
+
|
|
311
|
+
def merge_use(parent_use, child_use)
|
|
312
|
+
parent = parent_use.is_a?(Array) ? parent_use : []
|
|
313
|
+
child = child_use.is_a?(Array) ? child_use : []
|
|
314
|
+
(parent + child).uniq
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def reset!
|
|
318
|
+
@tries = {}
|
|
319
|
+
@metadata = {}
|
|
320
|
+
@named_routes = {}
|
|
321
|
+
@route_name_registry = Set.new
|
|
322
|
+
@segment_cache = {}
|
|
323
|
+
@cache_max_size = 1000
|
|
324
|
+
@@default_domain = nil
|
|
325
|
+
end
|
|
326
|
+
def compile!(config)
|
|
327
|
+
@domain_configs = {}
|
|
328
|
+
|
|
329
|
+
config.each do |domain_key, domain_config|
|
|
330
|
+
domain = domain_key.to_s.downcase
|
|
331
|
+
|
|
332
|
+
# Store domain configuration (locales, etc.)
|
|
333
|
+
if domain_config.is_a?(Hash) && (domain_config[:locales] || domain_config[:default_locale])
|
|
334
|
+
locales = Array(domain_config[:locales])
|
|
335
|
+
default_locale = domain_config[:default_locale]
|
|
336
|
+
|
|
337
|
+
# Validate that default_locale is in locales
|
|
338
|
+
if default_locale && !locales.include?(default_locale)
|
|
339
|
+
raise LocaleError, "Default locale '#{default_locale}' not found in locales #{locales.inspect} for domain '#{domain}'"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Validate that all localized routes use valid locales
|
|
343
|
+
validate_localized_routes(domain_config, locales, domain)
|
|
344
|
+
|
|
345
|
+
store_domain_config(domain, {
|
|
346
|
+
locales: locales,
|
|
347
|
+
default_locale: default_locale || locales.first,
|
|
348
|
+
root_locale_redirect: domain_config.fetch(:root_locale_redirect, true)
|
|
349
|
+
})
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Handle wildcard domains - store them as-is in tries
|
|
353
|
+
# This preserves the "*.example.com" pattern for matching later
|
|
354
|
+
@tries[domain] = new_trie_node
|
|
355
|
+
|
|
356
|
+
# Compile routes for this domain (wildcard or regular)
|
|
357
|
+
compile_scope(config: domain_config, domain: domain, path_segments: [], inherited_use: [])
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def validate_localized_routes(domain_config, valid_locales, domain)
|
|
362
|
+
check_config_for_locales(domain_config, valid_locales, domain, '')
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def check_config_for_locales(config, valid_locales, domain, path = '')
|
|
366
|
+
return unless config.is_a?(Hash)
|
|
367
|
+
|
|
368
|
+
config.each do |key, value|
|
|
369
|
+
if value.is_a?(Hash) && value[:localized]
|
|
370
|
+
invalid_locales = value[:localized].keys - valid_locales
|
|
371
|
+
if invalid_locales.any?
|
|
372
|
+
raise LocaleError, "Handler at #{path}/#{key} uses invalid locales #{invalid_locales.inspect} but domain only supports #{valid_locales.inspect}"
|
|
373
|
+
end
|
|
374
|
+
elsif value.is_a?(Hash)
|
|
375
|
+
# Recursively check nested routes
|
|
376
|
+
check_config_for_locales(value, valid_locales, domain, "#{path}/#{key}")
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def compile_scope(config:, domain:, path_segments:, inherited_use:)
|
|
382
|
+
return unless config.is_a?(Hash)
|
|
383
|
+
|
|
384
|
+
if config.key?(:use)
|
|
385
|
+
scope_use_value = config[:use]
|
|
386
|
+
if scope_use_value.nil?
|
|
387
|
+
current_use = []
|
|
388
|
+
else
|
|
389
|
+
resolved_scope_use = Array(scope_use_value).flat_map do |item|
|
|
390
|
+
if item.is_a?(Symbol)
|
|
391
|
+
Aris.resolve_plugin(item) # Returns array of classes
|
|
392
|
+
else
|
|
393
|
+
item
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
current_use = merge_use(inherited_use, resolved_scope_use)
|
|
397
|
+
end
|
|
398
|
+
else
|
|
399
|
+
current_use = inherited_use.is_a?(Array) ? inherited_use : []
|
|
400
|
+
end
|
|
401
|
+
config.each do |key, value|
|
|
402
|
+
next if key == :use
|
|
403
|
+
if HTTP_METHODS.include?(key)
|
|
404
|
+
register_route(domain: domain, method: key, path_segments: path_segments, route_config: value, inherited_use: current_use)
|
|
405
|
+
elsif key.is_a?(String) || key.is_a?(Symbol)
|
|
406
|
+
new_segments = parse_path_key(key.to_s)
|
|
407
|
+
compile_scope(config: value, domain: domain, path_segments: path_segments + new_segments, inherited_use: current_use)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def register_route(domain:, method:, path_segments:, route_config:, inherited_use:)
|
|
413
|
+
name = route_config[:as]
|
|
414
|
+
handler = route_config[:to]
|
|
415
|
+
constraints = route_config[:constraints] || {}
|
|
416
|
+
route_use_unresolved = route_config[:use]
|
|
417
|
+
localized_paths = route_config[:localized]
|
|
418
|
+
|
|
419
|
+
resolved_inherited = Array(inherited_use).flat_map do |item|
|
|
420
|
+
if item.is_a?(Symbol)
|
|
421
|
+
Aris.resolve_plugin(item)
|
|
422
|
+
else
|
|
423
|
+
item
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
if route_use_unresolved
|
|
428
|
+
resolved_route_use = Array(route_use_unresolved).flat_map do |item|
|
|
429
|
+
if item.is_a?(Symbol)
|
|
430
|
+
Aris.resolve_plugin(item)
|
|
431
|
+
else
|
|
432
|
+
item
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
route_use = resolved_route_use
|
|
436
|
+
else
|
|
437
|
+
route_use = nil
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
if route_use
|
|
441
|
+
final_use = merge_use(resolved_inherited, route_use)
|
|
442
|
+
else
|
|
443
|
+
final_use = resolved_inherited
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Handle localized routes
|
|
447
|
+
if localized_paths && !localized_paths.empty?
|
|
448
|
+
domain_config = self.domain_config(domain)
|
|
449
|
+
if domain_config && domain_config[:locales]
|
|
450
|
+
# Warn about missing locales (routes that don't cover all domain locales)
|
|
451
|
+
missing_locales = domain_config[:locales] - localized_paths.keys
|
|
452
|
+
if missing_locales.any?
|
|
453
|
+
warn "Warning: Route '#{build_pattern(path_segments)}' missing locales: #{missing_locales.inspect}"
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
# Register the base route for URL generation (without locale prefix)
|
|
457
|
+
if name
|
|
458
|
+
@route_name_registry.add(name)
|
|
459
|
+
base_metadata_key = build_metadata_key(domain, method, build_pattern(path_segments))
|
|
460
|
+
@metadata[base_metadata_key] = {
|
|
461
|
+
domain: domain,
|
|
462
|
+
name: name,
|
|
463
|
+
handler: handler,
|
|
464
|
+
use: final_use,
|
|
465
|
+
pattern: build_pattern(path_segments),
|
|
466
|
+
params: extract_param_names(path_segments),
|
|
467
|
+
segments: path_segments.dup,
|
|
468
|
+
constraints: constraints,
|
|
469
|
+
localized: true
|
|
470
|
+
}
|
|
471
|
+
@named_routes[name] = base_metadata_key
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Register locale-specific routes (with locale prefix and localized segment)
|
|
475
|
+
localized_paths.each do |locale, localized_path|
|
|
476
|
+
if domain_config[:locales].include?(locale)
|
|
477
|
+
# Parse the localized path (e.g., "about" or "products/:id")
|
|
478
|
+
localized_segments = parse_path_key(localized_path.to_s)
|
|
479
|
+
|
|
480
|
+
# Combine: locale + localized_segments
|
|
481
|
+
full_path_segments = [locale.to_s] + localized_segments
|
|
482
|
+
|
|
483
|
+
register_localized_route(
|
|
484
|
+
domain: domain,
|
|
485
|
+
method: method,
|
|
486
|
+
path_segments: full_path_segments,
|
|
487
|
+
route_config: route_config,
|
|
488
|
+
inherited_use: final_use,
|
|
489
|
+
name: name,
|
|
490
|
+
handler: handler,
|
|
491
|
+
constraints: constraints,
|
|
492
|
+
locale: locale
|
|
493
|
+
)
|
|
494
|
+
else
|
|
495
|
+
raise LocaleError, "Locale '#{locale}' not found in domain '#{domain}' locales: #{domain_config[:locales].inspect}"
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
return
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Original registration for non-localized routes
|
|
503
|
+
if name
|
|
504
|
+
if @route_name_registry.include?(name)
|
|
505
|
+
existing = @named_routes[name]
|
|
506
|
+
existing_meta = @metadata[existing]
|
|
507
|
+
raise DuplicateRouteNameError.new(
|
|
508
|
+
name: name,
|
|
509
|
+
existing_domain: existing_meta[:domain],
|
|
510
|
+
existing_pattern: existing_meta[:pattern],
|
|
511
|
+
new_domain: domain,
|
|
512
|
+
new_pattern: build_pattern(path_segments)
|
|
513
|
+
)
|
|
514
|
+
end
|
|
515
|
+
@route_name_registry.add(name)
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
pattern = build_pattern(path_segments)
|
|
519
|
+
param_names = extract_param_names(path_segments)
|
|
520
|
+
metadata_key = build_metadata_key(domain, method, pattern)
|
|
521
|
+
@metadata[metadata_key] = {
|
|
522
|
+
domain: domain,
|
|
523
|
+
name: name,
|
|
524
|
+
handler: handler,
|
|
525
|
+
use: final_use,
|
|
526
|
+
pattern: pattern,
|
|
527
|
+
params: param_names,
|
|
528
|
+
segments: path_segments.dup,
|
|
529
|
+
constraints: constraints
|
|
530
|
+
}
|
|
531
|
+
@named_routes[name] = metadata_key if name
|
|
532
|
+
insert_into_trie(domain, path_segments, method, metadata_key)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def register_localized_route(domain:, method:, path_segments:, route_config:, inherited_use:, name:, handler:, constraints:, locale:)
|
|
536
|
+
# Create locale-specific name
|
|
537
|
+
localized_name = name ? "#{name}_#{locale}".to_sym : nil
|
|
538
|
+
|
|
539
|
+
if localized_name && @route_name_registry.include?(localized_name)
|
|
540
|
+
existing = @named_routes[localized_name]
|
|
541
|
+
existing_meta = @metadata[existing]
|
|
542
|
+
raise DuplicateRouteNameError.new(
|
|
543
|
+
name: localized_name,
|
|
544
|
+
existing_domain: existing_meta[:domain],
|
|
545
|
+
existing_pattern: existing_meta[:pattern],
|
|
546
|
+
new_domain: domain,
|
|
547
|
+
new_pattern: build_pattern(path_segments)
|
|
548
|
+
)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# DO NOT add locale prefix here - the path_segments already include it from the route config
|
|
552
|
+
pattern = build_pattern(path_segments)
|
|
553
|
+
param_names = extract_param_names(path_segments)
|
|
554
|
+
metadata_key = build_metadata_key(domain, method, pattern)
|
|
555
|
+
|
|
556
|
+
@metadata[metadata_key] = {
|
|
557
|
+
domain: domain,
|
|
558
|
+
name: localized_name,
|
|
559
|
+
handler: handler,
|
|
560
|
+
use: inherited_use,
|
|
561
|
+
pattern: pattern,
|
|
562
|
+
params: param_names,
|
|
563
|
+
segments: path_segments.dup,
|
|
564
|
+
constraints: constraints,
|
|
565
|
+
locale: locale
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
@route_name_registry.add(localized_name) if localized_name
|
|
569
|
+
@named_routes[localized_name] = metadata_key if localized_name
|
|
570
|
+
insert_into_trie(domain, path_segments, method, metadata_key)
|
|
571
|
+
end
|
|
572
|
+
def enforce_constraints(params, constraints)
|
|
573
|
+
constraints.each do |param_name, regex|
|
|
574
|
+
value = params[param_name]
|
|
575
|
+
if value && !value.match?(regex)
|
|
576
|
+
return false
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
true
|
|
580
|
+
end
|
|
581
|
+
def extract_use(config)
|
|
582
|
+
return nil unless config.is_a?(Hash)
|
|
583
|
+
return nil unless config.key?(:use)
|
|
584
|
+
use_value = config[:use]
|
|
585
|
+
return [] if use_value.nil?
|
|
586
|
+
Array(use_value)
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def new_trie_node
|
|
590
|
+
{ literal_children: {}, param_child: nil, wildcard_child: nil, handlers: {} }
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def insert_into_trie(domain, path_segments, method, metadata_key)
|
|
594
|
+
node = @tries[domain]
|
|
595
|
+
path_segments.each { |segment| node = insert_segment(node, segment) }
|
|
596
|
+
node[:handlers][method] = metadata_key
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def insert_segment(node, segment)
|
|
600
|
+
if segment.start_with?('*')
|
|
601
|
+
wildcard_name = segment[1..].to_s
|
|
602
|
+
wildcard_name = 'path' if wildcard_name.empty?
|
|
603
|
+
unless node[:wildcard_child]
|
|
604
|
+
node[:wildcard_child] = { name: wildcard_name.to_sym, node: new_trie_node }
|
|
605
|
+
end
|
|
606
|
+
node[:wildcard_child][:node]
|
|
607
|
+
elsif segment.start_with?(':')
|
|
608
|
+
param_name = segment[1..].to_sym
|
|
609
|
+
unless node[:param_child]
|
|
610
|
+
node[:param_child] = { name: param_name, node: new_trie_node }
|
|
611
|
+
end
|
|
612
|
+
node[:param_child][:node]
|
|
613
|
+
else
|
|
614
|
+
node[:literal_children][segment] ||= new_trie_node
|
|
615
|
+
end
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def traverse_trie(node, segments, params = {})
|
|
619
|
+
return nil unless node
|
|
620
|
+
return [node, params] if segments.empty?
|
|
621
|
+
current_segment = segments.first
|
|
622
|
+
remaining_segments = segments[1..]
|
|
623
|
+
if node[:literal_children][current_segment]
|
|
624
|
+
result = traverse_trie(node[:literal_children][current_segment], remaining_segments, params)
|
|
625
|
+
return result if result
|
|
626
|
+
end
|
|
627
|
+
if node[:param_child]
|
|
628
|
+
param_name = node[:param_child][:name]
|
|
629
|
+
params[param_name] = current_segment
|
|
630
|
+
result = traverse_trie(node[:param_child][:node], remaining_segments, params)
|
|
631
|
+
if result
|
|
632
|
+
return result
|
|
633
|
+
else
|
|
634
|
+
params.delete(param_name)
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
if node[:wildcard_child]
|
|
638
|
+
wildcard_name = node[:wildcard_child][:name]
|
|
639
|
+
(0..segments.size).each do |i|
|
|
640
|
+
captured_segments = segments[0..i]
|
|
641
|
+
remaining_after_wildcard = segments[(i + 1)..] || []
|
|
642
|
+
captured_path = captured_segments.join('/')
|
|
643
|
+
params[wildcard_name] = captured_path
|
|
644
|
+
result = traverse_trie(node[:wildcard_child][:node], remaining_after_wildcard, params)
|
|
645
|
+
if result
|
|
646
|
+
return result
|
|
647
|
+
else
|
|
648
|
+
params.delete(wildcard_name)
|
|
649
|
+
end
|
|
650
|
+
end
|
|
651
|
+
end
|
|
652
|
+
nil
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def build_path(metadata, params)
|
|
656
|
+
segments = metadata[:segments].dup
|
|
657
|
+
required_params = metadata[:params]
|
|
658
|
+
provided_params = params.dup
|
|
659
|
+
missing_params = required_params - provided_params.keys
|
|
660
|
+
unless missing_params.empty?
|
|
661
|
+
raise ArgumentError, "Missing required param(s) #{missing_params.map { |p| "'#{p}'" }.join(', ')} for route :#{metadata[:name]}"
|
|
662
|
+
end
|
|
663
|
+
path_parts = segments.map do |segment|
|
|
664
|
+
if segment.start_with?(':')
|
|
665
|
+
param_name = segment[1..].to_sym
|
|
666
|
+
value = provided_params.delete(param_name)
|
|
667
|
+
URI.encode_www_form_component(value.to_s)
|
|
668
|
+
elsif segment.start_with?('*')
|
|
669
|
+
param_name = segment[1..].to_s
|
|
670
|
+
param_name = 'path' if param_name.empty?
|
|
671
|
+
value = provided_params.delete(param_name.to_sym)
|
|
672
|
+
value.to_s
|
|
673
|
+
else
|
|
674
|
+
segment
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
path = path_parts.empty? ? '/' : '/' + path_parts.join('/')
|
|
678
|
+
unless provided_params.empty?
|
|
679
|
+
query_string = URI.encode_www_form(provided_params)
|
|
680
|
+
path += "?#{query_string}"
|
|
681
|
+
end
|
|
682
|
+
path
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
def parse_path_key(key)
|
|
686
|
+
key.split('/').reject(&:empty?)
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def build_pattern(segments)
|
|
690
|
+
segments.empty? ? '/' : '/' + segments.join('/')
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def extract_param_names(segments)
|
|
694
|
+
segments.select { |s| s.start_with?(':') || s.start_with?('*') }.map { |s| s[1..].to_sym }
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def build_metadata_key(domain, method, pattern)
|
|
698
|
+
"#{domain}:#{method.to_s.upcase}:#{pattern}"
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def get_path_segments(normalized_path)
|
|
702
|
+
if cached = @segment_cache[normalized_path]
|
|
703
|
+
return cached
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
if normalized_path == '/'
|
|
707
|
+
segments = []
|
|
708
|
+
else
|
|
709
|
+
segments = normalized_path.split('/').reject(&:empty?)
|
|
710
|
+
|
|
711
|
+
# In strict mode, preserve trailing slash by appending to last segment
|
|
712
|
+
if Aris::Config.trailing_slash == :strict &&
|
|
713
|
+
normalized_path.end_with?('/') &&
|
|
714
|
+
normalized_path != '/' &&
|
|
715
|
+
segments.any?
|
|
716
|
+
segments[-1] = segments[-1] + '/'
|
|
717
|
+
end
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
if @segment_cache.size >= @cache_max_size
|
|
721
|
+
@segment_cache.clear
|
|
722
|
+
end
|
|
723
|
+
@segment_cache[normalized_path] = segments
|
|
724
|
+
segments
|
|
725
|
+
end
|
|
726
|
+
def normalize_path(path)
|
|
727
|
+
return '/' if path.empty?
|
|
728
|
+
return path if path == '/' || (path == '/users' && !path.include?('//'))
|
|
729
|
+
|
|
730
|
+
if path.include?('//')
|
|
731
|
+
path = path.gsub(%r{/+}, '/')
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
# Don't strip trailing slash in strict mode
|
|
735
|
+
normalized = if Aris::Config.trailing_slash == :strict
|
|
736
|
+
path # Keep trailing slash in strict mode
|
|
737
|
+
else
|
|
738
|
+
path.length > 1 && path.end_with?('/') ? path.chomp('/') : path
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
if normalized.include?('%')
|
|
742
|
+
begin
|
|
743
|
+
normalized = URI.decode_www_form_component(normalized)
|
|
744
|
+
rescue
|
|
745
|
+
end
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
normalized.downcase
|
|
749
|
+
end
|
|
750
|
+
class RouteNotFoundError < StandardError
|
|
751
|
+
def initialize(name, domain)
|
|
752
|
+
# FIX: Concatenate arguments into a single string before calling super
|
|
753
|
+
super("Named route :#{name} not found on domain '#{domain}' or '*' fallback.")
|
|
754
|
+
end
|
|
755
|
+
end
|
|
756
|
+
class DuplicateRouteNameError < StandardError
|
|
757
|
+
end
|
|
758
|
+
class LocaleError < StandardError
|
|
759
|
+
def initialize(message)
|
|
760
|
+
super(message)
|
|
761
|
+
end
|
|
762
|
+
end
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
module CurrentDomain
|
|
766
|
+
extend self
|
|
767
|
+
THREAD_KEY = :aris_current_domain
|
|
768
|
+
def current
|
|
769
|
+
Thread.current[THREAD_KEY] ||
|
|
770
|
+
Aris::Router.default_domain ||
|
|
771
|
+
raise("No domain context available. Set Thread.current[:aris_current_domain] or Aris::Router.default_domain")
|
|
772
|
+
end
|
|
773
|
+
|
|
774
|
+
def with_domain(domain, &block)
|
|
775
|
+
original = Thread.current[THREAD_KEY]
|
|
776
|
+
Thread.current[THREAD_KEY] = domain
|
|
777
|
+
yield
|
|
778
|
+
ensure
|
|
779
|
+
Thread.current[:aris_current_domain] = original
|
|
780
|
+
end
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
module PathHelper
|
|
784
|
+
extend self
|
|
785
|
+
|
|
786
|
+
def extract_domain_and_route(args)
|
|
787
|
+
case args.size
|
|
788
|
+
when 1
|
|
789
|
+
[CurrentDomain.current, args[0]]
|
|
790
|
+
when 2
|
|
791
|
+
[args[0].to_s.downcase, args[1]]
|
|
792
|
+
else
|
|
793
|
+
raise ArgumentError, "Expected 1 or 2 arguments, got #{args.size}"
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def call(*args, **params)
|
|
798
|
+
domain, route_name = extract_domain_and_route(args)
|
|
799
|
+
clean_domain = domain.to_s.sub(%r{^https?://}, '').downcase
|
|
800
|
+
Aris::Router.build_named_path(route_name, domain: clean_domain, **params)
|
|
801
|
+
end
|
|
802
|
+
end
|
|
803
|
+
|
|
804
|
+
module URLHelper
|
|
805
|
+
extend self
|
|
806
|
+
def call(*args, protocol: 'https', **params)
|
|
807
|
+
domain, route_name = PathHelper.extract_domain_and_route(args)
|
|
808
|
+
path = PathHelper.call(*args, **params)
|
|
809
|
+
clean_assembly_domain = domain.sub(%r{^https?://}, '')
|
|
810
|
+
"#{protocol}://#{clean_assembly_domain}#{path}"
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
private
|
|
815
|
+
|
|
816
|
+
def extract_domain_and_route(args)
|
|
817
|
+
case args.size
|
|
818
|
+
when 1
|
|
819
|
+
[CurrentDomain.current, args[0]]
|
|
820
|
+
when 2
|
|
821
|
+
[args[0].to_s.downcase, args[1]]
|
|
822
|
+
else
|
|
823
|
+
raise ArgumentError, "Expected 1 or 2 arguments, got #{args.size}"
|
|
824
|
+
end
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def current_domain
|
|
828
|
+
CurrentDomain.current
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def handle_trailing_slash(path)
|
|
832
|
+
mode = Config.trailing_slash
|
|
833
|
+
|
|
834
|
+
return [nil, path] if mode == :strict
|
|
835
|
+
return [nil, path] unless path.end_with?('/')
|
|
836
|
+
return [nil, path] if path == '/' # Root path exception
|
|
837
|
+
|
|
838
|
+
normalized = path.chomp('/')
|
|
839
|
+
|
|
840
|
+
case mode
|
|
841
|
+
when :redirect
|
|
842
|
+
[[Config.trailing_slash_status, {'Location' => normalized}, []], nil]
|
|
843
|
+
when :ignore
|
|
844
|
+
[nil, normalized]
|
|
845
|
+
else
|
|
846
|
+
[nil, path]
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
def path(*args, **kwargs)
|
|
851
|
+
domain_to_match, route_name = PathHelper.extract_domain_and_route(args)
|
|
852
|
+
clean_domain = domain_to_match.sub(%r{^https?://}, '').downcase
|
|
853
|
+
Router.build_named_path(route_name, domain: clean_domain, **kwargs)
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def url(*args, protocol: 'https', **kwargs)
|
|
857
|
+
domain_to_assemble = if args.size == 2
|
|
858
|
+
args[0].to_s
|
|
859
|
+
else
|
|
860
|
+
current_domain
|
|
861
|
+
end
|
|
862
|
+
path = self.path(*args, **kwargs)
|
|
863
|
+
clean_assembly_domain = domain_to_assemble.sub(%r{^https?://}, '')
|
|
864
|
+
"#{protocol}://#{clean_assembly_domain}#{path}"
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def with_domain(domain, &block)
|
|
868
|
+
CurrentDomain.with_domain(domain, &block)
|
|
869
|
+
end
|
|
870
|
+
def default(config); Router.set_defaults(config); end
|
|
871
|
+
|
|
872
|
+
def redirect(target, status: 302, **params)
|
|
873
|
+
url = if target.is_a?(Symbol)
|
|
874
|
+
self.url(target, **params)
|
|
875
|
+
else
|
|
876
|
+
target.to_s
|
|
877
|
+
end
|
|
878
|
+
[status, {'content-type' => 'text/plain', 'Location' => url}, []]
|
|
879
|
+
end
|
|
880
|
+
|
|
881
|
+
def routes(config = nil, from_discovery: false, **kwargs)
|
|
882
|
+
# Ruby 3 compatibility: if config is nil and kwargs present, treat kwargs as config
|
|
883
|
+
config = kwargs if config.nil? && !kwargs.empty?
|
|
884
|
+
|
|
885
|
+
if config.nil? || (!config.is_a?(Hash) && kwargs.empty?)
|
|
886
|
+
raise ArgumentError, "Aris.routes requires a configuration hash."
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
unless from_discovery
|
|
890
|
+
Aris::Utils::Sitemap.reset! if defined?(Aris::Utils::Sitemap)
|
|
891
|
+
Aris::Utils::Redirects.reset! if defined?(Aris::Utils::Redirects)
|
|
892
|
+
extract_utils_metadata(config) if config.is_a?(Hash)
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
raise ArgumentError, "Aris.routes requires a configuration hash." unless config.is_a?(Hash)
|
|
896
|
+
Router.define(config)
|
|
897
|
+
end
|
|
898
|
+
def extract_utils_metadata(routes_hash, domain: nil, path_parts: [])
|
|
899
|
+
return unless routes_hash.is_a?(Hash)
|
|
900
|
+
|
|
901
|
+
# Store domain config if present
|
|
902
|
+
if domain && (routes_hash[:locales] || routes_hash[:default_locale])
|
|
903
|
+
Aris::Router.store_domain_config(domain, {
|
|
904
|
+
locales: Array(routes_hash[:locales]),
|
|
905
|
+
default_locale: routes_hash[:default_locale],
|
|
906
|
+
root_locale_redirect: routes_hash.fetch(:root_locale_redirect, true)
|
|
907
|
+
})
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
routes_hash.each do |key, value|
|
|
911
|
+
next unless value.is_a?(Hash)
|
|
912
|
+
|
|
913
|
+
if key.is_a?(Symbol) && [:get, :post, :put, :patch, :delete, :options].include?(key)
|
|
914
|
+
path = path_parts.empty? ? '/' : path_parts.join('')
|
|
915
|
+
|
|
916
|
+
if defined?(Aris::Utils::Sitemap) && value[:sitemap]
|
|
917
|
+
Aris::Utils::Sitemap.register(domain: domain, path: path, method: key, metadata: value[:sitemap])
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
if defined?(Aris::Utils::Redirects) && value[:redirects_from]
|
|
921
|
+
Aris::Utils::Redirects.register(from_paths: value[:redirects_from], to_path: path, status: value[:redirect_status] || 301)
|
|
922
|
+
end
|
|
923
|
+
else
|
|
924
|
+
new_domain = domain || key.to_s
|
|
925
|
+
new_path = domain ? path_parts + [key.to_s] : path_parts
|
|
926
|
+
extract_utils_metadata(value, domain: new_domain, path_parts: new_path)
|
|
927
|
+
end
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
module_function :path, :url, :with_domain, :current_domain, :routes, :default, :redirect, :extract_utils_metadata, :handle_trailing_slash
|
|
931
|
+
end
|