itsi-server 0.2.2 → 0.2.4

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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +28 -29
  3. data/ext/itsi_scheduler/Cargo.toml +1 -1
  4. data/ext/itsi_server/Cargo.toml +1 -1
  5. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +26 -3
  6. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +28 -11
  7. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +1 -1
  8. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +1 -2
  9. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +14 -2
  10. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +86 -41
  11. data/ext/itsi_server/src/services/itsi_http_service.rs +46 -35
  12. data/ext/itsi_server/src/services/static_file_server.rs +31 -3
  13. data/lib/itsi/http_request.rb +31 -34
  14. data/lib/itsi/http_response.rb +10 -8
  15. data/lib/itsi/passfile.rb +6 -6
  16. data/lib/itsi/server/config/config_helpers.rb +33 -33
  17. data/lib/itsi/server/config/dsl.rb +16 -21
  18. data/lib/itsi/server/config/known_paths.rb +11 -7
  19. data/lib/itsi/server/config/middleware/endpoint/endpoint.rb +0 -4
  20. data/lib/itsi/server/config/middleware/error_response.md +13 -0
  21. data/lib/itsi/server/config/middleware/location.rb +25 -21
  22. data/lib/itsi/server/config/middleware/proxy.rb +15 -14
  23. data/lib/itsi/server/config/middleware/rackup_file.rb +7 -10
  24. data/lib/itsi/server/config/middleware/static_assets.md +40 -0
  25. data/lib/itsi/server/config/middleware/static_assets.rb +8 -4
  26. data/lib/itsi/server/config/middleware/string_rewrite.md +14 -0
  27. data/lib/itsi/server/config/option.rb +0 -1
  28. data/lib/itsi/server/config/options/include.rb +1 -1
  29. data/lib/itsi/server/config/options/nodelay.md +2 -2
  30. data/lib/itsi/server/config/options/reuse_address.md +1 -1
  31. data/lib/itsi/server/config/typed_struct.rb +32 -35
  32. data/lib/itsi/server/config.rb +107 -92
  33. data/lib/itsi/server/default_app/default_app.rb +1 -1
  34. data/lib/itsi/server/grpc/grpc_call.rb +4 -5
  35. data/lib/itsi/server/grpc/grpc_interface.rb +6 -7
  36. data/lib/itsi/server/rack/handler/itsi.rb +0 -1
  37. data/lib/itsi/server/rack_interface.rb +1 -2
  38. data/lib/itsi/server/route_tester.rb +26 -24
  39. data/lib/itsi/server/typed_handlers/param_parser.rb +25 -0
  40. data/lib/itsi/server/typed_handlers/source_parser.rb +9 -7
  41. data/lib/itsi/server/version.rb +1 -1
  42. data/lib/itsi/server.rb +22 -22
  43. data/lib/itsi/standard_headers.rb +80 -80
  44. metadata +3 -3
