otto 1.1.0.pre.alpha4 → 1.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1ad2b9c043b3b58202eadcae63354ebead1b70178fd7723dc24820462a3831a
4
- data.tar.gz: f95e243be8e6e59c5a17f7ec98d7a05d1a31599e0970e9e20763c569a65acfb8
3
+ metadata.gz: c17a841334ba79be6d1c4625eb8e8e39eda301be3a77813c8ea6386bb3d0d6b5
4
+ data.tar.gz: 2639d6f7ef18aa85bf75646d5c9d97814d1bf3a585e53003de7ccb4e8ce1eccf
5
5
  SHA512:
6
- metadata.gz: 4ed91d559e7e339df4c5a28be0864ff2151e3e06e9dea577dedb346eaf4476e0d62adc84de508d3ff1ea1c55633789643b5f5bcee15d88bd6c8a43c6b7f04a59
7
- data.tar.gz: 7d5f8bfb03f7e9ce91eebf632df7cc5ccf8b804beac9777662ef490507d1dba3a3ed1401d9e498ecb749617b8ab561a73c2226cb66c02f4796e18148b6afa479
6
+ metadata.gz: 40e9f5ed072e4d86dd27c49d397fa3ece8904420a3264166ad187aeea9c26c3818c32c45986903914df664cbb7671c8edabad18601f60013c8f05d2ce742d65f
7
+ data.tar.gz: 95d85cabd00d26658ce816021289ca7a21600dee34ac051ccfa6cf79b8713742d5389e67042d49991a1a63794fe30e3cd76d11bfb7d3ee52a8281e42e184174b
data/.gitignore CHANGED
@@ -16,3 +16,5 @@ log
16
16
  tmp
17
17
  vendor
18
18
  *.gem
19
+ .ruby-lsp
20
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,4 @@
1
+ --color
2
+ --format documentation
3
+ --require spec_helper
4
+ --order random
data/.rubocop.yml CHANGED
@@ -1,3 +1,5 @@
1
+ # .rubocop.yml
2
+
1
3
  ##
2
4
  # This is the RuboCop configuration file.
3
5
  # It contains the rules and settings for the RuboCop linter.
@@ -13,7 +15,7 @@
13
15
  #
14
16
  inherit_from: .rubocop_todo.yml
15
17
 
16
- require:
18
+ plugins:
17
19
  - rubocop-performance
18
20
  - rubocop-thread_safety
19
21
 
@@ -21,13 +23,13 @@ AllCops:
21
23
  NewCops: enable
22
24
  UseCache: true
23
25
  MaxFilesInCache: 100
24
- TargetRubyVersion: 3.0
26
+ TargetRubyVersion: 3.4
25
27
  Exclude:
26
- - 'migrate/**/*.rb'
27
- - 'migrate/*.rb'
28
- - 'try/**/*'
29
- - 'try/*.rb'
30
- - 'vendor/**/*'
28
+ - "migrate/**/*.rb"
29
+ - "migrate/*.rb"
30
+ - "try/**/*"
31
+ - "try/*.rb"
32
+ - "vendor/**/*"
31
33
 
32
34
  Gemspec/DeprecatedAttributeAssignment:
33
35
  Enabled: true
@@ -55,12 +57,12 @@ Metrics/CyclomaticComplexity:
55
57
  Metrics/MethodLength:
56
58
  Enabled: true
57
59
  Max: 40
58
- CountAsOne: ['method_call']
60
+ CountAsOne: ["method_call"]
59
61
 
60
62
  Metrics/ModuleLength:
61
63
  Enabled: true
62
64
  Max: 250
63
- CountAsOne: ['method_call']
65
+ CountAsOne: ["method_call"]
64
66
 
65
67
  Performance/Size:
