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
@@ -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: [],
@@ -52,17 +52,12 @@ module Itsi
52
52
  @accepts = accepts.map { |s| s.is_a?(Regexp) ? s : s.to_s }
53
53
 
54
54
  @options = {
55
- middleware_loaders: [],
55
+ nested_locations: [],
56
56
  middleware_loader: lambda do
57
- @options[:middleware_loaders].each(&:call)
57
+ @options[:nested_locations].each(&:call)
58
58
  @middleware[:app] ||= {}
59
59
  @middleware[:app][:app_proc] = @middleware[:app]&.[](:preloader)&.call || DEFAULT_APP[]
60
- if errors.any?
61
- error = errors.first.first
62
- error.set_backtrace(error.backtrace.drop_while{|r| r =~ /itsi\/server\/config/ })
63
- raise error
64
- end
65
- flatten_routes
60
+ [flatten_routes, Config.errors_to_error_lines(errors)]
66
61
  end
67
62
  }
68
63
 
@@ -78,7 +73,7 @@ module Itsi
78
73
  option_name = option.option_name
79
74
  define_method(option_name) do |*args, **kwargs, &blk|
80
75
  option.new(self, *args, **kwargs, &blk).build!
81
- rescue => e
76
+ rescue Exception => e # rubocop:disable Lint/RescueException
82
77
  @errors << [e, caller[1]]
83
78
  end
84
79
  end
@@ -89,7 +84,7 @@ module Itsi
89
84
  middleware.new(self, *args, **kwargs, &blk).build!
90
85
  rescue Config::Endpoint::InvalidHandlerException => e
91
86
  @errors << [e, "#{e.backtrace[0]}:in #{e.message}"]
92
- rescue => e
87
+ rescue Exception => e # rubocop:disable Lint/RescueException
93
88
  @errors << [e, caller[1]]
94
89
  end
95
90
  end
@@ -99,7 +94,7 @@ module Itsi
99
94
  @grpc_reflected_services.concat(handlers)
100
95
 
101
96
  location("grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
102
- "grpc.reflection.v1.ServerReflection/ServerReflectionInfo") do
97
+ "grpc.reflection.v1.ServerReflection/ServerReflectionInfo") do
103
98
  @middleware[:app] = { preloader: lambda {
104
99
  Itsi::Server::GrpcInterface.reflection_for(handlers)
105
100
  }, request_type: "grpc" }
@@ -141,7 +136,7 @@ module Itsi
141
136
  when Regexp
142
137
  seg.source
143
138
  else
144
- parts = seg.split('/')
139
+ parts = seg.split("/")
145
140
  parts.map do |part|
146
141
  case part
147
142
  when /^:([A-Za-z_]\w*)(?:\(([^)]*)\))?$/
@@ -184,11 +179,11 @@ module Itsi
184
179
 
185
180
  chain.each do |n|
186
181
  n.middleware.each do |k, v|
187
- if v[:combine]
188
- merged[k] = ([merged[k] || []] + [v]).flatten
189
- else
190
- merged[k] = v
191
- end
182
+ merged[k] = if v[:combine]
183
+ ([merged[k] || []] + [v]).flatten
184
+ else
185
+ v
186
+ end
192
187
  end
193
188
  end
194
189
  deep_stringify_keys(merged)
@@ -2,16 +2,20 @@ module Itsi
2
2
  class Server
3
3
  module KnownPaths
4
4
  ALL = []
5
- Dir.glob(File.join(__dir__, 'known_paths', '**', '*.txt')).each do |file|
6
- method_name = file[/known_paths\/(.*?)\.txt/,1].gsub(/([a-z])([A-Z])/, "\\1_\\2")
7
- .gsub(/-|\.|\//, "_")
8
- .gsub(/(^|\/)[0-9]/){|match| "FO"}.downcase.to_sym
5
+ Dir.glob(File.join(__dir__, "known_paths", "**", "*.txt")).each do |file|
6
+ method_name = file[%r{known_paths/(.*?)\.txt}, 1].gsub(/([a-z])([A-Z])/, "\\1_\\2")
7
+ .gsub(%r{-|\.|/}, "_")
8
+ .gsub(%r{(^|/)[0-9]}) do |match|
9
+ match.gsub(/\d/) do |digit|
10
+ %w[zero one two three four five six seven eight nine][digit.to_i]
11
+ end
12
+ end.downcase.to_sym
9
13
 
10
14
  ALL << method_name
11
- self.define_singleton_method(method_name) do
15
+ define_singleton_method(method_name) do
12
16
  File.readlines(file).map do |s|
13
- s.force_encoding('UTF-8')
14
- s.valid_encoding? ? s.strip : s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '').strip
17
+ s.force_encoding("UTF-8")
18
+ s.valid_encoding? ? s.strip : s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "").strip
15
19
  end
