itsi-server 0.2.2 → 0.2.3

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.
@@ -9,7 +9,7 @@ module Itsi
9
9
  attr_reader :parent, :children, :middleware, :controller, :routes, :http_methods, :protocols,
10
10
  :hosts, :ports, :extensions, :content_types, :accepts, :options
11
11
 
12
- def self.evaluate(config = Itsi::Server::Config.config_file_path, &blk)
12
+ def self.evaluate(config = Itsi::Server::Config.config_file_path, &blk) # rubocop:disable Metrics/MethodLength
13
13
  config = new(routes: ["/"]) do
14
14
  if blk
15
15
  instance_exec(&blk)
@@ -17,14 +17,14 @@ module Itsi
17
17
  code = IO.read(config)
18
18
  instance_eval(code, config.to_s, 1)
19
19
  end
20
- location("*"){}
20
+ location("*") {}
21
21
  end
22
22
  [config.options, config.errors]
23
- rescue Exception => e
23
+ rescue Exception => e # rubocop:disable Lint/RescueException
24
24
  [{}, [[e, e.backtrace[0]]]]
25
25
  end
26
26
 
27
- def initialize(
27
+ def initialize( # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
28
28
  parent = nil,
29
29
  routes: [],
30
30
  methods: [],
@@ -57,12 +57,7 @@ module Itsi
57
57
  @options[:middleware_loaders].each(&:call)
58
58
  @middleware[:app] ||= {}
59
59
  @middleware[:app][:app_proc] = @middleware[:app]&.[](:preloader)&.call || DEFAULT_APP[]
60
- if errors.any?
61
- error = errors.first.first
62
- error.set_backtrace(error.backtrace.drop_while{|r| r =~ /itsi\/server\/config/ })
63
- raise error
64
- end
65
- flatten_routes
60
+ [flatten_routes, Config.errors_to_error_lines(errors)]
66
61
  end
67
62
  }
68
63
 
@@ -78,7 +73,7 @@ module Itsi
78
73
  option_name = option.option_name
79
74
  define_method(option_name) do |*args, **kwargs, &blk|
80
75
  option.new(self, *args, **kwargs, &blk).build!
81
- rescue => e
76
+ rescue Exception => e # rubocop:disable Lint/RescueException
82
77
  @errors << [e, caller[1]]
83
78
  end
84
79
  end
@@ -89,7 +84,7 @@ module Itsi
89
84
  middleware.new(self, *args, **kwargs, &blk).build!
90
85
  rescue Config::Endpoint::InvalidHandlerException => e
91
86
  @errors << [e, "#{e.backtrace[0]}:in #{e.message}"]
92
- rescue => e
87
+ rescue Exception => e # rubocop:disable Lint/RescueException
93
88
  @errors << [e, caller[1]]
94
89
  end
95
90
  end
@@ -99,7 +94,7 @@ module Itsi
99
94
  @grpc_reflected_services.concat(handlers)
100
95
 
101
96
  location("grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
102
- "grpc.reflection.v1.ServerReflection/ServerReflectionInfo") do
97
+ "grpc.reflection.v1.ServerReflection/ServerReflectionInfo") do
103
98
  @middleware[:app] = { preloader: lambda {
104
99
  Itsi::Server::GrpcInterface.reflection_for(handlers)
105
100
  }, request_type: "grpc" }
@@ -141,7 +136,7 @@ module Itsi
141
136
  when Regexp
142
137
  seg.source
143
138
  else
144
- parts = seg.split('/')
139
+ parts = seg.split("/")
145
140
  parts.map do |part|
146
141
  case part
147
142
  when /^:([A-Za-z_]\w*)(?:\(([^)]*)\))?$/
@@ -184,11 +179,11 @@ module Itsi
184
179
 
185
180
  chain.each do |n|
186
181
  n.middleware.each do |k, v|