66
68
  Enabled: true
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,152 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2025-07-18 21:29:04 UTC using RuboCop version 1.78.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 2
10
+ # This cop supports unsafe autocorrection (--autocorrect-all).
11
+ # Configuration parameters: AllowSafeAssignment.
12
+ Lint/AssignmentInCondition:
13
+ Exclude:
14
+ - 'lib/otto.rb'
15
+
16
+ # Offense count: 1
17
+ # Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns.
18
+ Metrics/MethodLength:
19
+ Exclude:
20
+ - 'lib/otto.rb'
21
+
22
+ # Offense count: 5
23
+ # Configuration parameters: AllowedMethods, AllowedPatterns.
24
+ Metrics/PerceivedComplexity:
25
+ Max: 36
26
+
27
+ # Offense count: 1
28
+ Security/Eval:
29
+ Exclude:
30
+ - 'lib/otto/route.rb'
31
+
32
+ # Offense count: 6
33
+ # Configuration parameters: AllowedConstants.
34
+ Style/Documentation:
35
+ Exclude:
36
+ - 'spec/**/*'
37
+ - 'test/**/*'
38
+ - 'example/app.rb'
39
+ - 'lib/otto.rb'
40
+ - 'lib/otto/helpers/request.rb'
41
+ - 'lib/otto/helpers/response.rb'
42
+ - 'lib/otto/route.rb'
43
+ - 'lib/otto/static.rb'
44
+
45
+ # Offense count: 9
46
+ # This cop supports unsafe autocorrection (--autocorrect-all).
47
+ # Configuration parameters: EnforcedStyle.
48
+ # SupportedStyles: always, always_true, never
49
+ Style/FrozenStringLiteralComment:
50
+ Exclude:
51
+ - '**/*.arb'
52
+ - 'example/app.rb'
53
+ - 'example/config.ru'
54
+ - 'lib/otto.rb'
55
+ - 'lib/otto/helpers/request.rb'
56
+ - 'lib/otto/helpers/response.rb'
57
+ - 'lib/otto/route.rb'
58
+ - 'lib/otto/static.rb'
59
+ - 'lib/otto/version.rb'
60
+ - 'otto.gemspec'
61
+
62
+ # Offense count: 1
63
+ # This cop supports unsafe autocorrection (--autocorrect-all).
64
+ # Configuration parameters: EnforcedStyle, Autocorrect.
65
+ # SupportedStyles: module_function, extend_self, forbidden
66
+ Style/ModuleFunction:
67
+ Exclude:
68
+ - 'lib/otto/static.rb'
69
+
70
+ # Offense count: 2
71
+ Style/MultilineBlockChain:
72
+ Exclude:
73
+ - 'lib/otto.rb'
74
+
75
+ # Offense count: 2
76
+ # This cop supports unsafe autocorrection (--autocorrect-all).
77
+ # Configuration parameters: EnforcedStyle.
78
+ # SupportedStyles: literals, strict
79
+ Style/MutableConstant:
80
+ Exclude:
81
+ - 'example/config.ru'
82
+
83
+ # Offense count: 1
84
+ # Configuration parameters: AllowedMethods.
85
+ # AllowedMethods: respond_to_missing?
86
+ Style/OptionalBooleanParameter:
87
+ Exclude:
88
+ - 'lib/otto/helpers/response.rb'
89
+
90
+ # Offense count: 1
91
+ # This cop supports unsafe autocorrection (--autocorrect-all).
92
+ # Configuration parameters: EnforcedStyle.
93
+ # SupportedStyles: short, verbose
94
+ Style/PreferredHashMethods:
95
+ Exclude:
96
+ - 'lib/otto.rb'
97
+
98
+ # Offense count: 1
99
+ # This cop supports unsafe autocorrection (--autocorrect-all).
100
+ Style/RedundantFilterChain:
101
+ Exclude:
102
+ - 'lib/otto.rb'
103
+
104
+ # Offense count: 1
105
+ # This cop supports unsafe autocorrection (--autocorrect-all).
106
+ Style/RedundantInterpolation:
107
+ Exclude:
108
+ - 'example/config.ru'
109
+
110
+ # Offense count: 1
111
+ # This cop supports unsafe autocorrection (--autocorrect-all).
112
+ Style/SelectByRegexp:
113
+ Exclude:
114
+ - 'lib/otto.rb'
115
+
116
+ # Offense count: 1
117
+ # This cop supports unsafe autocorrection (--autocorrect-all).
118
+ Style/SlicingWithRange:
119
+ Exclude:
120
+ - 'lib/otto/route.rb'
121
+
122
+ # Offense count: 4
123
+ # This cop supports unsafe autocorrection (--autocorrect-all).
124
+ # Configuration parameters: RequireEnglish.
125
+ # SupportedStyles: use_perl_names, use_english_names, use_builtin_english_names
126
+ Style/SpecialGlobalVars:
127
+ EnforcedStyle: use_perl_names
128
+
129
+ # Offense count: 1
130
+ # Configuration parameters: ActiveSupportClassAttributeAllowed.
131
+ ThreadSafety/ClassAndModuleAttributes:
132
+ Exclude:
133
+ - 'lib/otto.rb'
134
+
135
+ # Offense count: 9
136
+ ThreadSafety/ClassInstanceVariable:
137
+ Exclude:
138
+ - 'lib/otto.rb'
139
+ - 'lib/otto/version.rb'
140
+
141
+ # Offense count: 1
142
+ # Configuration parameters: AllowCallWithBlock.
143
+ ThreadSafety/DirChdir:
144
+ Exclude:
145
+ - 'otto.gemspec'
146
+
147
+ # Offense count: 1
148
+ # This cop supports safe autocorrection (--autocorrect).
149
+ # Configuration parameters: AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
150
+ # URISchemes: http, https
151
+ Layout/LineLength:
152
+ Max: 135
data/Gemfile CHANGED
@@ -1,11 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
3
+ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
- group "development" do
8
- gem "pry-byebug"
9
- gem "rubocop"
10
- gem "tryouts"
7
+ group :development, :test do
8
+ gem 'rspec', '~> 3.12'
9
+ gem 'rack-test'
10
+ end
11
+
12
+ group 'development' do
13
+ gem 'pry-byebug', require: false
14
+ gem 'rubocop', require: false
15
+ gem 'rubocop-performance', require: false
16
+ gem 'rubocop-rspec', require: false
17
+ gem 'rubocop-thread_safety', require: false
18
+ gem 'ruby-lsp', require: false
19
+ gem 'stackprof', require: false
20
+ gem 'syntax_tree', require: false
21
+ gem 'tryouts', require: false
11
22
  end
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- otto (1.1.0.pre.alpha3)
4
+ otto (1.2.0)
5
5
  addressable (~> 2.2, < 3)