16
20
  end
17
21
  end
@@ -84,10 +84,6 @@ module Itsi
84
84
  end
85
85
  else
86
86
  @params[:paths] << "" if @params[:paths].empty?
87
- @params[:paths] = @params[:paths].flat_map do |p|
88
- stripped_trailing = p[/(.*)\/?$/, 1]
89
- [stripped_trailing, stripped_trailing + "/"]
90
- end.uniq
91
87
  location.location(*@params[:paths], methods: @params[:http_methods]) do
92
88
  @middleware[:app] = app
93
89
  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.
@@ -27,7 +27,7 @@ module Itsi
27
27
  end
28
28
 
29
29
  attr_accessor :location, :routes, :block, :protocols, :hosts, :ports,
30
- :extensions, :content_types, :accepts, :block
30
+ :extensions, :content_types, :accepts
31
31
 
32
32
  def initialize(location,
33
33
  *routes,
@@ -56,13 +56,13 @@ module Itsi
56
56
  block: block
57
57
  }).to_h
58
58
  @routes = params[:routes].empty? ? ["*"] : params[:routes]
59
- @methods = params[:methods]
60
- @protocols = params[:protocols] | params[:schemes]
61
- @hosts = params[:hosts]
62
- @ports = params[:ports]
63
- @extensions = params[:extensions]
64
- @content_types = params[:content_types]
65
- @accepts = params[:accepts]
59
+ @methods = params[:methods].map { |s| s.is_a?(Regexp) ? s : s.to_s }
60
+ @protocols = (params[:protocols] | params[:schemes]).map { |s| s.is_a?(Regexp) ? s : s.to_s }
61
+ @hosts = params[:hosts].map { |s| s.is_a?(Regexp) ? s : s.to_s }
62
+ @ports = params[:ports].map { |s| s.is_a?(Regexp) ? s : s.to_s }
63
+ @extensions = params[:extensions].map { |s| s.is_a?(Regexp) ? s : s.to_s }
64
+ @content_types = params[:content_types].map { |s| s.is_a?(Regexp) ? s : s.to_s }
65
+ @accepts = params[:accepts].map { |s| s.is_a?(Regexp) ? s : s.to_s }
66
66
  @block = block
67
67
  end
68
68
 
@@ -70,27 +70,31 @@ module Itsi
70
70
  @methods
71
71
  end
72
72
 
73
+ def intersect(a, b)
74
+ return b if a.empty?
75
+ return a if b.empty?
76
+ a & b
77
+ end
78
+
73
79
  def build!
