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.
- checksums.yaml +4 -4
- data/Cargo.lock +28 -29
- data/ext/itsi_scheduler/Cargo.toml +1 -1
- data/ext/itsi_server/Cargo.toml +1 -1
- data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +26 -3
- data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +28 -11
- data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +1 -1
- data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +1 -2
- data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +14 -2
- data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +86 -41
- data/ext/itsi_server/src/services/itsi_http_service.rs +46 -35
- data/ext/itsi_server/src/services/static_file_server.rs +31 -3
- data/lib/itsi/http_request.rb +31 -34
- data/lib/itsi/http_response.rb +10 -8
- data/lib/itsi/passfile.rb +6 -6
- data/lib/itsi/server/config/config_helpers.rb +33 -33
- data/lib/itsi/server/config/dsl.rb +16 -21
- data/lib/itsi/server/config/known_paths.rb +11 -7
- data/lib/itsi/server/config/middleware/endpoint/endpoint.rb +0 -4
- data/lib/itsi/server/config/middleware/error_response.md +13 -0
- data/lib/itsi/server/config/middleware/location.rb +25 -21
- data/lib/itsi/server/config/middleware/proxy.rb +15 -14
- data/lib/itsi/server/config/middleware/rackup_file.rb +7 -10
- data/lib/itsi/server/config/middleware/static_assets.md +40 -0
- data/lib/itsi/server/config/middleware/static_assets.rb +8 -4
- data/lib/itsi/server/config/middleware/string_rewrite.md +14 -0
- data/lib/itsi/server/config/option.rb +0 -1
- data/lib/itsi/server/config/options/include.rb +1 -1
- data/lib/itsi/server/config/options/nodelay.md +2 -2
- data/lib/itsi/server/config/options/reuse_address.md +1 -1
- data/lib/itsi/server/config/typed_struct.rb +32 -35
- data/lib/itsi/server/config.rb +107 -92
- data/lib/itsi/server/default_app/default_app.rb +1 -1
- data/lib/itsi/server/grpc/grpc_call.rb +4 -5
- data/lib/itsi/server/grpc/grpc_interface.rb +6 -7
- data/lib/itsi/server/rack/handler/itsi.rb +0 -1
- data/lib/itsi/server/rack_interface.rb +1 -2
- data/lib/itsi/server/route_tester.rb +26 -24
- data/lib/itsi/server/typed_handlers/param_parser.rb +25 -0
- data/lib/itsi/server/typed_handlers/source_parser.rb +9 -7
- data/lib/itsi/server/version.rb +1 -1
- data/lib/itsi/server.rb +22 -22
- data/lib/itsi/standard_headers.rb +80 -80
- metadata +3 -3
@@ -9,7 +9,7 @@ module Itsi
|
|
9
9
|
attr_reader :parent, :children, :middleware, :controller, :routes, :http_methods, :protocols,
|
10
10
|
:hosts, :ports, :extensions, :content_types, :accepts, :options
|
11
11
|
|
12
|
-
def self.evaluate(config = Itsi::Server::Config.config_file_path, &blk)
|
12
|
+
def self.evaluate(config = Itsi::Server::Config.config_file_path, &blk) # rubocop:disable Metrics/MethodLength
|
13
13
|
config = new(routes: ["/"]) do
|
14
14
|
if blk
|
15
15
|
instance_exec(&blk)
|
@@ -17,14 +17,14 @@ module Itsi
|
|
17
17
|
code = IO.read(config)
|
18
18
|
instance_eval(code, config.to_s, 1)
|
19
19
|
end
|
20
|
-
location("*"){}
|
20
|
+
location("*") {}
|
21
21
|
end
|
22
22
|
[config.options, config.errors]
|
23
|
-
rescue Exception => e
|
23
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
24
24
|
[{}, [[e, e.backtrace[0]]]]
|
25
25
|
end
|
26
26
|
|
27
|
-
def initialize(
|
27
|
+
def initialize( # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
|
28
28
|
parent = nil,
|
29
29
|
routes: [],
|
30
30
|
methods: [],
|
@@ -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
|
-
|
55
|
+
nested_locations: [],
|
56
56
|
middleware_loader: lambda do
|
57
|
-
@options[:
|
57
|
+
@options[:nested_locations].each(&:call)
|
58
58
|
@middleware[:app] ||= {}
|
59
59
|
@middleware[:app][:app_proc] = @middleware[:app]&.[](:preloader)&.call || DEFAULT_APP[]
|
60
|
-
|
61
|
-
error = errors.first.first
|
62
|
-
error.set_backtrace(error.backtrace.drop_while{|r| r =~ /itsi\/server\/config/ })
|
63
|
-
raise error
|
64
|
-
end
|
65
|
-
flatten_routes
|
60
|
+
[flatten_routes, Config.errors_to_error_lines(errors)]
|
66
61
|
end
|
67
62
|
}
|
68
63
|
|
@@ -78,7 +73,7 @@ module Itsi
|
|
78
73
|
option_name = option.option_name
|
79
74
|
define_method(option_name) do |*args, **kwargs, &blk|
|
80
75
|
option.new(self, *args, **kwargs, &blk).build!
|
81
|
-
rescue => e
|
76
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
82
77
|
@errors << [e, caller[1]]
|
83
78
|
end
|
84
79
|
end
|
@@ -89,7 +84,7 @@ module Itsi
|
|
89
84
|
middleware.new(self, *args, **kwargs, &blk).build!
|
90
85
|
rescue Config::Endpoint::InvalidHandlerException => e
|
91
86
|
@errors << [e, "#{e.backtrace[0]}:in #{e.message}"]
|
92
|
-
rescue => e
|
87
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
93
88
|
@errors << [e, caller[1]]
|
94
89
|
end
|
95
90
|
end
|
@@ -99,7 +94,7 @@ module Itsi
|
|
99
94
|
@grpc_reflected_services.concat(handlers)
|
100
95
|
|
101
96
|
location("grpc.reflection.v1alpha.ServerReflection/ServerReflectionInfo",
|
102
|
-
|
97
|
+
"grpc.reflection.v1.ServerReflection/ServerReflectionInfo") do
|
103
98
|
@middleware[:app] = { preloader: lambda {
|
104
99
|
Itsi::Server::GrpcInterface.reflection_for(handlers)
|
105
100
|
}, request_type: "grpc" }
|
@@ -141,7 +136,7 @@ module Itsi
|
|
141
136
|
when Regexp
|
142
137
|
seg.source
|
143
138
|
else
|
144
|
-
parts = seg.split(
|
139
|
+
parts = seg.split("/")
|
145
140
|
parts.map do |part|
|
146
141
|
case part
|
147
142
|
when /^:([A-Za-z_]\w*)(?:\(([^)]*)\))?$/
|
@@ -184,11 +179,11 @@ module Itsi
|
|
184
179
|
|
185
180
|
chain.each do |n|
|
186
181
|
n.middleware.each do |k, v|
|
187
|
-
if v[:combine]
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
182
|
+
merged[k] = if v[:combine]
|
183
|
+
([merged[k] || []] + [v]).flatten
|
184
|
+
else
|
185
|
+
v
|
186
|
+
end
|
192
187
|
end
|
193
188
|
end
|
194
189
|
deep_stringify_keys(merged)
|
@@ -2,16 +2,20 @@ module Itsi
|
|
2
2
|
class Server
|
3
3
|
module KnownPaths
|
4
4
|
ALL = []
|
5
|
-
Dir.glob(File.join(__dir__,
|
6
|
-
method_name = file[/
|
7
|
-
|
8
|
-
|
5
|
+
Dir.glob(File.join(__dir__, "known_paths", "**", "*.txt")).each do |file|
|
6
|
+
method_name = file[%r{known_paths/(.*?)\.txt}, 1].gsub(/([a-z])([A-Z])/, "\\1_\\2")
|
7
|
+
.gsub(%r{-|\.|/}, "_")
|
8
|
+
.gsub(%r{(^|/)[0-9]}) do |match|
|
9
|
+
match.gsub(/\d/) do |digit|
|
10
|
+
%w[zero one two three four five six seven eight nine][digit.to_i]
|
11
|
+
end
|
12
|
+
end.downcase.to_sym
|
9
13
|
|
10
14
|
ALL << method_name
|
11
|
-
|
15
|
+
define_singleton_method(method_name) do
|
12
16
|
File.readlines(file).map do |s|
|
13
|
-
s.force_encoding(
|
14
|
-
s.valid_encoding? ? s.strip : s.encode(
|
17
|
+
s.force_encoding("UTF-8")
|
18
|
+
s.valid_encoding? ? s.strip : s.encode("UTF-8", invalid: :replace, undef: :replace, replace: "").strip
|
15
19
|
end
|
16
20
|
end
|
17
21
|
end
|
@@ -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
|
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
|
-
|
81
|
+
child = DSL.new(
|
76
82
|
location,
|
77
83
|
routes: routes,
|
78
|
-
methods:
|
79
|
-
protocols:
|
80
|
-
hosts:
|
81
|
-
ports:
|
82
|
-
extensions:
|
83
|
-
content_types:
|
84
|
-
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
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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([
|
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
|
35
|
-
@params[:backends]||=
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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[:
|
79
|
-
|
80
|
-
|
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:
|
@@ -3,7 +3,7 @@ module Itsi
|
|
3
3
|
module Config
|
4
4
|
class Include < Option
|
5
5
|
|
6
|
-
insert_text "include \"${1|other_file
|
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
|
|
@@ -4,7 +4,7 @@ url: /options/reuse_address
|
|
4
4
|
---
|
5
5
|
|
6
6
|
Configures whether the server should bind to the underlying socket using the `SO_REUSEADDR` option.
|
7
|
-
This
|
7
|
+
This option determines whether the server allows the reuse of local addresses during binding. This can be useful in scenarios where a socket needs to be quickly rebound without waiting for the operating system to release the address.
|
8
8
|
|
9
9
|
## Configuration
|
10
10
|
```ruby {filename=Itsi.rb}
|
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
require "time"
|
3
5
|
|
4
6
|
module Itsi
|
5
7
|
class Server
|
@@ -10,25 +12,19 @@ module Itsi
|
|
10
12
|
|
11
13
|
def self.new(defaults = nil, &defaults_blk)
|
12
14
|
defaults = TypedStruct.module_eval(&defaults_blk) if defaults_blk
|
13
|
-
unless defaults.is_a?(Hash)
|
14
|
-
|
15
|
-
end
|
15
|
+
return defaults unless defaults.is_a?(Hash)
|
16
|
+
|
16
17
|
defaults.transform_values! { _1.is_a?(Validation) ? _1.default(nil) : _1 }
|
17
18
|
Struct.new(*defaults.keys, keyword_init: true) do
|
18
19
|
define_method(:initialize) do |*input, validate: true, **raw_input|
|
20
|
+
raise "─ Invalid input to #{self}: #{input.last}" if input.last && !input.last.is_a?(Hash)
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
raw_input.transform_keys!{|k| k.to_s.downcase.to_sym }
|
24
|
-
raw_input.merge!(input.pop.transform_keys!{|k| k.to_s.downcase.to_sym}) if input.last.is_a?(Hash)
|
22
|
+
raw_input.transform_keys! { |k| k.to_s.downcase.to_sym }
|
23
|
+
raw_input.merge!(input.pop.transform_keys! { |k| k.to_s.downcase.to_sym }) if input.last.is_a?(Hash)
|
25
24
|
|
26
25
|
excess_keys = raw_input.keys - defaults.keys
|
27
26
|
|
28
|
-
if excess_keys.any?
|
29
|
-
raise "─ Unsupported keys #{excess_keys}"
|
30
|
-
|
31
|
-
end
|
27
|
+
raise "─ Unsupported keys #{excess_keys}" if excess_keys.any?
|
32
28
|
|
33
29
|
initial_values = defaults.each_with_object({}) do |(k, default_config), inputs|
|
34
30
|
value = raw_input.key?(k) ? raw_input[k] : default_config[VALUE].dup
|
@@ -106,9 +102,7 @@ module Itsi
|
|
106
102
|
|
107
103
|
def &(other)
|
108
104
|
tail = self
|
109
|
-
while tail.next
|
110
|
-
tail = tail.next
|
111
|
-
end
|
105
|
+
tail = tail.next while tail.next
|
112
106
|
tail.next = other
|
113
107
|
self
|
114
108
|
end
|
@@ -148,10 +142,14 @@ module Itsi
|
|
148
142
|
elsif validation.eql?(::Date) then Date.parse(value.to_s)
|
149
143
|
elsif validation.eql?(Float) then Float(value)
|
150
144
|
elsif validation.eql?(Integer) then Integer(value)
|
151
|
-
elsif validation.eql?(Proc)
|
145
|
+
elsif validation.eql?(Proc)
|
152
146
|
raise ArgumentError, "Invalid #{validation} value: #{value.inspect}" unless value.is_a?(Proc)
|
153
147
|
elsif validation.eql?(String) || validation.eql?(Symbol)
|
154
|
-
|
148
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
149
|
+
raise ArgumentError,
|
150
|
+
"Invalid #{validation} value: #{value.inspect}"
|
151
|
+
end
|
152
|
+
|
155
153
|
if validation.eql?(String)
|
156
154
|
value.to_s
|
157
155
|
elsif validation.eql?(Symbol)
|
@@ -178,33 +176,32 @@ module Itsi
|
|
178
176
|
{
|
179
177
|
Bool: Validation.new(:Bool, [[true, false]]),
|
180
178
|
Required: Validation.new(:Required, ->(value) { !value.nil? }),
|
181
|
-
Or:
|
182
|
-
Validation.new(:Or,
|
183
|
-
|
179
|
+
Or: lambda { |*validations|
|
180
|
+
Validation.new(:Or, lambda { |v|
|
184
181
|
return true if v.nil?
|
182
|
+
|
185
183
|
errs = []
|
186
184
|
validations.each do |validation|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
errs << e.message
|
192
|
-
end
|
185
|
+
v = validation.validate!(v)
|
186
|
+
return v
|
187
|
+
rescue StandardError => e
|
188
|
+
errs << e.message
|
193
189
|
end
|
194
190
|
raise StandardError.new("─ Validation failed (None match:) \n └#{errs.join("\n └")}")
|
195
191
|
})
|
196
192
|
},
|
197
|
-
Range:
|
193
|
+
Range: lambda { |input_range|
|
198
194
|
Validation.new(:Range, [input_range])
|
199
195
|
},
|
200
196
|
Length: lambda { |input_length|
|
201
197
|
Validation.new(:Length, ->(value) { input_length === value.length })
|
202
198
|
},
|
203
|
-
Hash:
|
204
|
-
Validation.new(:Hash,
|
205
|
-
return true if
|
206
|
-
raise StandardError.new("Expected hash got #{
|
207
|
-
|
199
|
+
Hash: lambda { |key_type, value_type|
|
200
|
+
Validation.new(:Hash, lambda { |hash|
|
201
|
+
return true if hash.nil?
|
202
|
+
raise StandardError.new("Expected hash got #{hash.class}") unless hash.is_a?(Hash)
|
203
|
+
|
204
|
+
hash.map do |k, v|
|
208
205
|
[
|
209
206
|
key_type.validate!(k),
|
210
207
|
value_type.validate!(v)
|
@@ -213,7 +210,7 @@ module Itsi
|
|
213
210
|
})
|
214
211
|
},
|
215
212
|
Type: ->(input_type) { Validation.new(:Type, input_type) },
|
216
|
-
Enum: ->(allowed_values) { Validation.new(:Enum, [allowed_values.map{|v| v.
|
213
|
+
Enum: ->(allowed_values) { Validation.new(:Enum, [allowed_values.map { |v| v.is_a?(Symbol) ? v.to_s : v }]) },
|
217
214
|
Array: lambda { |*value_validations|
|
218
215
|
Validation.new(:Array, [::Array, lambda { |value|
|
219
216
|
return true unless value
|