itsi 0.1.20 → 0.2.2

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 (319) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -8
  3. data/Cargo.lock +2 -2
  4. data/LICENSE.txt +698 -0
  5. data/README.md +15 -4
  6. data/Rakefile +9 -5
  7. data/crates/itsi_acme/.gitignore +4 -0
  8. data/crates/itsi_acme/Cargo.toml +86 -0
  9. data/crates/itsi_acme/LICENSE-APACHE +201 -0
  10. data/crates/itsi_acme/LICENSE-MIT +23 -0
  11. data/crates/itsi_acme/README.md +9 -0
  12. data/crates/itsi_acme/examples/high_level.rs +63 -0
  13. data/crates/itsi_acme/examples/high_level_warp.rs +52 -0
  14. data/crates/itsi_acme/examples/low_level.rs +87 -0
  15. data/crates/itsi_acme/examples/low_level_axum.rs +66 -0
  16. data/crates/itsi_acme/src/acceptor.rs +81 -0
  17. data/crates/itsi_acme/src/acme.rs +354 -0
  18. data/crates/itsi_acme/src/axum.rs +86 -0
  19. data/crates/itsi_acme/src/cache.rs +39 -0
  20. data/crates/itsi_acme/src/caches/boxed.rs +80 -0
  21. data/crates/itsi_acme/src/caches/composite.rs +69 -0
  22. data/crates/itsi_acme/src/caches/dir.rs +106 -0
  23. data/crates/itsi_acme/src/caches/mod.rs +11 -0
  24. data/crates/itsi_acme/src/caches/no.rs +78 -0
  25. data/crates/itsi_acme/src/caches/test.rs +136 -0
  26. data/crates/itsi_acme/src/config.rs +172 -0
  27. data/crates/itsi_acme/src/https_helper.rs +69 -0
  28. data/crates/itsi_acme/src/incoming.rs +142 -0
  29. data/crates/itsi_acme/src/jose.rs +161 -0
  30. data/crates/itsi_acme/src/lib.rs +142 -0
  31. data/crates/itsi_acme/src/resolver.rs +59 -0
  32. data/crates/itsi_acme/src/state.rs +424 -0
  33. data/crates/itsi_rb_helpers/src/lib.rs +4 -3
  34. data/crates/itsi_scheduler/Cargo.toml +1 -1
  35. data/crates/itsi_scheduler/src/itsi_scheduler.rs +8 -2
  36. data/crates/itsi_scheduler/src/lib.rs +1 -0
  37. data/crates/itsi_server/Cargo.toml +1 -1
  38. data/crates/itsi_server/src/lib.rs +2 -1
  39. data/crates/itsi_server/src/ruby_types/itsi_http_request.rs +18 -1
  40. data/crates/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +11 -3
  41. data/crates/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +122 -63
  42. data/crates/itsi_server/src/ruby_types/itsi_server.rs +2 -0
  43. data/crates/itsi_server/src/server/binds/bind.rs +3 -0
  44. data/crates/itsi_server/src/server/binds/listener.rs +12 -5
  45. data/crates/itsi_server/src/server/binds/tls.rs +13 -5
  46. data/crates/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +12 -5
  47. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +8 -1
  48. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +9 -1
  49. data/crates/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +48 -43
  50. data/crates/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +11 -2
  51. data/crates/itsi_server/src/server/middleware_stack/middlewares/compression.rs +39 -12
  52. data/crates/itsi_server/src/server/middleware_stack/middlewares/cors.rs +36 -27
  53. data/crates/itsi_server/src/server/middleware_stack/middlewares/csp.rs +25 -11
  54. data/crates/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +12 -3
  55. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +74 -72
  56. data/crates/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +15 -1
  57. data/crates/itsi_server/src/server/middleware_stack/middlewares/etag.rs +11 -8
  58. data/crates/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +19 -11
  59. data/crates/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +5 -5
  60. data/crates/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +2 -2
  61. data/crates/itsi_server/src/server/middleware_stack/middlewares/mod.rs +11 -5
  62. data/crates/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +17 -20
  63. data/crates/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +19 -8
  64. data/crates/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +16 -37
  65. data/crates/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +22 -12
  66. data/crates/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +26 -11
  67. data/crates/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +7 -1
  68. data/crates/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +14 -4
  69. data/crates/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +19 -0
  70. data/crates/itsi_server/src/server/middleware_stack/mod.rs +49 -13
  71. data/crates/itsi_server/src/server/mod.rs +1 -0
  72. data/crates/itsi_server/src/server/redirect_type.rs +26 -0
  73. data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +22 -16
  74. data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +49 -12
  75. data/crates/itsi_server/src/server/signal.rs +1 -0
  76. data/crates/itsi_server/src/server/size_limited_incoming.rs +6 -0
  77. data/crates/itsi_server/src/server/thread_worker.rs +5 -1
  78. data/crates/itsi_server/src/services/itsi_http_service.rs +20 -2
  79. data/crates/itsi_server/src/services/rate_limiter.rs +15 -4
  80. data/crates/itsi_server/src/services/static_file_server.rs +33 -19
  81. data/crates/itsi_tracing/src/lib.rs +42 -22
  82. data/docs/content/_index.md +1 -2
  83. data/docs/content/acknowledgements/_index.md +5 -2
  84. data/docs/content/configuration/_index.md +8 -5
  85. data/docs/content/contact/_index.md +8 -1
  86. data/docs/content/faqs/_index.md +5 -3
  87. data/docs/content/features/_index.md +56 -50
  88. data/docs/content/getting_started/_index.md +8 -5
  89. data/docs/content/getting_started/local_development.md +68 -8
  90. data/docs/content/getting_started/logging.md +16 -9
  91. data/docs/content/getting_started/running_itsi_in_production.md +5 -3
  92. data/docs/content/getting_started/signals.md +38 -0
  93. data/docs/content/itsi_scheduler/_index.md +8 -7
  94. data/docs/content/utilities/_index.md +13 -0
  95. data/docs/content/utilities/config_file_testing.md +17 -0
  96. data/docs/content/utilities/passfile_generator.md +41 -0
  97. data/docs/content/utilities/route_testing.md +27 -0
  98. data/docs/content/utilities/secrets_management.md +30 -0
  99. data/docs/hugo.yaml +1 -1
  100. data/fairytale.txt +3 -4
  101. data/gems/scheduler/Cargo.lock +1 -1
  102. data/gems/scheduler/README.md +4 -5
  103. data/gems/scheduler/Rakefile +0 -4
  104. data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
  105. data/gems/scheduler/lib/itsi/scheduler.rb +9 -4
  106. data/gems/scheduler/test/test_active_record.rb +12 -7
  107. data/gems/server/Cargo.lock +1 -1
  108. data/gems/server/Rakefile +0 -4
  109. data/gems/server/exe/itsi +13 -2
  110. data/gems/server/lib/itsi/http_request/response_status_shortcodes.rb +2 -0
  111. data/gems/server/lib/itsi/http_request.rb +40 -9
  112. data/gems/server/lib/itsi/http_response.rb +2 -1
  113. data/gems/server/lib/itsi/passfile.rb +0 -1
  114. data/gems/server/lib/itsi/server/config/config_helpers.rb +20 -8
  115. data/gems/server/lib/itsi/server/config/dsl.rb +20 -435
  116. data/gems/server/lib/itsi/server/config/known_paths.rb +4 -1
  117. data/gems/server/lib/itsi/server/config/middleware/_index.md +6 -4
  118. data/gems/server/lib/itsi/server/config/middleware/allow_list.md +46 -0
  119. data/gems/server/lib/itsi/server/config/middleware/allow_list.rb +42 -0
  120. data/gems/server/lib/itsi/server/config/middleware/auth_api_key.md +90 -0
  121. data/gems/server/lib/itsi/server/config/middleware/auth_api_key.rb +51 -0
  122. data/gems/server/lib/itsi/server/config/middleware/auth_basic.md +45 -0
  123. data/gems/server/lib/itsi/server/config/middleware/auth_basic.rb +44 -0
  124. data/gems/server/lib/itsi/server/config/middleware/auth_jwt.md +82 -0
  125. data/gems/server/lib/itsi/server/config/middleware/auth_jwt.rb +38 -0
  126. data/gems/server/lib/itsi/server/config/middleware/cache_control.md +78 -0
  127. data/gems/server/lib/itsi/server/config/middleware/cache_control.rb +45 -0
  128. data/gems/server/lib/itsi/server/config/middleware/cidr_to_regex.rb +50 -0
  129. data/gems/server/lib/itsi/server/config/middleware/compression.md +50 -0
  130. data/gems/server/lib/itsi/server/config/middleware/compression.rb +37 -0
  131. data/gems/server/lib/itsi/server/config/middleware/cors.md +93 -0
  132. data/gems/server/lib/itsi/server/config/middleware/cors.rb +32 -0
  133. data/gems/server/lib/itsi/server/config/middleware/csp.md +37 -0
  134. data/gems/server/lib/itsi/server/config/middleware/csp.rb +44 -0
  135. data/gems/server/lib/itsi/server/config/middleware/deny_list.md +45 -0
  136. data/gems/server/lib/itsi/server/config/middleware/deny_list.rb +42 -0
  137. data/gems/server/lib/itsi/server/config/middleware/endpoint/_index.md +159 -0
  138. data/gems/server/lib/itsi/server/config/middleware/endpoint/controller.md +186 -0
  139. data/gems/server/lib/itsi/server/config/middleware/endpoint/controller.rb +33 -0
  140. data/gems/server/lib/itsi/server/config/middleware/endpoint/delete.md +12 -0
  141. data/gems/server/lib/itsi/server/config/middleware/endpoint/delete.rb +42 -0
  142. data/gems/server/lib/itsi/server/config/middleware/endpoint/endpoint.rb +99 -0
  143. data/gems/server/lib/itsi/server/config/middleware/endpoint/get.md +12 -0
  144. data/gems/server/lib/itsi/server/config/middleware/endpoint/get.rb +42 -0
  145. data/gems/server/lib/itsi/server/config/middleware/endpoint/http_request.md +44 -0
  146. data/gems/server/lib/itsi/server/config/middleware/endpoint/http_response.md +39 -0
  147. data/gems/server/lib/itsi/server/config/middleware/endpoint/patch.md +12 -0
  148. data/gems/server/lib/itsi/server/config/middleware/endpoint/patch.rb +42 -0
  149. data/gems/server/lib/itsi/server/config/middleware/endpoint/post.md +12 -0
  150. data/gems/server/lib/itsi/server/config/middleware/endpoint/post.rb +42 -0
  151. data/gems/server/lib/itsi/server/config/middleware/endpoint/put.md +12 -0
  152. data/gems/server/lib/itsi/server/config/middleware/endpoint/put.rb +42 -0
  153. data/gems/server/lib/itsi/server/config/middleware/endpoint/schemas.md +122 -0
  154. data/gems/server/lib/itsi/server/config/middleware/error_response.md +61 -0
  155. data/gems/server/lib/itsi/server/config/middleware/error_response.rb +36 -0
  156. data/gems/server/lib/itsi/server/config/middleware/etag.md +59 -0
  157. data/gems/server/lib/itsi/server/config/middleware/etag.rb +27 -0
  158. data/gems/server/lib/itsi/server/config/middleware/grpc.md +172 -0
  159. data/gems/server/lib/itsi/server/config/middleware/grpc.rb +54 -0
  160. data/gems/server/lib/itsi/server/config/middleware/intrusion_protection.md +124 -0
  161. data/gems/server/lib/itsi/server/config/middleware/intrusion_protection.rb +61 -0
  162. data/gems/server/lib/itsi/server/config/middleware/location.md +107 -0
  163. data/gems/server/lib/itsi/server/config/middleware/location.rb +99 -0
  164. data/gems/server/lib/itsi/server/config/middleware/log_requests.md +13 -11
  165. data/gems/server/lib/itsi/server/config/middleware/log_requests.rb +1 -3
  166. data/gems/server/lib/itsi/server/config/middleware/max_body.md +18 -0
  167. data/gems/server/lib/itsi/server/config/middleware/max_body.rb +21 -0
  168. data/gems/server/lib/itsi/server/config/middleware/proxy.md +62 -0
  169. data/gems/server/lib/itsi/server/config/middleware/proxy.rb +41 -0
  170. data/gems/server/lib/itsi/server/config/middleware/rackup_file.md +54 -0
  171. data/gems/server/lib/itsi/server/config/middleware/rackup_file.rb +44 -0
  172. data/gems/server/lib/itsi/server/config/middleware/rate_limit.md +126 -0
  173. data/gems/server/lib/itsi/server/config/middleware/rate_limit.rb +34 -0
  174. data/gems/server/lib/itsi/server/config/middleware/rate_limit_store.rb +25 -0
  175. data/gems/server/lib/itsi/server/config/middleware/redirect.md +55 -0
  176. data/gems/server/lib/itsi/server/config/middleware/redirect.rb +25 -0
  177. data/gems/server/lib/itsi/server/config/middleware/request_headers.md +34 -0
  178. data/gems/server/lib/itsi/server/config/middleware/request_headers.rb +24 -0
  179. data/gems/server/lib/itsi/server/config/middleware/response_headers.md +33 -0
  180. data/gems/server/lib/itsi/server/config/middleware/response_headers.rb +25 -0
  181. data/gems/server/lib/itsi/server/config/middleware/run.md +60 -0
  182. data/gems/server/lib/itsi/server/config/middleware/run.rb +43 -0
  183. data/gems/server/lib/itsi/server/config/middleware/static_assets.md +73 -0
  184. data/gems/server/lib/itsi/server/config/middleware/static_assets.rb +87 -0
  185. data/gems/server/lib/itsi/server/config/middleware/static_response.md +44 -0
  186. data/gems/server/lib/itsi/server/config/middleware/static_response.rb +29 -0
  187. data/gems/server/lib/itsi/server/config/middleware/string_rewrite.md +67 -0
  188. data/gems/server/lib/itsi/server/config/middleware/token_source.rb +32 -0
  189. data/gems/server/lib/itsi/server/config/middleware.rb +4 -0
  190. data/gems/server/lib/itsi/server/config/option.rb +5 -0
  191. data/gems/server/lib/itsi/server/config/options/_index.md +3 -2
  192. data/gems/server/lib/itsi/server/config/options/auto_reload_config.md +13 -0
  193. data/gems/server/lib/itsi/server/config/options/auto_reload_config.rb +41 -0
  194. data/gems/server/lib/itsi/server/config/options/bind.md +71 -0
  195. data/gems/server/lib/itsi/server/config/options/bind.rb +26 -0
  196. data/gems/server/lib/itsi/server/config/options/certificates.md +65 -0
  197. data/gems/server/lib/itsi/server/config/options/daemonize.md +14 -0
  198. data/gems/server/lib/itsi/server/config/options/daemonize.rb +19 -0
  199. data/gems/server/lib/itsi/server/config/options/fiber_scheduler.md +1 -2
  200. data/gems/server/lib/itsi/server/config/options/fiber_scheduler.rb +6 -3
  201. data/gems/server/lib/itsi/server/config/options/header_read_timeout.md +17 -0
  202. data/gems/server/lib/itsi/server/config/options/header_read_timeout.rb +19 -0
  203. data/gems/server/lib/itsi/server/config/options/hooks/_index.md +11 -0
  204. data/gems/server/lib/itsi/server/config/options/hooks/after_fork.md +13 -0
  205. data/gems/server/lib/itsi/server/config/options/hooks/after_fork.rb +28 -0
  206. data/gems/server/lib/itsi/server/config/options/hooks/after_memory_limit_reached.md +14 -0
  207. data/gems/server/lib/itsi/server/config/options/hooks/after_memory_limit_reached.rb +28 -0
  208. data/gems/server/lib/itsi/server/config/options/hooks/after_start.md +12 -0
  209. data/gems/server/lib/itsi/server/config/options/hooks/after_start.rb +28 -0
  210. data/gems/server/lib/itsi/server/config/options/hooks/before_fork.md +13 -0
  211. data/gems/server/lib/itsi/server/config/options/hooks/before_fork.rb +28 -0
  212. data/gems/server/lib/itsi/server/config/options/hooks/before_restart.md +12 -0
  213. data/gems/server/lib/itsi/server/config/options/hooks/before_restart.rb +28 -0
  214. data/gems/server/lib/itsi/server/config/options/hooks/before_shutdown.md +12 -0
  215. data/gems/server/lib/itsi/server/config/options/hooks/before_shutdown.rb +28 -0
  216. data/gems/server/lib/itsi/server/config/options/include.md +20 -0
  217. data/gems/server/lib/itsi/server/config/options/include.rb +36 -0
  218. data/gems/server/lib/itsi/server/config/options/listen_backlog.md +11 -0
  219. data/gems/server/lib/itsi/server/config/options/listen_backlog.rb +19 -0
  220. data/gems/server/lib/itsi/server/config/options/log_format.md +18 -0
  221. data/gems/server/lib/itsi/server/config/options/log_format.rb +19 -0
  222. data/gems/server/lib/itsi/server/config/options/log_level.md +34 -0
  223. data/gems/server/lib/itsi/server/config/options/log_level.rb +20 -0
  224. data/gems/server/lib/itsi/server/config/options/log_target.md +38 -0
  225. data/gems/server/lib/itsi/server/config/options/log_target.rb +19 -0
  226. data/gems/server/lib/itsi/server/config/options/log_target_filters.md +17 -0
  227. data/gems/server/lib/itsi/server/config/options/log_target_filters.rb +19 -0
  228. data/gems/server/lib/itsi/server/config/options/multithreaded_reactor.md +27 -0
  229. data/gems/server/lib/itsi/server/config/options/multithreaded_reactor.rb +24 -0
  230. data/gems/server/lib/itsi/server/config/options/nodelay.md +16 -0
  231. data/gems/server/lib/itsi/server/config/options/nodelay.rb +19 -0
  232. data/gems/server/lib/itsi/server/config/options/oob_gc_responses_threshold.md +19 -0
  233. data/gems/server/lib/itsi/server/config/options/oob_gc_responses_threshold.rb +18 -0
  234. data/gems/server/lib/itsi/server/config/options/pin_worker_cores.md +17 -0
  235. data/gems/server/lib/itsi/server/config/options/pin_worker_cores.rb +19 -0
  236. data/gems/server/lib/itsi/server/config/options/preload.md +21 -0
  237. data/gems/server/lib/itsi/server/config/options/preload.rb +18 -0
  238. data/gems/server/lib/itsi/server/config/options/recv_buffer_size.md +15 -0
  239. data/gems/server/lib/itsi/server/config/options/recv_buffer_size.rb +19 -0
  240. data/gems/server/lib/itsi/server/config/options/redirect_http_to_https.md +21 -0
  241. data/gems/server/lib/itsi/server/config/options/redirect_http_to_https.rb +30 -0
  242. data/gems/server/lib/itsi/server/config/options/request_timeout.md +23 -0
  243. data/gems/server/lib/itsi/server/config/options/request_timeout.rb +19 -0
  244. data/gems/server/lib/itsi/server/config/options/reuse_address.md +16 -0
  245. data/gems/server/lib/itsi/server/config/options/reuse_address.rb +19 -0
  246. data/gems/server/lib/itsi/server/config/options/reuse_port.md +16 -0
  247. data/gems/server/lib/itsi/server/config/options/reuse_port.rb +19 -0
  248. data/gems/server/lib/itsi/server/config/options/scheduler_threads.md +34 -0
  249. data/gems/server/lib/itsi/server/config/options/scheduler_threads.rb +17 -0
  250. data/gems/server/lib/itsi/server/config/options/shutdown_timeout.md +17 -0
  251. data/gems/server/lib/itsi/server/config/options/shutdown_timeout.rb +19 -0
  252. data/gems/server/lib/itsi/server/config/options/stream_body.md +32 -0
  253. data/gems/server/lib/itsi/server/config/options/stream_body.rb +18 -0
  254. data/gems/server/lib/itsi/server/config/options/threads.md +7 -2
  255. data/gems/server/lib/itsi/server/config/options/threads.rb +1 -1
  256. data/gems/server/lib/itsi/server/config/options/watch.md +16 -0
  257. data/gems/server/lib/itsi/server/config/options/watch.rb +28 -0
  258. data/gems/server/lib/itsi/server/config/options/worker_memory_limit.md +22 -0
  259. data/gems/server/lib/itsi/server/config/options/worker_memory_limit.rb +18 -0
  260. data/gems/server/lib/itsi/server/config/options/workers.md +1 -2
  261. data/gems/server/lib/itsi/server/config/options/workers.rb +1 -1
  262. data/gems/server/lib/itsi/server/config/typed_struct.rb +59 -20
  263. data/gems/server/lib/itsi/server/config.rb +77 -48
  264. data/gems/server/lib/itsi/server/default_config/Itsi.rb +3 -3
  265. data/gems/server/lib/itsi/server/grpc/grpc_call.rb +1 -1
  266. data/gems/server/lib/itsi/server/grpc/grpc_interface.rb +11 -4
  267. data/gems/server/lib/itsi/server/rack/handler/itsi.rb +3 -3
  268. data/gems/server/lib/itsi/server/route_tester.rb +58 -8
  269. data/gems/server/lib/itsi/server/signal_trap.rb +1 -1
  270. data/gems/server/lib/itsi/server/typed_handlers/param_parser.rb +14 -18
  271. data/gems/server/lib/itsi/server/typed_handlers/source_parser.rb +5 -4
  272. data/gems/server/lib/itsi/server/typed_handlers.rb +12 -4
  273. data/gems/server/lib/itsi/server/version.rb +1 -1
  274. data/gems/server/lib/itsi/server.rb +98 -14
  275. data/gems/server/lib/ruby_lsp/itsi/addon.rb +20 -18
  276. data/gems/server/test/helpers/test_helper.rb +89 -29
  277. data/gems/server/test/middleware/allow_list.rb +128 -0
  278. data/gems/server/test/middleware/auth_api_key.rb +141 -0
  279. data/gems/server/test/middleware/auth_basic.rb +91 -0
  280. data/gems/server/test/middleware/auth_jwt.rb +214 -0
  281. data/gems/server/test/middleware/cache_control.rb +82 -0
  282. data/gems/server/test/middleware/cidr_to_regex.rb +46 -0
  283. data/gems/server/test/middleware/compression.rb +89 -0
  284. data/gems/server/test/middleware/cors.rb +113 -0
  285. data/gems/server/test/middleware/csp.rb +62 -0
  286. data/gems/server/test/middleware/deny_list.rb +131 -0
  287. data/gems/server/test/middleware/endpoint.rb +300 -0
  288. data/gems/server/test/middleware/etag.rb +75 -0
  289. data/gems/server/test/middleware/grpc/grpc.rb +158 -0
  290. data/gems/server/test/middleware/grpc/test_service.proto +32 -0
  291. data/gems/server/test/middleware/grpc/test_service_impl.rb +28 -0
  292. data/gems/server/test/middleware/grpc/test_service_pb.rb +18 -0
  293. data/gems/server/test/middleware/grpc/test_service_services_pb.rb +30 -0
  294. data/gems/server/test/middleware/header_interpolation.rb +35 -0
  295. data/gems/server/test/middleware/intrusion_protection.rb +259 -0
  296. data/gems/server/test/middleware/location.rb +220 -0
  297. data/gems/server/test/middleware/max_body.rb +20 -0
  298. data/gems/server/test/middleware/proxy.rb +415 -0
  299. data/gems/server/test/middleware/rate_limit.rb +211 -0
  300. data/gems/server/test/middleware/redirect.rb +85 -0
  301. data/gems/server/test/middleware/request_headers.rb +50 -0
  302. data/gems/server/test/middleware/response_headers.rb +50 -0
  303. data/gems/server/test/middleware/static_assets.rb +374 -0
  304. data/gems/server/test/middleware/static_response.rb +56 -0
  305. data/gems/server/test/middleware/string_rewrite.rb +112 -0
  306. data/gems/server/test/options/bind.rb +47 -0
  307. data/gems/server/test/options/header_read_timeout.rb +23 -0
  308. data/gems/server/test/options/test_request_timeout.rb +16 -0
  309. data/gems/server/test/options/test_workers.rb +2 -4
  310. data/gems/server/test/{test_itsi_server.rb → rack/test_rack_server.rb} +2 -2
  311. data/grpc_test/Itsi.rb +11 -0
  312. data/grpc_test/echo.proto +14 -0
  313. data/grpc_test/echo_pb.rb +16 -0
  314. data/grpc_test/echo_service_impl.rb +8 -0
  315. data/grpc_test/echo_services_pb.rb +22 -0
  316. data/lib/itsi/version.rb +1 -1
  317. data/tasks.txt +15 -72
  318. metadata +210 -7
  319. data/gems/server/lib/itsi/server/default_config/Itsi-rackup.rb +0 -119
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class RecvBufferSize < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ recv_buffer_size ${1|262_144,1_048_576|}
8
+ SNIPPET
9
+
10
+ detail "Specifies the size of the receive buffer for the socket. Larger buffer sizes can improve performance for high-throughput applications but may increase memory usage. The default value is 262,144 bytes."
11
+
12
+ schema do
13
+ (Type(Integer) & Range(1..Float::INFINITY) & Required()).default(262_144)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ ---
2
+ title: Redirect HTTP to HTTPS
3
+ url: /options/redirect_http_to_https
4
+ ---
5
+ This simple option installs a location block to redirect all HTTP traffic to HTTPS.
6
+
7
+ ```ruby {filename=Itsi.rb}
8
+ redirect_http_to_https!
9
+ ```
10
+
11
+ It is an alias for:
12
+ ```ruby
13
+ location protocols: [:http] do
14
+ redirect \
15
+ to: "https://{host}{path_and_query}", \
16
+ type: "moved_permanently"
17
+ end
18
+ ```
19
+
20
+ It is subject to ordinary [location](/middleware/location) resolution rules.
21
+ To make sure this rule takes precedence, place it *above* other locations in the `Itsi.rb` file
@@ -0,0 +1,30 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class RedirectHttpToHttps < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ redirect_http_to_https! # Install a location block to redirect HTTP to HTTPS
8
+ # (Only works if listening on ports 443 and 80).
9
+ # For non-conventional or development ports use a manual redirect
10
+ SNIPPET
11
+
12
+ detail "Install a location block to redirect HTTP to HTTPS."
13
+
14
+ def self.option_name
15
+ :redirect_http_to_https!
16
+ end
17
+
18
+ def build!
19
+ location.instance_eval do
20
+ location protocols: [:http] do
21
+ redirect \
22
+ to: "https://{host}{path_and_query}", \
23
+ type: "moved_permanently"
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,23 @@
1
+ ---
2
+ title: Request Timeout
3
+ url: /options/request_timeout
4
+ ---
5
+
6
+ Sets the maximum duration (in seconds) allowed for the entire request to complete. If exceeded, the request will be terminated.
7
+
8
+
9
+ ### Default
10
+
11
+ ```ruby {filename=Itsi.rb}
12
+ request_timeout 300 # 5 minutes
13
+ ```
14
+ ### Example
15
+
16
+ ```ruby {filename=Itsi.rb}
17
+ request_timeout 60 # 1 minute
18
+ ```
19
+
20
+
21
+ {{< callout type="warn" >}}
22
+ If the request is held-up inside a Ruby worker, the worker will be gracefully restarted (as killing in-progress threads is not safe).
23
+ {{< /callout >}}
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class RequestTimeout < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ request_timeout 300.0 # Request Timeout in Seconds
8
+ SNIPPET
9
+
10
+ detail "Set Request Timeout"
11
+
12
+ schema do
13
+ (Type(Float) & Required()).default(300.0) # Default 5 minutes
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Reuse Address
3
+ url: /options/reuse_address
4
+ ---
5
+
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.
8
+
9
+ ## Configuration
10
+ ```ruby {filename=Itsi.rb}
11
+ reuse_address true
12
+ ```
13
+
14
+ ```ruby {filename=Itsi.rb}
15
+ reuse_address false
16
+ ```
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class ReuseAddress < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ reuse_address ${1|true,false|}
8
+ SNIPPET
9
+
10
+ detail "Configures whether the server should set the reuse_address option on the underlying socket."
11
+
12
+ schema do
13
+ (Bool() & Required()).default(true)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Reuse Port
3
+ url: /options/reuse_port
4
+ ---
5
+
6
+ Configures whether the server should bind to the underlying socket using the `SO_REUSEPORT` option.
7
+ This option determines whether multiple sockets can listen on the same IP and port combination, which can improve load balancing and fault tolerance in multi-threaded or multi-process server applications.
8
+
9
+ ## Configuration
10
+ ```ruby {filename=Itsi.rb}
11
+ reuse_port true
12
+ ```
13
+
14
+ ```ruby {filename=Itsi.rb}
15
+ reuse_port false
16
+ ```
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class ReusePort < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ reuse_port ${1|true,false|}
8
+ SNIPPET
9
+
10
+ detail "Configures whether the server should set the reuse_port option on the underlying socket."
11
+
12
+ schema do
13
+ (Bool() & Required()).default(true)
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ ---
2
+ title: Scheduler Threads
3
+ url: /options/scheduler_threads
4
+ ---
5
+
6
+ You can explicitly spawn a pool of non-blocking scheduler threads and divide work across traditional blocking and non-blocking threads, using [location](/middleware/location) blocks.
7
+
8
+ This allows you to safely dip your toes into using non-blocking threads for specific I/O heavy operations without having to port an entire application to non-blocking I/O.
9
+
10
+ To use blocking and non-blocking threads in conjunction you need to perform several steps.
11
+ 1. Configure a thread pool using [threads](/middleware/threads)
12
+ 2. Configure a **separate** non-blocking thread pool using [scheduler_threads](/middleware/scheduler_threads) (By opting into this option you automatically make the ordinary thread pool blocking)
13
+ 3. Enable a [fiber_scheduler](/options/fiber_scheduler)
14
+ 4. Mount your app as non-blocking for selected routes. (Using either [run](/middleware/run), [rackup_file](/middleware/rackup_file) or [endpoint](/middleware/endpoint))
15
+
16
+ ## Configuration
17
+ Here is an example configuration of the all of the above E.g.
18
+
19
+ ```ruby {filename=Itsi.rb}
20
+
21
+ threads 3 # 3 threads (opting into scheduler threads make these blocking)
22
+ scheduler_threads 1 # 1 non-blocking scheduler thread
23
+ fiber_scheduler true
24
+
25
+ # We mount the same app *twice*.
26
+ # For a specific route prefix, all requests will be sent to non blocking threads.
27
+ # All others fall through to the default mount
28
+ location "/heavy_io/*" do
29
+ rackup_file "./config.ru", nonblocking: true
30
+ end
31
+
32
+ rackup_file "./config.ru"
33
+
34
+ ```
@@ -0,0 +1,17 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class SchedulerThreads < Option
5
+
6
+ insert_text "scheduler_threads ${1|1,2,Etc.nprocessors|} # Number of non-blocking scheduler threads to run per worker"
7
+
8
+ detail "Number of non-blocking scheduler threads to run per worker"
9
+
10
+ schema do
11
+ Range(1..255)
12
+ end
13
+
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ ---
2
+ title: Shutdown Timeout
3
+ url: /options/shutdown_timeout
4
+ ---
5
+
6
+ Sets the timeout for graceful shutdown of the server. Itsi will stop accepting new connections immediately after receiving a shutdown signal. Existing connections will be allowed to complete their requests within the specified timeout. Any connections that do not complete within the timeout will be forcefully closed.
7
+
8
+ ### Default
9
+
10
+ ```ruby {filename=Itsi.rb}
11
+ shutdown_timeout 5.0 # 5 seconds
12
+ ```
13
+ ### Example
14
+
15
+ ```ruby {filename=Itsi.rb}
16
+ shutdown_timeout 20.0 # 20 seconds
17
+ ```
@@ -0,0 +1,19 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class PinWorkerCores < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ pin_worker_cores ${1|true,false|} # Configure whether worker processes should attempt to pin to a single CPU core.
8
+ SNIPPET
9
+
10
+ detail "Configure whether worker processes should attempt to pin to a single CPU core"
11
+
12
+ schema do
13
+ (Bool() & Required()).default(true) # Default 5 seconds
14
+ end
15
+
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ ---
2
+ title: Stream Body
3
+ url: /options/stream_body
4
+ ---
5
+
6
+ The **stream_body** option controls whether Itsi buffers the request body in memory before passing it to your application code. Several Rack applications rely on this behaviour (so that the request can be rewound and/or parsed in its entirety before being processed), for this reason the default behaviour is `true`.
7
+ However for light-weight requests that benefit from not holding the entire request body in memory, you can set this option to `false`.
8
+
9
+
10
+ {{< callout >}}
11
+ To prevent excessive memory usage, Itsi will store any large buffered request bodies in temporary storage on the file-system when request body streaming is disabled.
12
+ {{< /callout >}}
13
+
14
+
15
+
16
+ Streaming bodies cooperate particularly well with [Fiber scheduler](/options/fiber_scheduler) mode, to allow you to process many large and long-running incoming data streams simultaneously. For this use case, be sure to adjust both [max_body](/options/max_body) and [request_timeout](/options/request_timeout) to ensure large incoming requests are not terminated prematurely.
17
+
18
+
19
+
20
+ ## Configuration File
21
+
22
+ ```ruby
23
+ # Disable streaming the request body
24
+ stream_body false
25
+
26
+ # Enable streaming the request body
27
+ stream_body true
28
+ post("/test_stream") do |req|
29
+ # req.body is an IO
30
+ first_five_bytes = req.body.read(5)
31
+ end
32
+ ```
@@ -0,0 +1,18 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class StreamBody < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ stream_body ${1|true,false|}
8
+ SNIPPET
9
+
10
+ detail "Configures whether request bodies should be completed buffered *before* they are forwarded to the application."
11
+
12
+ schema do
13
+ Or(Bool(), Type(Symbol)).default(false)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  title: Threads
3
3
  url: /options/threads