74
80
  build_child = lambda {
75
- location.children << DSL.new(
81
+ child = DSL.new(
76
82
  location,
77
83
  routes: routes,
78
- methods: Array(http_methods) | location.http_methods,
79
- protocols: Array(protocols) | location.protocols,
80
- hosts: Array(hosts) | location.hosts,
81
- ports: Array(ports) | location.ports,
82
- extensions: Array(extensions) | location.extensions,
83
- content_types: Array(content_types) | location.content_types,
84
- accepts: Array(accepts) | location.accepts,
84
+ methods: intersect(http_methods, location.http_methods),
85
+ protocols: intersect(protocols, location.protocols),
86
+ hosts: intersect(hosts, location.hosts),
87
+ ports: intersect(ports, location.ports),
88
+ extensions: intersect(extensions, location.extensions),
89
+ content_types: intersect(content_types, location.content_types),
90
+ accepts: intersect(accepts, location.accepts),
85
91
  controller: location.controller,
86
92
  &block
87
93
  )
94
+ child.options[:nested_locations].each(&:call)
95
+ location.children << child
88
96
  }
89
- if location.parent.nil?
90
- location.options[:middleware_loaders] << build_child
91
- else
92
- build_child[]
93
- end
97
+ location.options[:nested_locations] << build_child
94
98
  end
95
99
 
96
100
  end
@@ -2,17 +2,16 @@ module Itsi
2
2
  class Server
3
3
  module Config
4
4
  class Proxy < Middleware
5
-
6
5
  insert_text <<~SNIPPET
7
- proxy \\
8
- to: "${1:http://backend.example.com/api{path}{query}}", \\
9
- backends: [${2:"127.0.0.1:3001", "127.0.0.1:3002"}], \\
10
- backend_priority: ${3|"round_robin","ordered","random"|}, \\
11
- headers: { ${4| "X-Forwarded-For" => { rewrite: "{addr}" },|} }, \\
12
- verify_ssl: ${5|true,false|}, \\
13
- timeout: ${6|30,60|}, \\
14
- tls_sni: ${7|true,false|}, \\
15
- error_response: ${8|"bad_gateway", "service_unavailable", { code: 503\\, default_format: "html"\\, html: { inline: "<h1>Service Unavailable</h1>" } }|}
6
+ proxy \\
7
+ to: "${1:http://backend.example.com{path_and_query}",
8
+ backends: [${2:"127.0.0.1:3001", "127.0.0.1:3002"}],
9
+ backend_priority: ${3|"round_robin","ordered","random"|},
10
+ headers: { ${4| "X-Forwarded-For" => { rewrite: "{addr}" },|} },
11
+ verify_ssl: ${5|true,false|},
12
+ timeout: ${6|30,60|},
13
+ tls_sni: ${7|true,false|},
14
+ error_response: ${8|"bad_gateway", "service_unavailable", { code: 503\\, default_format: "html"\\, html: { inline: "<h1>Service Unavailable</h1>" } }|}
16
15
  SNIPPET
17
16
 
18
17
  detail "Forwards incoming requests to a backend server using dynamic URL rewriting. Supports various backend selection strategies and header overriding."
@@ -21,18 +20,20 @@ module Itsi
21
20
  {
22
21
  to: Type(String) & Required(),
23
22
  backends: Array(Type(String)),
24
- backend_priority: Enum(["round_robin", "ordered", "random"]).default("round_robin"),
23
+ backend_priority: Enum(%w[round_robin ordered random]).default("round_robin"),
25
24
  headers: Hash(Type(String), Type(String)).default({}),
26
25
  verify_ssl: Bool().default(true),
27
26
  tls_sni: Bool().default(true),
28
27
  timeout: Type(Integer).default(30),
29
- error_response: Type(ErrorResponseDef).default("bad_gateway"),
28
+ error_response: Type(ErrorResponseDef).default("bad_gateway")
30
29
  }
31
30
  end
32
31
 
33
32
  def build!
34
- require 'uri'
35
- @params[:backends]||= URI.extract(@params[:to]).map(&URI.method(:parse)).map{|u| "#{u.scheme}://#{u.host}:#{u.port}" }
33
+ require "uri"
34
+ @params[:backends] ||= URI.extract(@params[:to]).map(&URI.method(:parse)).map do |u|
35
+ "#{u.scheme}://#{u.host}:#{u.port}"
36
+ end
36
37
  super
37
38
  end
38
39
  end
@@ -2,12 +2,11 @@ module Itsi
2
2
  class Server
3
3
  module Config
4
4
  class RackupFile < Middleware
5
-
6
5
  insert_text <<~SNIPPET
7
- rackup_file \\
8
- "config.ru",
9
- nonblocking: ${2|true,false|},
10
- sendfile: ${3|true,false|}
6
+ rackup_file \\
7
+ "config.ru",
8
+ nonblocking: ${2|true,false|},
9
+ sendfile: ${3|true,false|}
11
10
 
12
11
  SNIPPET
13
12
 
@@ -23,20 +22,18 @@ module Itsi
23
22
  def initialize(location, app, **params)
24
23
  super(location, params)
25
24
  raise "Rackup file must be a string" unless app.is_a?(String)
25
+
26
26
  @app = Itsi::Server::RackInterface.for(app)
27
27
  end
28
28
 
29
29
  def build!
30
30
  app_args = {
31
- preloader: -> { @app},
31
+ preloader: -> { @app },
32
32
  sendfile: @params[:sendfile],
33
33
  nonblocking: @params[:nonblocking],
34
- base_path: "^(?<base_path>#{location.paths_from_parent.gsub(/\.\*\)$/, ')')}).*$"
34
+ base_path: "^(?<base_path>#{location.paths_from_parent.gsub(/\.\*\)$/, ")")}).*$"
35
35
  }
36
36
  location.middleware[:app] = app_args
37
- location.location("*") do
38
- @middleware[:app] = app_args
39
- end
40
37
  end
41
38
  end
42
39
  end
@@ -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
 
@@ -75,11 +75,15 @@ module Itsi
75
75
  @params[:allowed_extensions] << ""
76
76
  end
77
77
 
78
- @params[:base_path] = "^(?<base_path>#{location.paths_from_parent}).*$"
79
- params = @params
80
- location.location("*", extensions: @params[:allowed_extensions]) do
81
- @middleware[:static_assets] = params
78
+ if @params[:allowed_extensions].any? && @params[:auto_index]
79
+ @params[:allowed_extensions] |= ["html"]
80
+ @params[:allowed_extensions] |= [""]
82
81
  end
