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.
- checksums.yaml +4 -4
- data/Cargo.lock +28 -29
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_server/Cargo.toml +1 -1
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +26 -3
- data/lib/itsi/http_request.rb +31 -34
- data/lib/itsi/http_response.rb +10 -8
- data/lib/itsi/passfile.rb +6 -6
- data/lib/itsi/server/config/config_helpers.rb +33 -33
- data/lib/itsi/server/config/dsl.rb +14 -19
- data/lib/itsi/server/config/known_paths.rb +11 -7
- data/lib/itsi/server/config/middleware/error_response.md +13 -0
- data/lib/itsi/server/config/middleware/static_assets.md +40 -0
- data/lib/itsi/server/config/option.rb +0 -1
- data/lib/itsi/server/config/options/nodelay.md +2 -2
- data/lib/itsi/server/config/options/reuse_address.md +1 -1
- data/lib/itsi/server/config/typed_struct.rb +32 -35
- data/lib/itsi/server/config.rb +107 -92
- data/lib/itsi/server/default_app/default_app.rb +1 -1
- data/lib/itsi/server/grpc/grpc_call.rb +4 -5
- data/lib/itsi/server/grpc/grpc_interface.rb +6 -7
- data/lib/itsi/server/rack/handler/itsi.rb +0 -1
- data/lib/itsi/server/rack_interface.rb +0 -1
- data/lib/itsi/server/route_tester.rb +25 -23
- data/lib/itsi/server/typed_handlers/source_parser.rb +9 -7
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +21 -21
- data/lib/itsi/standard_headers.rb +80 -80
- metadata +3 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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__,
|
|
6
|
-
method_name = file[/
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
15
|
+
define_singleton_method(method_name) do
|
|
12
16
|
File.readlines(file).map do |s|
|
|
13
|
-
s.force_encoding(
|
|
14
|
-
s.valid_encoding? ? s.strip : s.encode(
|
|
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,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
|
|
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
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
-
|
|
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)
|
|
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
|
-
|
|
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:
|
|
182
|
-
Validation.new(:Or,
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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:
|
|
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:
|
|
204
|
-
Validation.new(:Hash,
|
|
205
|
-
return true if
|
|
206
|
-
raise StandardError.new("Expected hash got #{
|
|
207
|
-
|
|
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.
|
|
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
|
data/lib/itsi/server/config.rb
CHANGED
|
@@ -41,15 +41,18 @@ module Itsi
|
|
|
41
41
|
elsif args[:static]
|
|
42
42
|
DSL.evaluate do
|
|
43
43
|
location "/" do
|
|
44
|
-
rate_limit key:
|
|
45
|
-
etag type:
|
|
46
|
-
compress min_size: 1024 * 1024, level:
|
|
47
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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]
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|