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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/Cargo.lock +124 -109
  3. data/Cargo.toml +6 -0
  4. data/crates/itsi_error/Cargo.toml +1 -0
  5. data/crates/itsi_error/src/lib.rs +100 -10
  6. data/crates/itsi_scheduler/src/itsi_scheduler.rs +1 -1
  7. data/crates/itsi_server/Cargo.toml +8 -10
  8. data/crates/itsi_server/src/default_responses/html/401.html +68 -0
  9. data/crates/itsi_server/src/default_responses/html/403.html +68 -0
  10. data/crates/itsi_server/src/default_responses/html/404.html +68 -0
  11. data/crates/itsi_server/src/default_responses/html/413.html +71 -0
  12. data/crates/itsi_server/src/default_responses/html/429.html +68 -0
  13. data/crates/itsi_server/src/default_responses/html/500.html +71 -0
  14. data/crates/itsi_server/src/default_responses/html/502.html +71 -0
  15. data/crates/itsi_server/src/default_responses/html/503.html +68 -0
  16. data/crates/itsi_server/src/default_responses/html/504.html +69 -0
  17. data/crates/itsi_server/src/default_responses/html/index.html +238 -0
  18. data/crates/itsi_server/src/default_responses/json/401.json +6 -0
  19. data/crates/itsi_server/src/default_responses/json/403.json +6 -0
  20. data/crates/itsi_server/src/default_responses/json/404.json +6 -0
  21. data/crates/itsi_server/src/default_responses/json/413.json +6 -0
  22. data/crates/itsi_server/src/default_responses/json/429.json +6 -0
  23. data/crates/itsi_server/src/default_responses/json/500.json +6 -0
  24. data/crates/itsi_server/src/default_responses/json/502.json +6 -0
  25. data/crates/itsi_server/src/default_responses/json/503.json +6 -0
  26. data/crates/itsi_server/src/default_responses/json/504.json +6 -0
  27. data/crates/itsi_server/src/default_responses/mod.rs +11 -0
  28. data/crates/itsi_server/src/lib.rs +58 -26
  29. data/crates/itsi_server/src/prelude.rs +2 -0
  30. data/crates/itsi_server/src/ruby_types/README.md +21 -0
  31. data/crates/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +8 -6
  32. data/crates/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  33. data/crates/itsi_server/src/ruby_types/{itsi_grpc_stream → itsi_grpc_response_stream}/mod.rs +121 -73
  34. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +103 -40
  35. data/crates/itsi_server/src/ruby_types/itsi_http_response.rs +8 -5
  36. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +4 -4
  37. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +37 -17
  38. data/crates/itsi_server/src/ruby_types/itsi_server.rs +4 -3
  39. data/crates/itsi_server/src/ruby_types/mod.rs +6 -13
  40. data/crates/itsi_server/src/server/{bind.rs → binds/bind.rs} +23 -4
  41. data/crates/itsi_server/src/server/{listener.rs → binds/listener.rs} +24 -10
  42. data/crates/itsi_server/src/server/binds/mod.rs +4 -0
  43. data/crates/itsi_server/src/server/{tls.rs → binds/tls.rs} +9 -4
  44. data/crates/itsi_server/src/server/http_message_types.rs +97 -0
  45. data/crates/itsi_server/src/server/io_stream.rs +2 -1
  46. data/crates/itsi_server/src/server/middleware_stack/middleware.rs +28 -16
  47. data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +17 -8
  48. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +47 -18
  49. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +13 -9
  50. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +50 -29
  51. data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +5 -2
  52. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +37 -48
  53. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +25 -20
  54. data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +14 -7
  55. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  56. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +125 -95
  57. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +9 -5
  58. data/crates/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +1 -4
  59. data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +25 -19
  60. data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +4 -4
  61. data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  62. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +9 -4
  63. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +260 -62
  64. data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +29 -22
  65. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +6 -6
  66. data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +6 -5
  67. data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +4 -2
  68. data/crates/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +51 -18
  69. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +31 -13
  70. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  71. data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +13 -8
  72. data/crates/itsi_server/src/server/middleware_stack/mod.rs +101 -69
  73. data/crates/itsi_server/src/server/mod.rs +3 -9
  74. data/crates/itsi_server/src/server/process_worker.rs +21 -3
  75. data/crates/itsi_server/src/server/request_job.rs +2 -2
  76. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +8 -3
  77. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +26 -26
  78. data/crates/itsi_server/src/server/signal.rs +24 -41
  79. data/crates/itsi_server/src/server/size_limited_incoming.rs +101 -0
  80. data/crates/itsi_server/src/server/thread_worker.rs +59 -28
  81. data/crates/itsi_server/src/services/itsi_http_service.rs +239 -0
  82. data/crates/itsi_server/src/services/mime_types.rs +1416 -0
  83. data/crates/itsi_server/src/services/mod.rs +6 -0
  84. data/crates/itsi_server/src/services/password_hasher.rs +83 -0
  85. data/crates/itsi_server/src/{server → services}/rate_limiter.rs +35 -31
  86. data/crates/itsi_server/src/{server → services}/static_file_server.rs +521 -181
  87. data/crates/itsi_tracing/src/lib.rs +145 -55
  88. data/{Itsi.rb → foo/Itsi.rb} +6 -9
  89. data/gems/scheduler/Cargo.lock +7 -0
  90. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  91. data/gems/scheduler/test/helpers/test_helper.rb +0 -1
  92. data/gems/scheduler/test/test_address_resolve.rb +0 -1
  93. data/gems/scheduler/test/test_network_io.rb +1 -1
  94. data/gems/scheduler/test/test_process_wait.rb +0 -1
  95. data/gems/server/Cargo.lock +124 -109
  96. data/gems/server/exe/itsi +65 -19
  97. data/gems/server/itsi-server.gemspec +4 -3
  98. data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
  99. data/gems/server/lib/itsi/http_request.rb +116 -17
  100. data/gems/server/lib/itsi/http_response.rb +2 -0
  101. data/gems/server/lib/itsi/passfile.rb +109 -0
  102. data/gems/server/lib/itsi/server/config/dsl.rb +160 -101
  103. data/gems/server/lib/itsi/server/config.rb +58 -23
  104. data/gems/server/lib/itsi/server/default_app/default_app.rb +25 -29
  105. data/gems/server/lib/itsi/server/default_app/index.html +113 -89
  106. data/gems/server/lib/itsi/server/{Itsi.rb → default_config/Itsi-rackup.rb} +1 -1
  107. data/gems/server/lib/itsi/server/default_config/Itsi.rb +107 -0
  108. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +246 -0
  109. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +100 -0
  110. data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  111. data/gems/server/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  112. data/gems/server/lib/itsi/server/route_tester.rb +107 -0
  113. data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
  114. data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
  115. data/gems/server/lib/itsi/server/typed_handlers.rb +17 -0
  116. data/gems/server/lib/itsi/server/version.rb +1 -1
  117. data/gems/server/lib/itsi/server.rb +82 -12
  118. data/gems/server/lib/ruby_lsp/itsi/addon.rb +111 -0
  119. data/gems/server/lib/shell_completions/completions.rb +26 -0
  120. data/gems/server/test/helpers/test_helper.rb +2 -1
  121. data/lib/itsi/version.rb +1 -1
  122. data/sandbox/README.md +5 -0
  123. data/sandbox/itsi_file/Gemfile +4 -2
  124. data/sandbox/itsi_file/Gemfile.lock +48 -6
  125. data/sandbox/itsi_file/Itsi.rb +326 -129
  126. data/sandbox/itsi_file/call.json +1 -0
  127. data/sandbox/itsi_file/echo_client/Gemfile +10 -0
  128. data/sandbox/itsi_file/echo_client/Gemfile.lock +27 -0
  129. data/sandbox/itsi_file/echo_client/README.md +95 -0
  130. data/sandbox/itsi_file/echo_client/echo_client.rb +164 -0
  131. data/sandbox/itsi_file/echo_client/gen_proto.sh +17 -0
  132. data/sandbox/itsi_file/echo_client/lib/echo_pb.rb +16 -0
  133. data/sandbox/itsi_file/echo_client/lib/echo_services_pb.rb +29 -0
  134. data/sandbox/itsi_file/echo_client/run_client.rb +64 -0
  135. data/sandbox/itsi_file/echo_client/test_compressions.sh +20 -0
  136. data/sandbox/itsi_file/echo_service_nonitsi/Gemfile +10 -0
  137. data/sandbox/itsi_file/echo_service_nonitsi/Gemfile.lock +79 -0
  138. data/sandbox/itsi_file/echo_service_nonitsi/echo.proto +26 -0
  139. data/sandbox/itsi_file/echo_service_nonitsi/echo_pb.rb +16 -0
  140. data/sandbox/itsi_file/echo_service_nonitsi/echo_services_pb.rb +29 -0
  141. data/sandbox/itsi_file/echo_service_nonitsi/server.rb +52 -0
  142. data/sandbox/itsi_sandbox_async/config.ru +0 -1
  143. data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
  144. data/sandbox/itsi_sandbox_rails/Gemfile +2 -2
  145. data/sandbox/itsi_sandbox_rails/Gemfile.lock +76 -2
  146. data/sandbox/itsi_sandbox_rails/app/controllers/home_controller.rb +15 -0
  147. data/sandbox/itsi_sandbox_rails/config/environments/development.rb +1 -0
  148. data/sandbox/itsi_sandbox_rails/config/environments/production.rb +1 -0
  149. data/sandbox/itsi_sandbox_rails/config/routes.rb +2 -0
  150. data/sandbox/itsi_sinatra/app.rb +0 -1
  151. data/sandbox/static_files/.env +1 -0
  152. data/sandbox/static_files/404.html +25 -0
  153. data/sandbox/static_files/_DSC0102.NEF.jpg +0 -0
  154. data/sandbox/static_files/about.html +68 -0
  155. data/sandbox/static_files/tiny.html +1 -0
  156. data/sandbox/static_files/writebook.zip +0 -0
  157. data/tasks.txt +28 -33
  158. metadata +87 -26
  159. data/crates/itsi_error/src/from.rs +0 -68
  160. data/crates/itsi_server/src/ruby_types/itsi_grpc_request.rs +0 -147
  161. data/crates/itsi_server/src/ruby_types/itsi_grpc_response.rs +0 -19
  162. data/crates/itsi_server/src/server/itsi_service.rs +0 -172
  163. data/crates/itsi_server/src/server/middleware_stack/middlewares/grpc_service.rs +0 -72
  164. data/crates/itsi_server/src/server/types.rs +0 -43
  165. data/gems/server/lib/itsi/server/grpc_interface.rb +0 -213
  166. data/sandbox/itsi_file/public/assets/index.html +0 -1
  167. /data/crates/itsi_server/src/server/{bind_protocol.rs → binds/bind_protocol.rs} +0 -0
  168. /data/crates/itsi_server/src/server/{tls → binds/tls}/locked_dir_cache.rs +0 -0
  169. /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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Itsi
4
4
  class Server
5
- VERSION = "0.1.14"
5
+ VERSION = "0.1.18"
6
6
  end
7
7
  end