6
- rack (~> 2.2, < 3.0)
6
+ rack (~> 3.1, < 4.0)
7
+ rack-parser (~> 0.7)
7
8
  rexml (>= 3.3.6)
8
9
 
9
10
  GEM
@@ -14,14 +15,18 @@ GEM
14
15
  ast (2.4.2)
15
16
  byebug (11.1.3)
16
17
  coderay (1.1.3)
18
+ diff-lcs (1.6.2)
17
19
  drydock (0.6.9)
18
20
  json (2.7.2)
19
21
  language_server-protocol (3.17.0.3)
22
+ logger (1.7.0)
20
23
  method_source (1.0.0)
21
24
  parallel (1.24.0)
22
25
  parser (3.3.0.5)
23
26
  ast (~> 2.4.1)
24
27
  racc
28
+ prettier_print (1.2.1)
29
+ prism (1.4.0)
25
30
  pry (0.14.2)
26
31
  coderay (~> 1.1)
27
32
  method_source (~> 1.0)
@@ -30,10 +35,29 @@ GEM
30
35
  pry (>= 0.13, < 0.15)
31
36
  public_suffix (6.0.1)
32
37
  racc (1.7.3)
33
- rack (2.2.9)
38
+ rack (3.1.16)
39
+ rack-parser (0.7.0)
40
+ rack
41
+ rack-test (2.2.0)
42
+ rack (>= 1.3)
34
43
  rainbow (3.1.1)