@@ -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
  [
@@ -7,11 +7,11 @@ module Itsi
7
7
  attr_accessor :rpc_desc
8
8
 
9
9
  def input_stream?
10
- @input_stream ||= @rpc_desc&.input&.is_a?(GRPC::RpcDesc::Stream) || false
10
+ @input_stream ||= @rpc_desc&.input.is_a?(GRPC::RpcDesc::Stream) || false
11
11
  end
12
12
 
13
13
  def output_stream?
14
- @output_stream ||= @rpc_desc&.output&.is_a?(GRPC::RpcDesc::Stream) || false
14
+ @output_stream ||= @rpc_desc&.output.is_a?(GRPC::RpcDesc::Stream) || false
15
15
  end
16
16
 
17
17
  def input_type
@@ -64,7 +64,7 @@ module Itsi
64
64
  return nil if first_char.nil?
65
65
 
66
66
  # Step 2: Process objects until we hit the end of the JSON stream or array.
67
- loop do
67
+ loop do # rubocop:disable Lint/UnreachableLoop,Metrics/BlockLength
68
68
  # Skip any whitespace or commas preceding an object.
69
69
  char = nil
70
70
  loop do
@@ -89,7 +89,6 @@ module Itsi
89
89
 
90
90
  buffer << ch
91
91
 
92
-
93
92
  if in_string
94
93
  if escape
95
94
  escape = false
@@ -120,7 +119,7 @@ module Itsi
120
119
  def remote_read
121
120
  if content_type == "application/json"
122
121
  if input_stream?
123
- if next_item = parse_from_json_stream(reader)
122
+ if (next_item = parse_from_json_stream(reader))
124
123
  input_type.decode_json(next_item)
125
124
  end
126
125
  else
@@ -29,7 +29,6 @@ module Itsi
29
29
  end
30
30
 
31
31
  def handle_request(active_call)
32
- puts "Handling active call"
33
32
  unless (active_call.rpc_desc = @rpc_descs[active_call.method_name])
34
33
  active_call.stream.write("\n")
35
34
  active_call.send_status(13, "Method not found")
@@ -89,18 +88,18 @@ module Itsi
89
88
  result = service.send(active_call.method_name, message, active_call) do |response|
90
89
  active_call.remote_send(response)
91
90
  end
92
- if result.kind_of?(Enumerator)
93
- result.each { |response| active_call.remote_send(response) }
94
- end
91
+ return unless result.is_a?(Enumerator)
92
+
93
+ result.each { |response| active_call.remote_send(response) }
95
94
  end
96
95
 
97
96
  def handle_bidi_streaming(active_call)
98
97
  result = service.send(active_call.method_name, active_call.each_remote_read, active_call) do |response|
99
98
  active_call.remote_send(response)
100
99
  end
101
- if result.kind_of?(Enumerator)
102
- result.each { |response| active_call.remote_send(response) }
103
- end
100
+ return unless result.is_a?(Enumerator)
101
+
102
+ result.each { |response| active_call.remote_send(response) }
104
103
  end
105
104
  end
106
105
  end
@@ -1,6 +1,5 @@
1
1
  return unless defined?(::Rackup::Handler) || defined?(Rack::Handler)
2
2
 
3
-
4
3
  module Rack
5
4
  module Handler
6
5
  module Itsi
@@ -1,14 +1,13 @@
1
1
  module Itsi
2
2
  class Server
3
3
  module RackInterface
4
-
5
4
  # Builds a handler proc that is compatible with Rack applications.
6
5
  def self.for(app)
7
6
  require "rack"
8
7
  if app.is_a?(String)
9
8
  dir = File.expand_path(File.dirname(app))
10
9
  Dir.chdir(dir) do
11
- loaded_app = ::Rack::Builder.parse_file(app)
10
+ loaded_app = ::Rack::Builder.parse_file(File.basename(app))
12
11
  app = loaded_app.is_a?(Array) ? loaded_app.first : loaded_app
13
12
  end
14
13
  end
@@ -1,53 +1,55 @@
1
1
  module Itsi
2
2
  class Server
3
+ # Utility module for printing Itsi route information
3
4
  module RouteTester
4
-
5
5
  require "set"
6
6
  require "strscan"
7
- require "debug"
7
+
8
8
  def format_mw(mw)
9
9
  mw_name, mw_args = mw
10
10
  case mw_name
11
11
  when "app"
12
- "\e[33mapp\e[0m(#{mw_args['app_proc'].inspect.split(' ')[1][0...-1]})"
12
+ "\e[33mapp\e[0m(#{mw_args["app_proc"].inspect.split(" ")[1][0...-1]})"
13
13
  when "log_requests"
14
- if mw_args['before'] && mw_args['after']
15
- "\e[33mlog_requests\e[0m(before: #{mw_args['before']['format'][0..6]}..., after: #{mw_args['after']['format'][0..6]}...)"
16
- elsif mw_args['before']
17
- "\e[33mlog_requests\e[0m(before: #{mw_args['before']['format'][0..6]}...)"
18
- elsif mw_args['after']
19
- "\e[33mlog_requests\e[0m(before: nil, after: #{mw_args['after']['format'][0..6]}...)"
14
+ if mw_args["before"] && mw_args["after"]
15
+ "\e[33mlog_requests\e[0m(before: #{mw_args["before"]["format"][0..6]}..., after: #{mw_args["after"]["format"][0..6]}...)"
16
+ elsif mw_args["before"]
17
+ "\e[33mlog_requests\e[0m(before: #{mw_args["before"]["format"][0..6]}...)"
18
+ elsif mw_args["after"]
19
+ "\e[33mlog_requests\e[0m(before: nil, after: #{mw_args["after"]["format"][0..6]}...)"
20
20
  end
21
21
  when "compress"
22
- "\e[33mcompress\e[0m(#{mw_args['algorithms'].join(' ')}, #{mw_args['mime_types']})"
22
+ "\e[33mcompress\e[0m(#{mw_args["algorithms"].join(" ")}, #{mw_args["mime_types"]})"
23
23
  when "cors"
24
- "\e[33mcors\e[0m(#{mw_args['allow_origins'].join(' ')}, #{mw_args['allow_methods'].join(' ')})"
24
+ "\e[33mcors\e[0m(#{mw_args["allow_origins"].join(" ")}, #{mw_args["allow_methods"].join(" ")})"
25
25
  when "etag"
26
- "\e[33metag\e[0m(#{mw_args['type']}/#{mw_args['algorithm']}, #{mw_args['handle_if_none_match'] ? 'if_none_match' : ''})"
26
+ "\e[33metag\e[0m(#{mw_args["type"]}/#{mw_args["algorithm"]}, #{mw_args["handle_if_none_match"] ? "if_none_match" : ""})"
27
27
  when "cache_control"
28
- "\e[33mcache_control\e[0m(max_age: #{mw_args['max_age']}, #{mw_args.select{|_,v| v == true }.keys.join(", ")})"
28
+ "\e[33mcache_control\e[0m(max_age: #{mw_args["max_age"]}, #{mw_args.select do |_, v|
29
+ v == true
30
+ end.keys.join(", ")})"
29
31
  when "redirect"