187
- if v[:combine]
188
- merged[k] = ([merged[k] || []] + [v]).flatten
189
- else
190
- merged[k] = v
191
- end
182
+ merged[k] = if v[:combine]
183
+ ([merged[k] || []] + [v]).flatten
184
+ else
185
+ v
186
+ end
192
187
  end
193
188
  end
194
189
  deep_stringify_keys(merged)
@@ -2,16 +2,20 @@ module Itsi
2
2
  class Server
3
3
  module KnownPaths
4
4
  ALL = []
5
- Dir.glob(File.join(__dir__, 'known_paths', '**', '*.txt')).each do |file|
6
- method_name = file[/known_paths\/(.*?)\.txt/,1].gsub(/([a-z])([A-Z])/, "\\1_\\2")
7
- .gsub(/-|\.|\//, "_")
8
- .gsub(/(^|\/)[0-9]/){|match| "FO"}.downcase.to_sym
5
+ Dir.glob(File.join(__dir__, "known_paths", "**", "*.txt")).each do |file|
6
+ method_name = file[%r{known_paths/(.*?)\.txt}, 1].gsub(/([a-z])([A-Z])/, "\\1_\\2")
7
+ .gsub(%r{-|\.|/}, "_")
8
+ .gsub(%r{(^|/)[0-9]}) do |match|
9
+ match.gsub(/\d/) do |digit|
10
+ %w[zero one two three four five six seven eight nine][digit.to_i]
11
+ end
12
+ end.downcase.to_sym
9
13
 
10
14
  ALL << method_name
11
- self.define_singleton_method(method_name) do
15
+ define_singleton_method(method_name) do
12
16
  File.readlines(file).map do |s|
13
- s.force_encoding('UTF-8')
14
- s.valid_encoding? ? s.strip : s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '').strip
17
+ s.force_encoding("UTF-8")
18
+ s.valid_encoding? ? s.strip : s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "").strip
15
19
  end
16
20
  end
17
21
  end
@@ -34,6 +34,19 @@ E.g.
34
34
  auth_api_key .. other options.., error_response: 'forbidden'
35
35
  ```
36
36
 
37
+ ## Example of built-in response
38
+ ### HTML
39
+ {{< card title="Built-in error page" image="/error_page.jpg" subtitle="Default Itsi Error Page." method="Resize" options="10x q80 webp" >}}
40
+ ### JSON
41
+ ```json
42
+ {
43
+ "error": "Too Many Requests",
44
+ "message": "Too many requests within a limited time frame.",
45
+ "code": 429,
46
+ "status": "error"
47
+ }
48
+ ```
49
+
37
50
  ## Override the error response
38
51
  You may instead wish to completely override the error response. You can provide a status code, and a message in up to three
39
52
  formats: plain-text, JSON, or HTML (at least one must be provided). Itsi will serve the appropriate type based on the `Accept` header of the incoming request, or fall back to the default if the requested type is not available.
@@ -20,6 +20,46 @@ It can auto-index directories for simple directory listings.
20
20
  static_assets root_dir: "./"
21
21
  ```
22
22
 
23
+ ### Directory Index
24
+ #### HTML
25
+
26
+ {{< card link="/" title="Static File Server" image="/directory_listing.jpg" subtitle="Static File Listing, Powered by Itsi." method="Resize" options="500x q80 webp" >}}
27
+
28
+ #### JSON
29
+ Directory indexes also support responding in JSON format. E.g.
30
+
31
+ `curl -H "Accept: application/json" http://0.0.0.0`
32
+
33
+ ```json
34
+ {
35
+ "directory": ".",
36
+ "items": [
37
+ {
38
+ "is_dir": false,
39
+ "modified": "2025-04-22 04:21:43",
40
+ "name": "Gemfile",
41
+ "path": "Gemfile",
42
+ "size": "42 B"
43
+ },
44
+ {
45
+ "is_dir": false,
46
+ "modified": "2025-04-22 04:21:48",
47
+ "name": "Gemfile.lock",
48
+ "path": "Gemfile%2Elock",
49
+ "size": "463 B"
50
+ },
51
+ {
52
+ "is_dir": false,
53
+ "modified": "2025-04-22 02:46:45",
54
+ "name": "Itsi.rb",
55
+ "path": "Itsi%2Erb",
56
+ "size": "80 B"
57
+ }
58
+ ],
59
+ "title": "Directory listing for ."
60
+ }
61
+ ```
62
+
23
63
  ## Configuration Options
