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
@@ -1,14 +1,15 @@
1
1
  use crate::default_responses::{NOT_FOUND_RESPONSE, TIMEOUT_RESPONSE};
2
2
  use crate::ruby_types::itsi_server::itsi_server_config::{ItsiServerTokenPreference, ServerParams};
3
3
  use crate::server::binds::listener::ListenerInfo;
4
- use crate::server::http_message_types::{ConversionExt, HttpResponse, RequestExt, ResponseFormat};
4
+ use crate::server::http_message_types::{
5
+ ConversionExt, HttpRequest, HttpResponse, RequestExt, ResponseFormat,
6
+ };
5
7
  use crate::server::lifecycle_event::LifecycleEvent;
6
8
  use crate::server::middleware_stack::MiddlewareLayer;
7
9
  use crate::server::request_job::RequestJob;
8
10
  use crate::server::serve_strategy::single_mode::RunningPhase;
9
11
  use crate::server::signal::send_lifecycle_event;
10
- use chrono;
11
- use chrono::Local;
12
+ use chrono::{self, DateTime, Local};
12
13
  use either::Either;
13
14
  use http::header::ACCEPT_ENCODING;
14
15
  use http::{HeaderValue, Request};
@@ -18,6 +19,7 @@ use itsi_error::ItsiError;
18
19
  use regex::Regex;
19
20
  use std::sync::atomic::{AtomicBool, Ordering};
20
21
  use std::sync::OnceLock;
22
+ use std::time::{Duration, Instant};
21
23
  use tracing::error;
22
24
 
23
25
  use std::{future::Future, ops::Deref, pin::Pin, sync::Arc};
@@ -68,17 +70,16 @@ impl Deref for RequestContextInner {
68
70
  }
69
71
 
70
72
  pub struct RequestContextInner {
71
- pub request_id: u128,
73
+ pub request_id: u64,
72
74
  pub service: ItsiHttpService,
73
75
  pub accept: ResponseFormat,
74
76
  pub matching_pattern: Option<Arc<Regex>>,
75
77
  pub origin: OnceLock<Option<String>>,
76
78
  pub response_format: OnceLock<ResponseFormat>,
77
- pub start_time: chrono::DateTime<chrono::Utc>,
78
- pub request: Option<Arc<Request<Incoming>>>,
79
- pub request_start_time: OnceLock<chrono::DateTime<Local>>,
79
+ pub request_start_time: OnceLock<DateTime<Local>>,
80
+ pub start_instant: Instant,
80
81
  pub if_none_match: OnceLock<Option<String>>,
81
- pub supported_encoding_set: Vec<HeaderValue>,
82
+ pub supported_encoding_set: OnceLock<Vec<HeaderValue>>,
82
83
  pub is_ruby_request: Arc<AtomicBool>,
83
84
  }
84
85
 
@@ -87,27 +88,38 @@ impl HttpRequestContext {
87
88
  service: ItsiHttpService,
88
89
  matching_pattern: Option<Arc<Regex>>,
89
90
  accept: ResponseFormat,
90
- supported_encoding_set: Vec<HeaderValue>,
91
91
  is_ruby_request: Arc<AtomicBool>,
92
92
  ) -> Self {
93
93
  HttpRequestContext {
94
94
  inner: Arc::new(RequestContextInner {
95
- request_id: rand::random::<u128>(),
95
+ request_id: rand::random::<u64>(),
96
96
  service,
97
97
  matching_pattern,
98
98
  accept,
99
99
  origin: OnceLock::new(),
100
100
  response_format: OnceLock::new(),
101
- start_time: chrono::Utc::now(),
102
- request: None,
103
101
  request_start_time: OnceLock::new(),
102
+ start_instant: Instant::now(),
104
103
  if_none_match: OnceLock::new(),
105
- supported_encoding_set,
104
+ supported_encoding_set: OnceLock::new(),
106
105
  is_ruby_request,
107
106
  }),
108
107
  }
109
108
  }
110
109
 
110
+ pub fn set_supported_encoding_set(&self, req: &HttpRequest) {
111
+ let supported_encoding_set = req
112
+ .headers()
113
+ .get_all(ACCEPT_ENCODING)
114
+ .into_iter()
115
+ .cloned()
116
+ .collect::<Vec<_>>();
117
+ self.inner
118
+ .supported_encoding_set
119
+ .set(supported_encoding_set)
120
+ .unwrap();
121
+ }
122
+
111
123
  pub fn set_origin(&self, origin: Option<String>) {
112
124
  self.inner.origin.set(origin).unwrap();
113
125
  }
