itsi 0.1.8 → 0.1.11

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +11 -2
  3. data/Rakefile +6 -2
  4. data/crates/itsi_rb_helpers/src/lib.rs +27 -4
  5. data/crates/itsi_server/Cargo.toml +4 -1
  6. data/crates/itsi_server/src/lib.rs +74 -1
  7. data/crates/itsi_server/src/request/itsi_request.rs +32 -11
  8. data/crates/itsi_server/src/response/itsi_response.rs +14 -4
  9. data/crates/itsi_server/src/server/bind.rs +16 -12
  10. data/crates/itsi_server/src/server/itsi_server.rs +146 -95
  11. data/crates/itsi_server/src/server/listener.rs +10 -10
  12. data/crates/itsi_server/src/server/process_worker.rs +10 -3
  13. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  14. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
  15. data/crates/itsi_server/src/server/signal.rs +4 -0
  16. data/crates/itsi_server/src/server/thread_worker.rs +55 -24
  17. data/crates/itsi_server/src/server/tls.rs +11 -8
  18. data/crates/itsi_tracing/src/lib.rs +18 -1
  19. data/gems/scheduler/Cargo.lock +12 -12
  20. data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +27 -4
  21. data/gems/scheduler/ext/itsi_server/Cargo.toml +4 -1
  22. data/gems/scheduler/ext/itsi_server/src/lib.rs +74 -1
  23. data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +32 -11
  24. data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +14 -4
  25. data/gems/scheduler/ext/itsi_server/src/server/bind.rs +16 -12
  26. data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +146 -95
  27. data/gems/scheduler/ext/itsi_server/src/server/listener.rs +10 -10
  28. data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +10 -3
  29. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  30. data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
  31. data/gems/scheduler/ext/itsi_server/src/server/signal.rs +4 -0
  32. data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +55 -24
  33. data/gems/scheduler/ext/itsi_server/src/server/tls.rs +11 -8
  34. data/gems/scheduler/ext/itsi_tracing/src/lib.rs +18 -1
  35. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  36. data/gems/scheduler/test/test_address_resolve.rb +0 -1
  37. data/gems/scheduler/test/test_file_io.rb +0 -1
  38. data/gems/scheduler/test/test_kernel_sleep.rb +3 -4
  39. data/gems/server/Cargo.lock +11 -2
  40. data/gems/server/Rakefile +8 -1
  41. data/gems/server/exe/itsi +53 -23
  42. data/gems/server/ext/itsi_rb_helpers/src/lib.rs +27 -4
  43. data/gems/server/ext/itsi_server/Cargo.toml +4 -1
  44. data/gems/server/ext/itsi_server/src/lib.rs +74 -1
  45. data/gems/server/ext/itsi_server/src/request/itsi_request.rs +32 -11
  46. data/gems/server/ext/itsi_server/src/response/itsi_response.rs +14 -4
  47. data/gems/server/ext/itsi_server/src/server/bind.rs +16 -12
  48. data/gems/server/ext/itsi_server/src/server/itsi_server.rs +146 -95
  49. data/gems/server/ext/itsi_server/src/server/listener.rs +10 -10
  50. data/gems/server/ext/itsi_server/src/server/process_worker.rs +10 -3
  51. data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
  52. data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
  53. data/gems/server/ext/itsi_server/src/server/signal.rs +4 -0
  54. data/gems/server/ext/itsi_server/src/server/thread_worker.rs +55 -24
  55. data/gems/server/ext/itsi_server/src/server/tls.rs +11 -8
  56. data/gems/server/ext/itsi_tracing/src/lib.rs +18 -1
  57. data/gems/server/lib/itsi/request.rb +29 -21
  58. data/gems/server/lib/itsi/server/Itsi.rb +127 -0
  59. data/gems/server/lib/itsi/server/config.rb +36 -0
  60. data/gems/server/lib/itsi/server/options_dsl.rb +401 -0
  61. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +18 -7
  62. data/gems/server/lib/itsi/server/rack_interface.rb +75 -0
  63. data/gems/server/lib/itsi/server/scheduler_interface.rb +21 -0
  64. data/gems/server/lib/itsi/server/signal_trap.rb +23 -0
  65. data/gems/server/lib/itsi/server/version.rb +1 -1
  66. data/gems/server/lib/itsi/server.rb +71 -101
  67. data/gems/server/test/helpers/test_helper.rb +30 -0
  68. data/gems/server/test/test_itsi_server.rb +294 -3
  69. data/lib/itsi/version.rb +1 -1
  70. data/location_dsl.rb +381 -0
  71. data/sandbox/deploy/main.tf +1 -0
  72. data/sandbox/itsi_itsi_file/Itsi.rb +119 -0
  73. data/sandbox/itsi_sandbox_async/Gemfile +1 -1
  74. data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
  75. data/sandbox/itsi_sandbox_rails/Gemfile.lock +2 -2
  76. data/tasks.txt +25 -8
  77. metadata +21 -14
  78. data/gems/server/lib/itsi/signals.rb +0 -23
  79. data/gems/server/test/test_helper.rb +0 -7
  80. /data/gems/server/lib/itsi/{index.html.erb → index.html} +0 -0
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
-
4
3
  class TestKernelSleep < Minitest::Test