30
- "\e[33mredirect\e[0m(to: #{mw_args['to']}, type: #{mw_args['type']})"
32
+ "\e[33mredirect\e[0m(to: #{mw_args["to"]}, type: #{mw_args["type"]})"
31
33
  when "static_assets"
32
- "\e[33mstatic_assets\e[0m(path: #{mw_args['root_dir']})"
34
+ "\e[33mstatic_assets\e[0m(path: #{mw_args["root_dir"]})"
33
35
  when "auth_api_key"
34
- "\e[33mauth_api_key\e[0m(keys: #{mw_args['valid_keys'].keys}#{mw_args['credentials_file'] ? ", credentials_file: #{mw_args['credentials_file']}" : ""})"
36
+ "\e[33mauth_api_key\e[0m(keys: #{mw_args["valid_keys"].keys}#{mw_args["credentials_file"] ? ", credentials_file: #{mw_args["credentials_file"]}" : ""})"
35
37
  when "auth_basic"
36
- "\e[33mbasic_auth\e[0m(keys: #{mw_args['realm']}#{mw_args['credentials_file'] ? ", credentials_file: #{mw_args['credentials_file']}" : ""})"
38
+ "\e[33mbasic_auth\e[0m(keys: #{mw_args["realm"]}#{mw_args["credentials_file"] ? ", credentials_file: #{mw_args["credentials_file"]}" : ""})"
37
39
  when "auth_jwt"
38
- "\e[33mjwt_auth\e[0m(#{mw_args['verifiers'].keys.join(",")})"
40
+ "\e[33mjwt_auth\e[0m(#{mw_args["verifiers"].keys.join(",")})"
39
41
  when "rate_limit"
40
- key = mw_args['key'].kind_of?(Hash) ? mw_args['key']["parameter"] : mw_args['key']
41
- "\e[33mrate_limit\e[0m(rps: #{mw_args['requests']}/#{mw_args['seconds']}, key: #{key})"
42
+ key = mw_args["key"].is_a?(Hash) ? mw_args["key"]["parameter"] : mw_args["key"]
43
+ "\e[33mrate_limit\e[0m(rps: #{mw_args["requests"]}/#{mw_args["seconds"]}, key: #{key})"
42
44
  when "allow_list"
43
- "\e[33mallow_list\e[0m(patterns: #{mw_args['allowed_patterns'].join(", ")})"
45
+ "\e[33mallow_list\e[0m(patterns: #{mw_args["allowed_patterns"].join(", ")})"
44
46
  when "deny_list"
45
- "\e[33mdeny_list\e[0m(patterns: #{mw_args['denied_patterns'].join(", ")})"
47
+ "\e[33mdeny_list\e[0m(patterns: #{mw_args["denied_patterns"].join(", ")})"
46
48
  when "csp"
47
- "\e[33mcsp\e[0m(#{mw_args['policy'].map{|k,v| "#{k}: #{v.join(",")}"}.join(", ")})"
49
+ "\e[33mcsp\e[0m(#{mw_args["policy"].map { |k, v| "#{k}: #{v.join(",")}" }.join(", ")})"
48
50
  when "intrusion_protection"