@@ -121,28 +133,29 @@ impl HttpRequestContext {
121
133
  }
122
134
 
123
135
  pub fn short_request_id(&self) -> String {
124
- format!("{:016x}", self.inner.request_id & 0xffff_ffff_ffff_ffff)
136
+ format!("{:08x}", self.inner.request_id & 0xffff_ffff)
125
137
  }
126
138
 
127
139
  pub fn request_id(&self) -> String {
128
- format!("{:016x}", self.inner.request_id)
140
+ format!("{:08x}", self.inner.request_id)
129
141
  }
130
142
 
131
- pub fn track_start_time(&self) {
143
+ pub fn init_logging_params(&self) {
132
144
  self.inner
133
145
  .request_start_time
134
146
  .get_or_init(chrono::Local::now);
135
147
  }
136
148
 
137
- pub fn start_time(&self) -> Option<chrono::DateTime<Local>> {
149
+ pub fn start_instant(&self) -> Instant {
150
+ self.inner.start_instant
151
+ }
152
+
153
+ pub fn start_time(&self) -> Option<DateTime<Local>> {
138
154
  self.inner.request_start_time.get().cloned()
139
155
  }
140
156
 
141
- pub fn get_response_time(&self) -> Option<chrono::TimeDelta> {
142
- self.inner
143
- .request_start_time
144
- .get()
145
- .map(|instant| Local::now() - instant)
157
+ pub fn get_response_time(&self) -> Duration {
158
+ self.inner.start_instant.elapsed()
146
159
  }
147
160
 
148
161
  pub fn set_response_format(&self, format: ResponseFormat) {
@@ -152,6 +165,10 @@ impl HttpRequestContext {
152
165
  pub fn response_format(&self) -> &ResponseFormat {
153
166
  self.inner.response_format.get().unwrap()
154
167
  }
168
+
169
+ pub fn supported_encoding_set(&self) -> Option<&Vec<HeaderValue>> {
170
+ self.inner.supported_encoding_set.get()
171
+ }
155
172
  }
156
173
 
157
174
  const SERVER_TOKEN_VERSION: HeaderValue =
@@ -170,12 +187,7 @@ impl Service<Request<Incoming>> for ItsiHttpService {
170
187
  let accept: ResponseFormat = req.accept().into();
171
188
  let accept_clone = accept.clone();
172
189
  let is_single_mode = self.server_params.workers == 1;
173
- let supported_encoding_set = req
174
- .headers()
175
- .get_all(ACCEPT_ENCODING)
176
- .into_iter()
177
- .cloned()
178
- .collect::<Vec<_>>();
190
+
179
191
  let request_timeout = self.server_params.request_timeout;
180
192
  let is_ruby_request = Arc::new(AtomicBool::new(false));
181
193
  let irr_clone = is_ruby_request.clone();
@@ -187,7 +199,6 @@ impl Service<Request<Incoming>> for ItsiHttpService {
187
199
  self_clone,
188
200
  matching_pattern,
189
201
  accept_clone.clone(),
190
- supported_encoding_set,
191
202
  irr_clone,
192
203
  );
193
204
  let mut depth = 0;
@@ -229,8 +240,8 @@ impl Service<Request<Incoming>> for ItsiHttpService {
229
240
  Ok(resp)
230
241
  };
231
242
 
232
- Box::pin(async move {
233
- if let Some(timeout_duration) = request_timeout {
243
+ if let Some(timeout_duration) = request_timeout {
244
+ Box::pin(async move {
234
245
  match timeout(timeout_duration, service_future).await {
235
246
  Ok(result) => result,
236
247
  Err(_) => {
@@ -249,9 +260,9 @@ impl Service<Request<Incoming>> for ItsiHttpService {
249
260
  Ok(TIMEOUT_RESPONSE.to_http_response(accept).await)
250
261
  }
251
262
  }
252
- } else {
253
- service_future.await
254
- }
255
- })
263
+ })
264
+ } else {
265
+ Box::pin(service_future)
266
+ }
256
267
  }
257
268
  }
@@ -51,6 +51,10 @@ pub static ROOT_STATIC_FILE_SERVER: LazyLock<StaticFileServer> = LazyLock::new(|
51
51
  not_found_behavior: NotFoundBehavior::Error(ErrorResponse::not_found()),
52
52
  serve_hidden_files: false,
53
53
  allowed_extensions: vec!["html".to_string(), "css".to_string(), "js".to_string()],
54
+ miss_cache: Cache::builder()
55
+ .max_capacity(1000)
56
+ .time_to_live(Duration::from_secs(1))
57
+ .build(),
54
58
  })
55
59
  .unwrap()
56
60
  });
@@ -85,6 +89,7 @@ pub struct StaticFileServerConfig {
85
89
  pub headers: Option<HashMap<String, String>>,
86
90
  pub serve_hidden_files: bool,
87
91
  pub allowed_extensions: Vec<String>,
92
+ pub miss_cache: Cache<String, NotFoundBehavior>,
88
93
  }
89
94
 
90
95
  #[derive(Debug, Clone)]
@@ -389,6 +394,29 @@ impl StaticFileServer {
389
394
  abs_path: &str,
390
395
  accept: ResponseFormat,
391
396
  ) -> std::result::Result<ResolvedAsset, NotFoundBehavior> {
397
+ let ext_opt = Path::new(key)
398
+ .extension()
399
+ .and_then(|e| e.to_str())
400
+ .map(|s| s.to_lowercase());
401
+
402
+ // If the allowed list is non-empty, enforce membership
403
+ if !self.allowed_extensions.is_empty() {
404
+ match ext_opt {
405
+ Some(ref ext)
406
+ if self
407
+ .allowed_extensions
408
+ .iter()
409
+ .any(|ae| ae.eq_ignore_ascii_case(ext)) => {}
410
+ None if self.config.try_html_extension => {}
411
+ _ => {
412
+ return Err(self.config.not_found_behavior.clone());
413
+ }
414
+ }
415
+ }
416
+
417
+ if let Some(cached_nf) = self.miss_cache.get(key) {
418
+ return Err(cached_nf.clone());
419
+ }
392
420
  // First check if we have a cached mapping for this key
393
421
  if let Some(path) = self.key_to_path.lock().await.get(key) {
394
422
  // Check if the cached entry is still valid
@@ -449,7 +477,6 @@ impl StaticFileServer {
449
477
 
450
478
  let mut full_path = self.config.root_dir.clone();
451
479
  full_path.push(normalized_path);
452
- debug!("Resolving path {:?}", full_path);
453
480
  // Check if path exists and is a file
454
481
  match tokio::fs::metadata(&full_path).await {
455
482
  Ok(metadata) => {
@@ -561,7 +588,6 @@ impl StaticFileServer {
561
588
  }
562
589
  Err(_) => {
563
590
  // Path doesn't exist, try with .html extension if configured
564
- debug!("Path doesn't exist");
565
591
  if self.config.try_html_extension {
566
592
  let mut html_path = full_path.clone();
567
593
  html_path.set_extension("html");
@@ -592,7 +618,9 @@ impl StaticFileServer {
592
618
  }
593
619
 
594
620
  // If we get here, we couldn't resolve the key to a file
595
- Err(self.config.not_found_behavior.clone())
621
+ let nf = self.config.not_found_behavior.clone();
622
+ self.miss_cache.insert(key.to_string(), nf.clone());
623
+ Err(nf)
596
624
  }
597
625
 
598
626
  async fn stream_file_range(
@@ -3,7 +3,7 @@
3
3
  require "stringio"
4
4
  require "socket"
5
5
  require "uri"
6
- require_relative 'http_request/response_status_shortcodes'
6
+ require_relative "http_request/response_status_shortcodes"
7
7
 
8
8
  module Itsi
9
9
  class HttpRequest
@@ -22,7 +22,7 @@ module Itsi
22
22
  end
23
23
  [header, rack_form]
24
24
  end.to_h.tap do |hm|
25
- hm.default_proc = proc { |hsh, key| "HTTP_#{key.upcase.gsub(/-/, "_")}" }
25
+ hm.default_proc = proc { |_, key| "HTTP_#{key.upcase.gsub(/-/, "_")}" }
26
26
  end
27
27
 
28
28
  def to_rack_env
@@ -78,7 +78,7 @@ module Itsi
78
78
  end
79
79
 
80
80
  def respond(
81
- _body = nil, _status = 200, _headers = nil,
81
+ _body = nil, _status = 200, _headers = nil, # rubocop:disable Lint/UnderscorePrefixedVariableName
82
82
  json: nil,
83
83
  html: nil,
84
84
  text: nil,
@@ -90,13 +90,12 @@ module Itsi
90
90
  body: _body,
91
91
  &blk
92
92
  )
93
-
94
93
  if json
95
94
  if as
96
95
  begin
97
96
  validate!(json, as: as)
98
97
  rescue ValidationError => e
99
- json = {type: 'error', message: "Validation Error: #{e.message}"}
98
+ json = { type: "error", message: "Validation Error: #{e.message}" }
100
99
  status = 400
101
100
  end
102
101
  end
@@ -118,15 +117,13 @@ module Itsi
118
117
  end
119
118
 
120
119
  response.respond(status: status, headers: headers, body: body, hijack: hijack, &blk)
121
-
122
-
123
120
  end
124
121
 
125
122
  def hijack
126
123
  self.hijacked = true
127
124
  UNIXSocket.pair.yield_self do |(server_sock, app_sock)|
128
125
  server_sock.autoclose = false
129
- self.response.hijack(server_sock.fileno)
126
+ response.hijack(server_sock.fileno)
130
127
  server_sock.sync = true
131
128
  app_sock.sync = true
132
129
  app_sock
@@ -150,37 +147,37 @@ module Itsi
150
147
  as ? apply_schema!(params, as) : params
151
148
  end
152
149
 
153
- def params(schema=nil)
154
- params = case
155
- when url_encoded? then URI.decode_www_form(build_input_io.read).to_h
156
- when json? then JSON.parse(build_input_io.read)
157
- when multipart?
158
- Rack::Multipart::Parser.parse(
159
- build_input_io,
160
- content_length,
161
- content_type,
162
- Rack::Multipart::Parser::TEMPFILE_FACTORY,
163
- Rack::Multipart::Parser::BUFSIZE,
164
- Rack::Utils.default_query_parser
165
- ).params
166
- else
167
- {}
168
- end
150
+ def params(schema = nil)
151
+ params = if url_encoded?
152
+ URI.decode_www_form(build_input_io.read).to_h
153
+ elsif json?
154
+ JSON.parse(build_input_io.read)
155
+ elsif multipart?
156
+ Rack::Multipart::Parser.parse(
157
+ build_input_io,
158
+ content_length,
159
+ content_type,
160
+ Rack::Multipart::Parser::TEMPFILE_FACTORY,
161
+ Rack::Multipart::Parser::BUFSIZE,
162
+ Rack::Utils.default_query_parser
163
+ ).params
164
+ else
165
+ {}
166
+ end
169
167
 
170
168
  params.merge!(query_params).merge!(url_params)
171
169
  validated = schema ? apply_schema!(params, schema) : params
172
- unless block_given?
173
- if multipart?
174
- raise "#params must take a block for multipart requests"
175
- else
176
- return validated
177
- end
178
- else
170
+ if block_given?
179
171
  yield validated
172
+ else
173
+ raise "#params must take a block for multipart requests" if multipart?
174
+
175
+ validated
176
+
180
177
  end
181
178
  rescue ValidationError => e
182
179
  if response.json?
183
- respond(json: {error: e.message}, status: 400)
180
+ respond(json: { error: e.message }, status: 400)
184
181
  else
185
182
  respond(e.message, 400)
186
183
  end
@@ -191,7 +188,7 @@ module Itsi
191
188
  # Unexpected error.
192
189
  # Don't reveal potential sensitive information to client.
193
190
  if response.json?
194
- respond(json: {error: "Internal Server Error"}, status: 500)
191
+ respond(json: { error: "Internal Server Error" }, status: 500)
195
192
  else
196
193
  respond("Internal Server Error", 500)
197
194
  end
@@ -205,7 +202,7 @@ module Itsi
205
202
  if params.key?(:tempfile)
206
203
  params[:tempfile].unlink
207
204
  else
208
- params.each_value { |v| clean_temp_files(v) }
205
+ params.each_value { |v| clean_temp_files(v) }
209
206
  end
210
207
  when Array then params.each { |v| clean_temp_files(v) }
211
208
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
- require 'forwardable'
2
+
3
+ require "forwardable"
3
4
  require "stringio"
4
5
  require "socket"
5
6
 
6
7
  module Itsi
7
-
8
8
  class HttpResponse
9
-
10
- def respond _body=nil, _status=200, _header=nil, status: _status, headers: _header, body: _body, hijack: false, &blk
11
-
12
- self.status = status.kind_of?(Symbol) ? HTTP_STATUS_NAME_TO_CODE_MAP.fetch(status) : status.to_i
9
+ def respond(
10
+ _body = nil, _status = 200, _header = nil, # rubocop:disable Lint/UnderscorePrefixedVariableName
11
+ status: _status, headers: _header, body: _body,
12
+ hijack: false
13
+ )
14
+ self.status = status.is_a?(Symbol) ? HTTP_STATUS_NAME_TO_CODE_MAP.fetch(status) : status.to_i
13
15
 
14
16
  body = body.to_s unless body.is_a?(String)
15
17
 
@@ -33,9 +35,9 @@ module Itsi
33
35
 
34
36
  # If you hijack the connection, you are responsible for closing it.
35
37
  # Otherwise, the response will be closed automatically.
36
- self.close unless hijack
38
+ close unless hijack
37
39
  else
38
- self.close
40
+ close
39
41
  end
40
42
  end
41
43
  end
data/lib/itsi/passfile.rb CHANGED
@@ -1,8 +1,7 @@
1
1
  module Itsi
2
2
  class Server
3
-
4
3
  module Passfile
5
- require 'io/console'
4
+ require "io/console"
6
5
 
7
6
  module_function
8
7
 
@@ -18,7 +17,7 @@ module Itsi
18
17
  line.chomp!
19
18
  next if line.empty?
20
19
 
21
- user, pass = line.split(':', 2)
20
+ user, pass = line.split(":", 2)
22
21
  creds[user] = pass
23
22
  end
24
23
  end
@@ -26,14 +25,14 @@ module Itsi
26
25
  end
27
26
 
28
27
  def save(creds, filename)
29
- File.open(filename, 'w', 0o600) do |f|
28
+ File.open(filename, "w", 0o600) do |f|
30
29
  creds.each do |u, p|
31
30
  f.puts "#{u}:#{p}"
32
31
  end
33
32
  end
34
33
  end
35
34
 
36
- def echo(filename, algorithm)
35
+ def echo(_, algorithm)
37
36
  print "Enter username: "
38
37
  username = $stdin.gets.chomp
39
38
 
@@ -56,6 +55,7 @@ module Itsi
56
55
 
57
56
  def add(filename, algorithm)
58
57
  return unless (creds = load(filename))
58
+
59
59
  print "Enter username: "
60
60
  username = $stdin.gets.chomp
61
61
 
@@ -98,11 +98,11 @@ module Itsi
98
98
  def list(filename)
99
99
  puts "Current credentials in '#{filename}':"
100
100
  return unless (creds = load(filename))
101
+
101
102
  creds.each do |u, p|
102
103
  puts "#{u}:#{p}"
103
104
  end
104
105
  end
105
-
106
106
  end
107
107
  end
108
108
  end
@@ -2,7 +2,6 @@ module Itsi
2
2
  class Server
3
3
  module Config
4
4
  module ConfigHelpers
5
-
6
5
  def self.load_and_register(klass)
7
6
  config_type = klass.name.split("::").last.downcase.gsub(/([a-z]()[A-Z])/, '\1_\2')
8
7
 
@@ -17,35 +16,35 @@ module Itsi
17
16
  following = klass.subclasses
18
17
  new_class = (following - current).first
19
18
 
20
- documentation_file = "#{file[/(.*)\.rb/,1]}.md"
21
- if ! File.exist?(documentation_file)
22
- documentation_file = "#{file[/(.*)\/[^\/]+\.rb/,1]}/_index.md"
23
- end
24
- if File.exist?(documentation_file) && new_class
25
- new_class.documentation IO.read(documentation_file)
26
- .gsub(/^---.*?\n.*?-+/m,'') # Strip frontmatter
27
- .gsub(/^(```.*?)\{.*?\}.*$/, "\\1") # Strip filename from code blocks
28
- .gsub(/^\{\{[^\}]+\}\}/, "") # Strip Hugo blocks
29
- end
19
+ documentation_file = "#{file[/(.*)\.rb/, 1]}.md"
20
+ documentation_file = "#{file[%r{(.*)/[^/]+\.rb}, 1]}/_index.md" unless File.exist?(documentation_file)
21
+ next unless File.exist?(documentation_file) && new_class
22
+
23
+ new_class.documentation IO.read(documentation_file)
24
+ .gsub(/^---.*?\n.*?-+/m, "") # Strip frontmatter
25
+ .gsub(/^(```.*?)\{.*?\}.*$/, "\\1") # Strip filename from code blocks
26
+ .gsub(/^\{\{[^}]+\}\}/, "") # Strip Hugo blocks
30
27
  end
31
28
  end
32
29
 
33
- def normalize_keys!(hash, expected=[])
30
+ def normalize_keys!(hash, expected = [])
34
31
  hash.keys.each do |key|
35
32
  value = hash.delete(key)
36
33
  key = key.to_s.downcase.to_sym
37
34
  hash[key] = value
38
35
  raise "Unexpected key: #{key}" unless expected.include?(key)
36
+
39
37
  expected -= [key]
40
38
  end
41
- raise "Missing required keys: #{expected.join(', ')}" unless expected.empty?
39
+ raise "Missing required keys: #{expected.join(", ")}" unless expected.empty?
40
+
42
41
  hash
43
42
  end
44
43
 
45
- def self.included(cls)
46
- def cls.inherited(base)
44
+ def self.included(cls) # rubocop:disable Metrics/PerceivedComplexity,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/MethodLength
45
+ def cls.inherited(base) # rubocop:disable Metrics/MethodLength,Lint/MissingSuper,Metrics/PerceivedComplexity
47
46
  %i[detail documentation insert_text schema].each do |attr|
48
- base.define_singleton_method(attr) do |value=nil|
47
+ base.define_singleton_method(attr) do |value = nil|
49
48
  @middleware_class_attrs ||= {}
50
49
  if value
51
50
  @middleware_class_attrs[attr] = value
@@ -54,12 +53,12 @@ module Itsi
54
53
  end
55
54
  end
56
55
 
57
- base.define_method(attr) do |value=nil|
56
+ base.define_method(attr) do |_value = nil|
58
57
  self.class.send(attr)
59
58
  end
60
59
  end
61
60
 
62
- def base.schema(value=nil, &blk)
61
+ def base.schema(value = nil, &blk)
63
62
  @middleware_class_attrs ||= {}
64
63
  if blk
65
64
  @middleware_class_attrs[:schema] = TypedStruct.new(&blk)
@@ -75,29 +74,30 @@ module Itsi
75
74
 
76
75
  config_type = cls.name.split("::").last.downcase
77
76
 
78
- cls.define_singleton_method("#{config_type}_name") do |name=self.name|
77
+ cls.define_singleton_method("#{config_type}_name") do |name = self.name|
79
78
  @config_name ||= name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym
80
79
  end
81
- cls.define_method(:opt_name){ self.class.send("#{config_type}_name") }
82
- cls.define_method(:location){ @location }
80
+ cls.define_method(:opt_name) { self.class.send("#{config_type}_name") }
81
+ cls.define_method(:location) { @location }
83
82
  end
84
83
 
85
- def initialize(location, params={})
84
+ def initialize(location, params = {})
86
85
  if !self.class.ancestors.include?(Middleware) && !location.parent.nil?
87
86
  raise "#{opt_name} must be set at the top level"
88
87
  end
88
+
89
89
  @location = location
90
- @params = case self.schema
91
- when TypedStruct::Validation
92
- self.schema.validate!(params)
93
- when Array
94
- default, validation = self.schema
95
- params ? validation.validate!(params) : default
96
- when nil
97
- nil
98
- else
99
- self.schema.new(params).to_h
100
- end
90
+ @params = case schema
91
+ when TypedStruct::Validation
92
+ schema.validate!(params)
93
+ when Array
94
+ default, validation = schema
95
+ params ? validation.validate!(params) : default
96
+ when nil
97
+ nil
98
+ else
99
+ schema.new(params).to_h
100
+ end
101
101
  end
102
102
  end
103
103
  end