otto 1.2.0 → 1.4.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 +4 -4
- data/.github/dependabot.yml +15 -0
- data/.github/workflows/ci.yml +34 -0
- data/.gitignore +1 -0
- data/.pre-commit-config.yaml +107 -0
- data/.pre-push-config.yaml +88 -0
- data/.rubocop.yml +365 -21
- data/Gemfile +1 -3
- data/Gemfile.lock +78 -46
- data/README.md +58 -2
- data/examples/basic/app.rb +20 -20
- data/examples/basic/config.ru +1 -1
- data/examples/dynamic_pages/app.rb +30 -30
- data/examples/dynamic_pages/config.ru +1 -1
- data/examples/helpers_demo/app.rb +244 -0
- data/examples/helpers_demo/config.ru +26 -0
- data/examples/helpers_demo/routes +7 -0
- data/examples/security_features/app.rb +90 -87
- data/examples/security_features/config.ru +19 -17
- data/lib/otto/design_system.rb +22 -22
- data/lib/otto/helpers/base.rb +27 -0
- data/lib/otto/helpers/request.rb +226 -9
- data/lib/otto/helpers/response.rb +85 -38
- data/lib/otto/route.rb +17 -12
- data/lib/otto/security/config.rb +132 -38
- data/lib/otto/security/csrf.rb +23 -27
- data/lib/otto/security/validator.rb +33 -30
- data/lib/otto/static.rb +1 -1
- data/lib/otto/version.rb +1 -25
- data/lib/otto.rb +171 -61
- data/otto.gemspec +11 -12
- metadata +15 -15
- data/.rubocop_todo.yml +0 -152
- data/VERSION.yml +0 -5
data/lib/otto/version.rb
CHANGED
@@ -1,29 +1,5 @@
|
|
1
1
|
# lib/otto/version.rb
|
2
2
|
|
3
3
|
class Otto
|
4
|
-
|
5
|
-
#
|
6
|
-
module VERSION
|
7
|
-
def self.to_a
|
8
|
-
load_config
|
9
|
-
version = [@version[:MAJOR], @version[:MINOR], @version[:PATCH]]
|
10
|
-
version << @version[:PRE] unless @version.fetch(:PRE, nil).to_s.empty?
|
11
|
-
version
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.to_s
|
15
|
-
to_a.join('.')
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.inspect
|
19
|
-
to_s
|
20
|
-
end
|
21
|
-
|
22
|
-
def self.load_config
|
23
|
-
return if @version
|
24
|
-
|
25
|
-
require 'yaml'
|
26
|
-
@version = YAML.load_file(File.join(__dir__, '..', '..', 'VERSION.yml'))
|
27
|
-
end
|
28
|
-
end
|
4
|
+
VERSION = '1.4.0'.freeze
|
29
5
|
end
|
data/lib/otto.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
|
+
require 'json'
|
1
2
|
require 'logger'
|
3
|
+
require 'ostruct'
|
2
4
|
require 'securerandom'
|
5
|
+
require 'uri'
|
3
6
|
|
4
7
|
require 'rack/request'
|
5
8
|
require 'rack/response'
|
6
9
|
require 'rack/utils'
|
7
|
-
require 'addressable/uri'
|
8
10
|
|
9
11
|
require_relative 'otto/route'
|
10
12
|
require_relative 'otto/static'
|
@@ -39,23 +41,38 @@ require_relative 'otto/security/validator'
|
|
39
41
|
class Otto
|
40
42
|
LIB_HOME = __dir__ unless defined?(Otto::LIB_HOME)
|
41
43
|
|
42
|
-
@debug
|
44
|
+
@debug = ENV['OTTO_DEBUG'] == 'true'
|
43
45
|
@logger = Logger.new($stdout, Logger::INFO)
|
46
|
+
@global_config = {}
|
44
47
|
|
45
|
-
|
48
|
+
# Global configuration for all Otto instances
|
49
|
+
def self.configure
|
50
|
+
config = OpenStruct.new(@global_config)
|
51
|
+
yield config
|
52
|
+
@global_config = config.to_h
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.global_config
|
56
|
+
@global_config
|
57
|
+
end
|
58
|
+
|
59
|
+
attr_reader :routes, :routes_literal, :routes_static, :route_definitions, :option, :static_route, :security_config, :locale_config
|
46
60
|
attr_accessor :not_found, :server_error, :middleware_stack
|
47
61
|
|
48
62
|
def initialize(path = nil, opts = {})
|
49
|
-
@routes_static
|
50
|
-
@routes
|
51
|
-
@routes_literal
|
63
|
+
@routes_static = { GET: {} }
|
64
|
+
@routes = { GET: [] }
|
65
|
+
@routes_literal = { GET: {} }
|
52
66
|
@route_definitions = {}
|
53
|
-
@option
|
67
|
+
@option = {
|
54
68
|
public: nil,
|
55
|
-
locale: 'en'
|
69
|
+
locale: 'en',
|
56
70
|
}.merge(opts)
|
57
|
-
@security_config
|
58
|
-
@middleware_stack
|
71
|
+
@security_config = Otto::Security::Config.new
|
72
|
+
@middleware_stack = []
|
73
|
+
|
74
|
+
# Configure locale support (merge global config with instance options)
|
75
|
+
configure_locale(opts)
|
59
76
|
|
60
77
|
# Configure security based on options
|
61
78
|
configure_security(opts)
|
@@ -72,15 +89,15 @@ class Otto
|
|
72
89
|
|
73
90
|
raw = File.readlines(path).select { |line| line =~ /^\w/ }.collect { |line| line.strip.split(/\s+/) }
|
74
91
|
raw.each do |entry|
|
75
|
-
verb, path, definition
|
76
|
-
route
|
77
|
-
route.otto
|
78
|
-
path_clean
|
79
|
-
@route_definitions[route.definition]
|
92
|
+
verb, path, definition = *entry
|
93
|
+
route = Otto::Route.new verb, path, definition
|
94
|
+
route.otto = self
|
95
|
+
path_clean = path.gsub(%r{/$}, '')
|
96
|
+
@route_definitions[route.definition] = route
|
80
97
|
Otto.logger.debug "route: #{route.pattern}" if Otto.debug
|
81
|
-
@routes[route.verb]
|
98
|
+
@routes[route.verb] ||= []
|
82
99
|
@routes[route.verb] << route
|
83
|
-
@routes_literal[route.verb]
|
100
|
+
@routes_literal[route.verb] ||= {}
|
84
101
|
@routes_literal[route.verb][path_clean] = route
|
85
102
|
rescue StandardError
|
86
103
|
Otto.logger.error "Bad route in #{path}: #{entry}"
|
@@ -97,7 +114,7 @@ class Otto
|
|
97
114
|
return false unless File.directory?(public_dir)
|
98
115
|
|
99
116
|
# Clean the requested path - remove null bytes and normalize
|
100
|
-
clean_path = path.
|
117
|
+
clean_path = path.delete("\0").strip
|
101
118
|
return false if clean_path.empty?
|
102
119
|
|
103
120
|
# Join and expand to get the full resolved path
|
@@ -111,13 +128,13 @@ class Otto
|
|
111
128
|
File.readable?(requested_path) &&
|
112
129
|
!File.directory?(requested_path) &&
|
113
130
|
(File.owned?(requested_path) || File.grpowned?(requested_path))
|
114
|
-
end
|
131
|
+
end
|
115
132
|
|
116
133
|
def safe_dir?(path)
|
117
134
|
return false if path.nil? || path.empty?
|
118
135
|
|
119
136
|
# Clean and expand the path
|
120
|
-
clean_path = path.
|
137
|
+
clean_path = path.delete("\0").strip
|
121
138
|
return false if clean_path.empty?
|
122
139
|
|
123
140
|
expanded_path = File.expand_path(clean_path)
|
@@ -131,9 +148,9 @@ end
|
|
131
148
|
def add_static_path(path)
|
132
149
|
return unless safe_file?(path)
|
133
150
|
|
134
|
-
base_path
|
151
|
+
base_path = File.split(path).first
|
135
152
|
# Files in the root directory can refer to themselves
|
136
|
-
base_path
|
153
|
+
base_path = path if base_path == '/'
|
137
154
|
File.join(option[:public], base_path)
|
138
155
|
Otto.logger.debug "new static route: #{base_path} (#{path})" if Otto.debug
|
139
156
|
routes_static[:GET][base_path] = base_path
|
@@ -141,46 +158,47 @@ end
|
|
141
158
|
|
142
159
|
def call(env)
|
143
160
|
# Apply middleware stack
|
144
|
-
app =
|
145
|
-
@middleware_stack.
|
161
|
+
app = ->(e) { handle_request(e) }
|
162
|
+
@middleware_stack.reverse_each do |middleware|
|
146
163
|
app = middleware.new(app, @security_config)
|
147
164
|
end
|
148
165
|
|
149
166
|
begin
|
150
167
|
app.call(env)
|
151
|
-
rescue StandardError =>
|
152
|
-
handle_error(
|
168
|
+
rescue StandardError => ex
|
169
|
+
handle_error(ex, env)
|
153
170
|
end
|
154
171
|
end
|
155
172
|
|
156
173
|
def handle_request(env)
|
157
|
-
locale
|
174
|
+
locale = determine_locale env
|
158
175
|
env['rack.locale'] = locale
|
159
|
-
|
160
|
-
|
161
|
-
path_info
|
176
|
+
env['otto.locale_config'] = @locale_config if @locale_config
|
177
|
+
@static_route ||= Rack::Files.new(option[:public]) if option[:public] && safe_dir?(option[:public])
|
178
|
+
path_info = Rack::Utils.unescape(env['PATH_INFO'])
|
179
|
+
path_info = '/' if path_info.to_s.empty?
|
162
180
|
|
163
181
|
begin
|
164
182
|
path_info_clean = path_info
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
rescue ArgumentError =>
|
183
|
+
.encode(
|
184
|
+
'UTF-8', # Target encoding
|
185
|
+
invalid: :replace, # Replace invalid byte sequences
|
186
|
+
undef: :replace, # Replace characters undefined in UTF-8
|
187
|
+
replace: '', # Use empty string for replacement
|
188
|
+
)
|
189
|
+
.gsub(%r{/$}, '') # Remove trailing slash, if present
|
190
|
+
rescue ArgumentError => ex
|
173
191
|
# Log the error but don't expose details
|
174
|
-
Otto.logger.error
|
175
|
-
Otto.logger.debug "[Otto.handle_request] Error details: #{
|
192
|
+
Otto.logger.error '[Otto.handle_request] Path encoding error'
|
193
|
+
Otto.logger.debug "[Otto.handle_request] Error details: #{ex.message}" if Otto.debug
|
176
194
|
# Set a default value or use the original path_info
|
177
195
|
path_info_clean = path_info
|
178
196
|
end
|
179
197
|
|
180
|
-
base_path
|
198
|
+
base_path = File.split(path_info).first
|
181
199
|
# Files in the root directory can refer to themselves
|
182
|
-
base_path
|
183
|
-
http_verb
|
200
|
+
base_path = path_info if base_path == '/'
|
201
|
+
http_verb = env['REQUEST_METHOD'].upcase.to_sym
|
184
202
|
literal_routes = routes_literal[http_verb] || {}
|
185
203
|
literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
|
186
204
|
if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
|
@@ -195,15 +213,15 @@ end
|
|
195
213
|
routes_static[:GET][base_path] = base_path
|
196
214
|
static_route.call(env)
|
197
215
|
else
|
198
|
-
extra_params
|
199
|
-
found_route
|
200
|
-
valid_routes
|
216
|
+
extra_params = {}
|
217
|
+
found_route = nil
|
218
|
+
valid_routes = routes[http_verb] || []
|
201
219
|
valid_routes.push(*routes[:GET]) if http_verb == :HEAD
|
202
220
|
valid_routes.each do |route|
|
203
221
|
# Otto.logger.debug " request: #{http_verb} #{path_info} (trying route: #{route.verb} #{route.pattern})"
|
204
222
|
next unless (match = route.pattern.match(path_info))
|
205
223
|
|
206
|
-
values
|
224
|
+
values = match.captures.to_a
|
207
225
|
# The first capture returned is the entire matched string b/c
|
208
226
|
# we wrapped the entire regex in parens. We don't need it to
|
209
227
|
# the full match.
|
@@ -222,7 +240,7 @@ end
|
|
222
240
|
else
|
223
241
|
{}
|
224
242
|
end
|
225
|
-
found_route
|
243
|
+
found_route = route
|
226
244
|
break
|
227
245
|
end
|
228
246
|
found_route ||= literal_routes['/404']
|
@@ -245,7 +263,7 @@ end
|
|
245
263
|
return if route.nil?
|
246
264
|
|
247
265
|
local_params = params.clone
|
248
|
-
local_path
|
266
|
+
local_path = route.path.clone
|
249
267
|
|
250
268
|
local_params.each_pair do |k, v|
|
251
269
|
next unless local_path.match(":#{k}")
|
@@ -253,16 +271,19 @@ end
|
|
253
271
|
local_path.gsub!(":#{k}", v.to_s)
|
254
272
|
local_params.delete(k)
|
255
273
|
end
|
256
|
-
|
257
|
-
uri
|
258
|
-
|
274
|
+
|
275
|
+
uri = URI::HTTP.new(nil, nil, nil, nil, nil, local_path, nil, nil, nil)
|
276
|
+
unless local_params.empty?
|
277
|
+
query_string = local_params.map { |k, v| "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }.join('&')
|
278
|
+
uri.query = query_string
|
279
|
+
end
|
259
280
|
uri.to_s
|
260
281
|
end
|
261
282
|
|
262
283
|
def determine_locale(env)
|
263
284
|
accept_langs = env['HTTP_ACCEPT_LANGUAGE']
|
264
285
|
accept_langs = option[:locale] if accept_langs.to_s.empty?
|
265
|
-
locales
|
286
|
+
locales = []
|
266
287
|
unless accept_langs.empty?
|
267
288
|
locales = accept_langs.split(',').map do |l|
|
268
289
|
l += ';q=1.0' unless /;q=\d+(?:\.\d+)?$/.match?(l)
|
@@ -282,7 +303,7 @@ end
|
|
282
303
|
# @param middleware [Class] The middleware class to add
|
283
304
|
# @param args [Array] Additional arguments for the middleware
|
284
305
|
# @param block [Proc] Optional block for middleware configuration
|
285
|
-
def use(middleware,
|
306
|
+
def use(middleware, *, &)
|
286
307
|
@middleware_stack << middleware
|
287
308
|
end
|
288
309
|
|
@@ -343,7 +364,7 @@ end
|
|
343
364
|
# @param include_subdomains [Boolean] Apply to all subdomains (default: true)
|
344
365
|
# @example
|
345
366
|
# otto.enable_hsts!(max_age: 86400, include_subdomains: false)
|
346
|
-
def enable_hsts!(max_age:
|
367
|
+
def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
|
347
368
|
@security_config.enable_hsts!(max_age: max_age, include_subdomains: include_subdomains)
|
348
369
|
end
|
349
370
|
|
@@ -366,8 +387,64 @@ end
|
|
366
387
|
@security_config.enable_frame_protection!(option)
|
367
388
|
end
|
368
389
|
|
390
|
+
# Enable Content Security Policy (CSP) with nonce support for dynamic header generation.
|
391
|
+
# This enables the res.send_csp_headers response helper method.
|
392
|
+
#
|
393
|
+
# @param debug [Boolean] Enable debug logging for CSP headers (default: false)
|
394
|
+
# @example
|
395
|
+
# otto.enable_csp_with_nonce!(debug: true)
|
396
|
+
def enable_csp_with_nonce!(debug: false)
|
397
|
+
@security_config.enable_csp_with_nonce!(debug: debug)
|
398
|
+
end
|
399
|
+
|
400
|
+
# Configure locale settings for the application
|
401
|
+
#
|
402
|
+
# @param available_locales [Hash] Hash of available locales (e.g., { 'en' => 'English', 'es' => 'Spanish' })
|
403
|
+
# @param default_locale [String] Default locale to use as fallback
|
404
|
+
# @example
|
405
|
+
# otto.configure(
|
406
|
+
# available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
|
407
|
+
# default_locale: 'en'
|
408
|
+
# )
|
409
|
+
def configure(available_locales: nil, default_locale: nil)
|
410
|
+
@locale_config ||= {}
|
411
|
+
@locale_config[:available_locales] = available_locales if available_locales
|
412
|
+
@locale_config[:default_locale] = default_locale if default_locale
|
413
|
+
end
|
414
|
+
|
369
415
|
private
|
370
416
|
|
417
|
+
def configure_locale(opts)
|
418
|
+
# Start with global configuration
|
419
|
+
global_config = self.class.global_config
|
420
|
+
@locale_config = nil
|
421
|
+
|
422
|
+
# Check if we have any locale configuration from any source
|
423
|
+
has_global_locale = global_config && (global_config[:available_locales] || global_config[:default_locale])
|
424
|
+
has_direct_options = opts[:available_locales] || opts[:default_locale]
|
425
|
+
has_legacy_config = opts[:locale_config]
|
426
|
+
|
427
|
+
# Only create locale_config if we have configuration from somewhere
|
428
|
+
if has_global_locale || has_direct_options || has_legacy_config
|
429
|
+
@locale_config = {}
|
430
|
+
|
431
|
+
# Apply global configuration first
|
432
|
+
@locale_config[:available_locales] = global_config[:available_locales] if global_config && global_config[:available_locales]
|
433
|
+
@locale_config[:default_locale] = global_config[:default_locale] if global_config && global_config[:default_locale]
|
434
|
+
|
435
|
+
# Apply direct instance options (these override global config)
|
436
|
+
@locale_config[:available_locales] = opts[:available_locales] if opts[:available_locales]
|
437
|
+
@locale_config[:default_locale] = opts[:default_locale] if opts[:default_locale]
|
438
|
+
|
439
|
+
# Legacy support: Configure locale if provided in initialization options via locale_config hash
|
440
|
+
if opts[:locale_config]
|
441
|
+
locale_opts = opts[:locale_config]
|
442
|
+
@locale_config[:available_locales] = locale_opts[:available_locales] || locale_opts[:available] if locale_opts[:available_locales] || locale_opts[:available]
|
443
|
+
@locale_config[:default_locale] = locale_opts[:default_locale] || locale_opts[:default] if locale_opts[:default_locale] || locale_opts[:default]
|
444
|
+
end
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
371
448
|
def configure_security(opts)
|
372
449
|
# Enable CSRF protection if requested
|
373
450
|
enable_csrf_protection! if opts[:csrf_protection]
|
@@ -396,8 +473,12 @@ end
|
|
396
473
|
Otto.logger.error "[#{error_id}] #{error.class}: #{error.message}"
|
397
474
|
Otto.logger.debug "[#{error_id}] Backtrace: #{error.backtrace.join("\n")}" if Otto.debug
|
398
475
|
|
399
|
-
#
|
400
|
-
|
476
|
+
# Parse request for content negotiation
|
477
|
+
begin
|
478
|
+
Rack::Request.new(env)
|
479
|
+
rescue StandardError
|
480
|
+
nil
|
481
|
+
end
|
401
482
|
literal_routes = @routes_literal[:GET] || {}
|
402
483
|
|
403
484
|
# Try custom 500 route first
|
@@ -405,11 +486,17 @@ end
|
|
405
486
|
begin
|
406
487
|
env['otto.error_id'] = error_id
|
407
488
|
return found_route.call(env)
|
408
|
-
rescue StandardError =>
|
409
|
-
Otto.logger.error "[#{error_id}] Error in custom error handler: #{
|
489
|
+
rescue StandardError => ex
|
490
|
+
Otto.logger.error "[#{error_id}] Error in custom error handler: #{ex.message}"
|
410
491
|
end
|
411
492
|
end
|
412
493
|
|
494
|
+
# Content negotiation for built-in error response
|
495
|
+
accept_header = env['HTTP_ACCEPT'].to_s
|
496
|
+
if accept_header.include?('application/json')
|
497
|
+
return json_error_response(error_id)
|
498
|
+
end
|
499
|
+
|
413
500
|
# Fallback to built-in error response
|
414
501
|
@server_error || secure_error_response(error_id)
|
415
502
|
end
|
@@ -418,12 +505,35 @@ end
|
|
418
505
|
body = if Otto.env?(:dev, :development)
|
419
506
|
"Server error (ID: #{error_id}). Check logs for details."
|
420
507
|
else
|
421
|
-
|
508
|
+
'An error occurred. Please try again later.'
|
422
509
|
end
|
423
510
|
|
424
511
|
headers = {
|
425
512
|
'content-type' => 'text/plain',
|
426
|
-
'content-length' => body.bytesize.to_s
|
513
|
+
'content-length' => body.bytesize.to_s,
|
514
|
+
}.merge(@security_config.security_headers)
|
515
|
+
|
516
|
+
[500, headers, [body]]
|
517
|
+
end
|
518
|
+
|
519
|
+
def json_error_response(error_id)
|
520
|
+
error_data = if Otto.env?(:dev, :development)
|
521
|
+
{
|
522
|
+
error: 'Internal Server Error',
|
523
|
+
message: 'Server error occurred. Check logs for details.',
|
524
|
+
error_id: error_id,
|
525
|
+
}
|
526
|
+
else
|
527
|
+
{
|
528
|
+
error: 'Internal Server Error',
|
529
|
+
message: 'An error occurred. Please try again later.',
|
530
|
+
}
|
531
|
+
end
|
532
|
+
|
533
|
+
body = JSON.generate(error_data)
|
534
|
+
headers = {
|
535
|
+
'content-type' => 'application/json',
|
536
|
+
'content-length' => body.bytesize.to_s,
|
427
537
|
}.merge(@security_config.security_headers)
|
428
538
|
|
429
539
|
[500, headers, [body]]
|
data/otto.gemspec
CHANGED
@@ -3,25 +3,24 @@
|
|
3
3
|
require_relative 'lib/otto/version'
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name
|
7
|
-
spec.version
|
8
|
-
spec.summary
|
9
|
-
spec.description
|
10
|
-
spec.email
|
11
|
-
spec.authors
|
12
|
-
spec.license
|
13
|
-
spec.files
|
6
|
+
spec.name = 'otto'
|
7
|
+
spec.version = Otto::VERSION.to_s
|
8
|
+
spec.summary = 'Auto-define your rack-apps in plaintext.'
|
9
|
+
spec.description = "Otto: #{spec.summary}"
|
10
|
+
spec.email = 'gems@solutious.com'
|
11
|
+
spec.authors = ['Delano Mandelbaum']
|
12
|
+
spec.license = 'MIT'
|
13
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
14
14
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
15
15
|
end
|
16
|
-
spec.homepage
|
16
|
+
spec.homepage = 'https://github.com/delano/otto'
|
17
17
|
spec.require_paths = ['lib']
|
18
18
|
|
19
|
-
spec.required_ruby_version = ['>= 3.
|
19
|
+
spec.required_ruby_version = ['>= 3.2', '< 4.0']
|
20
20
|
|
21
21
|
# https://github.com/delano/otto/security/dependabot/5
|
22
22
|
spec.add_dependency 'rexml', '>= 3.3.6'
|
23
|
-
|
24
|
-
spec.add_dependency 'addressable', '~> 2.2', '< 3'
|
23
|
+
spec.add_dependency 'ostruct'
|
25
24
|
spec.add_dependency 'rack', '~> 3.1', '< 4.0'
|
26
25
|
spec.add_dependency 'rack-parser', '~> 0.7'
|
27
26
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: otto
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Delano Mandelbaum
|
@@ -24,25 +24,19 @@ dependencies:
|
|
24
24
|
- !ruby/object:Gem::Version
|
25
25
|
version: 3.3.6
|
26
26
|
- !ruby/object:Gem::Dependency
|
27
|
-
name:
|
27
|
+
name: ostruct
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
29
29
|
requirements:
|
30
|
-
- - "
|
31
|
-
- !ruby/object:Gem::Version
|
32
|
-
version: '2.2'
|
33
|
-
- - "<"
|
30
|
+
- - ">="
|
34
31
|
- !ruby/object:Gem::Version
|
35
|
-
version: '
|
32
|
+
version: '0'
|
36
33
|
type: :runtime
|
37
34
|
prerelease: false
|
38
35
|
version_requirements: !ruby/object:Gem::Requirement
|
39
36
|
requirements:
|
40
|
-
- - "
|
41
|
-
- !ruby/object:Gem::Version
|
42
|
-
version: '2.2'
|
43
|
-
- - "<"
|
37
|
+
- - ">="
|
44
38
|
- !ruby/object:Gem::Version
|
45
|
-
version: '
|
39
|
+
version: '0'
|
46
40
|
- !ruby/object:Gem::Dependency
|
47
41
|
name: rack
|
48
42
|
requirement: !ruby/object:Gem::Requirement
|
@@ -83,26 +77,32 @@ executables: []
|
|
83
77
|
extensions: []
|
84
78
|
extra_rdoc_files: []
|
85
79
|
files:
|
80
|
+
- ".github/dependabot.yml"
|
81
|
+
- ".github/workflows/ci.yml"
|
86
82
|
- ".gitignore"
|
83
|
+
- ".pre-commit-config.yaml"
|
84
|
+
- ".pre-push-config.yaml"
|
87
85
|
- ".rspec"
|
88
86
|
- ".rubocop.yml"
|
89
|
-
- ".rubocop_todo.yml"
|
90
87
|
- Gemfile
|
91
88
|
- Gemfile.lock
|
92
89
|
- LICENSE.txt
|
93
90
|
- README.md
|
94
|
-
- VERSION.yml
|
95
91
|
- examples/basic/app.rb
|
96
92
|
- examples/basic/config.ru
|
97
93
|
- examples/basic/routes
|
98
94
|
- examples/dynamic_pages/app.rb
|
99
95
|
- examples/dynamic_pages/config.ru
|
100
96
|
- examples/dynamic_pages/routes
|
97
|
+
- examples/helpers_demo/app.rb
|
98
|
+
- examples/helpers_demo/config.ru
|
99
|
+
- examples/helpers_demo/routes
|
101
100
|
- examples/security_features/app.rb
|
102
101
|
- examples/security_features/config.ru
|
103
102
|
- examples/security_features/routes
|
104
103
|
- lib/otto.rb
|
105
104
|
- lib/otto/design_system.rb
|
105
|
+
- lib/otto/helpers/base.rb
|
106
106
|
- lib/otto/helpers/request.rb
|
107
107
|
- lib/otto/helpers/response.rb
|
108
108
|
- lib/otto/route.rb
|
@@ -126,7 +126,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
126
|
requirements:
|
127
127
|
- - ">="
|
128
128
|
- !ruby/object:Gem::Version
|
129
|
-
version: '3.
|
129
|
+
version: '3.2'
|
130
130
|
- - "<"
|
131
131
|
- !ruby/object:Gem::Version
|
132
132
|
version: '4.0'
|