49
51
  [mw_args].flatten.map do |mw_args|
50
- "\e[33mintrusion_protection\e[0m(banned_url_patterns: #{mw_args['banned_url_patterns']&.length}, banned_header_patterns: #{mw_args['banned_header_patterns']&.keys&.join(", ")}, #{mw_args['banned_time_seconds']}s)"
52
+ "\e[33mintrusion_protection\e[0m(banned_url_patterns: #{mw_args["banned_url_patterns"]&.length}, banned_header_patterns: #{mw_args["banned_header_patterns"]&.keys&.join(", ")}, #{mw_args["banned_time_seconds"]}s)"
51
53
  end.join("\n")
52
54
  when "request_headers"
53
55
  [mw_args].flatten.map do |mw_args|
@@ -143,6 +143,31 @@ module Itsi
143
143
  # Fixed keys are converted to symbols, and regex-matched keys remain as strings.
144
144
  # The current location in the params is tracked as an array of path segments.
145
145
  def apply_schema!(params, schema, path = [])
146
+ # Support top-level array schema: homogeneous arrays.
147
+ if schema.is_a?(Array)
148
+ # Only allow homogeneous array types
149
+ unless schema.size == 1
150
+ raise ValidationError.new(["Schema Array must contain exactly one type. Got #{schema.size}"])
151
+ end
152
+ expected_type = schema.first
153
+ # Expect params to be an Array
154
+ unless params.is_a?(Array)
155
+ raise ValidationError.new(["Expected Array at #{format_path(path)}, got #{params.class}"])
156
+ end
157
+ errors = []
158
+ params.each_with_index do |_, idx|
159
+ err = cast_value!(params, idx, expected_type, path + [idx])
160
+ errors << err if err
161
+ end
162
+ raise ValidationError.new(errors) unless errors.empty?
163
+ return params
164
+ end
165
+
166
+ # Ensure schema is a Hash
167
+ unless schema.is_a?(Hash)
168
+ raise ValidationError.new(["Unsupported schema type: #{schema.class} at #{format_path(path)}"])
169
+ end
170
+
146
171
  errors = []
147
172
  processed = processed_schema(schema)
148
173
  fixed_schema = processed[0]
@@ -2,9 +2,10 @@ module Itsi
2
2
  class Server
3
3
  module TypedHandlers
4
4
  module SourceParser
5
- require 'prism'
6
-
5
+ require "prism"
7
6
 
7
+ # Source Parser interprets endpoint handlers
8
+ # and extracts arity and schema information.
8
9
  def self.extract_expr_from_source_location(proc)
9
10
  source_location = proc.source_location
10
11
  source_lines = IO.readlines(source_location.first)
@@ -23,17 +24,19 @@ module Itsi
23
24
 
24
25
  lines[1..-1].each do |line|
25
26
  break if intermediate.success?
27
+
26
28
  token_count = 0
27
29
  line.split(/(?=\s|;|\)|\})/).each do |token|
28
30
  src_str << token
29
31
  token_count += 1
30
32
  intermediate = Prism.parse(src_str)
31
33
  next unless intermediate.success? && token_count > 1
34
+
32
35
  break
33
36
  end
34
37
  end
35
38
 
36
- raise 'Source Extraction Failed' unless intermediate.success?
39
+ raise "Source Extraction Failed" unless intermediate.success?
37
40
 
38
41
  src = intermediate.value.statements.body.first.yield_self do |s|
39
42
  s.type == :call_node ? s.block : s
@@ -41,14 +44,13 @@ module Itsi
41
44
  params = src.parameters
42
45
  params = params.parameters if params.respond_to?(:parameters)
43
46
  requireds = (params&.requireds || []).map(&:name)
44
- optionals = params&.optionals || []
45
47
  keywords = (params&.keywords || []).map do |kw|
46
- [kw.name, kw.value.slice.gsub(/^_\./, '$.')]
48
+ [kw.name, kw.value.slice.gsub(/^_\./, "$.")]
47
49
  end.to_h
48
50
 
49
51
  [requireds.length, keywords]
50
- rescue
51
- [ proc.parameters.select{|p| p == :req }&.length || 0, {}]
52
+ rescue StandardError
53
+ [proc.parameters.select { |p| p == :req }&.length || 0, {}]
52
54
  end
53
55
  end
54
56
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.2.2"
5
+ VERSION = "0.2.4"
6
6
  end
7
7
  end