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.
data/Gemfile.lock CHANGED
@@ -1,8 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- otto (1.2.0)
5
- addressable (~> 2.2, < 3)
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
- addressable (2.8.7)
14
- public_suffix (>= 2.0.2, < 7.0)
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
- drydock (0.6.9)
20
- json (2.7.2)
21
- language_server-protocol (3.17.0.3)
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.0.0)
24
- parallel (1.24.0)
25
- parser (3.3.0.5)
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.14.2)
42
+ pry (0.15.2)
31
43
  coderay (~> 1.1)
32
44
  method_source (~> 1.0)
33
- pry-byebug (3.10.1)
34
- byebug (~> 11.0)
35
- pry (>= 0.13, < 0.15)
36
- public_suffix (6.0.1)
37
- racc (1.7.3)
38
- rack (3.1.16)
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
- regexp_parser (2.9.0)
47
- rexml (3.3.7)
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.62.1)
80
+ rubocop (1.79.1)
62
81
  json (~> 2.3)
63
- language_server-protocol (>= 3.17.0)
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 (>= 1.8, < 3.0)
68
- rexml (>= 3.2.5, < 4.0)
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, < 3.0)
72
- rubocop-ast (1.31.2)
73
- parser (>= 3.3.0.4)
74
- rubocop-performance (1.23.1)
75
- rubocop (>= 1.48.1, < 2.0)
76
- rubocop-ast (>= 1.31.1, < 2.0)
77
- rubocop-rspec (3.4.0)
78
- rubocop (~> 1.61)
79
- rubocop-thread_safety (0.6.0)
80
- rubocop (>= 1.48.1)
81
- ruby-lsp (0.26.0)
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
- storable (0.10.0)
111
+ stringio (3.1.7)
88
112
  syntax_tree (6.3.0)
89
113
  prettier_print (>= 1.2.0)
90
- sysinfo (0.10.0)
91
- drydock (< 1.0)
92
- storable (~> 0.10)
93
- tryouts (2.2.0)
94
- sysinfo (~> 0.10)
95
- unicode-display_width (2.5.0)
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-22
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.5.7
148
+ 2.6.9
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Otto - 1.2 (2025-08-18)
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.4+
135
+ - Ruby 3.2+
80
136
  - Rack 3.1+
81
137
 
82
138
  ## Installation
@@ -8,8 +8,8 @@ class App
8
8
  attr_reader :req, :res
9
9
 
10
10
  def initialize(req, res)
11
- @req = req
12
- @res = 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("Send Feedback") do
24
+ #{otto_card('Send Feedback') do
25
25
  otto_form_wrapper do
26
- otto_input("msg", placeholder: "Your message...") +
27
- otto_button("Send Feedback")
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.send_cookie :sess, 1_234_567, 3600
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("error", "Empty Message", "Please enter a message before submitting.")
40
+ otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
41
41
  else
42
- otto_alert("success", "Feedback Received", "Thanks for your message!") +
43
- otto_card("Your Message") { otto_code_block(message, 'text') }
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("← Back", "/")}</p>"
47
- res.body = otto_page(content, "Feedback")
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 = ['User-agent: *', 'Disallow: /private'].join($/)
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 = req.params[:prodid]
62
- res.body = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
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 = 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")
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 = otto_alert("error", "Server Error", "An internal server error occurred.")
75
- content += "<p>#{otto_link("← Home", "/")}</p>"
76
- res.body = otto_page(content, "500")
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
@@ -11,7 +11,7 @@ public_path = File.expand_path('../../public', __dir__)
11
11
  require_relative '../../lib/otto'
12
12
  require_relative 'app'
13
13
 
14
- app = Otto.new("routes")
14
+ app = Otto.new('routes')
15
15
 
16
16
  # DEV: Run web apps with extra logging and reloading
17
17
  if Otto.env?(:dev)
@@ -8,8 +8,8 @@ class App
8
8
  attr_reader :req, :res
9
9
 
10
10
  def initialize(req, res)
11
- @req = req
12
- @res = 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("Dynamic Pages") do
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("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
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.send_cookie :sess, 1_234_567, 3600
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("error", "Empty Message", "Please enter a message before submitting.")
46
+ otto_alert('error', 'Empty Message', 'Please enter a message before submitting.')
47
47
  else
48
- otto_alert("success", "Feedback Received", "Thanks for your message!") +
49
- otto_card("Your Message") { otto_code_block(message, 'text') }
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("← Back", "/")}</p>"
53
- res.body = otto_page(content, "Feedback")
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 = ['User-agent: *', 'Disallow: /private'].join($/)
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 = format('{"product":%s,"msg":"Hint: try another value"}', prodid)
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
- "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"
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 { |key, value|
85
+ product_html = product_data.map do |key, value|
86
86
  "<p><strong>#{key}:</strong> #{escape_html(value.to_s)}</p>"
87
- }.join
87
+ end.join
88
88
 
89
89
  content = <<~HTML
90
- #{otto_card("Product Details") { product_html }}
90
+ #{otto_card('Product Details') { product_html }}
91
91
 
92
92
  <p>
93
- #{otto_link("← Back to Home", "/")} |
94
- #{otto_link("View as JSON", "/product/#{prodid}.json")}
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 = 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")
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 = otto_alert("error", "Server Error", "An internal server error occurred.")
112
- content += "<p>#{otto_link("← Home", "/")}</p>"
113
- res.body = otto_page(content, "500")
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
@@ -11,7 +11,7 @@ public_path = File.expand_path('../../public', __dir__)
11
11
  require_relative '../../lib/otto'
12
12
  require_relative 'app'
13
13
 
14
- app = Otto.new("routes")
14
+ app = Otto.new('routes')
15
15
 
16
16
  # DEV: Run web apps with extra logging and reloading
17
17
  if Otto.env?(:dev)