44
+ rbs (3.9.4)
45
+ logger
35
46
  regexp_parser (2.9.0)
36
47
  rexml (3.3.7)
48
+ rspec (3.13.1)
49
+ rspec-core (~> 3.13.0)
50
+ rspec-expectations (~> 3.13.0)
51
+ rspec-mocks (~> 3.13.0)
52
+ rspec-core (3.13.5)
53
+ rspec-support (~> 3.13.0)
54
+ rspec-expectations (3.13.5)
55
+ diff-lcs (>= 1.2.0, < 2.0)
56
+ rspec-support (~> 3.13.0)
57
+ rspec-mocks (3.13.5)
58
+ diff-lcs (>= 1.2.0, < 2.0)
59
+ rspec-support (~> 3.13.0)
60
+ rspec-support (3.13.4)
37
61
  rubocop (1.62.1)
38
62
  json (~> 2.3)
39
63
  language_server-protocol (>= 3.17.0)
@@ -47,8 +71,22 @@ GEM
47
71
  unicode-display_width (>= 2.4.0, < 3.0)
48
72
  rubocop-ast (1.31.2)
49
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)
82
+ language_server-protocol (~> 3.17.0)
83
+ prism (>= 1.2, < 2.0)
84
+ rbs (>= 3, < 5)
50
85
  ruby-progressbar (1.13.0)
86
+ stackprof (0.2.27)
51
87
  storable (0.10.0)
88
+ syntax_tree (6.3.0)
89
+ prettier_print (>= 1.2.0)
52
90
  sysinfo (0.10.0)
53
91
  drydock (< 1.0)
54
92
  storable (~> 0.10)
@@ -63,7 +101,15 @@ PLATFORMS
63
101
  DEPENDENCIES
64
102
  otto!
65
103
  pry-byebug
104
+ rack-test
105
+ rspec (~> 3.12)
66
106
  rubocop
107
+ rubocop-performance
108
+ rubocop-rspec
109
+ rubocop-thread_safety
110
+ ruby-lsp
111
+ stackprof
112
+ syntax_tree
67
113
  tryouts
68
114
 
69
115
  BUNDLED WITH
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2011-2024 delano
3
+ Copyright (c) 2011 Delano Mandelbaum
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,150 +1,99 @@
1
- # Otto - 1.0 (2024-04-05)
1
+ # Otto - 1.2 (2025-08-18)
2
2
 
3
- **Auto-define your rack-apps in plain-text.**
3
+ **Define your rack-apps in plain-text with built-in security.**
4
4
 
5
- ## Overview
5
+ ![Otto mascot](public/img/otto.jpg "Otto - All Rack, no Pinion")
6
6
 
7
- Apps built with Otto have three, basic parts: a rackup file, a ruby file, and a routes file. If you've built a [Rack app](https://github.com/rack/rack) before, then you've seen a rackup file before. The ruby file is your actual app and the routes file is what Otto uses to map URI paths to a Ruby class and method.
8
-
9
- A barebones app directory looks something like this:
7
+ Otto apps have three files: a rackup file, a Ruby class, and a routes file. The routes file is just plain text that maps URLs to Ruby methods.
10
8
 
11
9
  ```bash
12
- $ cd myapp
13
- $ ls
14
- config.ru app.rb routes
10
+ $ cd myapp && ls
11
+ config.ru app.rb routes
15
12
  ```
16
13
 
17
- See the examples/ directory for a working app.
14
+ ## Routes File
15
+ ```
16
+ # routes
18
17
 
18
+ GET / App#index
19
+ POST /feedback App#receive_feedback
20
+ GET /product/:id App#show_product
21
+ GET /robots.txt App#robots_text
22
+ GET /404 App#not_found
23
+ ```
19
24
 