4
- weight: 2
5
4
  ---
6
5
  Itsi supports running in threaded mode. Spawning more than one thread per worker is helpful for applications that require high concurrency and low latency.
7
6
 
@@ -11,6 +10,12 @@ To minimize this overhead, it's recommended to keep the number of threads low, a
11
10
 
12
11
  When running Itsi in blocking mode, the total number of concurrent requests will be equal to `workers` x `threads`
13
12
 
13
+
14
+ {{< callout >}}
15
+ If you enable a [fiber_scheduler](/options/fiber_scheduler), the threads will be non-blocking, unless you are using both [threads](/options/threads) and [scheduler_threads](/options/scheduler_threads) **together**.
16
+ {{< /callout >}}
17
+
18
+
14
19
  ## Configuration File
15
20
  The number of threads to use can be specified inside the configuration file (usually `./Itsi.rb` at the project root)
16
21
  using the `threads` function.
@@ -27,7 +32,7 @@ threads 4
27
32
  ```
28
33
 
29
34
  Threads increase concurrency at the expense of memory usage and CPU overhead.
30
- For IO heavy workloads, consider using non-blocking mode instead, which can achieve higher concurrency with fewer threads.
35
+ For IO heavy workloads, consider using [non-blocking mode](/options/fiber_scheduler) instead, which can achieve higher concurrency with fewer threads.
31
36
  Non-blocking mode and threads can be used simultaneously in a hybrid configuration.
32
37
 
33
38
  ## Command Line
@@ -3,7 +3,7 @@ module Itsi
3
3
  module Config
4
4
  class Threads < Option
5
5
 
6
- insert_text "threads ${1|1,2,Etc.nprocessors|}"
6
+ insert_text "threads ${1|1,2,Etc.nprocessors|} # Number of threads to run per worker"
7
7
 
8
8
  detail "Number of threads to run per worker"
9
9
 
@@ -0,0 +1,16 @@
1
+ ---
2
+ title: Watch
3
+ url: /options/watch
4
+ ---
5
+
6
+ The `watch` option uses Itsi's built-in file watching mechanism to execute a list of commands each time any file matching the path glob parameter is modified.
7
+
8
+ You can have several active watches at once.
9
+
10
+ E.g.
11
+
12
+ ```ruby {filename=Itsi.rb}
13
+ watch 'config/*.yml', [%w[npm run build], %w[bundle exec cache:mark_dirty]]
14
+
15
+ watch 'app/**/*.rb', [%w[bundle exec itsi restart]]
16
+ ```
@@ -0,0 +1,28 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class Watch < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ watch "${1:./**/*.rb}", [${2:%w[bundle exec itsi restart]}] # Run a command each time a watched set of files changes
8
+ SNIPPET
9
+
10
+ detail "Run a command each time a watched set of files changes"
11
+
12
+ def initialize(location, path, commands)
13
+ @path = path
14
+ @commands = commands
15
+ super(location, nil)
16
+ end
17
+
18
+ def build!
19
+ path, commands = @path, @commands
20
+ location.instance_eval do
21
+ @options[:notify_watchers] ||= []
22
+ @options[:notify_watchers] << [path, commands]
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: Worker Memory Limit
3
+ url: /options/worker_memory_limit
4
+ ---
5
+
6
+ The **Worker Memory Limit** option sets the maximum amount of memory a worker process can use before it is terminated. If an [`after_memory_threshold_reached`](/options/after_memory_limit_reached) hook is also set, you can define custom behaviour (e.g. an alert or exception notification) that should occur before the worker is rebooted.
7
+
8
+
9
+ ## Configuration File
10
+
11
+ ### Examples
12
+
13
+ ```ruby {filename="Itsi.rb"}
14
+ worker_memory_limit 256 * 1024 ** 2 # 256 MB
15
+ ```
16
+
17
+ ```ruby {filename="Itsi.rb"}
18
+ worker_memory_limit 256 * 1024 ** 2 # 256 MB
19
+ after_memory_limit_reached do |pid|
20
+ send_slack_alert("Worker #{pid} exceeded memory limit")
21
+ end
22
+ ```
@@ -0,0 +1,18 @@
1
+ module Itsi
2
+ class Server
3
+ module Config
4
+ class WorkerMemoryLimit < Option
5
+
6
+ insert_text <<~SNIPPET
7
+ worker_memory_limit ${1|256,512,1024,2048|} * 1024 ** 2 # Worker memory limit in bytes
8
+ SNIPPET
9
+
10
+ detail "Set the maximum amount of memory a worker process can use before it is terminated."
11
+
12
+ schema do
13
+ Type(Integer) & Required()
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,8 +1,7 @@
1
1
  ---
2
2
  title: Workers
3
3
  url: /options/workers
4
- prev: /options
5
- weight: 1
4
+ next: middleware/
6
5
  ---
7
6
  Itsi is a preforking server. It can run as either a single process (`workers 1`) or in clustered mode (`workers > 1`).
8
7
  <br/>You can switch between the two without downtime.
@@ -3,7 +3,7 @@ module Itsi
3
3
  module Config
4
4
  class Workers < Option
5
5
 
6
- insert_text "workers ${1|1,2,Etc.nprocessors|}"
6
+ insert_text "workers ${1|1,2,Etc.nprocessors|} # Number of worker processes to run"
7
7
 
8
8
  detail "Number of worker processes to run"
9
9
 
@@ -16,12 +16,19 @@ module Itsi
16
16
  defaults.transform_values! { _1.is_a?(Validation) ? _1.default(nil) : _1 }
17
17
  Struct.new(*defaults.keys, keyword_init: true) do
18
18
  define_method(:initialize) do |*input, validate: true, **raw_input|
19
+
20
+ if input.last && !input.last.is_a?(Hash)
21
+ raise "─ Invalid input to #{self}: #{input.last}"
22
+ end
19
23
  raw_input.transform_keys!{|k| k.to_s.downcase.to_sym }
20
- raw_input.merge!(input.pop) if input.last.is_a?(Hash)
24
+ raw_input.merge!(input.pop.transform_keys!{|k| k.to_s.downcase.to_sym}) if input.last.is_a?(Hash)
21
25
 
22
26
  excess_keys = raw_input.keys - defaults.keys
23
27
 
24
- raise "Unsupported keys #{excess_keys}" if excess_keys.any?
28
+ if excess_keys.any?
29
+ raise "─ Unsupported keys #{excess_keys}"
30
+
31
+ end
25
32
 
26
33
  initial_values = defaults.each_with_object({}) do |(k, default_config), inputs|
27
34
  value = raw_input.key?(k) ? raw_input[k] : default_config[VALUE].dup
@@ -30,7 +37,7 @@ module Itsi
30
37
  begin
31
38
  inputs[k] = default_config[VALIDATE].validate!(value)
32
39
  rescue StandardError => e
33
- raise ArgumentError, e.message
40
+ raise ArgumentError, "─ #{k}: #{e.message}"
34
41
  end
35
42
  end
36
43
 
@@ -97,7 +104,7 @@ module Itsi
97
104
  "#{@name}"
98
105
  end
99
106
 
100
- def +(other)
107
+ def &(other)
101
108
  tail = self
102
109
  while tail.next
103
110
  tail = tail.next
@@ -115,40 +122,47 @@ module Itsi
115
122
  @validations.each do |validation|
116
123
  case validation
117
124
  when Proc
118
- unless validation.call(value)
119
- raise ArgumentError,
120
- "─ `#{@name}` validation failed. Invalid value: #{value.inspect}"
121
- end
125
+ validation.call(value)
122
126
  when Array
