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/Gemfile.lock
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
otto (1.
|
5
|
-
|
4
|
+
otto (1.4.0)
|
5
|
+
ostruct
|
6
6
|
rack (~> 3.1, < 4.0)
|
7
7
|
rack-parser (~> 0.7)
|
8
8
|
rexml (>= 3.3.6)
|
@@ -10,32 +10,46 @@ PATH
|
|
10
10
|
GEM
|
11
11
|
remote: https://rubygems.org/
|
12
12
|
specs:
|
13
|
-
|
14
|
-
|
15
|
-
ast (2.4.2)
|
16
|
-
byebug (11.1.3)
|
13
|
+
ast (2.4.3)
|
14
|
+
byebug (12.0.0)
|
17
15
|
coderay (1.1.3)
|
16
|
+
date (3.4.1)
|
18
17
|
diff-lcs (1.6.2)
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
erb (5.0.2)
|
19
|
+
io-console (0.8.1)
|
20
|
+
irb (1.15.2)
|
21
|
+
pp (>= 0.6.0)
|
22
|
+
rdoc (>= 4.0.0)
|
23
|
+
reline (>= 0.4.2)
|
24
|
+
json (2.13.2)
|
25
|
+
language_server-protocol (3.17.0.5)
|
26
|
+
lint_roller (1.1.0)
|
22
27
|
logger (1.7.0)
|
23
|
-
method_source (1.
|
24
|
-
|
25
|
-
|
28
|
+
method_source (1.1.0)
|
29
|
+
minitest (5.25.5)
|
30
|
+
ostruct (0.6.3)
|
31
|
+
parallel (1.27.0)
|
32
|
+
parser (3.3.9.0)
|
26
33
|
ast (~> 2.4.1)
|
27
34
|
racc
|
35
|
+
pastel (0.8.0)
|
36
|
+
tty-color (~> 0.5)
|
37
|
+
pp (0.6.2)
|
38
|
+
prettyprint
|
28
39
|
prettier_print (1.2.1)
|
40
|
+
prettyprint (0.2.0)
|
29
41
|
prism (1.4.0)
|
30
|
-
pry (0.
|
42
|
+
pry (0.15.2)
|
31
43
|
coderay (~> 1.1)
|
32
44
|
method_source (~> 1.0)
|
33
|
-
pry-byebug (3.
|
34
|
-
byebug (~>
|
35
|
-
pry (>= 0.13, < 0.
|
36
|
-
|
37
|
-
|
38
|
-
|
45
|
+
pry-byebug (3.11.0)
|
46
|
+
byebug (~> 12.0)
|
47
|
+
pry (>= 0.13, < 0.16)
|
48
|
+
psych (5.2.6)
|
49
|
+
date
|
50
|
+
stringio
|
51
|
+
racc (1.8.1)
|
52
|
+
rack (3.2.0)
|
39
53
|
rack-parser (0.7.0)
|
40
54
|
rack
|
41
55
|
rack-test (2.2.0)
|
@@ -43,8 +57,13 @@ GEM
|
|
43
57
|
rainbow (3.1.1)
|
44
58
|
rbs (3.9.4)
|
45
59
|
logger
|
46
|
-
|
47
|
-
|
60
|
+
rdoc (6.14.2)
|
61
|
+
erb
|
62
|
+
psych (>= 4.0.0)
|
63
|
+
regexp_parser (2.11.0)
|
64
|
+
reline (0.6.2)
|
65
|
+
io-console (~> 0.5)
|
66
|
+
rexml (3.4.1)
|
48
67
|
rspec (3.13.1)
|
49
68
|
rspec-core (~> 3.13.0)
|
50
69
|
rspec-expectations (~> 3.13.0)
|
@@ -58,44 +77,57 @@ GEM
|
|
58
77
|
diff-lcs (>= 1.2.0, < 2.0)
|
59
78
|
rspec-support (~> 3.13.0)
|
60
79
|
rspec-support (3.13.4)
|
61
|
-
rubocop (1.
|
80
|
+
rubocop (1.79.1)
|
62
81
|
json (~> 2.3)
|
63
|
-
language_server-protocol (
|
82
|
+
language_server-protocol (~> 3.17.0.2)
|
83
|
+
lint_roller (~> 1.1.0)
|
64
84
|
parallel (~> 1.10)
|
65
85
|
parser (>= 3.3.0.2)
|
66
86
|
rainbow (>= 2.2.2, < 4.0)
|
67
|
-
regexp_parser (>=
|
68
|
-
|
69
|
-
rubocop-ast (>= 1.31.1, < 2.0)
|
87
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
88
|
+
rubocop-ast (>= 1.46.0, < 2.0)
|
70
89
|
ruby-progressbar (~> 1.7)
|
71
|
-
unicode-display_width (>= 2.4.0, <
|
72
|
-
rubocop-ast (1.
|
73
|
-
parser (>= 3.3.
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
rubocop (
|
79
|
-
rubocop-
|
80
|
-
|
81
|
-
|
90
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
91
|
+
rubocop-ast (1.46.0)
|
92
|
+
parser (>= 3.3.7.2)
|
93
|
+
prism (~> 1.4)
|
94
|
+
rubocop-performance (1.25.0)
|
95
|
+
lint_roller (~> 1.1)
|
96
|
+
rubocop (>= 1.75.0, < 2.0)
|
97
|
+
rubocop-ast (>= 1.38.0, < 2.0)
|
98
|
+
rubocop-rspec (3.6.0)
|
99
|
+
lint_roller (~> 1.1)
|
100
|
+
rubocop (~> 1.72, >= 1.72.1)
|
101
|
+
rubocop-thread_safety (0.7.3)
|
102
|
+
lint_roller (~> 1.1)
|
103
|
+
rubocop (~> 1.72, >= 1.72.1)
|
104
|
+
rubocop-ast (>= 1.44.0, < 2.0)
|
105
|
+
ruby-lsp (0.26.1)
|
82
106
|
language_server-protocol (~> 3.17.0)
|
83
107
|
prism (>= 1.2, < 2.0)
|
84
108
|
rbs (>= 3, < 5)
|
85
109
|
ruby-progressbar (1.13.0)
|
86
110
|
stackprof (0.2.27)
|
87
|
-
|
111
|
+
stringio (3.1.7)
|
88
112
|
syntax_tree (6.3.0)
|
89
113
|
prettier_print (>= 1.2.0)
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
114
|
+
tryouts (3.2.1)
|
115
|
+
irb
|
116
|
+
minitest (~> 5.0)
|
117
|
+
pastel (~> 0.8)
|
118
|
+
prism (~> 1.0)
|
119
|
+
rspec (~> 3.0)
|
120
|
+
tty-cursor (~> 0.7)
|
121
|
+
tty-screen (~> 0.8)
|
122
|
+
tty-color (0.6.0)
|
123
|
+
tty-cursor (0.7.1)
|
124
|
+
tty-screen (0.8.2)
|
125
|
+
unicode-display_width (3.1.4)
|
126
|
+
unicode-emoji (~> 4.0, >= 4.0.4)
|
127
|
+
unicode-emoji (4.0.4)
|
96
128
|
|
97
129
|
PLATFORMS
|
98
|
-
arm64-darwin-
|
130
|
+
arm64-darwin-24
|
99
131
|
ruby
|
100
132
|
|
101
133
|
DEPENDENCIES
|
@@ -113,4 +145,4 @@ DEPENDENCIES
|
|
113
145
|
tryouts
|
114
146
|
|
115
147
|
BUNDLED WITH
|
116
|
-
2.
|
148
|
+
2.6.9
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Otto -
|
1
|
+
# Otto - A Ruby Gem
|
2
2
|
|
3
3
|
**Define your rack-apps in plain-text with built-in security.**
|
4
4
|
|
@@ -74,9 +74,65 @@ app = Otto.new("./routes", {
|
|
74
74
|
|
75
75
|
Security features include CSRF protection, input validation, security headers, and trusted proxy configuration.
|
76
76
|
|
77
|
+
## Internationalization Support
|
78
|
+
|
79
|
+
Otto provides built-in locale detection and management:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
# Global configuration (affects all Otto instances)
|
83
|
+
Otto.configure do |opts|
|
84
|
+
opts.available_locales = { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' }
|
85
|
+
opts.default_locale = 'en'
|
86
|
+
end
|
87
|
+
|
88
|
+
# Or configure during initialization
|
89
|
+
app = Otto.new("./routes", {
|
90
|
+
available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
|
91
|
+
default_locale: 'en'
|
92
|
+
})
|
93
|
+
|
94
|
+
# Or configure at runtime
|
95
|
+
app.configure(
|
96
|
+
available_locales: { 'en' => 'English', 'es' => 'Spanish' },
|
97
|
+
default_locale: 'en'
|
98
|
+
)
|
99
|
+
|
100
|
+
# Legacy support (still works)
|
101
|
+
app = Otto.new("./routes", {
|
102
|
+
locale_config: {
|
103
|
+
available_locales: { 'en' => 'English', 'es' => 'Spanish', 'fr' => 'French' },
|
104
|
+
default_locale: 'en'
|
105
|
+
}
|
106
|
+
})
|
107
|
+
```
|
108
|
+
|
109
|
+
In your application, use the locale helper:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
class App
|
113
|
+
def initialize(req, res)
|
114
|
+
@req, @res = req, res
|
115
|
+
end
|
116
|
+
|
117
|
+
def show_product
|
118
|
+
# Automatically detects locale from:
|
119
|
+
# 1. URL parameter: ?locale=es
|
120
|
+
# 2. User preference (if provided)
|
121
|
+
# 3. Accept-Language header
|
122
|
+
# 4. Default locale
|
123
|
+
locale = req.check_locale!
|
124
|
+
|
125
|
+
# Use locale for localized content
|
126
|
+
res.body = localized_content(locale)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
The locale helper checks multiple sources in order of precedence and validates against your configured locales.
|
132
|
+
|
77
133
|
## Requirements
|
78
134
|
|
79
|
-
- Ruby 3.
|
135
|
+
- Ruby 3.2+
|
80
136
|
- Rack 3.1+
|
81
137
|
|
82
138
|
## Installation
|
data/examples/basic/app.rb
CHANGED
@@ -8,8 +8,8 @@ class App
|
|
8
8
|
attr_reader :req, :res
|
9
9
|
|
10
10
|
def initialize(req, res)
|
11
|
-
@req
|
12
|
-
@res
|
11
|
+
@req = req
|
12
|
+
@res = res
|
13
13
|
res.headers['content-type'] = 'text/html; charset=utf-8'
|
14
14
|
end
|
15
15
|
|
@@ -21,15 +21,15 @@ class App
|
|
21
21
|
<p>Minimal Ruby web framework with style</p>
|
22
22
|
</div>
|
23
23
|
|
24
|
-
#{otto_card(
|
24
|
+
#{otto_card('Send Feedback') do
|
25
25
|
otto_form_wrapper do
|
26
|
-
otto_input(
|
27
|
-
otto_button(
|
26
|
+
otto_input('msg', placeholder: 'Your message...') +
|
27
|
+
otto_button('Send Feedback')
|
28
28
|
end
|
29
29
|
end}
|
30
30
|
HTML
|
31
31
|
|
32
|
-
res.
|
32
|
+
res.send_secure_cookie :sess, 1_234_567, 3600
|
33
33
|
res.body = otto_page(content)
|
34
34
|
end
|
35
35
|
|
@@ -37,14 +37,14 @@ class App
|
|
37
37
|
message = req.params['msg']&.strip
|
38
38
|
|
39
39
|
content = if message.nil? || message.empty?
|
40
|
-
otto_alert(
|
40
|
+
otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
|
41
41
|
else
|
42
|
-
otto_alert(
|
43
|
-
|
42
|
+
otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
|
43
|
+
otto_card('Your Message') { otto_code_block(message, 'text') }
|
44
44
|
end
|
45
45
|
|
46
|
-
content += "<p>#{otto_link(
|
47
|
-
res.body = otto_page(content,
|
46
|
+
content += "<p>#{otto_link('← Back', '/')}</p>"
|
47
|
+
res.body = otto_page(content, 'Feedback')
|
48
48
|
end
|
49
49
|
|
50
50
|
def redirect
|
@@ -53,26 +53,26 @@ class App
|
|
53
53
|
|
54
54
|
def robots_text
|
55
55
|
res.headers['content-type'] = 'text/plain'
|
56
|
-
res.body
|
56
|
+
res.body = ['User-agent: *', 'Disallow: /private'].join($/)
|
57
57
|
end
|
58
58
|
|
59
59
|
def display_product
|
60
60
|
res.headers['content-type'] = 'application/json; charset=utf-8'
|
61
|
-
prodid
|
62
|
-
res.body
|
61
|
+
prodid = req.params[:prodid]
|
62
|
+
res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
|
63
63
|
end
|
64
64
|
|
65
65
|
def not_found
|
66
66
|
res.status = 404
|
67
|
-
content
|
68
|
-
content
|
69
|
-
res.body
|
67
|
+
content = otto_alert('error', 'Not Found', 'The requested page could not be found.')
|
68
|
+
content += "<p>#{otto_link('← Home', '/')}</p>"
|
69
|
+
res.body = otto_page(content, '404')
|
70
70
|
end
|
71
71
|
|
72
72
|
def server_error
|
73
73
|
res.status = 500
|
74
|
-
content
|
75
|
-
content
|
76
|
-
res.body
|
74
|
+
content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
|
75
|
+
content += "<p>#{otto_link('← Home', '/')}</p>"
|
76
|
+
res.body = otto_page(content, '500')
|
77
77
|
end
|
78
78
|
end
|
data/examples/basic/config.ru
CHANGED
@@ -8,8 +8,8 @@ class App
|
|
8
8
|
attr_reader :req, :res
|
9
9
|
|
10
10
|
def initialize(req, res)
|
11
|
-
@req
|
12
|
-
@res
|
11
|
+
@req = req
|
12
|
+
@res = res
|
13
13
|
res.headers['content-type'] = 'text/html; charset=utf-8'
|
14
14
|
end
|
15
15
|
|
@@ -23,19 +23,19 @@ class App
|
|
23
23
|
<p>Minimal Ruby web framework with style</p>
|
24
24
|
</div>
|
25
25
|
|
26
|
-
#{otto_card(
|
26
|
+
#{otto_card('Dynamic Pages') do
|
27
27
|
# This is a nested heredoc within the outer HTML heredoc
|
28
28
|
# It uses a different delimiter (EXAMPLES) to avoid conflicts
|
29
29
|
# The #{} interpolation allows the inner heredoc to be embedded
|
30
30
|
<<~EXAMPLES
|
31
|
-
#{otto_link(
|
32
|
-
#{otto_link(
|
33
|
-
#{otto_link(
|
31
|
+
#{otto_link('Product #100', '/product/100')} - View product page<br>
|
32
|
+
#{otto_link('Product #42', '/product/42')} - Different product<br>
|
33
|
+
#{otto_link('API Data', '/product/100.json')} - JSON endpoint
|
34
34
|
EXAMPLES
|
35
35
|
end}
|
36
36
|
HTML
|
37
37
|
|
38
|
-
res.
|
38
|
+
res.send_secure_cookie :sess, 1_234_567, 3600
|
39
39
|
res.body = otto_page(content)
|
40
40
|
end
|
41
41
|
|
@@ -43,14 +43,14 @@ class App
|
|
43
43
|
message = req.params['msg']&.strip
|
44
44
|
|
45
45
|
content = if message.nil? || message.empty?
|
46
|
-
otto_alert(
|
46
|
+
otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
|
47
47
|
else
|
48
|
-
otto_alert(
|
49
|
-
|
48
|
+
otto_alert('success', 'Feedback Received', 'Thanks for your message!') +
|
49
|
+
otto_card('Your Message') { otto_code_block(message, 'text') }
|
50
50
|
end
|
51
51
|
|
52
|
-
content += "<p>#{otto_link(
|
53
|
-
res.body = otto_page(content,
|
52
|
+
content += "<p>#{otto_link('← Back', '/')}</p>"
|
53
|
+
res.body = otto_page(content, 'Feedback')
|
54
54
|
end
|
55
55
|
|
56
56
|
def redirect
|
@@ -59,7 +59,7 @@ class App
|
|
59
59
|
|
60
60
|
def robots_text
|
61
61
|
res.headers['content-type'] = 'text/plain'
|
62
|
-
res.body
|
62
|
+
res.body = ['User-agent: *', 'Disallow: /private'].join($/)
|
63
63
|
end
|
64
64
|
|
65
65
|
def display_product
|
@@ -71,27 +71,27 @@ class App
|
|
71
71
|
|
72
72
|
if wants_json
|
73
73
|
res.headers['content-type'] = 'application/json; charset=utf-8'
|
74
|
-
res.body
|
74
|
+
res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
|
75
75
|
else
|
76
76
|
# Return HTML product page
|
77
77
|
product_data = {
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
78
|
+
'Product ID' => prodid,
|
79
|
+
'Name' => "Sample Product ##{prodid}",
|
80
|
+
'Price' => "$#{rand(10..999)}.99",
|
81
|
+
'Description' => 'This is a demonstration product showing dynamic routing with parameter :prodid',
|
82
|
+
'Stock' => rand(0..50) > 5 ? 'In Stock' : 'Out of Stock',
|
83
83
|
}
|
84
84
|
|
85
|
-
product_html = product_data.map
|
85
|
+
product_html = product_data.map do |key, value|
|
86
86
|
"<p><strong>#{key}:</strong> #{escape_html(value.to_s)}</p>"
|
87
|
-
|
87
|
+
end.join
|
88
88
|
|
89
89
|
content = <<~HTML
|
90
|
-
#{otto_card(
|
90
|
+
#{otto_card('Product Details') { product_html }}
|
91
91
|
|
92
92
|
<p>
|
93
|
-
#{otto_link(
|
94
|
-
#{otto_link(
|
93
|
+
#{otto_link('← Back to Home', '/')} |
|
94
|
+
#{otto_link('View as JSON', "/product/#{prodid}.json")}
|
95
95
|
</p>
|
96
96
|
HTML
|
97
97
|
|
@@ -101,15 +101,15 @@ class App
|
|
101
101
|
|
102
102
|
def not_found
|
103
103
|
res.status = 404
|
104
|
-
content
|
105
|
-
content
|
106
|
-
res.body
|
104
|
+
content = otto_alert('error', 'Not Found', 'The requested page could not be found.')
|
105
|
+
content += "<p>#{otto_link('← Home', '/')}</p>"
|
106
|
+
res.body = otto_page(content, '404')
|
107
107
|
end
|
108
108
|
|
109
109
|
def server_error
|
110
110
|
res.status = 500
|
111
|
-
content
|
112
|
-
content
|
113
|
-
res.body
|
111
|
+
content = otto_alert('error', 'Server Error', 'An internal server error occurred.')
|
112
|
+
content += "<p>#{otto_link('← Home', '/')}</p>"
|
113
|
+
res.body = otto_page(content, '500')
|
114
114
|
end
|
115
115
|
end
|