82
+
83
+ @params[:base_path] = "^(?<base_path>#{location.paths_from_parent.gsub(/\.\*\)$/,")")}).*$"
84
+ params = @params
85
+
86
+ location.middleware[:static_assets] = params
83
87
  end
84
88
  end
85
89
  end
@@ -13,6 +13,20 @@ The String Rewrite mechanism is used when configuring Itsi for
13
13
 
14
14
  It allows you to create dynamic strings from a template by combining literal text with placeholders. Placeholders (denoted using curly braces: `{}`) are replaced at runtime with data derived from the HTTP request, response, or context.
15
15
 
16
+ Modifiers can be appended after a pipe | to transform the substituted value.
17
+
18
+ ## Modifiers
19
+
20
+ After a placeholder name, add |<modifier>:<arg> (or for replace, |replace:<from>,<to>). Available modifiers:
21
+
22
+ `strip_prefix:<text>` If the substituted value starts with <text>, remove that prefix.
23
+
24
+ `strip_suffix:<text>` If the substituted value ends with <text>, remove that suffix.
25
+
26
+ `replace:<from>,<to>` Replace all occurrences of <from> in the substituted value with <to>.
27
+
28
+ Modifiers are applied in the order they appear. You can chain multiple modifiers by repeating the |<modifier>:<arg> syntax (e.g. `{path|strip_prefix:/rails|replace:old,new}`).
29
+
16
30
  ### Rewriting a Request
17
31
 
18
32
  The following placeholders are supported:
@@ -4,7 +4,6 @@ module Itsi
4
4
  class Option
5
5
  include ConfigHelpers
6
6
 
7
-
8
7
  def build!
9
8
  location.options[self.class.option_name] = @params
10
9
  end
@@ -3,7 +3,7 @@ module Itsi
3
3
  module Config
4
4
  class Include < Option
5
5
 
6
- insert_text "include \"${1|other_file.rb|}\" # Include another file to be loaded within the current configuration"
6
+ insert_text "include \"${1|other_file|}\" # Include another file to be loaded within the current configuration"
7
7
 
8
8
  detail "Include another file to be loaded within the current configuration"
9
9
 
@@ -8,9 +8,9 @@ This option determines whether the Nagle's algorithm is disabled, allowing small
8
8
 
9
9
  ## Configuration
10
10
  ```ruby {filename=Itsi.rb}
11
- reuse_address true
11
+ nodelay true
12
12
  ```
13
13
 
14
14
  ```ruby {filename=Itsi.rb}
15
- reuse_address false
15
+ nodelay false
16
16
  ```
@@ -4,7 +4,7 @@ url: /options/reuse_address
4
4
  ---
5
5
 
6
6
  Configures whether the server should bind to the underlying socket using the `SO_REUSEADDR` option.
7
- This optiondetermines whether the server allows the reuse of local addresses during binding. This can be useful in scenarios where a socket needs to be quickly rebound without waiting for the operating system to release the address.
7
+ This option determines whether the server allows the reuse of local addresses during binding. This can be useful in scenarios where a socket needs to be quickly rebound without waiting for the operating system to release the address.
8
8
 
9
9
  ## Configuration