20
- ### Routes
25
+ ## Ruby Class
26
+ ```ruby
27
+ # app.rb
21
28
 
22
- The routes file is just a plain-text file which defines the end points of your application. Each route has three parts:
29
+ class App
30
+ def initialize(req, res)
31
+ @req, @res = req, res
32
+ end
23
33
 
24
- * HTTP verb (GET, POST, PUT, DELETE or HEAD)
25
- * URI path
26
- * Ruby class and method to call
34
+ def index
35
+ res.body = '<h1>Hello Otto</h1>'
36
+ end
27
37
 
28
- Here is an example:
38
+ def show_product
39
+ product_id = req.params[:id]
40
+ res.body = "Product: #{product_id}"
41
+ end
29
42
 
30
- ```ruby
31
- GET / App#index
32
- POST / App#receive_feedback
33
- GET /redirect App#redirect
34
- GET /robots.txt App#robots_text
35
- GET /product/:prodid App#display_product
36
-
37
- # You can also define these handlers when no
38
- # route can be found or there's a server error. (optional)
39
- GET /404 App#not_found
40
- GET /500 App#server_error
43
+ def robots_text
44
+ res.header['Content-Type'] = "text/plain"
45
+ rules = 'User-agent: *', 'Disallow: /private/keep/out'
46
+ res.body = rules.join($/)
47
+ end
48
+ end
41
49
  ```
42
50
 
43
- ### App
51
+ ## Rackup File
52
+ ```ruby
53
+ # config.ru
44
54
 
45
- There is nothing special about the Ruby class. The only requirement is that the first two arguments to initialize be a Rack::Request object and a Rack::Response object. Otherwise, you can do anything you want. You're free to use any templating engine, any database mapper, etc. There is no magic.
55
+ require 'otto'
56
+ require 'app'
46
57
 
47
- ```ruby
48
- class App
49
- attr_reader :req, :res
50
-
51
- # Otto creates an instance of this class for every request
52
- # and passess the Rack::Request and Rack::Response objects.
53
- def initialize req, res
54
- @req, @res = req, res
55
- end
56
-
57
- def index
58
- res.header['Content-Type'] = "text/html; charset=utf-8"
59
- lines = [
60
- '<img src="/img/otto.jpg" /><br/><br/>',
61
- 'Send feedback:<br/>',
62
- '<form method="post"><input name="msg" /><input type="submit" /></form>',
63
- '<a href="/product/100">A product example</a>'
64
- ]
65
- res.body = lines.join($/)
66
- end
67
-
68
- def receive_feedback
69
- res.body = req.params.inspect
70
- end
71
-
72
- def redirect
73
- res.redirect '/robots.txt'
74
- end
75
-
76
- def robots_text
77
- res.header['Content-Type'] = "text/plain"
78
- rules = 'User-agent: *', 'Disallow: /private'
79
- res.body = rules.join($/)
80
- end
81
-
82
- def display_product
83
- res.header['Content-Type'] = "application/json; charset=utf-8"
84
- prodid = req.params[:prodid]
85
- res.body = '{"product":%s,"msg":"Hint: try another value"}' % [prodid]
86
- end
87
-
88
- def not_found
89
- res.status = 404
90
- res.body = "Item not found!"
91
- end
92
-
93
- def server_error
94
- res.status = 500
95
- res.body = "There was a server error!"
96
- end
97
- end
58
+ run Otto.new("./routes")
98
59
  ```
99
60
 
100
61
 
101
- ### Rackup
62
+ ## Security Features
102
63
 
103
- There is also nothing special about the rackup file. It just builds a Rack app using your routes file.
64
+ Otto includes optional security features for production apps:
104
65
 
105
66
  ```ruby
106
- require 'otto'
107
- require 'app'
108
-
109
- app = Otto.new("./routes")
110
-
111
- map('/') {
112
- run app
113
- }
67
+ # Enable security features
68
+ app = Otto.new("./routes", {
69
+ csrf_protection: true, # CSRF tokens and validation
70
+ request_validation: true, # Input sanitization and limits
71
+ trusted_proxies: ['10.0.0.0/8']
72
+ })
114
73
  ```
115
74
 
116
- See the examples/ directory for a working app.
75
+ Security features include CSRF protection, input validation, security headers, and trusted proxy configuration.
117
76
 