5
4
  include Itsi::Scheduler::TestHelper
6
5
 
@@ -14,7 +13,7 @@ class TestKernelSleep < Minitest::Test
14
13
  # Run the scheduler in a dedicated thread to avoid interference with the
15
14
  # main thread’s scheduler state.
16
15
  with_scheduler do |_scheduler|
17
- 5.times do
16
+ 10.times do
18
17
  Fiber.schedule do
19
18
  sleep 0.05
20
19
  results << "first"
@@ -26,9 +25,9 @@ class TestKernelSleep < Minitest::Test
26
25
 
27
26
  ends_at = Time.now
28
27
  # We expect 10 sleep completions overall (2 per fiber).
29
- assert_equal 10, results.size
28
+ assert_equal 20, results.size
30
29
  # Because all sleeps run concurrently, the total elapsed time should be about 1 second.
31
- assert_in_delta 0.1, ends_at - start_at, 0.02, "Total elapsed time should be close to 0.1 second"
30
+ assert_in_delta 0.1, ends_at - start_at, 0.1, "Total elapsed time should be close to 0.1 second"
32
31
  end
33
32
 
34
33
  def test_sleep_zero_duration
@@ -993,6 +993,7 @@ dependencies = [
993
993
  "crossbeam",
994
994
  "derive_more",
995
995
  "dirs",
996
+ "fnv",
996
997
  "fs2",
997
998
  "futures",
998
999
  "http",
@@ -1009,7 +1010,9 @@ dependencies = [
1009
1010
  "pin-project",
1010
1011
  "rb-sys",
1011
1012
  "rcgen",
1013
+ "regex",
1012
1014
  "ring",
1015
+ "route-recognizer",
1013
1016
  "rustls",
1014
1017
  "rustls-pemfile",
1015
1018
  "socket2",
@@ -1730,6 +1733,12 @@ dependencies = [
1730
1733
  "windows-sys 0.52.0",
1731
1734
  ]
1732
1735
 
1736
+ [[package]]
1737
+ name = "route-recognizer"
1738
+ version = "0.3.1"
1739
+ source = "registry+https://github.com/rust-lang/crates.io-index"
1740
+ checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746"
1741
+
1733
1742
  [[package]]
1734
1743
  name = "rustc-demangle"
1735
1744
  version = "0.1.24"
@@ -2131,9 +2140,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
2131
2140
 
2132
2141
  [[package]]
2133
2142
  name = "tokio"
2134
- version = "1.43.0"
2143
+ version = "1.44.1"
2135
2144
  source = "registry+https://github.com/rust-lang/crates.io-index"
2136
- checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
2145
+ checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
2137
2146
  dependencies = [
2138
2147
  "backtrace",
2139
2148
  "bytes",
data/gems/server/Rakefile CHANGED
@@ -3,7 +3,14 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "minitest/test_task"
5
5
 
6
- Minitest::TestTask.create
6
+
7
+ Minitest::TestTask.create(:test) do |t|
8
+ t.libs << 'test'
9
+ t.libs << 'lib'
10
+ t.warning = false
11
+ t.test_globs = ['test/**/*.rb']
12
+ t.test_prelude = 'require "helpers/test_helper.rb"'
13
+ end
7
14
 
8
15
  require "rubocop/rake_task"
9
16
 
data/gems/server/exe/itsi CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
+
3
4
  require "optparse"
4
5
 
5
6
  # Default options used when starting Osprey from the CLI using `osprey`
@@ -19,7 +20,9 @@ DEFAULT_OPTIONS = {
19
20
  # Scheduler class
20
21
  scheduler_class: nil,
21
22
  # Whether to stream the body or not
22
- stream_body: false
23
+ stream_body: false,
24
+ # Config file
25
+ config_file: nil
23
26
  }
24
27
 
25
28
  options = DEFAULT_OPTIONS.to_a.select(&:last).to_h
@@ -28,6 +31,10 @@ options = DEFAULT_OPTIONS.to_a.select(&:last).to_h
28
31
  OptionParser.new do |opts|
29
32
  opts.banner = "Usage: itsi [options]"
30
33
 
34
+ opts.on("-C", "--config CONFIG_FILE", String, "Itsi Configuration file to use (default: Itsi.rb)") do |config_file|
35
+ options[:config_file] = config_file
36
+ end
37
+
31
38
  opts.on("-w", "--workers WORKERS", Integer, "Number of workers (default: #{options[:workers]})") do |w|
32
39
  options[:workers] = w
33
40
  end
@@ -41,11 +48,12 @@ OptionParser.new do |opts|
41
48
  end
42
49
 
43
50
  opts.on("--worker-memory-limit MEMORY_LIMIT", Integer,
44
- "Memory limit for each worker (default: #{options[:worker_memory_limit] || 'None'}). If this limit is breached the worker is gracefully restarted") do |ml|
51
+ "Memory limit for each worker (default: #{options[:worker_memory_limit] || "None"}). If this limit is breached the worker is gracefully restarted") do |ml|
45
52
  options[:worker_memory_limit] = ml
46
53
  end
47
54
 
48
- opts.on("-f", "--fiber_scheduler [CLASS_NAME]", [String], "Scheduler class to use (default: nil). Provide blank or true to use Itsi::Scheduler, or a classname to use an alternative scheduler") do |scheduler_class|
55
+ opts.on("-f", "--fiber_scheduler [CLASS_NAME]", [String],
56
+ "Scheduler class to use (default: nil). Provide blank or true to use Itsi::Scheduler, or a classname to use an alternative scheduler") do |scheduler_class|
49
57
  if scheduler_class.nil? || scheduler_class == "true"
50
58
  options[:scheduler_class] = "Itsi::Scheduler"
51
59
  elsif scheduler_class == "false"
@@ -65,8 +73,8 @@ OptionParser.new do |opts|
65
73
  end
66
74
  end
67
75
 
68
-
69
- opts.on("-b", "--bind BIND", String, "Bind address (default: #{options[:binds].join(", ")}). You can specify this flag multiple times to bind to multiple addresses.") do |bind|
76
+ opts.on("-b", "--bind BIND", String,
77
+ "Bind address (default: #{options[:binds].join(", ")}). You can specify this flag multiple times to bind to multiple addresses.") do |bind|
70
78
  options[:binds].pop if options[:binds].first.frozen?
71
79
  options[:binds] << bind
72
80
  end
@@ -74,6 +82,7 @@ OptionParser.new do |opts|
74
82
  opts.on("-c", "--cert_path CERT_PATH", String,
75
83
  "Path to the SSL certificate file (must follow a --bind option). You can specify this flag multiple times.") do |cp|
76
84
  raise OptionParser::InvalidOption, "--cert_path must follow a --bind" if options[:binds].empty?
85
+
77
86
  require "uri"
78
87
 
79
88
  # Modify the last bind entry to add/update the cert query parameter
@@ -84,8 +93,10 @@ OptionParser.new do |opts|
84
93
  options[:binds][-1] = "#{uri.host}?#{query_string}"
85
94
  end
86
95
 
87
- opts.on("-k", "--key_path KEY_PATH", String, "Path to the SSL key file (must follow a --bind option). You can specify this flag multiple times.") do |kp|
96
+ opts.on("-k", "--key_path KEY_PATH", String,
97
+ "Path to the SSL key file (must follow a --bind option). You can specify this flag multiple times.") do |kp|
88
98
  raise OptionParser::InvalidOption, "--key_path must follow a --bind" if options[:binds].empty?
99
+
89
100
  require "uri"
90
101
 
91
102
  # Modify the last bind entry to add/update the key query parameter
@@ -115,30 +126,49 @@ OptionParser.new do |opts|
115
126
  end
116
127
  end.parse!
117
128
 
118
- # Rack app loader, invoked per worker.
119
- # This is a no-op if preloading is enabled (we just return the preloaded app).
120
- preloader = \
121
- if options[:preload]
129
+ require "itsi/server/config"
130
+ if ARGV.pop == "init"
131
+ Itsi::Server::Config.write_default
132
+ exit(0)
133
+ # Add initialization code here
134
+ end
135
+
136
+ options = Itsi::Server::Config.load(options)
137
+ loader = nil
138
+
139
+ if options[:app] || File.exist?(options[:rackup_file])
140
+ loader ||= lambda do
122
141
  require "rack"
142
+ require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
143
+ require "itsi/scheduler" if [true, "Itsi::Scheduler"].include?(options[:scheduler_class])
144
+ require "itsi/server"
145
+ return options[:app] if options[:app]
146
+ Array(Rack::Builder.parse_file(options[:rackup_file])).first
147
+ end
123
148
 
124
- app = Array(Rack::Builder.parse_file(options[:rackup_file])).first
149
+ if options[:preload] == true
150
+ # If preload is true, we'll invoke our loader now, and just return a no-op proc
151
+ # which returns the app as loader. We still need to optionally load the scheduler class and rack
152
+ # in case the app has been provided directly in Itsi.rb instead of in config.ru
153
+ require "rack"
125
154
  require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
126
- app.method(:itself).to_proc
127
- else
128
- lambda do
129
- require "rack"
130
-
131
- app = Array(Rack::Builder.parse_file(options[:rackup_file])).first
132
- require_relative "../lib/itsi/server/scheduler_mode" if options[:scheduler_class]
133
- app
134
- end
155
+ require "itsi/scheduler" if [true, "Itsi::Scheduler"].include?(options[:scheduler_class])
156
+ loader = (options[:app] || loader[]).method(:itself).to_proc
157
+ elsif options[:preload]
158
+ # If a group name is given, we'll stick to just loading the gem group by the same name from the Gemfile.
159
+ # But we'll fall through to the default delayed loader for the full app.
160
+ require "bundler"
161
+ Bundler.setup
162
+ Bundler.require(options[:preload])
163
+ nil
135
164
  end
165
+ end
136
166
 
137
167
  # Make sure Itsi is loaded, if not already loaded by the rack_app above.
138
168
  # Start the Itsi server
139
169
  require "itsi/server"
140
170
 
141
- Itsi::Server.new(
142
- app: preloader,
171
+ Itsi::Server.start(
172
+ loader: loader,
143
173
  **options.except(:preload, :rackup_file)
144
- ).start
174
+ )
@@ -1,7 +1,8 @@
1
- use std::{os::raw::c_void, ptr::null_mut, sync::Arc};
1
+ use std::{os::raw::c_void, ptr::null_mut};
2
2
 
3
3
  use magnus::{
4
4
  RArray, Ruby, Thread, Value,
5
+ block::Proc,
5
6
  rb_sys::FromRawValue,
6
7
  value::{LazyId, ReprValue},
7
8
  };
@@ -17,6 +18,7 @@ static ID_LIST: LazyId = LazyId::new("list");
17
18
  static ID_EQ: LazyId = LazyId::new("==");
18
19
  static ID_ALIVE: LazyId = LazyId::new("alive?");
19
20
  static ID_THREAD_VARIABLE_GET: LazyId = LazyId::new("thread_variable_get");
21
+ static ID_BACKTRACE: LazyId = LazyId::new("backtrace");
20
22
 
21
23
  pub fn schedule_thread() {
22
24
  unsafe {
@@ -120,20 +122,30 @@ where
120
122
  *result_box
121
123
  }
122
124
 
123
- pub fn fork(after_fork: Arc<Option<impl Fn()>>) -> Option<i32> {
125
+ pub fn fork(after_fork: Option<HeapValue<Proc>>) -> Option<i32> {
124
126
  let ruby = Ruby::get().unwrap();
125
127
  let fork_result = ruby
126
128
  .module_kernel()
127
129
  .funcall::<_, _, Option<i32>>(*ID_FORK, ())
128
130
  .unwrap();
129
131
  if fork_result.is_none() {
130
- if let Some(f) = &*after_fork {
131
- f()
132
+ if let Some(proc) = after_fork {
133
+ call_proc_and_log_errors(proc)
132
134
  }
133
135
  }
134
136
  fork_result
135
137
  }
136
138
 
139
+ pub fn call_proc_and_log_errors(proc: HeapValue<Proc>) {
140
+ if let Err(e) = proc.call::<_, Value>(()) {
141
+ if let Some(value) = e.value() {
142
+ print_rb_backtrace(value);
143
+ } else {
144
+ eprintln!("Error occurred {:?}", e);
145
+ }
146
+ }
147
+ }
148
+
137
149
  pub fn kill_threads<T>(threads: Vec<T>)
138
150
  where
139
151
  T: ReprValue,
@@ -176,3 +188,14 @@ pub fn terminate_non_fork_safe_threads() {
176
188
 
177
189
  kill_threads(non_fork_safe_threads);
178
190
  }
191
+
192
+ pub fn print_rb_backtrace(rb_err: Value) {
193
+ let backtrace = rb_err
194
+ .funcall::<_, _, Vec<String>>(*ID_BACKTRACE, ())
195
+ .unwrap_or_default();
196
+
197
+ eprintln!("Ruby exception {:?}", rb_err);
198
+ for line in backtrace {
199
+ eprintln!("{}", line);
200
+ }
201
+ }
@@ -24,7 +24,7 @@ rcgen = { version = "0.13.2", features = ["x509-parser", "pem"] }
24
24
  base64 = "0.22.1"
25
25
  http-body-util = "0.1.2"
26
26
  hyper = { version = "1.5.0", features = ["full", "server", "http1", "http2"] }
27
- tokio = { version = "1", features = ["full"] }
27
+ tokio = { version = "1.44.1", features = ["full"] }
28
28
  hyper-util = { version = "0.1.10", features = ["full"] }
29
29
  derive_more = { version = "2.0.1", features = ["debug"] }
30
30
  http = "1.3.1"
@@ -45,3 +45,6 @@ fs2 = "0.4.3"
45
45
  ring = "0.17.14"
46
46
  async-trait = "0.1.87"
47
47
  dirs = "6.0.0"
48
+ regex = "1.11.1"
49
+ route-recognizer = "0.3.1"
50
+ fnv = "1.0.7"
@@ -1,5 +1,8 @@
1
1
  use body_proxy::itsi_body_proxy::ItsiBodyProxy;
2
- use magnus::{error::Result, function, method, value::Lazy, Module, Object, RClass, RModule, Ruby};
2
+ use magnus::{
3
+ error::Result, function, method, value::Lazy, Module, Object, RClass, RHash, RModule, Ruby,
4
+ };
5
+ use regex::{Regex, RegexSet};
3
6
  use request::itsi_request::ItsiRequest;
4
7
  use response::itsi_response::ItsiResponse;
5
8
  use server::{itsi_server::Server, signal::reset_signal_handlers};
@@ -54,6 +57,70 @@ pub fn log_error(msg: String) {
54
57
  error!(msg);
55
58
  }
56
59
 
60
+ const ROUTES: [&str; 39] = [
61
+ r"(?-u)^/organisations/(?<organisation_id>\d+)/users/(?<user_id>\d+)$",
62
+ r"(?-u)^/projects/(?<project_id>\d+)/tasks/(?<task_id>\d+)$",
63
+ r"(?-u)^/products/(?<product_id>\d+)(?:/reviews/(?<review_id>\d+))?$",
64
+ r"(?-u)^/orders/(?<order_id>\d+)/items(?:/(?<item_id>\d+))?$",
65
+ r"(?-u)^/posts/(?<post_id>\d+)/comments(?:/(?<comment_id>\d+))?$",
66
+ r"(?-u)^/teams/(?<team_id>\d+)(?:/members/(?<member_id>\d+))?$",
67
+ r"(?-u)^/categories/(?<category_id>\d+)/subcategories(?:/(?<subcategory_id>\d+))?$",
68
+ r"(?-u)^/departments/(?<department_id>\d+)/employees/(?<employee_id>\d+)$",
69
+ r"(?-u)^/events/(?<event_id>\d+)(?:/sessions/(?<session_id>\d+))?$",
70
+ r"(?-u)^/invoices/(?<invoice_id>\d+)/payments(?:/(?<payment_id>\d+))?$",
71
+ r"(?-u)^/tickets/(?<ticket_id>\d+)(?:/responses/(?<response_id>\d+))?$",
72
+ r"(?-u)^/forums/(?<forum_id>\d+)(?:/threads/(?<thread_id>\d+))?$",
73
+ r"(?-u)^/subscriptions/(?<subscription_id>\d+)/plans(?:/(?<plan_id>\d+))?$",
74
+ r"(?-u)^/profiles/(?<profile_id>\d+)/settings$",
75
+ r"(?-u)^/organizations/(?<organization_id>\d+)/billing(?:/(?<billing_id>\d+))?$",
76
+ r"(?-u)^/vendors/(?<vendor_id>\d+)/products(?:/(?<product_id>\d+))?$",
77
+ r"(?-u)^/courses/(?<course_id>\d+)/modules(?:/(?<module_id>\d+))?$",
78
+ r"(?-u)^/accounts/(?<account_id>\d+)(?:/transactions/(?<transaction_id>\d+))?$",
79
+ r"(?-u)^/warehouses/(?<warehouse_id>\d+)/inventory(?:/(?<inventory_id>\d+))?$",
80
+ r"(?-u)^/campaigns/(?<campaign_id>\d+)/ads(?:/(?<ad_id>\d+))?$",
81
+ r"(?-u)^/applications/(?<application_id>\d+)/stages(?:/(?<stage_id>\d+))?$",
82
+ r"(?-u)^/notifications/(?<notification_id>\d+)$",
83
+ r"(?-u)^/albums/(?<album_id>\d+)/photos(?:/(?<photo_id>\d+))?$",
84
+ r"(?-u)^/news/(?<news_id>\d+)/articles(?:/(?<article_id>\d+))?$",
85
+ r"(?-u)^/libraries/(?<library_id>\d+)/books(?:/(?<book_id>\d+))?$",
86
+ r"(?-u)^/universities/(?<university_id>\d+)/students(?:/(?<student_id>\d+))?$",
87
+ r"(?-u)^/banks/(?<bank_id>\d+)/branches(?:/(?<branch_id>\d+))?$",
88
+ r"(?-u)^/vehicles/(?<vehicle_id>\d+)/services(?:/(?<service_id>\d+))?$",
89
+ r"(?-u)^/hotels/(?<hotel_id>\d+)/rooms(?:/(?<room_id>\d+))?$",
90
+ r"(?-u)^/doctors/(?<doctor_id>\d+)/appointments(?:/(?<appointment_id>\d+))?$",
91
+ r"(?-u)^/gyms/(?<gym_id>\d+)/memberships(?:/(?<membership_id>\d+))?$",
92
+ r"(?-u)^/restaurants/(?<restaurant_id>\d+)/menus(?:/(?<menu_id>\d+))?$",
93
+ r"(?-u)^/parks/(?<park_id>\d+)/events(?:/(?<event_id>\d+))?$",
94
+ r"(?-u)^/theaters/(?<theater_id>\d+)/shows(?:/(?<show_id>\d+))?$",
95
+ r"(?-u)^/museums/(?<museum_id>\d+)/exhibits(?:/(?<exhibit_id>\d+))?$",
96
+ r"(?-u)^/stadiums/(?<stadium_id>\d+)/games(?:/(?<game_id>\d+))?$",
97
+ r"(?-u)^/schools/(?<school_id>\d+)/classes(?:/(?<class_id>\d+))?$",
98
+ r"(?-u)^/clubs/(?<club_id>\d+)/events(?:/(?<event_id>\d+))?$",
99
+ r"(?-u)^/festivals/(?<festival_id>\d+)/tickets(?:/(?<ticket_id>\d+))?$",
100
+ ];
101
+ use std::sync::LazyLock;
102
+
103
+ static REGEX_SET: LazyLock<RegexSet> = LazyLock::new(|| RegexSet::new(ROUTES).unwrap());
104
+ static REGEXES: LazyLock<Vec<Regex>> =
105
+ LazyLock::new(|| ROUTES.iter().map(|&r| Regex::new(r).unwrap()).collect());
106
+
107
+ fn match_route(input: String) -> Result<Option<(usize, Option<RHash>)>> {
108
+ if let Some(index) = REGEX_SET.matches(&input).iter().next() {
109
+ let regex = &REGEXES[index];
110
+ if let Some(captures) = regex.captures(&input) {
111
+ let params = RHash::with_capacity(captures.len());
112
+ for name in regex.capture_names().flatten() {
113
+ if let Some(value) = captures.name(name) {
114
+ params.aset(name, value.as_str()).ok();
115
+ }
116
+ }
117
+ return Ok(Some((index, Some(params))));
118
+ }
119
+ return Ok(Some((index, None)));
120
+ }
121
+ Ok(None)
122
+ }
123
+
57
124
  #[magnus::init]
58
125
  fn init(ruby: &Ruby) -> Result<()> {
59
126
  itsi_tracing::init();
@@ -62,6 +129,7 @@ fn init(ruby: &Ruby) -> Result<()> {
62
129
  .ok();
63
130
 
64
131
  let itsi = ruby.get_inner(&ITSI_MODULE);
132
+ itsi.define_singleton_method("match_route", function!(match_route, 1))?;
65
133
  itsi.define_singleton_method("log_debug", function!(log_debug, 1))?;
66
134
  itsi.define_singleton_method("log_info", function!(log_info, 1))?;
67
135
  itsi.define_singleton_method("log_warn", function!(log_warn, 1))?;
@@ -71,6 +139,7 @@ fn init(ruby: &Ruby) -> Result<()> {
71
139
  server.define_singleton_method("new", function!(Server::new, -1))?;
72
140
  server.define_singleton_method("reset_signal_handlers", function!(reset_signal_handlers, 0))?;
73
141
  server.define_method("start", method!(Server::start, 0))?;
142
+ server.define_method("stop", method!(Server::stop, 0))?;
74
143
 
75
144
  let request = ruby.get_inner(&ITSI_REQUEST);
76
145
  request.define_method("path", method!(ItsiRequest::path, 0))?;
@@ -86,6 +155,8 @@ fn init(ruby: &Ruby) -> Result<()> {
86
155
  request.define_method("port", method!(ItsiRequest::port, 0))?;
87
156
  request.define_method("body", method!(ItsiRequest::body, 0))?;
88
157
  request.define_method("response", method!(ItsiRequest::response, 0))?;
158
+ request.define_method("json?", method!(ItsiRequest::is_json, 0))?;
159
+ request.define_method("html?", method!(ItsiRequest::is_html, 0))?;
89
160
 
90
161
  let body_proxy = ruby.get_inner(&ITSI_BODY_PROXY);
91
162
  body_proxy.define_method("gets", method!(ItsiBodyProxy::gets, 0))?;
@@ -102,6 +173,8 @@ fn init(ruby: &Ruby) -> Result<()> {
102
173
  response.define_method("close_read", method!(ItsiResponse::close_read, 0))?;
103
174
  response.define_method("close", method!(ItsiResponse::close, 0))?;
104
175
  response.define_method("hijack", method!(ItsiResponse::hijack, 1))?;
176
+ response.define_method("json?", method!(ItsiResponse::is_json, 0))?;
177
+ response.define_method("html?", method!(ItsiResponse::is_html, 0))?;
105
178
 
106
179
  Ok(())
107
180
  }
@@ -13,10 +13,11 @@ use crate::{
13
13
  use bytes::Bytes;
14
14
  use derive_more::Debug;
15
15
  use futures::StreamExt;
16
- use http::{request::Parts, Response, StatusCode};
16
+ use http::{request::Parts, HeaderValue, Response, StatusCode};
17
17
  use http_body_util::{combinators::BoxBody, BodyExt, Empty};
18
18
  use hyper::{body::Incoming, Request};
19
19
  use itsi_error::from::CLIENT_CONNECTION_CLOSED;
20
+ use itsi_rb_helpers::print_rb_backtrace;
20
21
  use itsi_tracing::{debug, error};
21
22
  use magnus::{
22
23
  error::{ErrorType, Result as MagnusResult},
@@ -33,7 +34,6 @@ use tokio::sync::{
33
34
  };
34
35
  static ID_CALL: LazyId = LazyId::new("call");
35
36
  static ID_MESSAGE: LazyId = LazyId::new("message");
36
- static ID_BACKTRACE: LazyId = LazyId::new("backtrace");
37
37
 
38
38
  #[derive(Debug)]
39
39
  #[magnus::wrap(class = "Itsi::Request", free_immediately, size)]
@@ -49,6 +49,7 @@ pub struct ItsiRequest {
49
49
  pub server: Arc<Server>,
50
50
  pub response: ItsiResponse,
51
51
  pub start: Instant,
52
+ pub content_type: String,
52
53
  }
53
54
 
54
55
  impl fmt::Display for ItsiRequest {
@@ -82,6 +83,14 @@ impl ItsiRequest {
82
83
  }
83
84
  }
84
85
 
86
+ pub fn is_json(&self) -> bool {
87
+ self.content_type.eq("application/json")
88
+ }
89
+
90
+ pub fn is_html(&self) -> bool {
91
+ self.content_type.eq("text/html")
92
+ }
93
+
85
94
  pub fn process(
86
95
  self,
87
96
  ruby: &Ruby,
@@ -106,14 +115,7 @@ impl ItsiRequest {
106
115
  debug!("Connection closed by client");
107
116
  response.close();
108
117
  } else if let Some(rb_err) = err.value() {
109
- let backtrace = rb_err
110
- .funcall::<_, _, Vec<String>>(*ID_BACKTRACE, ())
111
- .unwrap_or_default();
112
-
113
- error!("Error occurred in Handler: {:?}", rb_err);
114
- for line in backtrace {
115
- error!("{}", line);
116
- }
118
+ print_rb_backtrace(rb_err);
117
119
  response.internal_server_error(err.to_string());
118
120
  } else {
119
121
  response.internal_server_error(err.to_string());
@@ -175,8 +177,27 @@ impl ItsiRequest {
175
177
  server,
176
178
  listener,
177
179
  version: format!("{:?}", &parts.version),
178
- response: ItsiResponse::new(parts.clone(), response_channel.0),
180
+ response: ItsiResponse::new(
181
+ parts.clone(),
182
+ response_channel.0,
183
+ parts
184
+ .headers
185
+ .get("Accept")
186
+ .unwrap_or(&HeaderValue::from_static("text/html"))
187
+ .to_str()
188
+ .unwrap()
189
+ .to_string(),
190
+ ),
179
191
  start: Instant::now(),
192
+ content_type: parts
193
+ .headers
194
+ .get("Content-Type")
195
+ .unwrap_or(&HeaderValue::from_static(
196
+ "application/x-www-form-urlencoded",
197
+ ))
198
+ .to_str()
199
+ .unwrap()
200
+ .to_string(),
180
201
  parts,
181
202
  },
182
203
  response_channel.1,
@@ -37,6 +37,7 @@ use crate::server::serve_strategy::single_mode::RunningPhase;
37
37
  #[derive(Debug, Clone)]
38
38
  pub struct ItsiResponse {
39
39
  pub data: Arc<ResponseData>,
40
+ pub accept: String,
40
41
  }
41
42
 
42
43
  #[derive(Debug)]
@@ -76,7 +77,6 @@ impl ItsiResponse {
76
77
  (ReceiverStream::new(receiver), shutdown_rx),
77
78
  |(mut receiver, mut shutdown_rx)| async move {
78
79
  if let RunningPhase::ShutdownPending = *shutdown_rx.borrow() {
79
- warn!("Disconnecting streaming client.");
80
80
  return None;
81
81
  }
82
82
  loop {
@@ -279,7 +279,8 @@ impl ItsiResponse {
279
279
  if let Some(writer) = writer.write().as_ref() {
280
280
  writer
281
281
  .blocking_send(Some(frame))
282
- .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)?;
282
+ .map_err(|_| itsi_error::ItsiError::ClientConnectionClosed)
283
+ .ok();
283
284
  }
284
285
  Ok(0)
285
286
  }
@@ -293,11 +294,19 @@ impl ItsiResponse {
293
294
  Ok(true)
294
295
  }
295
296
 
297
+ pub fn is_html(&self) -> bool {
298
+ self.accept.starts_with("text/html")
299
+ }
300
+
301
+ pub fn is_json(&self) -> bool {
302
+ self.accept.starts_with("application/json")
303
+ }
304
+
296
305
  pub fn close_read(&self) -> MagnusResult<bool> {
297
- todo!();
306
+ Ok(true)
298
307
  }
299
308
 
300
- pub fn new(parts: Parts, response_writer: mpsc::Sender<Option<Bytes>>) -> Self {
309
+ pub fn new(parts: Parts, response_writer: mpsc::Sender<Option<Bytes>>, accept: String) -> Self {
301
310
  Self {
302
311
  data: Arc::new(ResponseData {
303
312
  response: RwLock::new(Some(Response::new(BoxBody::new(Empty::new())))),
@@ -306,6 +315,7 @@ impl ItsiResponse {
306
315
  hijacked_socket: RwLock::new(None),
307
316
  parts,
308
317
  }),
318
+ accept,
309
319
  }
310
320
  }
311
321
 
@@ -101,7 +101,7 @@ impl FromStr for Bind {
101
101
  "IPv6 addresses must use [ ] when specifying a port".to_owned(),
102
102
  ));
103
103
  } else {
104
- (h, None) // Treat as a hostname
104
+ (h, p.parse::<u16>().ok()) // Treat as a hostname
105
105
  }
106
106
  } else {
107
107
  (url, None)
@@ -110,18 +110,22 @@ impl FromStr for Bind {
110
110
  let address = if let Ok(ip) = host.parse::<IpAddr>() {
111
111
  BindAddress::Ip(ip)
112
112
  } else {
113
- resolve_hostname(host)
114
- .map(BindAddress::Ip)
115
- .ok_or(ItsiError::ArgumentError(format!(
116
- "Failed to resolve hostname {}",
117
- host
118
- )))?
113
+ match protocol {
114
+ BindProtocol::Https | BindProtocol::Http => resolve_hostname(host)
115
+ .map(BindAddress::Ip)
116
+ .ok_or(ItsiError::ArgumentError(format!(
117
+ "Failed to resolve hostname {}",
118
+ host
119
+ )))?,
120
+ BindProtocol::Unix | BindProtocol::Unixs => BindAddress::UnixSocket(host.into()),
121
+ }
119
122
  };
120
- let (port, address) = match protocol {
121
- BindProtocol::Http => (port.or(Some(80)), address),
122
- BindProtocol::Https => (port.or(Some(443)), address),
123
- BindProtocol::Unix => (None, BindAddress::UnixSocket(host.into())),
124
- BindProtocol::Unixs => (None, BindAddress::UnixSocket(host.into())),
123
+
124
+ let port = match protocol {
125
+ BindProtocol::Http => port.or(Some(80)),
126
+ BindProtocol::Https => port.or(Some(443)),
127
+ BindProtocol::Unix => None,
128
+ BindProtocol::Unixs => None,
125
129
  };
126
130
 
127
131
  let tls_config = match protocol {