10
10
  ```ruby {filename=Itsi.rb}
@@ -1,5 +1,7 @@
1
- require 'date'
2
- require 'time'
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
3
5
 
4
6
  module Itsi
5
7
  class Server
@@ -10,25 +12,19 @@ module Itsi
10
12
 
11
13
  def self.new(defaults = nil, &defaults_blk)
12
14
  defaults = TypedStruct.module_eval(&defaults_blk) if defaults_blk
13
- unless defaults.is_a?(Hash)
14
- return defaults
15
- end
15
+ return defaults unless defaults.is_a?(Hash)
16
+
16
17
  defaults.transform_values! { _1.is_a?(Validation) ? _1.default(nil) : _1 }
17
18
  Struct.new(*defaults.keys, keyword_init: true) do
18
19
  define_method(:initialize) do |*input, validate: true, **raw_input|
20
+ raise "─ Invalid input to #{self}: #{input.last}" if input.last && !input.last.is_a?(Hash)
19
21
 
20
- if input.last && !input.last.is_a?(Hash)
21
- raise "─ Invalid input to #{self}: #{input.last}"
22
- end
23
- raw_input.transform_keys!{|k| k.to_s.downcase.to_sym }
24
- raw_input.merge!(input.pop.transform_keys!{|k| k.to_s.downcase.to_sym}) if input.last.is_a?(Hash)
22
+ raw_input.transform_keys! { |k| k.to_s.downcase.to_sym }
23
+ raw_input.merge!(input.pop.transform_keys! { |k| k.to_s.downcase.to_sym }) if input.last.is_a?(Hash)
25
24
 
26
25
  excess_keys = raw_input.keys - defaults.keys
27
26
 
28
- if excess_keys.any?
29
- raise "─ Unsupported keys #{excess_keys}"
30
-
31
- end
27
+ raise "─ Unsupported keys #{excess_keys}" if excess_keys.any?
32
28
 
33
29
  initial_values = defaults.each_with_object({}) do |(k, default_config), inputs|
34
30
  value = raw_input.key?(k) ? raw_input[k] : default_config[VALUE].dup
@@ -106,9 +102,7 @@ module Itsi
106
102
 
107
103
  def &(other)
108
104
  tail = self
109
- while tail.next
110
- tail = tail.next
111
- end
105
+ tail = tail.next while tail.next
112
106
  tail.next = other
113
107
  self
114
108
  end
@@ -148,10 +142,14 @@ module Itsi
148
142
  elsif validation.eql?(::Date) then Date.parse(value.to_s)
149
143
  elsif validation.eql?(Float) then Float(value)
150
144
  elsif validation.eql?(Integer) then Integer(value)
151
- elsif validation.eql?(Proc) then
145
+ elsif validation.eql?(Proc)
152
146
  raise ArgumentError, "Invalid #{validation} value: #{value.inspect}" unless value.is_a?(Proc)
153
147
  elsif validation.eql?(String) || validation.eql?(Symbol)
154
- raise ArgumentError, "Invalid #{validation} value: #{value.inspect}" unless value.is_a?(String) || value.is_a?(Symbol)
148
+ unless value.is_a?(String) || value.is_a?(Symbol)
149
+ raise ArgumentError,
150
+ "Invalid #{validation} value: #{value.inspect}"
151
+ end
152
+
155
153
  if validation.eql?(String)
156
154
  value.to_s
157
155
  elsif validation.eql?(Symbol)
@@ -178,33 +176,32 @@ module Itsi
178
176
  {
179
177
  Bool: Validation.new(:Bool, [[true, false]]),
180
178
  Required: Validation.new(:Required, ->(value) { !value.nil? }),
181
- Or: ->(*validations){
182
- Validation.new(:Or, ->(v){
183
-
179
+ Or: lambda { |*validations|
180
+ Validation.new(:Or, lambda { |v|
184
181
  return true if v.nil?
182
+
185
183
  errs = []
186
184
  validations.each do |validation|
187
- begin
188
- v = validation.validate!(v)
189
- return v
190
- rescue StandardError => e
191
- errs << e.message
192
- end
185
+ v = validation.validate!(v)
186
+ return v
187
+ rescue StandardError => e
188
+ errs << e.message
193
189
  end
194
190
  raise StandardError.new("─ Validation failed (None match:) \n └#{errs.join("\n └")}")
195
191
  })
196
192
  },
197
- Range: ->(input_range) {
193
+ Range: lambda { |input_range|
198
194
  Validation.new(:Range, [input_range])
199
195
  },
200
196
  Length: lambda { |input_length|
201
197
  Validation.new(:Length, ->(value) { input_length === value.length })
202
198
  },
203
- Hash: ->(key_type, value_type) {
204
- Validation.new(:Hash, ->(v){
205
- return true if v.nil?
206
- raise StandardError.new("Expected hash got #{v.class}") unless v.is_a?(Hash)
207
- v.map do |k, v|
199
+ Hash: lambda { |key_type, value_type|
200
+ Validation.new(:Hash, lambda { |hash|
201
+ return true if hash.nil?
202
+ raise StandardError.new("Expected hash got #{hash.class}") unless hash.is_a?(Hash)
203
+
204
+ hash.map do |k, v|
208
205
  [
209
206
  key_type.validate!(k),
210
207
  value_type.validate!(v)
@@ -213,7 +210,7 @@ module Itsi
213
210
  })
214
211
  },
215
212
  Type: ->(input_type) { Validation.new(:Type, input_type) },
216
- Enum: ->(allowed_values) { Validation.new(:Enum, [allowed_values.map{|v| v.kind_of?(Symbol) ? v.to_s : v}]) },
213
+ Enum: ->(allowed_values) { Validation.new(:Enum, [allowed_values.map { |v| v.is_a?(Symbol) ? v.to_s : v }]) },
217
214
  Array: lambda { |*value_validations|
218
215
  Validation.new(:Array, [::Array, lambda { |value|
219
216
  return true unless value