123
127
  unless !value || validation.include?(value)
124
128
  raise ArgumentError,
125
- "─ `#{@name}` validation failed. Invalid #{validation} value: #{value.inspect}"
129
+ "─ `#{@name}` validation failed. Invalid #{validation} value: #{value.inspect}"
126
130
  end
127
131
  when Range
128
132
  unless !value || validation.include?(value)
129
133
  raise ArgumentError,
130
- "─ `#{@name}` validation failed. Invalid #{validation} value: #{value.inspect}"
134
+ "─ `#{@name}` validation failed. Invalid #{validation} value: #{value.inspect}"
131
135
  end
132
136
  when Regexp
133
137
  unless !value || validation.match?(value)
134
138
  raise ArgumentError,
135
- "─ `#{@name}` validation failed. Invalid #{validation} value: #{value.inspect}"
139
+ "─ `#{@name}` validation failed. Invalid #{validation} value: #{value.inspect}"
136
140
  end
141
+ when Validation
142
+ validation.validate!(value)
137
143
  when Class
138
-
139
144
  if value && !value.is_a?(validation)
140
145
  begin
141
146
  value = \
142
147
  if validation.eql?(Time) then Time.parse(value.to_s)
143
148
  elsif validation.eql?(::Date) then Date.parse(value.to_s)
