itsi 0.1.14 → 0.1.18
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 +124 -109
- data/Cargo.toml +6 -0
- data/crates/itsi_error/Cargo.toml +1 -0
- data/crates/itsi_error/src/lib.rs +100 -10
- data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
- data/crates/itsi_server/Cargo.toml +8 -10
- data/crates/itsi_server/src/default_responses/html/401.html +68 -0
- data/crates/itsi_server/src/default_responses/html/403.html +68 -0
- data/crates/itsi_server/src/default_responses/html/404.html +68 -0
- data/crates/itsi_server/src/default_responses/html/413.html +71 -0
- data/crates/itsi_server/src/default_responses/html/429.html +68 -0
- data/crates/itsi_server/src/default_responses/html/500.html +71 -0
- data/crates/itsi_server/src/default_responses/html/502.html +71 -0
- data/crates/itsi_server/src/default_responses/html/503.html +68 -0
- data/crates/itsi_server/src/default_responses/html/504.html +69 -0
- data/crates/itsi_server/src/default_responses/html/index.html +238 -0
- data/crates/itsi_server/src/default_responses/json/401.json +6 -0
- data/crates/itsi_server/src/default_responses/json/403.json +6 -0
- data/crates/itsi_server/src/default_responses/json/404.json +6 -0
- data/crates/itsi_server/src/default_responses/json/413.json +6 -0
- data/crates/itsi_server/src/default_responses/json/429.json +6 -0
- data/crates/itsi_server/src/default_responses/json/500.json +6 -0
- data/crates/itsi_server/src/default_responses/json/502.json +6 -0
- data/crates/itsi_server/src/default_responses/json/503.json +6 -0
- data/crates/itsi_server/src/default_responses/json/504.json +6 -0
- data/crates/itsi_server/src/default_responses/mod.rs +11 -0
- data/crates/itsi_server/src/lib.rs +58 -26
- data/crates/itsi_server/src/prelude.rs +2 -0
- data/crates/itsi_server/src/ruby_types/README.md +21 -0
- data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
- data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
- data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
- data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
- data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
- data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
- data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
- data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
- data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
- data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
- data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
- data/crates/itsi_server/src/server/binds/mod.rs +4 -0
- data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
- data/crates/itsi_server/src/server/http_message_types.rs +97 -0
- data/crates/itsi_server/src/server/io_stream.rs +2 -1
- data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
- data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
- data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +50 -29
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
- data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
- data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
- data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
- data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
- data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
- data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
- data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
- data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
- data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
- data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
- data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
- data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
- data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
- data/crates/itsi_server/src/server/mod.rs +3 -9
- data/crates/itsi_server/src/server/process_worker.rs +21 -3
- data/crates/itsi_server/src/server/request_job.rs +2 -2
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
- data/crates/itsi_server/src/server/signal.rs +24 -41
- data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
- data/crates/itsi_server/src/server/thread_worker.rs +59 -28
- data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
- data/crates/itsi_server/src/services/mime_types.rs +1416 -0
- data/crates/itsi_server/src/services/mod.rs +6 -0
- data/crates/itsi_server/src/services/password_hasher.rs +83 -0
- data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
- data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
- data/crates/itsi_tracing/src/lib.rs +145 -55
- data/{Itsi.rb → foo/Itsi.rb} +6 -9
- data/gems/scheduler/Cargo.lock +7 -0
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/test/helpers/test_helper.rb +0 -1
- data/gems/scheduler/test/test_address_resolve.rb +0 -1
- data/gems/scheduler/test/test_network_io.rb +1 -1
- data/gems/scheduler/test/test_process_wait.rb +0 -1
- data/gems/server/Cargo.lock +124 -109
- data/gems/server/exe/itsi +65 -19
- data/gems/server/itsi-server.gemspec +4 -3
- data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
- data/gems/server/lib/itsi/http_request.rb +116 -17
- data/gems/server/lib/itsi/http_response.rb +2 -0
- data/gems/server/lib/itsi/passfile.rb +109 -0
- data/gems/server/lib/itsi/server/config/dsl.rb +160 -101
- data/gems/server/lib/itsi/server/config.rb +58 -23
- data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
- data/gems/server/lib/itsi/server/default_app/index.html +113 -89
- data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
- data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
- data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
- data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
- data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
- data/gems/server/lib/itsi/server/route_tester.rb +107 -0
- data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
- data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
- data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +82 -12
- data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
- data/gems/server/lib/shell_completions/completions.rb +26 -0
- data/gems/server/test/helpers/test_helper.rb +2 -1
- data/lib/itsi/version.rb +1 -1
- data/sandbox/README.md +5 -0
- data/sandbox/itsi_file/Gemfile +4 -2
- data/sandbox/itsi_file/Gemfile.lock +48 -6
- data/sandbox/itsi_file/Itsi.rb +326 -129
- data/sandbox/itsi_file/call.json +1 -0
- data/sandbox/itsi_file/echo_client/Gemfile +10 -0
- data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
- data/sandbox/itsi_file/echo_client/README.md +95 -0
- data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
- data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
- data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
- data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
- data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
- data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
- data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
- data/sandbox/itsi_sandbox_async/config.ru +0 -1
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
- data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
- data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
- data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
- data/sandbox/itsi_sinatra/app.rb +0 -1
- data/sandbox/static_files/.env +1 -0
- data/sandbox/static_files/404.html +25 -0
- data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
- data/sandbox/static_files/about.html +68 -0
- data/sandbox/static_files/tiny.html +1 -0
- data/sandbox/static_files/writebook.zip +0 -0
- data/tasks.txt +28 -33
- metadata +87 -26
- data/crates/itsi_error/src/from.rs +0 -68
- data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
- data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
- data/crates/itsi_server/src/server/itsi_service.rs +0 -172
- data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
- data/crates/itsi_server/src/server/types.rs +0 -43
- data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
- data/sandbox/itsi_file/public/assets/index.html +0 -1
- /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
- /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
- /data/crates/itsi_server/src/{server → services}/cache_store.rs +0 -0
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
3
|
+
# source: reflection.proto
|
4
|
+
|
5
|
+
require 'google/protobuf'
|
6
|
+
require "google/protobuf/descriptor_pb"
|
7
|
+
|
8
|
+
descriptor_data = "\n\x10reflection.proto\x12\x12grpc.reflection.v1\"\x85\x02\n\x17ServerReflectionRequest\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x1a\n\x10\x66ile_by_filename\x18\x03 \x01(\tH\x00\x12 \n\x16\x66ile_containing_symbol\x18\x04 \x01(\tH\x00\x12I\n\x19\x66ile_containing_extension\x18\x05 \x01(\x0b\x32$.grpc.reflection.v1.ExtensionRequestH\x00\x12\'\n\x1d\x61ll_extension_numbers_of_type\x18\x06 \x01(\tH\x00\x12\x17\n\rlist_services\x18\x07 \x01(\tH\x00\x42\x11\n\x0fmessage_request\"E\n\x10\x45xtensionRequest\x12\x17\n\x0f\x63ontaining_type\x18\x01 \x01(\t\x12\x18\n\x10\x65xtension_number\x18\x02 \x01(\x05\"\xb8\x03\n\x18ServerReflectionResponse\x12\x12\n\nvalid_host\x18\x01 \x01(\t\x12\x45\n\x10original_request\x18\x02 \x01(\x0b\x32+.grpc.reflection.v1.ServerReflectionRequest\x12N\n\x18\x66ile_descriptor_response\x18\x04 \x01(\x0b\x32*.grpc.reflection.v1.FileDescriptorResponseH\x00\x12U\n\x1e\x61ll_extension_numbers_response\x18\x05 \x01(\x0b\x32+.grpc.reflection.v1.ExtensionNumberResponseH\x00\x12I\n\x16list_services_response\x18\x06 \x01(\x0b\x32\'.grpc.reflection.v1.ListServiceResponseH\x00\x12;\n\x0e\x65rror_response\x18\x07 \x01(\x0b\x32!.grpc.reflection.v1.ErrorResponseH\x00\x42\x12\n\x10message_response\"7\n\x16\x46ileDescriptorResponse\x12\x1d\n\x15\x66ile_descriptor_proto\x18\x01 \x03(\x0c\"K\n\x17\x45xtensionNumberResponse\x12\x16\n\x0e\x62\x61se_type_name\x18\x01 \x01(\t\x12\x18\n\x10\x65xtension_number\x18\x02 \x03(\x05\"K\n\x13ListServiceResponse\x12\x34\n\x07service\x18\x01 \x03(\x0b\x32#.grpc.reflection.v1.ServiceResponse\"\x1f\n\x0fServiceResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\":\n\rErrorResponse\x12\x12\n\nerror_code\x18\x01 \x01(\x05\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x89\x01\n\x10ServerReflection\x12u\n\x14ServerReflectionInfo\x12+.grpc.reflection.v1.ServerReflectionRequest\x1a,.grpc.reflection.v1.ServerReflectionResponse(\x01\x30\x01\x42\x66\n\x15io.grpc.reflection.v1B\x15ServerReflectionProtoP\x01Z4google.golang.org/grpc/reflection/grpc_reflection_v1b\x06proto3"
|
9
|
+
|
10
|
+
pool = Google::Protobuf::DescriptorPool.generated_pool
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
12
|
+
|
13
|
+
module Grpc
|
14
|
+
module Reflection
|
15
|
+
module V1
|
16
|
+
ServerReflectionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ServerReflectionRequest").msgclass
|
17
|
+
ExtensionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ExtensionRequest").msgclass
|
18
|
+
ServerReflectionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ServerReflectionResponse").msgclass
|
19
|
+
FileDescriptorResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.FileDescriptorResponse").msgclass
|
20
|
+
ExtensionNumberResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ExtensionNumberResponse").msgclass
|
21
|
+
ListServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ListServiceResponse").msgclass
|
22
|
+
ServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ServiceResponse").msgclass
|
23
|
+
ErrorResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ErrorResponse").msgclass
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
2
|
+
# Source: reflection.proto for package 'grpc.reflection.v1'
|
3
|
+
# Original file comments:
|
4
|
+
# Copyright 2016 The gRPC Authors
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
# Service exported by server reflection. A more complete description of how
|
19
|
+
# server reflection works can be found at
|
20
|
+
# https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
|
21
|
+
#
|
22
|
+
# The canonical version of this proto can be found at
|
23
|
+
# https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto
|
24
|
+
#
|
25
|
+
|
26
|
+
require "grpc"
|
27
|
+
require_relative "reflection_pb"
|
28
|
+
|
29
|
+
module Grpc
|
30
|
+
module Reflection
|
31
|
+
module V1
|
32
|
+
module ServerReflection
|
33
|
+
class Service
|
34
|
+
include ::GRPC::GenericService
|
35
|
+
|
36
|
+
self.marshal_class_method = :encode
|
37
|
+
self.unmarshal_class_method = :decode
|
38
|
+
self.service_name = "grpc.reflection.v1.ServerReflection"
|
39
|
+
|
40
|
+
# The reflection service is structured as a bidirectional stream, ensuring
|
41
|
+
# all related requests go to a single server.
|
42
|
+
rpc :ServerReflectionInfo, stream(::Grpc::Reflection::V1::ServerReflectionRequest),
|
43
|
+
stream(::Grpc::Reflection::V1::ServerReflectionResponse)
|
44
|
+
|
45
|
+
def initialize(handlers)
|
46
|
+
@handlers = handlers
|
47
|
+
super()
|
48
|
+
end
|
49
|
+
|
50
|
+
def server_reflection_info(req, _unused_call)
|
51
|
+
req.each do |request|
|
52
|
+
res = Grpc::Reflection::V1::ServerReflectionResponse.new
|
53
|
+
|
54
|
+
if !request.list_services.empty?
|
55
|
+
res.list_services_response = Grpc::Reflection::V1::ListServiceResponse.new(service: list_services_response)
|
56
|
+
elsif !request.file_containing_symbol.empty?
|
57
|
+
res.file_descriptor_response = Grpc::Reflection::V1::FileDescriptorResponse.new(
|
58
|
+
file_descriptor_proto: [
|
59
|
+
Google::Protobuf::FileDescriptorProto.encode(Google::Protobuf::DescriptorPool.generated_pool.lookup(request.file_containing_symbol).file_descriptor.to_proto)
|
60
|
+
]
|
61
|
+
)
|
62
|
+
elsif !request.file_by_filename.empty?
|
63
|
+
# Handle file_by_filename requests
|
64
|
+
file_descriptor = find_file_descriptor_by_filename(request.file_by_filename)
|
65
|
+
if file_descriptor
|
66
|
+
res.file_descriptor_response = Grpc::Reflection::V1::FileDescriptorResponse.new(
|
67
|
+
file_descriptor_proto: [Google::Protobuf::FileDescriptorProto.encode(file_descriptor)]
|
68
|
+
)
|
69
|
+
else
|
70
|
+
res.error_response = Grpc::Reflection::V1::ErrorResponse.new(
|
71
|
+
error_code: 5, # NOT_FOUND
|
72
|
+
error_message: "File not found: #{request.file_by_filename}"
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
yield res
|
77
|
+
# We can loop here if running in Fiber mode, but for best compatibility with blocking IO modes
|
78
|
+
# we'll close the connection and force the client to reconnect force
|
79
|
+
# subsequent reflection requests
|
80
|
+
break
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def find_file_descriptor_by_filename(filename)
|
85
|
+
# First try direct lookup in the pool
|
86
|
+
|
87
|
+
descriptor = Google::Protobuf::DescriptorPool.generated_pool.lookup(filename)&.file_descriptor
|
88
|
+
return descriptor.to_proto if descriptor
|
89
|
+
|
90
|
+
proto_name = convert_file_path_to_proto_name(filename)
|
91
|
+
descriptor = Google::Protobuf::DescriptorPool.generated_pool.lookup(proto_name)&.file_descriptor
|
92
|
+
return descriptor.to_proto if descriptor
|
93
|
+
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
def convert_file_path_to_proto_name(file_path)
|
98
|
+
# Remove .proto extension
|
99
|
+
file_path = file_path.sub(/\.proto$/, "")
|
100
|
+
|
101
|
+
# Split path into parts
|
102
|
+
parts = file_path.split("/")
|
103
|
+
|
104
|
+
# Convert last part to PascalCase (e.g., money -> Money)
|
105
|
+
parts[-1] = parts[-1].split("_").map(&:capitalize).join
|
106
|
+
|
107
|
+
# Join with dots
|
108
|
+
parts.join(".")
|
109
|
+
end
|
110
|
+
|
111
|
+
def list_services_response
|
112
|
+
@list_services_response ||= @handlers.map(&:class).map(&:service_name).map do |name|
|
113
|
+
Grpc::Reflection::V1::ServiceResponse.new(name: name)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
Stub = Service.rpc_stub_class
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module RouteTester
|
4
|
+
|
5
|
+
require "set"
|
6
|
+
require "strscan"
|
7
|
+
|
8
|
+
def format_mw(mw)
|
9
|
+
case mw["type"]
|
10
|
+
when "app"
|
11
|
+
"app #{mw["parameters"]["app_proc"].inspect.split(" ")[1]}"
|
12
|
+
else
|
13
|
+
mw["type"]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def print_route(route_str, stack)
|
18
|
+
filters = %w[methods ports protocols extensions].map do |key|
|
19
|
+
val = stack[key]
|
20
|
+
val ? "#{key}: #{Array(val).join(",")}" : nil
|
21
|
+
end.compact
|
22
|
+
filter_str = filters.any? ? filters.join(", ") : "(none)"
|
23
|
+
|
24
|
+
middlewares = stack["middleware"]
|
25
|
+
|
26
|
+
puts "─" * 76
|
27
|
+
puts "Route: #{route_str}"
|
28
|
+
puts "Conditions: #{filter_str}"
|
29
|
+
puts "Middleware: • #{format_mw(middlewares.first)}"
|
30
|
+
middlewares[1..].each do |mw|
|
31
|
+
puts " • #{format_mw(mw)}"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def explode_route_pattern(pattern)
|
36
|
+
pattern = pattern.gsub(/^\^|\$$/, "")
|
37
|
+
pattern = pattern.gsub("\\", "")
|
38
|
+
tokens = parse_expression(StringScanner.new(pattern))
|
39
|
+
expand_tokens(tokens)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Parses the expression into a nested tree of tokens
|
43
|
+
def parse_expression(scanner)
|
44
|
+
tokens = []
|
45
|
+
buffer = ""
|
46
|
+
|
47
|
+
until scanner.eos?
|
48
|
+
if scanner.scan(/\(\?:/)
|
49
|
+
tokens << buffer unless buffer.empty?
|
50
|
+
buffer = ""
|
51
|
+
tokens << parse_alternation(scanner)
|
52
|
+
elsif scanner.peek(1) == ")"
|
53
|
+
scanner.getch # consume ')'
|
54
|
+
break
|
55
|
+
else
|
56
|
+
buffer << scanner.getch
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
tokens << buffer unless buffer.empty?
|
61
|
+
tokens
|
62
|
+
end
|
63
|
+
|
64
|
+
# Parses inside a non-capturing group (?:A|B|C)
|
65
|
+
def parse_alternation(scanner)
|
66
|
+
options = []
|
67
|
+
current = []
|
68
|
+
|
69
|
+
until scanner.eos?
|
70
|
+
if scanner.scan(/\(\?:/)
|
71
|
+
current << parse_alternation(scanner)
|
72
|
+
elsif scanner.peek(1) == ")"
|
73
|
+
scanner.getch # consume ')'
|
74
|
+
break
|
75
|
+
elsif scanner.peek(1) == "|"
|
76
|
+
scanner.getch # consume '|'
|
77
|
+
options << current
|
78
|
+
current = []
|
79
|
+
else
|
80
|
+
current << scanner.getch
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
options << current
|
85
|
+
{ alt: options }
|
86
|
+
end
|
87
|
+
|
88
|
+
def expand_tokens(tokens)
|
89
|
+
parts = tokens.map do |token|
|
90
|
+
if token.is_a?(String)
|
91
|
+
[token]
|
92
|
+
elsif token.is_a?(Hash) && token[:alt]
|
93
|
+
# Recurse into each branch of the alternation
|
94
|
+
token[:alt].map { |branch| expand_tokens(branch) }.flatten
|
95
|
+
else
|
96
|
+
raise "Unexpected token: #{token.inspect}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Cartesian product of all parts
|
101
|
+
parts.inject([""]) do |acc, part|
|
102
|
+
acc.product(part).map { |a, b| a + b }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module TypedHandlers
|
4
|
+
module ParamParser
|
5
|
+
require 'date'
|
6
|
+
|
7
|
+
class ValidationError < StandardError
|
8
|
+
attr_reader :errors
|
9
|
+
def initialize(errors)
|
10
|
+
@errors = errors
|
11
|
+
super("Validation failed: #{errors.join('; ')}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Conversion map for primitive/base type conversions.
|
16
|
+
CONVERSION_MAP = {
|
17
|
+
String => ->(v){ v.to_s },
|
18
|
+
Symbol => ->(v){ v.to_sym },
|
19
|
+
Integer => ->(v){ Integer(v) },
|
20
|
+
Float => ->(v){ Float(v) },
|
21
|
+
:Number => ->(v){ Float(v) },
|
22
|
+
TrueClass => ->(v){
|
23
|
+
case v
|
24
|
+
when true, 'true', '1', 1 then true
|
25
|
+
when false, 'false', '0', 0 then false
|
26
|
+
else raise "Cannot cast #{v.inspect} to Boolean"
|
27
|
+
end
|
28
|
+
},
|
29
|
+
FalseClass => ->(v){
|
30
|
+
case v
|
31
|
+
when true, 'true', '1', 1 then true
|
32
|
+
when false, 'false', '0', 0 then false
|
33
|
+
else raise "Cannot cast #{v.inspect} to Boolean"
|
34
|
+
end
|
35
|
+
},
|
36
|
+
:Boolean => ->(v){
|
37
|
+
case v
|
38
|
+
when true, 'true', '1', 1 then true
|
39
|
+
when false, 'false', '0', 0 then false
|
40
|
+
else raise "Cannot cast #{v.inspect} to Boolean"
|
41
|
+
end
|
42
|
+
},
|
43
|
+
Date => ->(v){ Date.parse(v.to_s) },
|
44
|
+
Time => ->(v){ Time.parse(v.to_s) },
|
45
|
+
DateTime => ->(v){ DateTime.parse(v.to_s) }
|
46
|
+
}.compare_by_identity
|
47
|
+
|
48
|
+
# Extracts the expected type and required flag from a schema definition.
|
49
|
+
def extract_schema(schema_def)
|
50
|
+
if schema_def.is_a?(Hash) && schema_def.key?(:_type)
|
51
|
+
expected_type = schema_def[:_type]
|
52
|
+
required = schema_def.fetch(:required, true)
|
53
|
+
[expected_type, required]
|
54
|
+
else
|
55
|
+
[schema_def, true]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Preprocess the schema into fixed keys (as symbols) and regex keys.
|
60
|
+
# Memoizes the result based on the schema.
|
61
|
+
def processed_schema(schema)
|
62
|
+
@@schema_cache ||= {}
|
63
|
+
@@schema_cache[schema] ||= begin
|
64
|
+
fixed = {}
|
65
|
+
regex = []
|
66
|
+
schema.each do |k, schema_def|
|
67
|
+
expected_type, required = extract_schema(schema_def)
|
68
|
+
if k.is_a?(Regexp)
|
69
|
+
regex << [k, [expected_type, required]]
|
70
|
+
else
|
71
|
+
fixed[k.to_sym] = [expected_type, required]
|
72
|
+
end
|
73
|
+
end
|
74
|
+
[fixed, regex]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Helper that converts an array of path segments into a string.
|
79
|
+
# For example, [:user, "addresses", 0, :street] becomes "user.addresses[0].street".
|
80
|
+
def format_path(path)
|
81
|
+
result = "".dup
|
82
|
+
path.each do |seg|
|
83
|
+
if seg.is_a?(Integer)
|
84
|
+
result << "[#{seg}]"
|
85
|
+
else
|
86
|
+
result << (result.empty? ? seg.to_s : ".#{seg}")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
result
|
90
|
+
end
|
91
|
+
|
92
|
+
# In-place casts the value at container[key] according to expected_type.
|
93
|
+
# On success, updates container[key] and returns nil.
|
94
|
+
# On failure, returns an error message string that uses the formatted path.
|
95
|
+
def cast_value!(container, key, expected_type, path)
|
96
|
+
if expected_type.is_a?(Array)
|
97
|
+
# Only allow homogeneous array types.
|
98
|
+
return "Only homogeneous array types are supported at #{format_path(path)}" if expected_type.size != 1
|
99
|
+
|
100
|
+
# Expect container[key] to be an Array; process each element in place.
|
101
|
+
unless container[key].is_a?(Array)
|
102
|
+
return "Expected an Array at #{format_path(path)}, got #{container[key].class}"
|
103
|
+
end
|
104
|
+
container[key].each_with_index do |_, idx|
|
105
|
+
err = cast_value!(container[key], idx, expected_type.first, path + [idx])
|
106
|
+
return err if err
|
107
|
+
end
|
108
|
+
return nil
|
109
|
+
|
110
|
+
elsif expected_type.is_a?(Hash)
|
111
|
+
# Nested schema: expect container[key] to be a Hash; process it in place.
|
112
|
+
unless container[key].is_a?(Hash)
|
113
|
+
return "Expected a Hash at #{format_path(path)}, got #{container[key].class}"
|
114
|
+
end
|
115
|
+
begin
|
116
|
+
apply_schema!(container[key], expected_type, path)
|
117
|
+
return nil
|
118
|
+
rescue ValidationError => ve
|
119
|
+
return ve.errors.join('; ')
|
120
|
+
end
|
121
|
+
|
122
|
+
else
|
123
|
+
converter = CONVERSION_MAP[expected_type]
|
124
|
+
if converter
|
125
|
+
begin
|
126
|
+
container[key] = converter.call(container[key])
|
127
|
+
return nil
|
128
|
+
rescue => e
|
129
|
+
return "Invalid value for #{expected_type} at #{format_path(path)}: #{container[key].inspect} (#{e.message})"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Fallbacks.
|
134
|
+
if expected_type == Array
|
135
|
+
unless container[key].is_a?(Array)
|
136
|
+
return "Expected Array at #{format_path(path)}, got #{container[key].class}"
|
137
|
+
end
|
138
|
+
return nil
|
139
|
+
elsif expected_type == Hash
|
140
|
+
unless container[key].is_a?(Hash)
|
141
|
+
return "Expected Hash at #{format_path(path)}, got #{container[key].class}"
|
142
|
+
end
|
143
|
+
return nil
|
144
|
+
elsif expected_type == File && container[key].is_a?(Hash) && container[key][:tempfile].is_a?(Tempfile)
|
145
|
+
return nil
|
146
|
+
else
|
147
|
+
return "Unsupported type: #{expected_type.inspect} at #{format_path(path)}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# Applies the schema in place to the given params hash.
|
153
|
+
# Fixed keys are converted to symbols, and regex-matched keys remain as strings.
|
154
|
+
# The current location in the params is tracked as an array of path segments.
|
155
|
+
def apply_schema!(params, schema, path = [])
|
156
|
+
errors = []
|
157
|
+
processed = processed_schema(schema)
|
158
|
+
fixed_schema = processed[0]
|
159
|
+
regex_schema = processed[1]
|
160
|
+
|
161
|
+
# Process fixed keys.
|
162
|
+
fixed_schema.each do |fixed_key, (expected_type, required)|
|
163
|
+
new_path = path + [fixed_key]
|
164
|
+
if params.key?(fixed_key)
|
165
|
+
# Symbol key present.
|
166
|
+
elsif params.key?(fixed_key.to_s)
|
167
|
+
params[fixed_key] = params.delete(fixed_key.to_s)
|
168
|
+
else
|
169
|
+
if required
|
170
|
+
errors << "Missing required key: #{format_path(new_path)}"
|
171
|
+
else
|
172
|
+
params[fixed_key] = nil
|
173
|
+
end
|
174
|
+
next
|
175
|
+
end
|
176
|
+
|
177
|
+
err = cast_value!(params, fixed_key, expected_type, new_path)
|
178
|
+
errors << err if err
|
179
|
+
end
|
180
|
+
|
181
|
+
# Process regex keys (only string keys not already handled as fixed keys).
|
182
|
+
params.keys.select { |k| k.is_a?(String) }.each do |key|
|
183
|
+
next if fixed_schema.has_key?(key.to_sym) || fixed_schema.has_key?(key)
|
184
|
+
regex_schema.each do |regex, (expected_type, _required)|
|
185
|
+
if regex.match(key)
|
186
|
+
new_path = path + [key]
|
187
|
+
err = cast_value!(params, key, expected_type, new_path)
|
188
|
+
errors << err if err
|
189
|
+
break # only use the first matching regex
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
raise ValidationError.new(errors) unless errors.empty?
|
195
|
+
params
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module TypedHandlers
|
4
|
+
module SourceParser
|
5
|
+
require 'prism'
|
6
|
+
|
7
|
+
|
8
|
+
def self.extract_expr_from_source_location(proc)
|
9
|
+
source_lines = IO.readlines(proc.source_location.first)
|
10
|
+
|
11
|
+
proc_line = source_location.last - 1
|
12
|
+
first_line = source_lines[proc_line]
|
13
|
+
binding.b
|
14
|
+
until first_line =~ /(?:lambda|proc|->|def|.*?do\s*\|)/ || proc_line.zero?
|
15
|
+
proc_line -= 1
|
16
|
+
first_line = source_lines[proc_line]
|
17
|
+
end
|
18
|
+
lines = source_lines[proc_line..]
|
19
|
+
lines[0] = lines[0][/(?:lambda|proc|->|def|.*?do\s*\|).*/]
|
20
|
+
src_str = lines.first << "\n"
|
21
|
+
intermediate = Prism.parse(src_str)
|
22
|
+
|
23
|
+
lines[1..-1].each do |line|
|
24
|
+
break if intermediate.success?
|
25
|
+
token_count = 0
|
26
|
+
line.split(/(?=\s|;|\)|\})/).each do |token|
|
27
|
+
src_str << token
|
28
|
+
token_count += 1
|
29
|
+
intermediate = Prism.parse(src_str)
|
30
|
+
next unless intermediate.success? && token_count > 1
|
31
|
+
break
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
raise 'Source Extraction Failed' unless intermediate.success?
|
36
|
+
|
37
|
+
src = intermediate.value.statements.body.first.yield_self do |s|
|
38
|
+
s.type == :call_node ? s.block : s
|
39
|
+
end
|
40
|
+
params = src.parameters
|
41
|
+
params = params.parameters if params.respond_to?(:parameters)
|
42
|
+
requireds = (params&.requireds || []).map(&:name)
|
43
|
+
optionals = params&.optionals || []
|
44
|
+
keywords = (params&.keywords || []).map do |kw|
|
45
|
+
[kw.name, kw.value.slice.gsub(/^_\./, '$.')]
|
46
|
+
end.to_h
|
47
|
+
|
48
|
+
[requireds.length, keywords]
|
49
|
+
rescue
|
50
|
+
[ proc.parameters.first&.length || 0, {}]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require_relative "typed_handlers/source_parser"
|
2
|
+
require_relative "typed_handlers/param_parser"
|
3
|
+
|
4
|
+
module Itsi
|
5
|
+
class Server
|
6
|
+
module TypedHandlers
|
7
|
+
def self.handler_for(proc, input_schema)
|
8
|
+
input_schema = proc.binding.eval(input_schema) if input_schema
|
9
|
+
lambda do |req|
|
10
|
+
req.params(input_schema) do |params|
|
11
|
+
proc.call(req, params: params)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|