24
64
 
25
65
 
@@ -4,7 +4,6 @@ module Itsi
4
4
  class Option
5
5
  include ConfigHelpers
6
6
 
7
-
8
7
  def build!
9
8
  location.options[self.class.option_name] = @params
10
9
  end
@@ -8,9 +8,9 @@ This option determines whether the Nagle's algorithm is disabled, allowing small
8
8
 
9
9
  ## Configuration
10
10
  ```ruby {filename=Itsi.rb}
11
- reuse_address true
11
+ nodelay true
12
12
  ```
13
13
 
14
14
  ```ruby {filename=Itsi.rb}
15
- reuse_address false
15
+ nodelay false
16
16
  ```
@@ -4,7 +4,7 @@ url: /options/reuse_address
4
4
  ---
5
5
 
6
6
  Configures whether the server should bind to the underlying socket using the `SO_REUSEADDR` option.
7
- This optiondetermines whether the server allows the reuse of local addresses during binding. This can be useful in scenarios where a socket needs to be quickly rebound without waiting for the operating system to release the address.
7
+ This option determines whether the server allows the reuse of local addresses during binding. This can be useful in scenarios where a socket needs to be quickly rebound without waiting for the operating system to release the address.
8
8
 
9
9
  ## Configuration
10
10
  ```ruby {filename=Itsi.rb}
@@ -1,5 +1,7 @@
1
- require 'date'
2
- require 'time'
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
3
5
 
4
6
  module Itsi
5
7
  class Server
@@ -10,25 +12,19 @@ module Itsi
10
12
 
11
13
  def self.new(defaults = nil, &defaults_blk)
12
14
  defaults = TypedStruct.module_eval(&defaults_blk) if defaults_blk
13
- unless defaults.is_a?(Hash)
14
- return defaults
15
- end
15
+ return defaults unless defaults.is_a?(Hash)
16
+
16
17
  defaults.transform_values! { _1.is_a?(Validation) ? _1.default(nil) : _1 }
17
18
  Struct.new(*defaults.keys, keyword_init: true) do
18
19
  define_method(:initialize) do |*input, validate: true, **raw_input|
20
+ raise "─ Invalid input to #{self}: #{input.last}" if input.last && !input.last.is_a?(Hash)
19
21
 
20
- if input.last && !input.last.is_a?(Hash)
21
- raise "─ Invalid input to #{self}: #{input.last}"
22
- end
23
- raw_input.transform_keys!{|k| k.to_s.downcase.to_sym }
24
- raw_input.merge!(input.pop.transform_keys!{|k| k.to_s.downcase.to_sym}) if input.last.is_a?(Hash)
22
+ raw_input.transform_keys! { |k| k.to_s.downcase.to_sym }
23
+ raw_input.merge!(input.pop.transform_keys! { |k| k.to_s.downcase.to_sym }) if input.last.is_a?(Hash)
25
24
 
26
25
  excess_keys = raw_input.keys - defaults.keys
27
26
 
28
- if excess_keys.any?
29
- raise "─ Unsupported keys #{excess_keys}"
30
-
31
- end
27
+ raise "─ Unsupported keys #{excess_keys}" if excess_keys.any?
32
28
 
33
29
  initial_values = defaults.each_with_object({}) do |(k, default_config), inputs|
34
30
  value = raw_input.key?(k) ? raw_input[k] : default_config[VALUE].dup
@@ -106,9 +102,7 @@ module Itsi
106
102
 
107
103
  def &(other)
108
104
  tail = self
109
- while tail.next
110
- tail = tail.next
111
- end
105
+ tail = tail.next while tail.next
112
106
  tail.next = other
113
107
  self
114
108
  end
@@ -148,10 +142,14 @@ module Itsi
148
142
  elsif validation.eql?(::Date) then Date.parse(value.to_s)
149
143
  elsif validation.eql?(Float) then Float(value)
150
144
  elsif validation.eql?(Integer) then Integer(value)
151
- elsif validation.eql?(Proc) then
145
+ elsif validation.eql?(Proc)
152
146
  raise ArgumentError, "Invalid #{validation} value: #{value.inspect}" unless value.is_a?(Proc)
153
147
  elsif validation.eql?(String) || validation.eql?(Symbol)
154
- raise ArgumentError, "Invalid #{validation} value: #{value.inspect}" unless value.is_a?(String) || value.is_a?(Symbol)
148
+ unless value.is_a?(String) || value.is_a?(Symbol)
149
+ raise ArgumentError,
150
+ "Invalid #{validation} value: #{value.inspect}"
151
+ end
152
+
155
153
  if validation.eql?(String)
156
154
  value.to_s
157
155
  elsif validation.eql?(Symbol)
@@ -178,33 +176,32 @@ module Itsi
178
176
  {
179
177
  Bool: Validation.new(:Bool, [[true, false]]),
180
178
  Required: Validation.new(:Required, ->(value) { !value.nil? }),
181
- Or: ->(*validations){
182
- Validation.new(:Or, ->(v){
183
-
179
+ Or: lambda { |*validations|
180
+ Validation.new(:Or, lambda { |v|
184
181
  return true if v.nil?
182
+
185
183
  errs = []
186
184
  validations.each do |validation|
187
- begin
188
- v = validation.validate!(v)
189
- return v
190
- rescue StandardError => e
191
- errs << e.message
192
- end
185
+ v = validation.validate!(v)
186
+ return v
187
+ rescue StandardError => e
188
+ errs << e.message
193
189
  end
194
190
  raise StandardError.new("─ Validation failed (None match:) \n └#{errs.join("\n └")}")
195
191
  })
196
192
  },
197
- Range: ->(input_range) {
193
+ Range: lambda { |input_range|
198
194
  Validation.new(:Range, [input_range])
199
195
  },
200
196
  Length: lambda { |input_length|
201
197
  Validation.new(:Length, ->(value) { input_length === value.length })
202
198
  },
203
- Hash: ->(key_type, value_type) {
204
- Validation.new(:Hash, ->(v){
205
- return true if v.nil?
206
- raise StandardError.new("Expected hash got #{v.class}") unless v.is_a?(Hash)
207
- v.map do |k, v|
199
+ Hash: lambda { |key_type, value_type|
200
+ Validation.new(:Hash, lambda { |hash|
201
+ return true if hash.nil?
202
+ raise StandardError.new("Expected hash got #{hash.class}") unless hash.is_a?(Hash)
203
+
204
+ hash.map do |k, v|
208
205
  [
209
206
  key_type.validate!(k),
210
207
  value_type.validate!(v)
@@ -213,7 +210,7 @@ module Itsi
213
210
  })
214
211
  },
215
212
  Type: ->(input_type) { Validation.new(:Type, input_type) },
216
- Enum: ->(allowed_values) { Validation.new(:Enum, [allowed_values.map{|v| v.kind_of?(Symbol) ? v.to_s : v}]) },
213
+ Enum: ->(allowed_values) { Validation.new(:Enum, [allowed_values.map { |v| v.is_a?(Symbol) ? v.to_s : v }]) },
217
214
  Array: lambda { |*value_validations|
218
215
  Validation.new(:Array, [::Array, lambda { |value|
219
216
  return true unless value
@@ -41,15 +41,18 @@ module Itsi
41
41
  elsif args[:static]
42
42
  DSL.evaluate do
43
43
  location "/" do
44
- rate_limit key: 'address', store_config: 'in_memory', requests: 2, seconds: 5
45
- etag type: 'strong', algorithm: 'md5', min_body_size: 1024 * 1024
46
- compress min_size: 1024 * 1024, level: 'fastest', algorithms: %w[zstd gzip br deflate], mime_types: %w[all], compress_streams: true
47
- log_requests before: { level: "INFO", format: "[{request_id}] {method} {path_and_query} - {addr} " }, after: { level: "INFO", format: "[{request_id}] └─ {status} in {response_time}" }
44
+ rate_limit key: "address", store_config: "in_memory", requests: 2, seconds: 5
45
+ etag type: "strong", algorithm: "md5", min_body_size: 1024 * 1024
46
+ compress min_size: 1024 * 1024, level: "fastest", algorithms: %w[zstd gzip br deflate],
47
+ mime_types: %w[all], compress_streams: true
48
+ log_requests before: { level: "INFO", format: "[{request_id}] {method} {path_and_query} - {addr} " },
49
+ after: { level: "INFO",
50
+ format: "[{request_id}] └─ {status} in {response_time}" }
48
51
  static_assets \
49
52
  relative_path: true,
50
53
  allowed_extensions: [],
51
- root_dir: '.',
52
- not_found_behavior: {error: 'not_found'},
54
+ root_dir: ".",
55
+ not_found_behavior: { error: "not_found" },
53
56
  auto_index: true,
54
57
  try_html_extension: true,
55
58
  max_file_size_in_memory: 1024 * 1024, # 1MB
@@ -57,7 +60,7 @@ module Itsi
57
60
  file_check_interval: 1,
58
61
  serve_hidden_files: false,
59
62
  headers: {
60
- 'X-Content-Type-Options' => 'nosniff'
63
+ "X-Content-Type-Options" => "nosniff"
61
64
  }
62
65
  end
63
66
  end
@@ -69,7 +72,7 @@ module Itsi
69
72
  rackup_file args.fetch(:rackup_file, "./config.ru")
70
73
  end
71
74
  else
72
- DSL.evaluate{}
75
+ DSL.evaluate {}
73
76
  end
74
77
 
75
78
  itsifile_config.transform_keys!(&:to_sym)
@@ -85,7 +88,7 @@ module Itsi
85
88
  Itsi.log_debug("Preloading middleware and default rack app")
86
89
  preloaded_middleware = middleware_loader.call
87
90
  middleware_loader = -> { preloaded_middleware }
88
- rescue StandardError => e
91
+ rescue Exception => e # rubocop:disable Lint/RescueException
89
92
  errors << [e, e.backtrace[0]]
90
93
  end
91
94
  # If we're just preloading a specific gem group, we'll do that here too
@@ -107,7 +110,13 @@ module Itsi
107
110
  worker_memory_limit: args.fetch(:worker_memory_limit) { itsifile_config.fetch(:worker_memory_limit, nil) },
108
111
  silence: args.fetch(:silence) { itsifile_config.fetch(:silence, false) },
109
112
  shutdown_timeout: args.fetch(:shutdown_timeout) { itsifile_config.fetch(:shutdown_timeout, 5) },
110
- hooks: args[:hooks] && itsifile_config[:hooks] ? args[:hooks].merge(itsifile_config[:hooks]) : itsifile_config.fetch(:hooks, args[:hooks]),
113
+ hooks: if args[:hooks] && itsifile_config[:hooks]
114
+ args[:hooks].merge(itsifile_config[:hooks])
115
+ else
116
+ itsifile_config.fetch(
117
+ :hooks, args[:hooks]
118
+ )
119
+ end,
111
120
  preload: !!preload,
112
121
  request_timeout: itsifile_config.fetch(:request_timeout, nil),
113
122
  header_read_timeout: args.fetch(:header_read_timeout) { itsifile_config.fetch(:header_read_timeout, nil) },
@@ -132,53 +141,23 @@ module Itsi
132
141
  listeners: args.fetch(:listeners) { nil },
133
142
  reuse_address: itsifile_config.fetch(:reuse_address, true),
134
143
  reuse_port: itsifile_config.fetch(:reuse_port, true),
135
- listen_backlog: itsifile_config.fetch(:listen_backlog, 1024 ),
144
+ listen_backlog: itsifile_config.fetch(:listen_backlog, 1024),
136
145
  nodelay: itsifile_config.fetch(:nodelay, true),
137
146
  recv_buffer_size: itsifile_config.fetch(:recv_buffer_size, 262_144)
138
147
  }.transform_keys(&:to_s)
139
148
 
140
- error_lines = errors.flat_map do |(error, message)|
141
- location = message[/(.*?)\:in/,1]
142
- file, lineno = location.split(":")
143
- lineno = lineno.to_i
144
- err_message = error.kind_of?(NoMethodError) ? error.detailed_message : error.message
145
- file_lines = IO.readlines(file)
146
- info_lines = if error.kind_of?(SyntaxError)
147
- []
148
- else
149
- ([lineno-2, 0].max...[file_lines.length, lineno.succ.succ].min).map do |currline|
150
- if currline == lineno-1
151
- line = file_lines[currline][0...-1]
152
- padding = line[/^\s+/]&.length || 0
153
-
154
- [
155
- " \e[31m#{currline.succ.to_s.rjust(3)} | #{line}\e[0m",
156
- " | #{' ' * padding}\e[33m^^^\e[0m "
157
- ]
158
- else
159
- " #{currline.succ.to_s.rjust(3)} | #{file_lines[currline][0...-1]}"
160
- end
161
- end.flatten
162
- end
163
- [
164
- err_message,
165
- " --> #{File.expand_path(file)}:#{lineno}",
166
- *info_lines
167
- ]
168
- end
169
-
170
- return srv_config, error_lines
171
- rescue
149
+ [srv_config, errors_to_error_lines(errors)]
150
+ rescue StandardError
172
151
  Itsi.log_error e.message
173
152
  puts e.backtrace
174
153
  end
175
154
 
176
155
  def self.test!(cli_params)
177
- _, errors = build_config(cli_params, Itsi::Server::Config.config_file_path(cli_params[:config_file]))
156
+ config, errors = build_config(cli_params, Itsi::Server::Config.config_file_path(cli_params[:config_file]))
178
157
  unless errors.any?
179
158
  begin
180
- _["middleware_loader"][]
181
- rescue Exception => e
159
+ config["middleware_loader"][]
160
+ rescue Exception => e # rubocop:disable Lint/RescueException
182
161
  errors = [e]
183
162
  end
184
163
  end
@@ -191,6 +170,42 @@ module Itsi
191
170
  end
192
171
  end
193
172
 
173
+ def self.errors_to_error_lines(errors)
174
+ return unless errors
175
+
176
+ errors.flat_map do |(error, message)|
177
+ location = message[/(.*?):in/, 1]
178
+ file, lineno = location.split(":")
179
+ lineno = lineno.to_i
180
+ err_message = error.is_a?(NoMethodError) ? error.detailed_message : error.message
181
+ file_lines = IO.readlines(file)
182
+ info_lines = \
183
+ if error.is_a?(SyntaxError)
184
+ []
185
+ else
186
+
187
+ ([lineno - 2, 0].max...[file_lines.length, lineno.succ.succ].min).map do |currline|
188
+ if currline == lineno - 1
189
+ line = file_lines[currline][0...-1]
190
+ padding = line[/^\s+/]&.length || 0
191
+
192
+ [
193
+ " \e[31m#{currline.succ.to_s.rjust(3)} | #{line}\e[0m",
194
+ " | #{" " * padding}\e[33m^^^\e[0m "
195
+ ]
196
+ else
197
+ " #{currline.succ.to_s.rjust(3)} | #{file_lines[currline][0...-1]}"
198
+ end
199
+ end.flatten
200
+ end
201
+ [
202
+ err_message,
203
+ " --> #{File.expand_path(file)}:#{lineno}",
204
+ *info_lines
205
+ ]
206
+ end
207
+ end
208
+
194
209
  # Reloads the entire process
195
210
  # using exec, passing in any active file descriptors
196
211
  # and previous invocation arguments
@@ -233,56 +248,56 @@ module Itsi
233
248
 
234
249
  default_config = IO.read("#{__dir__}/default_config/Itsi.rb")
235
250
 
236
- if File.exist?("./config.ru")
237
- default_config << <<~RUBY
238
- # You can mount several Ruby apps as either
239
- # 1. rackup files
240
- # 2. inline rack apps
241
- # 3. inline Ruby endpoints
242
- #
243
- # 1. rackup_file
244
- rackup_file "./config.ru"
245
- #
246
- # 2. inline rack app
247
- # require 'rack'
248
- # run(Rack::Builder.app do
249
- # use Rack::CommonLogger
250
- # run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
251
- # end)
252
- #
253
- # 3. Endpoints
254
- # endpoint "/" do |req|
255
- # req.ok "Hello from Itsi"
256
- # end
257
- RUBY
258
- else
259
- default_config << <<~RUBY
260
- # You can mount several Ruby apps as either
261
- # 1. rackup files
262
- # 2. inline rack apps
263
- # 3. inline Ruby endpoints
264
- #
265
- # 1. rackup_file
266
- # Use `rackup_file` to specify the Rack app file name.
267
- #
268
- # 2. inline rack app
269
- # require 'rack'
270
- # run(Rack::Builder.app do
271
- # use Rack::CommonLogger
272
- # run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
273
- # end)
274
- #
275
- # 3. Endpoint
276
- endpoint "/" do |req|
277
- req.ok "Hello from Itsi"
278
- end
279
- RUBY
280
- end
251
+ default_config << \
252
+ if File.exist?("./config.ru")
253
+ <<~RUBY
254
+ # You can mount several Ruby apps as either
255
+ # 1. rackup files
256
+ # 2. inline rack apps
257
+ # 3. inline Ruby endpoints
258
+ #
259
+ # 1. rackup_file
260
+ rackup_file "./config.ru"
261
+ #
262
+ # 2. inline rack app
263
+ # require 'rack'
264
+ # run(Rack::Builder.app do
265
+ # use Rack::CommonLogger
266
+ # run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
267
+ # end)
268
+ #
269
+ # 3. Endpoints
270
+ # endpoint "/" do |req|
271
+ # req.ok "Hello from Itsi"
272
+ # end
273
+ RUBY
274
+ else
275
+ <<~RUBY
276
+ # You can mount several Ruby apps as either
277
+ # 1. rackup files
278
+ # 2. inline rack apps
279
+ # 3. inline Ruby endpoints
280
+ #
281
+ # 1. rackup_file
282
+ # Use `rackup_file` to specify the Rack app file name.
283
+ #
284
+ # 2. inline rack app
285
+ # require 'rack'
286
+ # run(Rack::Builder.app do
287
+ # use Rack::CommonLogger
288
+ # run ->(env) { [200, { 'content-type' => 'text/plain' }, ['OK']] }
289
+ # end)
290
+ #
291
+ # 3. Endpoint
292
+ endpoint "/" do |req|
293
+ req.ok "Hello from Itsi"
294
+ end
295
+ RUBY
296
+ end
281
297
 
282
298
  File.open(ITSI_DEFAULT_CONFIG_FILE, "w") do |file|
283
299
  file.write(default_config)
284
300
  end
285
-
286
301
  end
287
302
  end
288
303
  end
@@ -11,7 +11,7 @@ DEFAULT_APP = lambda {
11
11
  [
12
12
  { "Content-Type" => "application/json" },
13
13
  [{ "message" => "You're running on Itsi!", "rack_env" => env,
14
- "version" => Itsi::Server::VERSION }.to_json]
14
+ "version" => Itsi::Server::VERSION }.to_json]
15
15
  ]
16
16
  else
17
17
  [