144
149
  elsif validation.eql?(Float) then Float(value)
145
- elsif validation.eql?(String) then value.to_s
146
- elsif validation.eql?(Symbol) then value.is_a?(Symbol) ? value : value.to_s.to_sym
147
- else validation.new(value)
150
+ elsif validation.eql?(Integer) then Integer(value)
151
+ elsif validation.eql?(Proc) then
152
+ raise ArgumentError, "Invalid #{validation} value: #{value.inspect}" unless value.is_a?(Proc)
153
+ elsif validation.eql?(String) || validation.eql?(Symbol)
154
+ raise ArgumentError, "Invalid #{validation} value: #{value.inspect}" unless value.is_a?(String) || value.is_a?(Symbol)
155
+ if validation.eql?(String)
156
+ value.to_s
157
+ elsif validation.eql?(Symbol)
158
+ value.to_s.to_sym
159
+ end
160
+ else
161
+ validation.new(value)
148
162
  end
149
163
  rescue StandardError => e
150
164
  raise ArgumentError,
151
- "─ `#{@name}` Validation Failed. Invalid #{validation.to_s.split("::").last} value: #{value.inspect}. Failure reason: \n └#{e.message}"
165
+ "─ `#{@name}` Validation Failed. Invalid #{validation.to_s.split("::").last} value: #{value.inspect}. Failure reason: \n └─ #{e.message}"
152
166
  end