77
+ ## Requirements
118
78
 
119
- ## Installation
79
+ - Ruby 3.4+
80
+ - Rack 3.1+
120
81
 
121
- Get it in one of the following ways:
82
+ ## Installation
122
83
 
123
84
  ```bash
124
- $ gem install otto
125
-
126
- [ Add it to yer Gemfile]
127
- $ bundle install
128
-
129
- $ git clone git://github.com/delano/otto.git
85
+ gem install otto
130
86
  ```
131
87
 
88
+ ## AI Development Assistance
132
89
 
133
- You can also download via [tarball](https://github.com/delano/otto/tarball/latest) or [zip](https://github.com/delano/otto/zipball/latest).
134
-
135
-
136
- ## More Information
137
-
138
- * [Homepage](https://github.com/delano/otto)
139
-
140
-
141
- ## In the wild
142
-
143
- Services that use Otto:
144
-
145
- * [Onetime Secret](https://onetimesecret.com/) -- A safe way to share sensitive data.
90
+ Version 1.2.0's security features were developed with AI assistance:
146
91
 
92
+ * **Zed Agent (Claude Sonnet 4)** - Security implementation and testing
93
+ * **Claude Desktop** - Rack 3+ compatibility and debugging
94
+ * **GitHub Copilot** - Code completion
147
95
 
96
+ The maintainer remains responsible for all security decisions and implementation. We believe in transparency about development tools, especially for security-focused software.
148
97
 
149
98
  ## License
150
99
 
data/VERSION.yml CHANGED
@@ -1,4 +1,5 @@
1
+ # VERSION.yml
2
+ ---
1
3
  :MAJOR: 1
2
- :MINOR: 1
4
+ :MINOR: 2
3
5
  :PATCH: 0
4
- :PRE: alpha4
@@ -0,0 +1,78 @@
1
+ # examples/basic/app.rb (Streamlined with Design System)
2
+
3
+ require_relative '../../lib/otto/design_system'
4
+
5
+ class App
6
+ include Otto::DesignSystem
7
+
8
+ attr_reader :req, :res
9
+
10
+ def initialize(req, res)
11
+ @req = req
12
+ @res = res
13
+ res.headers['content-type'] = 'text/html; charset=utf-8'
14
+ end
15
+
16
+ def index
17
+ content = <<~HTML
18
+ <div class="otto-card otto-text-center">
19
+ <img src="/img/otto.jpg" alt="Otto Framework" class="otto-logo" />
20
+ <h1>Otto Framework</h1>
21
+ <p>Minimal Ruby web framework with style</p>
22
+ </div>
23
+
24
+ #{otto_card("Send Feedback") do
25
+ otto_form_wrapper do
26
+ otto_input("msg", placeholder: "Your message...") +
27
+ otto_button("Send Feedback")
28
+ end
29
+ end}
30
+ HTML
31
+
32
+ res.send_cookie :sess, 1_234_567, 3600
33
+ res.body = otto_page(content)
34
+ end
35
+
36
+ def receive_feedback
37
+ message = req.params['msg']&.strip
38
+
39
+ content = if message.nil? || message.empty?
40
+ otto_alert("error", "Empty Message", "Please enter a message before submitting.")
41
+ else
42
+ otto_alert("success", "Feedback Received", "Thanks for your message!") +
43
+ otto_card("Your Message") { otto_code_block(message, 'text') }
44
+ end
45
+
46
+ content += "<p>#{otto_link("← Back", "/")}</p>"
47
+ res.body = otto_page(content, "Feedback")
48
+ end
49
+
50
+ def redirect
51
+ res.redirect '/robots.txt'
52
+ end
53
+
54
+ def robots_text
55
+ res.headers['content-type'] = 'text/plain'
56
+ res.body = ['User-agent: *', 'Disallow: /private'].join($/)
57
+ end
58
+
59
+ def display_product
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)
63
+ end
64
+
65
+ def not_found
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")
70
+ end
71
+
72
+ def server_error
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")
77
+ end
78
+ end