153
167
  end
154
168
  end
@@ -164,9 +178,20 @@ module Itsi
164
178
  {
165
179
  Bool: Validation.new(:Bool, [[true, false]]),
166
180
  Required: Validation.new(:Required, ->(value) { !value.nil? }),
167
- Or: ->(validation_a, validation_b){
181
+ Or: ->(*validations){
168
182
  Validation.new(:Or, ->(v){
169
- validation_a.validate!(v) || validation_b.validate!(v)
183
+
184
+ return true if v.nil?
185
+ errs = []
186
+ validations.each do |validation|
187
+ begin
188
+ v = validation.validate!(v)
189
+ return v
190
+ rescue StandardError => e
191
+ errs << e.message
192
+ end
193
+ end
194
+ raise StandardError.new("─ Validation failed (None match:) \n └#{errs.join("\n └")}")
170
195
  })
171
196
  },
172
197
  Range: ->(input_range) {
@@ -175,12 +200,26 @@ module Itsi
175
200
  Length: lambda { |input_length|
176
201
  Validation.new(:Length, ->(value) { input_length === value.length })
177
202
  },
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|
208
+ [
209
+ key_type.validate!(k),
210
+ value_type.validate!(v)
211
+ ]
212
+ end.to_h
213
+ })
214
+ },
178
215
  Type: ->(input_type) { Validation.new(:Type, input_type) },
179
216
  Enum: ->(allowed_values) { Validation.new(:Enum, [allowed_values.map{|v| v.kind_of?(Symbol) ? v.to_s : v}]) },
180
217
  Array: lambda { |*value_validations|
181
218
  Validation.new(:Array, [::Array, lambda { |value|
182
219
  return true unless value
183
220
 
221
+ raise StandardError.new("Expected Array got #{value.class}") unless value.is_a?(Array)
222
+
184
223
  value&.map! do |v|
185
224
  value_validations.all? { v = _1.validate!(v) }
186
225
  v
@@ -193,8 +232,8 @@ module Itsi
193
232
  define_singleton_method(name.to_s.downcase, &factory)
194
233
  else
195
234
  const_set(name, factory)
196
- define_singleton_method(name, -> { factory })
197
- define_singleton_method(name.to_s.downcase, -> { factory })
235
+ define_singleton_method(name, -> { factory.dup })
236
+ define_singleton_method(name.to_s.downcase, -> { factory.dup })
198
237
  end
199
238
  end
200
239
  end