brut 0.0.28 → 0.1.0

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 (532) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.projections.json +10 -0
  4. data/.rspec +3 -0
  5. data/Dockerfile.dx +32 -14
  6. data/Gemfile.lock +1 -1
  7. data/README.md +23 -2
  8. data/assets/Logo-Square.pxd +0 -0
  9. data/assets/LogoPylon.pxd +0 -0
  10. data/assets/LogoStop.pxd +0 -0
  11. data/assets/LogoTall.pxd +0 -0
  12. data/assets/MetroLogo.graffle +0 -0
  13. data/assets/SocialImage.png +0 -0
  14. data/assets/SocialImage.pxd +0 -0
  15. data/bin/docs +24 -2
  16. data/bin/rspec +27 -0
  17. data/bin/setup +3 -3
  18. data/brutrb.com/.vitepress/config.mjs +45 -8
  19. data/brutrb.com/.vitepress/theme/custom.css +7 -0
  20. data/brutrb.com/.vitepress/theme/style.css +29 -17
  21. data/brutrb.com/ai.md +10 -15
  22. data/brutrb.com/assets.md +2 -9
  23. data/brutrb.com/brut-js.md +12 -2
  24. data/brutrb.com/cli.md +9 -13
  25. data/brutrb.com/components.md +118 -96
  26. data/brutrb.com/configuration.md +3 -4
  27. data/brutrb.com/css.md +2 -2
  28. data/brutrb.com/custom-element-tests.md +3 -4
  29. data/brutrb.com/database-access.md +1 -1
  30. data/brutrb.com/database-schema.md +29 -41
  31. data/brutrb.com/deployment.md +123 -45
  32. data/brutrb.com/dev-environment.md +7 -7
  33. data/brutrb.com/dir-structure.md +120 -0
  34. data/brutrb.com/doc-conventions.md +18 -15
  35. data/brutrb.com/dx +1 -0
  36. data/brutrb.com/end-to-end-tests.md +12 -10
  37. data/brutrb.com/features.md +373 -0
  38. data/brutrb.com/flash-and-session.md +115 -131
  39. data/brutrb.com/form-constraints.md +266 -0
  40. data/brutrb.com/forms.md +140 -765
  41. data/brutrb.com/getting-started.md +10 -11
  42. data/brutrb.com/handlers.md +119 -95
  43. data/brutrb.com/hooks.md +18 -20
  44. data/brutrb.com/i18n.md +6 -4
  45. data/brutrb.com/images/LogoPylon.png +0 -0
  46. data/brutrb.com/images/LogoSquare.png +0 -0
  47. data/brutrb.com/images/LogoStop.png +0 -0
  48. data/brutrb.com/images/LogoTall.png +0 -0
  49. data/brutrb.com/images/OverviewMetro.graffle +0 -0
  50. data/brutrb.com/images/OverviewMetro.png +0 -0
  51. data/brutrb.com/index.md +4 -3
  52. data/brutrb.com/instrumentation.md +7 -10
  53. data/brutrb.com/javascript.md +14 -14
  54. data/brutrb.com/keyword-injection.md +72 -114
  55. data/brutrb.com/layouts.md +20 -52
  56. data/brutrb.com/lsp.md +1 -1
  57. data/brutrb.com/overview.md +35 -377
  58. data/brutrb.com/pages.md +119 -207
  59. data/brutrb.com/public/SocialImage.png +0 -0
  60. data/brutrb.com/public/favicon.ico +0 -0
  61. data/brutrb.com/recipes/alternate-layouts.md +32 -0
  62. data/brutrb.com/recipes/authentication.md +315 -6
  63. data/brutrb.com/recipes/blank-layouts.md +22 -0
  64. data/brutrb.com/recipes/custom-flash.md +51 -0
  65. data/brutrb.com/recipes/indexed-forms.md +149 -0
  66. data/brutrb.com/recipes/text-field-component.md +182 -0
  67. data/brutrb.com/routes.md +56 -82
  68. data/brutrb.com/security.md +0 -3
  69. data/brutrb.com/space-time-continuum.md +8 -12
  70. data/brutrb.com/tutorial.md +1 -1
  71. data/brutrb.com/why.md +19 -0
  72. data/docker-compose.dx.yml +5 -2
  73. data/docs/404.html +8 -3
  74. data/docs/SocialImage.png +0 -0
  75. data/docs/ai.html +11 -6
  76. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  77. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  78. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  79. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  80. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  81. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  82. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  83. data/docs/api/Brut/BackEnd.html +1 -1
  84. data/docs/api/Brut/CLI/App.html +1 -1
  85. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  86. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  87. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  88. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  89. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  90. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  91. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  92. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  93. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  94. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  95. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  96. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  97. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  98. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  99. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  100. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  101. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  102. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  103. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  104. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  105. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  106. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  107. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  108. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  109. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  110. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  111. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  112. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  113. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  114. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  115. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  116. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  117. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  118. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  119. data/docs/api/Brut/CLI/Apps.html +1 -1
  120. data/docs/api/Brut/CLI/Command.html +1 -1
  121. data/docs/api/Brut/CLI/Error.html +1 -1
  122. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  123. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  124. data/docs/api/Brut/CLI/Executor.html +1 -1
  125. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  126. data/docs/api/Brut/CLI/Options.html +1 -1
  127. data/docs/api/Brut/CLI/Output.html +1 -1
  128. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  129. data/docs/api/Brut/CLI.html +1 -1
  130. data/docs/api/Brut/FactoryBot.html +1 -1
  131. data/docs/api/Brut/Framework/App.html +1 -1
  132. data/docs/api/Brut/Framework/Config.html +1 -1
  133. data/docs/api/Brut/Framework/Container.html +1 -1
  134. data/docs/api/Brut/Framework/Error.html +1 -1
  135. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  136. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  137. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  138. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  139. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  140. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  141. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  142. data/docs/api/Brut/Framework/Errors.html +1 -1
  143. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  144. data/docs/api/Brut/Framework/MCP.html +1 -1
  145. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  146. data/docs/api/Brut/Framework.html +1 -1
  147. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  152. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  153. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  154. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  155. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  156. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  157. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
  158. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  162. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  164. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  167. data/docs/api/Brut/FrontEnd/Form.html +9 -11
  168. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +201 -0
  170. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +535 -0
  171. data/docs/api/Brut/FrontEnd/Forms/Input.html +983 -35
  172. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  173. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +29 -19
  174. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +141 -20
  175. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  176. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +141 -20
  177. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  178. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  179. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  180. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  181. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  182. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  183. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  184. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  185. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  186. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  187. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  188. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  189. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  190. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  191. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  192. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  193. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  194. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  195. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  196. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  197. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  198. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  199. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  200. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  201. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  202. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  203. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  204. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  205. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  206. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  207. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  208. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  209. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  210. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  211. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  212. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  213. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  214. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  215. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  216. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  217. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  218. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  219. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  220. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  221. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  222. data/docs/api/Brut/FrontEnd.html +1 -1
  223. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  224. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  225. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  226. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  227. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  228. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  229. data/docs/api/Brut/I18n.html +1 -1
  230. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  231. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  232. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  233. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  234. data/docs/api/Brut/Instrumentation.html +1 -1
  235. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  236. data/docs/api/Brut/SinatraHelpers.html +1 -1
  237. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  238. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  239. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  240. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  241. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  242. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  243. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  244. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  245. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  246. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  247. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  248. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  249. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  250. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  251. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  252. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  253. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  254. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  255. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  256. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  257. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  258. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  259. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  260. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  261. data/docs/api/Brut/SpecSupport.html +1 -1
  262. data/docs/api/Brut.html +1 -1
  263. data/docs/api/Clock.html +1 -1
  264. data/docs/api/RichString.html +1 -1
  265. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  266. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  267. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  268. data/docs/api/Sequel/Extensions.html +1 -1
  269. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  270. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  271. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  272. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  273. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  274. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  275. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  276. data/docs/api/Sequel/Plugins.html +1 -1
  277. data/docs/api/Sequel.html +1 -1
  278. data/docs/api/_index.html +15 -1
  279. data/docs/api/class_list.html +1 -1
  280. data/docs/api/css/full_list.css +2 -1
  281. data/docs/api/css/style.css +14 -13
  282. data/docs/api/file.README.html +22 -3
  283. data/docs/api/index.html +22 -3
  284. data/docs/api/method_list.html +435 -275
  285. data/docs/api/top-level-namespace.html +1 -1
  286. data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
  287. data/docs/assets/OverviewMetro.DUS-5fUZ.png +0 -0
  288. data/docs/assets/{ai.md._6HCDL6d.js → ai.md.Cy9GWnER.js} +1 -1
  289. data/docs/assets/ai.md.Cy9GWnER.lean.js +1 -0
  290. data/docs/assets/{app.BX81XO4N.js → app.ClaS47Ru.js} +1 -1
  291. data/docs/assets/{assets.md.D3wunzLx.js → assets.md.7C3HWkga.js} +3 -3
  292. data/docs/assets/{assets.md.D3wunzLx.lean.js → assets.md.7C3HWkga.lean.js} +1 -1
  293. data/docs/assets/{brut-js.md.o2DAO2s2.js → brut-js.md.B4GYxQVw.js} +1 -1
  294. data/docs/assets/{brut-js.md.o2DAO2s2.lean.js → brut-js.md.B4GYxQVw.lean.js} +1 -1
  295. data/docs/assets/chunks/@localSearchIndexroot.Biqy1A4t.js +1 -0
  296. data/docs/assets/chunks/{VPLocalSearchBox.gABXcTWp.js → VPLocalSearchBox.DtgDfde2.js} +1 -1
  297. data/docs/assets/chunks/{theme.DwUXXAL3.js → theme.B45bvibT.js} +2 -2
  298. data/docs/assets/{cli.md.RmeA2b0i.js → cli.md.CjsktgFz.js} +15 -20
  299. data/docs/assets/components.md.DatoNgFo.js +96 -0
  300. data/docs/assets/{components.md.CRUMdRoN.lean.js → components.md.DatoNgFo.lean.js} +1 -1
  301. data/docs/assets/{configuration.md.BGHl8oRC.js → configuration.md.DeyhpqEx.js} +3 -3
  302. data/docs/assets/{css.md.DJgj2clw.js → css.md.CltvJqAa.js} +3 -3
  303. data/docs/assets/{custom-element-tests.md.BrYJQEl3.js → custom-element-tests.md.B_rbta32.js} +3 -3
  304. data/docs/assets/{database-access.md.C7l-Vuvb.js → database-access.md.gnluu54N.js} +1 -1
  305. data/docs/assets/{database-schema.md.BUjR0VS1.js → database-schema.md.CSYk6E6v.js} +6 -6
  306. data/docs/assets/{database-schema.md.BUjR0VS1.lean.js → database-schema.md.CSYk6E6v.lean.js} +1 -1
  307. data/docs/assets/deployment.md.BLseERGV.js +48 -0
  308. data/docs/assets/deployment.md.BLseERGV.lean.js +1 -0
  309. data/docs/assets/dev-environment.md.BroAOLhF.js +11 -0
  310. data/docs/assets/dir-structure.md.CWir1pic.js +46 -0
  311. data/docs/assets/dir-structure.md.CWir1pic.lean.js +1 -0
  312. data/docs/assets/doc-conventions.md.BzmSrTEW.js +1 -0
  313. data/docs/assets/doc-conventions.md.BzmSrTEW.lean.js +1 -0
  314. data/docs/assets/{end-to-end-tests.md.yfQHC0b5.js → end-to-end-tests.md.DzqRpZ43.js} +5 -3
  315. data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +1 -0
  316. data/docs/assets/features.md.DPFXsy0z.js +154 -0
  317. data/docs/assets/features.md.DPFXsy0z.lean.js +1 -0
  318. data/docs/assets/flash-and-session.md.nPvUpnUx.js +79 -0
  319. data/docs/assets/{flash-and-session.md.BXY8RvT0.lean.js → flash-and-session.md.nPvUpnUx.lean.js} +1 -1
  320. data/docs/assets/form-constraints.md.x5tNpTTI.js +90 -0
  321. data/docs/assets/form-constraints.md.x5tNpTTI.lean.js +1 -0
  322. data/docs/assets/forms.md.C2Dizvzq.js +64 -0
  323. data/docs/assets/forms.md.C2Dizvzq.lean.js +1 -0
  324. data/docs/assets/{getting-started.md.Ciz82L0m.js → getting-started.md.C93e0odB.js} +5 -5
  325. data/docs/assets/{getting-started.md.Ciz82L0m.lean.js → getting-started.md.C93e0odB.lean.js} +1 -1
  326. data/docs/assets/handlers.md.Chyri6KA.js +54 -0
  327. data/docs/assets/handlers.md.Chyri6KA.lean.js +1 -0
  328. data/docs/assets/{hooks.md.C4-moMny.js → hooks.md.Jmb5VOLA.js} +4 -4
  329. data/docs/assets/{hooks.md.C4-moMny.lean.js → hooks.md.Jmb5VOLA.lean.js} +1 -1
  330. data/docs/assets/{i18n.md.Do9i1qWl.js → i18n.md.xQhiGo1G.js} +2 -2
  331. data/docs/assets/{i18n.md.Do9i1qWl.lean.js → i18n.md.xQhiGo1G.lean.js} +1 -1
  332. data/docs/assets/index.md.CAMqGBJE.js +1 -0
  333. data/docs/assets/index.md.CAMqGBJE.lean.js +1 -0
  334. data/docs/assets/{instrumentation.md.a9Pjps4P.js → instrumentation.md.BgcaGVYH.js} +2 -2
  335. data/docs/assets/{instrumentation.md.a9Pjps4P.lean.js → instrumentation.md.BgcaGVYH.lean.js} +1 -1
  336. data/docs/assets/{javascript.md.GWbhRS51.js → javascript.md.DzrMxUmI.js} +7 -7
  337. data/docs/assets/{javascript.md.GWbhRS51.lean.js → javascript.md.DzrMxUmI.lean.js} +1 -1
  338. data/docs/assets/keyword-injection.md.95Zgh2eN.js +21 -0
  339. data/docs/assets/{keyword-injection.md.Dt2tKREs.lean.js → keyword-injection.md.95Zgh2eN.lean.js} +1 -1
  340. data/docs/assets/{layouts.md.cPnh3NId.js → layouts.md.CJGDFY-m.js} +2 -15
  341. data/docs/assets/layouts.md.CJGDFY-m.lean.js +1 -0
  342. data/docs/assets/{lsp.md.Bsu-f6VU.js → lsp.md.Dn1rIiW0.js} +1 -1
  343. data/docs/assets/{lsp.md.Bsu-f6VU.lean.js → lsp.md.Dn1rIiW0.lean.js} +1 -1
  344. data/docs/assets/overview.md.Bdq4qt3L.js +1 -0
  345. data/docs/assets/overview.md.Bdq4qt3L.lean.js +1 -0
  346. data/docs/assets/pages.md.B7Hc-i6H.js +45 -0
  347. data/docs/assets/pages.md.B7Hc-i6H.lean.js +1 -0
  348. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.js +22 -0
  349. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +1 -0
  350. data/docs/assets/recipes_authentication.md.Dzvi_g69.js +156 -0
  351. data/docs/assets/recipes_authentication.md.Dzvi_g69.lean.js +1 -0
  352. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +15 -0
  353. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +1 -0
  354. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.js +26 -0
  355. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +1 -0
  356. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.js +74 -0
  357. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +1 -0
  358. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.js +101 -0
  359. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +1 -0
  360. data/docs/assets/routes.md.B8kfUPHU.js +21 -0
  361. data/docs/assets/{routes.md.BMM7peut.lean.js → routes.md.B8kfUPHU.lean.js} +1 -1
  362. data/docs/assets/{security.md.C668yXCi.js → security.md.C0G_AZR-.js} +1 -1
  363. data/docs/assets/{security.md.C668yXCi.lean.js → security.md.C0G_AZR-.lean.js} +1 -1
  364. data/docs/assets/space-time-continuum.md.xl44xDos.js +1 -0
  365. data/docs/assets/{space-time-continuum.md.KPUIKysQ.lean.js → space-time-continuum.md.xl44xDos.lean.js} +1 -1
  366. data/docs/assets/{style.D73IYGCX.css → style.prAgp4yQ.css} +1 -1
  367. data/docs/assets/tutorial.md.a4a0eVOy.js +1 -0
  368. data/docs/assets/tutorial.md.a4a0eVOy.lean.js +1 -0
  369. data/docs/assets/why.md.C-hk5xgJ.js +1 -0
  370. data/docs/assets/why.md.C-hk5xgJ.lean.js +1 -0
  371. data/docs/assets.html +12 -7
  372. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  373. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  374. data/docs/brut-js/api/Autosubmit.html +1 -1
  375. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  376. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  377. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  378. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  379. data/docs/brut-js/api/BufferedLogger.html +1 -1
  380. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  381. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  382. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  383. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  384. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  385. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  386. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  387. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  388. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  389. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  390. data/docs/brut-js/api/Form.html +1 -1
  391. data/docs/brut-js/api/Form.js.html +1 -1
  392. data/docs/brut-js/api/I18nTranslation.html +1 -1
  393. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  394. data/docs/brut-js/api/LocaleDetection.html +1 -1
  395. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  396. data/docs/brut-js/api/Logger.html +1 -1
  397. data/docs/brut-js/api/Logger.js.html +1 -1
  398. data/docs/brut-js/api/Message.html +1 -1
  399. data/docs/brut-js/api/Message.js.html +1 -1
  400. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  401. data/docs/brut-js/api/RichString.html +1 -1
  402. data/docs/brut-js/api/RichString.js.html +1 -1
  403. data/docs/brut-js/api/Tabs.html +1 -1
  404. data/docs/brut-js/api/Tabs.js.html +1 -1
  405. data/docs/brut-js/api/Tracing.html +1 -1
  406. data/docs/brut-js/api/Tracing.js.html +1 -1
  407. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  408. data/docs/brut-js/api/external-Performance.html +1 -1
  409. data/docs/brut-js/api/external-Promise.html +1 -1
  410. data/docs/brut-js/api/external-ValidityState.html +1 -1
  411. data/docs/brut-js/api/external-Window.html +1 -1
  412. data/docs/brut-js/api/external-fetch.html +1 -1
  413. data/docs/brut-js/api/global.html +1 -1
  414. data/docs/brut-js/api/index.html +1 -1
  415. data/docs/brut-js/api/index.js.html +1 -1
  416. data/docs/brut-js/api/module-testing.html +1 -1
  417. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  418. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  419. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  420. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  421. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  422. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  423. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  424. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  425. data/docs/brut-js/api/testing_index.js.html +1 -1
  426. data/docs/brut-js.html +12 -7
  427. data/docs/business-logic.html +10 -5
  428. data/docs/cli.html +26 -26
  429. data/docs/components.html +61 -64
  430. data/docs/configuration.html +13 -8
  431. data/docs/css.html +14 -9
  432. data/docs/custom-element-tests.html +14 -9
  433. data/docs/database-access.html +12 -7
  434. data/docs/database-schema.html +15 -10
  435. data/docs/deployment.html +58 -6
  436. data/docs/dev-environment.html +12 -7
  437. data/docs/dir-structure.html +74 -0
  438. data/docs/doc-conventions.html +11 -6
  439. data/docs/end-to-end-tests.html +15 -8
  440. data/docs/favicon.ico +0 -0
  441. data/docs/features.html +182 -0
  442. data/docs/flash-and-session.html +73 -82
  443. data/docs/form-constraints.html +118 -0
  444. data/docs/forms.html +57 -367
  445. data/docs/getting-started.html +15 -10
  446. data/docs/handlers.html +51 -61
  447. data/docs/hashmap.json +1 -1
  448. data/docs/hooks.html +14 -9
  449. data/docs/i18n.html +12 -7
  450. data/docs/index.html +11 -6
  451. data/docs/instrumentation.html +12 -7
  452. data/docs/javascript.html +17 -12
  453. data/docs/jobs.html +10 -5
  454. data/docs/keyword-injection.html +22 -21
  455. data/docs/layouts.html +12 -20
  456. data/docs/lsp.html +11 -6
  457. data/docs/markdown-examples.html +10 -5
  458. data/docs/middleware.html +10 -5
  459. data/docs/not-released.html +10 -5
  460. data/docs/overview.html +11 -138
  461. data/docs/pages.html +49 -121
  462. data/docs/recipes/alternate-layouts.html +50 -0
  463. data/docs/recipes/authentication.html +166 -6
  464. data/docs/recipes/blank-layouts.html +43 -0
  465. data/docs/recipes/custom-flash.html +54 -0
  466. data/docs/recipes/indexed-forms.html +102 -0
  467. data/docs/recipes/text-field-component.html +129 -0
  468. data/docs/routes.html +16 -19
  469. data/docs/security.html +11 -6
  470. data/docs/seed-data.html +10 -5
  471. data/docs/space-time-continuum.html +11 -6
  472. data/docs/tutorial.html +11 -6
  473. data/docs/unit-tests.html +10 -5
  474. data/docs/why.html +29 -0
  475. data/dx/bash_customizations +7 -0
  476. data/dx/build +13 -2
  477. data/dx/docker-compose.env +1 -1
  478. data/dx/exec +25 -8
  479. data/lib/brut/front_end/form.rb +8 -8
  480. data/lib/brut/front_end/forms/input.rb +253 -20
  481. data/lib/brut/front_end/forms/input_definition.rb +15 -12
  482. data/lib/brut/front_end/forms/radio_button_group_input.rb +8 -1
  483. data/lib/brut/front_end/forms/select_input.rb +8 -1
  484. data/lib/brut/front_end.rb +1 -0
  485. data/lib/brut/version.rb +1 -1
  486. data/specs/brut/front_end/forms/input.spec.rb +978 -0
  487. data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +54 -0
  488. data/specs/brut/front_end/forms/select_input.spec.rb +54 -0
  489. data/specs/spec_helper.rb +27 -0
  490. data/specs/support/matchers/have_constraint_violation.rb +23 -0
  491. data/specs/support/matchers.rb +5 -0
  492. data/specs/support.rb +3 -0
  493. metadata +141 -77
  494. data/brutrb.com/public/images/logo-300.png +0 -0
  495. data/brutrb.com/public/images/logo.png +0 -0
  496. data/docs/assets/ai.md._6HCDL6d.lean.js +0 -1
  497. data/docs/assets/chunks/@localSearchIndexroot.CoYzciVi.js +0 -1
  498. data/docs/assets/components.md.CRUMdRoN.js +0 -104
  499. data/docs/assets/deployment.md.Dbka4OTr.js +0 -1
  500. data/docs/assets/deployment.md.Dbka4OTr.lean.js +0 -1
  501. data/docs/assets/dev-environment.md.GZv6xvi9.js +0 -11
  502. data/docs/assets/doc-conventions.md.-kN3Xo5C.js +0 -1
  503. data/docs/assets/doc-conventions.md.-kN3Xo5C.lean.js +0 -1
  504. data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +0 -1
  505. data/docs/assets/flash-and-session.md.BXY8RvT0.js +0 -93
  506. data/docs/assets/forms.md.B-koVgyw.js +0 -379
  507. data/docs/assets/forms.md.B-koVgyw.lean.js +0 -1
  508. data/docs/assets/handlers.md.089DVD3v.js +0 -69
  509. data/docs/assets/handlers.md.089DVD3v.lean.js +0 -1
  510. data/docs/assets/index.md.B28EwVpq.js +0 -1
  511. data/docs/assets/index.md.B28EwVpq.lean.js +0 -1
  512. data/docs/assets/keyword-injection.md.Dt2tKREs.js +0 -25
  513. data/docs/assets/layouts.md.cPnh3NId.lean.js +0 -1
  514. data/docs/assets/overview.Da81cB9R.png +0 -0
  515. data/docs/assets/overview.md.C5wlBcR5.js +0 -133
  516. data/docs/assets/overview.md.C5wlBcR5.lean.js +0 -1
  517. data/docs/assets/pages.md.BE3kfOc5.js +0 -122
  518. data/docs/assets/pages.md.BE3kfOc5.lean.js +0 -1
  519. data/docs/assets/recipes_authentication.md.CAsXf7hk.js +0 -1
  520. data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +0 -1
  521. data/docs/assets/routes.md.BMM7peut.js +0 -29
  522. data/docs/assets/space-time-continuum.md.KPUIKysQ.js +0 -1
  523. data/docs/assets/tutorial.md.BnoGjrdK.js +0 -1
  524. data/docs/assets/tutorial.md.BnoGjrdK.lean.js +0 -1
  525. data/docs/images/logo-300.png +0 -0
  526. data/docs/images/logo.png +0 -0
  527. /data/docs/assets/{cli.md.RmeA2b0i.lean.js → cli.md.CjsktgFz.lean.js} +0 -0
  528. /data/docs/assets/{configuration.md.BGHl8oRC.lean.js → configuration.md.DeyhpqEx.lean.js} +0 -0
  529. /data/docs/assets/{css.md.DJgj2clw.lean.js → css.md.CltvJqAa.lean.js} +0 -0
  530. /data/docs/assets/{custom-element-tests.md.BrYJQEl3.lean.js → custom-element-tests.md.B_rbta32.lean.js} +0 -0
  531. /data/docs/assets/{database-access.md.C7l-Vuvb.lean.js → database-access.md.gnluu54N.lean.js} +0 -0
  532. /data/docs/assets/{dev-environment.md.GZv6xvi9.lean.js → dev-environment.md.BroAOLhF.lean.js} +0 -0
data/docs/handlers.html CHANGED
@@ -6,87 +6,77 @@
6
6
  <title>Handlers &amp; Actions | Brut RB</title>
7
7
  <meta name="description" content="Documentation for the Brut.RB web framework.">
8
8
  <meta name="generator" content="VitePress v1.6.3">
9
- <link rel="preload stylesheet" href="/assets/style.D73IYGCX.css" as="style">
9
+ <link rel="preload stylesheet" href="/assets/style.prAgp4yQ.css" as="style">
10
10
  <link rel="preload stylesheet" href="/vp-icons.css" as="style">
11
11
 
12
- <script type="module" src="/assets/app.BX81XO4N.js"></script>
13
- <link rel="modulepreload" href="/assets/chunks/theme.DwUXXAL3.js">
12
+ <script type="module" src="/assets/app.ClaS47Ru.js"></script>
13
+ <link rel="modulepreload" href="/assets/chunks/theme.B45bvibT.js">
14
14
  <link rel="modulepreload" href="/assets/chunks/framework.1L-BeKqY.js">
15
- <link rel="modulepreload" href="/assets/handlers.md.089DVD3v.lean.js">
15
+ <link rel="modulepreload" href="/assets/handlers.md.Chyri6KA.lean.js">
16
+ <link rel="icon" href="/favicon.ico">
17
+ <meta property="og:title" content="BrutRB Documentation">
18
+ <meta property="og:type" content="website">
19
+ <meta property="og:image" content="https://github.com/thirdtank/brut/blob/main/assets/SocialImage.png?raw=true">
20
+ <script defer data-domain="brutrb.com" src="https://plausible.io/js/script.js"></script>
16
21
  <script id="check-dark-mode">(()=>{const e=localStorage.getItem("vitepress-theme-appearance")||"auto",a=window.matchMedia("(prefers-color-scheme: dark)").matches;(!e||e==="auto"?a:e==="dark")&&document.documentElement.classList.add("dark")})();</script>
17
22
  <script id="check-mac-os">document.documentElement.classList.toggle("mac",/Mac|iPhone|iPod|iPad/i.test(navigator.platform));</script>
18
23
  </head>
19
24
  <body>
20
- <div id="app"><div class="Layout" data-v-d8b57b2d><!--[--><!--]--><!--[--><span tabindex="-1" data-v-fcbfc0e0></span><a href="#VPContent" class="VPSkipLink visually-hidden" data-v-fcbfc0e0>Skip to content</a><!--]--><!----><header class="VPNav" data-v-d8b57b2d data-v-7ad780c2><div class="VPNavBar" data-v-7ad780c2 data-v-9fd4d1dd><div class="wrapper" data-v-9fd4d1dd><div class="container" data-v-9fd4d1dd><div class="title" data-v-9fd4d1dd><div class="VPNavBarTitle has-sidebar" data-v-9fd4d1dd data-v-9f43907a><a class="title" href="/" data-v-9f43907a><!--[--><!--]--><!----><span data-v-9f43907a>Brut RB</span><!--[--><!--]--></a></div></div><div class="content" data-v-9fd4d1dd><div class="content-body" data-v-9fd4d1dd><!--[--><!--]--><div class="VPNavBarSearch search" data-v-9fd4d1dd><!--[--><!----><div id="local-search"><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><span class="vp-icon DocSearch-Search-Icon"></span><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button></div><!--]--></div><nav aria-labelledby="main-nav-aria-label" class="VPNavBarMenu menu" data-v-9fd4d1dd data-v-afb2845e><span id="main-nav-aria-label" class="visually-hidden" data-v-afb2845e> Main Navigation </span><!--[--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Home</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/getting-started.html" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Getting Started</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/overview.html" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Overview</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/api/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Brut API</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/brut-js/api/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>BrutJS</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/brut-css/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>BrutCSS</span><!--]--></a><!--]--><!--]--></nav><!----><div class="VPNavBarAppearance appearance" data-v-9fd4d1dd data-v-3f90c1a5><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title aria-checked="false" data-v-3f90c1a5 data-v-be9742d9 data-v-b4ccac88><span class="check" data-v-b4ccac88><span class="icon" data-v-b4ccac88><!--[--><span class="vpi-sun sun" data-v-be9742d9></span><span class="vpi-moon moon" data-v-be9742d9></span><!--]--></span></span></button></div><div class="VPSocialLinks VPNavBarSocialLinks social-links" data-v-9fd4d1dd data-v-ef6192dc data-v-e71e869c><!--[--><a class="VPSocialLink no-icon" href="https://github.com/thirdtank/brut" aria-label="github" target="_blank" rel="noopener" data-v-e71e869c data-v-60a9a2d3><span class="vpi-social-github"></span></a><!--]--></div><div class="VPFlyout VPNavBarExtra extra" data-v-9fd4d1dd data-v-f953d92f data-v-bfe7971f><button type="button" class="button" aria-haspopup="true" aria-expanded="false" aria-label="extra navigation" data-v-bfe7971f><span class="vpi-more-horizontal icon" data-v-bfe7971f></span></button><div class="menu" data-v-bfe7971f><div class="VPMenu" data-v-bfe7971f data-v-20ed86d6><!----><!--[--><!--[--><!----><div class="group" data-v-f953d92f><div class="item appearance" data-v-f953d92f><p class="label" data-v-f953d92f>Appearance</p><div class="appearance-action" data-v-f953d92f><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title aria-checked="false" data-v-f953d92f data-v-be9742d9 data-v-b4ccac88><span class="check" data-v-b4ccac88><span class="icon" data-v-b4ccac88><!--[--><span class="vpi-sun sun" data-v-be9742d9></span><span class="vpi-moon moon" data-v-be9742d9></span><!--]--></span></span></button></div></div></div><div class="group" data-v-f953d92f><div class="item social-links" data-v-f953d92f><div class="VPSocialLinks social-links-list" data-v-f953d92f data-v-e71e869c><!--[--><a class="VPSocialLink no-icon" href="https://github.com/thirdtank/brut" aria-label="github" target="_blank" rel="noopener" data-v-e71e869c data-v-60a9a2d3><span class="vpi-social-github"></span></a><!--]--></div></div></div><!--]--><!--]--></div></div></div><!--[--><!--]--><button type="button" class="VPNavBarHamburger hamburger" aria-label="mobile navigation" aria-expanded="false" aria-controls="VPNavScreen" data-v-9fd4d1dd data-v-6bee1efd><span class="container" data-v-6bee1efd><span class="top" data-v-6bee1efd></span><span class="middle" data-v-6bee1efd></span><span class="bottom" data-v-6bee1efd></span></span></button></div></div></div></div><div class="divider" data-v-9fd4d1dd><div class="divider-line" data-v-9fd4d1dd></div></div></div><!----></header><div class="VPLocalNav has-sidebar empty" data-v-d8b57b2d data-v-2488c25a><div class="container" data-v-2488c25a><button class="menu" aria-expanded="false" aria-controls="VPSidebarNav" data-v-2488c25a><span class="vpi-align-left menu-icon" data-v-2488c25a></span><span class="menu-text" data-v-2488c25a>Menu</span></button><div class="VPLocalNavOutlineDropdown" style="--vp-vh:0px;" data-v-2488c25a data-v-6b867909><button data-v-6b867909>Return to top</button><!----></div></div></div><aside class="VPSidebar" data-v-d8b57b2d data-v-42c4c606><div class="curtain" data-v-42c4c606></div><nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1" data-v-42c4c606><span class="visually-hidden" id="sidebar-aria-label" data-v-42c4c606> Sidebar Navigation </span><!--[--><!--]--><!--[--><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Overview</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/getting-started.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Getting Started</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/overview.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Concepts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/doc-conventions.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Documentation Conventions</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/tutorial.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Tutorial</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/dev-environment.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Dev Environment</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/ai.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>AI Declaration</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible has-active" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Front-End</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/routes.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Routes</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/pages.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Pages</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/layouts.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Layouts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/forms.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Forms</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/handlers.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Handlers and Actions</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/components.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Components</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/flash-and-session.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Flash and Session</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/space-time-continuum.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Space/Time Continuum</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/javascript.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>JavaScript</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/css.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>CSS</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/assets.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Assets</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/brut-js.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>BrutJS</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Back-End</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/database-schema.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Database Schema</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/database-access.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Database Access</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/seed-data.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Seed Data</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/jobs.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Jobs</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/business-logic.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Business Logic</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Framework</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/configuration.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Configuration</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/keyword-injection.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Keyword Injection</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/i18n.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>I18n</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/cli.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>CLI / Tasks</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/deployment.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Deployment</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Testing</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/unit-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Unit Tests</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/end-to-end-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>End-to-End Tests</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/custom-element-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Testing Custom Elements</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible collapsed" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Advanced Topics</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/hooks.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Route Hooks</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/middleware.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Middleware</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/instrumentation.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Instrumentation</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/security.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Security</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/lsp.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>LSP Support</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible collapsed" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Recipes</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/authentication.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Authentication</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/form-validations.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Form Validations</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/database-migrations.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Database Migrations</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/ajax-form.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Ajax Form Submission</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/telemetry.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Custom Telemetry</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/cli-app.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>CLI App/Task</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><!--]--><!--[--><!--]--></nav></aside><div class="VPContent has-sidebar" id="VPContent" data-v-d8b57b2d data-v-9a6c75ad><div class="VPDoc has-sidebar has-aside" data-v-9a6c75ad data-v-e6f2a212><!--[--><!--]--><div class="container" data-v-e6f2a212><div class="aside" data-v-e6f2a212><div class="aside-curtain" data-v-e6f2a212></div><div class="aside-container" data-v-e6f2a212><div class="aside-content" data-v-e6f2a212><div class="VPDocAside" data-v-e6f2a212 data-v-cb998dce><!--[--><!--]--><!--[--><!--]--><nav aria-labelledby="doc-outline-aria-label" class="VPDocAsideOutline" data-v-cb998dce data-v-f610f197><div class="content" data-v-f610f197><div class="outline-marker" data-v-f610f197></div><div aria-level="2" class="outline-title" id="doc-outline-aria-label" role="heading" data-v-f610f197>On this page</div><ul class="VPDocOutlineItem root" data-v-f610f197 data-v-53c99d69><!--[--><!--]--></ul></div></nav><!--[--><!--]--><div class="spacer" data-v-cb998dce></div><!--[--><!--]--><!----><!--[--><!--]--><!--[--><!--]--></div></div></div></div><div class="content" data-v-e6f2a212><div class="content-container" data-v-e6f2a212><!--[--><!--]--><main class="main" data-v-e6f2a212><div style="position:relative;" class="vp-doc _handlers" data-v-e6f2a212><div><h1 id="handlers-actions" tabindex="-1">Handlers &amp; Actions <a class="header-anchor" href="#handlers-actions" aria-label="Permalink to &quot;Handlers &amp; Actions&quot;">​</a></h1><p>Handlers process form submissions, and <em>actions</em> work similarly to process any arbitrary HTTP request.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>Where a <a href="/pages.html">page</a> renders a web page in HTML, a <em>handler</em> responds to all other HTTP requests. To respond to such HTTP requests, you&#39;d first create a <a href="/routes.html">route</a>, using <code>form</code>, <code>action</code>, or <code>path</code>.</p><h3 id="declaring-routes" tabindex="-1">Declaring Routes <a class="header-anchor" href="#declaring-routes" aria-label="Permalink to &quot;Declaring Routes&quot;">​</a></h3><p><code>form</code> and <code>action</code> are intended to be used when an HTTP form is being submitted. The latter—<code>action</code>—is for when your form has no user-editable elements. This is akin to Rails&#39; <code>button_to</code> helper, where the contents of the URL contains everything needed to service the request.</p><p><code>path</code> is for arbitrary HTTP requests and methods, so to create a webhook that responds to a PUT, you&#39;d use:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># inside app/src/app.rb</span></span>
21
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">path </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/webhooks/stripe&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">method:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :put</span></span></code></pre></div><p>In all cases, the class to receive and process these requests is a handler, whose name is conventional based on the route. For example, the webhook above would be handled by <code>Webhooks::StripeHandler</code>.</p><h3 id="implementing-handlers" tabindex="-1">Implementing Handlers <a class="header-anchor" href="#implementing-handlers" aria-label="Permalink to &quot;Implementing Handlers&quot;">​</a></h3><p>A handler works like a page, in that its initializer can receive any injectible arguments. These would include a form object, dynamic elements of the route, or anything else available from the request. See <a href="/keyword-injection.html">Keyword Injection</a> for the details, noting that your handler can be inejcted with custom objects you&#39;ve configured in a route hook.</p><p>After the handler is created, it&#39;s <code>before_handle</code> method is called. If it returns <code>nil</code>, <code>handle</code> is called to trigger whatever logic the handler needs to trigger.</p><p>Both <code>handle</code> and <code>before_handle</code> can return a variety of objects that determine what will happen:</p><ul><li>An instance of a page or component means that page or component is rendered. This is not a redirect to the page, so it is more like Rails&#39; <code>render :new</code> and <strong>not</strong> a <code>redirect_to(new_widgets_path)</code>.</li><li>A <a href="/api/Brut/FrontEnd/HttpStatus.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::HttpStatus</code></a> which sends that status and an empty body back. This object can be created from an integer using the <code>http_status</code> helper, available to all handlers.</li><li>A <code>URI</code>, which will cause a redirect to that URI. You can create the <code>URI</code> yourself, or use the helper <code>redirect_to</code>, which accepts a string.</li><li>A two-element array with a page or component as the first element and an <a href="/api/Brut/FrontEnd/HttpStatus.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::HttpStatus</code></a> as the second. This will render that page or component&#39;s HTML, but use the given status instead of 200. This can be useful for Ajax requests where you want to use HTML your respond format, but also, say, a 422 status to indicate a constraint violation has occured.</li><li>A <a href="/api/Brut/FrontEnd/Download.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Download</code></a>, which encapsulates a file to be downloaded.</li><li>A <a href="/api/Brut/FrontEnd/GenericResponse.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::GenericResponse</code></a>, which wraps any Rack response with a defined type.</li></ul><p><code>before_handle</code> may also return <code>nil</code> to indicate that <code>handle</code> should be called. <code>handle</code> may not return <code>nil</code>.</p><p>Supposing our <code>LoginForm</code> and <code>LoginHandler</code> wanted to use a common pattern of re-rendering <code>LoginPage</code> on constraint violations, and forwarding on to, say, a <code>DashboardPage</code>. Your handler might look like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/handlers/login_handler.rb</span></span>
22
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
23
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># We&#39;ll discuss the session later</span></span>
24
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form</span></span>
25
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @session </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session</span></span>
25
+ <div id="app"><div class="Layout" data-v-d8b57b2d><!--[--><!--]--><!--[--><span tabindex="-1" data-v-fcbfc0e0></span><a href="#VPContent" class="VPSkipLink visually-hidden" data-v-fcbfc0e0>Skip to content</a><!--]--><!----><header class="VPNav" data-v-d8b57b2d data-v-7ad780c2><div class="VPNavBar" data-v-7ad780c2 data-v-9fd4d1dd><div class="wrapper" data-v-9fd4d1dd><div class="container" data-v-9fd4d1dd><div class="title" data-v-9fd4d1dd><div class="VPNavBarTitle has-sidebar" data-v-9fd4d1dd data-v-9f43907a><a class="title" href="/" data-v-9f43907a><!--[--><!--]--><!----><span data-v-9f43907a>Brut RB</span><!--[--><!--]--></a></div></div><div class="content" data-v-9fd4d1dd><div class="content-body" data-v-9fd4d1dd><!--[--><!--]--><div class="VPNavBarSearch search" data-v-9fd4d1dd><!--[--><!----><div id="local-search"><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><span class="vp-icon DocSearch-Search-Icon"></span><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button></div><!--]--></div><nav aria-labelledby="main-nav-aria-label" class="VPNavBarMenu menu" data-v-9fd4d1dd data-v-afb2845e><span id="main-nav-aria-label" class="visually-hidden" data-v-afb2845e> Main Navigation </span><!--[--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Home</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/getting-started.html" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Getting Started</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/overview.html" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Overview</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/api/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Brut API</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/brut-js/api/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>BrutJS</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/brut-css/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>BrutCSS</span><!--]--></a><!--]--><!--]--></nav><!----><div class="VPNavBarAppearance appearance" data-v-9fd4d1dd data-v-3f90c1a5><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title aria-checked="false" data-v-3f90c1a5 data-v-be9742d9 data-v-b4ccac88><span class="check" data-v-b4ccac88><span class="icon" data-v-b4ccac88><!--[--><span class="vpi-sun sun" data-v-be9742d9></span><span class="vpi-moon moon" data-v-be9742d9></span><!--]--></span></span></button></div><div class="VPSocialLinks VPNavBarSocialLinks social-links" data-v-9fd4d1dd data-v-ef6192dc data-v-e71e869c><!--[--><a class="VPSocialLink no-icon" href="https://github.com/thirdtank/brut" aria-label="github" target="_blank" rel="noopener" data-v-e71e869c data-v-60a9a2d3><span class="vpi-social-github"></span></a><!--]--></div><div class="VPFlyout VPNavBarExtra extra" data-v-9fd4d1dd data-v-f953d92f data-v-bfe7971f><button type="button" class="button" aria-haspopup="true" aria-expanded="false" aria-label="extra navigation" data-v-bfe7971f><span class="vpi-more-horizontal icon" data-v-bfe7971f></span></button><div class="menu" data-v-bfe7971f><div class="VPMenu" data-v-bfe7971f data-v-20ed86d6><!----><!--[--><!--[--><!----><div class="group" data-v-f953d92f><div class="item appearance" data-v-f953d92f><p class="label" data-v-f953d92f>Appearance</p><div class="appearance-action" data-v-f953d92f><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title aria-checked="false" data-v-f953d92f data-v-be9742d9 data-v-b4ccac88><span class="check" data-v-b4ccac88><span class="icon" data-v-b4ccac88><!--[--><span class="vpi-sun sun" data-v-be9742d9></span><span class="vpi-moon moon" data-v-be9742d9></span><!--]--></span></span></button></div></div></div><div class="group" data-v-f953d92f><div class="item social-links" data-v-f953d92f><div class="VPSocialLinks social-links-list" data-v-f953d92f data-v-e71e869c><!--[--><a class="VPSocialLink no-icon" href="https://github.com/thirdtank/brut" aria-label="github" target="_blank" rel="noopener" data-v-e71e869c data-v-60a9a2d3><span class="vpi-social-github"></span></a><!--]--></div></div></div><!--]--><!--]--></div></div></div><!--[--><!--]--><button type="button" class="VPNavBarHamburger hamburger" aria-label="mobile navigation" aria-expanded="false" aria-controls="VPNavScreen" data-v-9fd4d1dd data-v-6bee1efd><span class="container" data-v-6bee1efd><span class="top" data-v-6bee1efd></span><span class="middle" data-v-6bee1efd></span><span class="bottom" data-v-6bee1efd></span></span></button></div></div></div></div><div class="divider" data-v-9fd4d1dd><div class="divider-line" data-v-9fd4d1dd></div></div></div><!----></header><div class="VPLocalNav has-sidebar empty" data-v-d8b57b2d data-v-2488c25a><div class="container" data-v-2488c25a><button class="menu" aria-expanded="false" aria-controls="VPSidebarNav" data-v-2488c25a><span class="vpi-align-left menu-icon" data-v-2488c25a></span><span class="menu-text" data-v-2488c25a>Menu</span></button><div class="VPLocalNavOutlineDropdown" style="--vp-vh:0px;" data-v-2488c25a data-v-6b867909><button data-v-6b867909>Return to top</button><!----></div></div></div><aside class="VPSidebar" data-v-d8b57b2d data-v-42c4c606><div class="curtain" data-v-42c4c606></div><nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1" data-v-42c4c606><span class="visually-hidden" id="sidebar-aria-label" data-v-42c4c606> Sidebar Navigation </span><!--[--><!--]--><!--[--><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Overview</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/getting-started.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Getting Started</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/overview.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Concepts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/features.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Features</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/dir-structure.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Directory Structure</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/dev-environment.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Dev Environment</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/tutorial.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Tutorial</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/doc-conventions.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Documentation Conventions</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible has-active" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Front-End</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/routes.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Routes</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/pages.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Pages</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/layouts.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Layouts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/forms.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Forms</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/form-constraints.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Form Constraints</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/handlers.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Handlers and Actions</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/components.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Components</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/flash-and-session.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Flash and Session</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/space-time-continuum.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Space/Time Continuum</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/javascript.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>JavaScript</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/css.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>CSS</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/assets.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Assets</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/brut-js.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>BrutJS</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Back-End</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/database-schema.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Database Schema</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/database-access.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Database Access</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/seed-data.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Seed Data</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/jobs.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Jobs</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/business-logic.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Business Logic</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Framework</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/configuration.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Configuration</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/keyword-injection.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Keyword Injection</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/i18n.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>I18n</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/cli.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>CLI / Tasks</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/deployment.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Deployment</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Testing</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/unit-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Unit Tests</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/end-to-end-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>End-to-End Tests</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/custom-element-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Testing Custom Elements</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible collapsed" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Advanced Topics</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/hooks.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Route Hooks</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/middleware.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Middleware</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/instrumentation.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Instrumentation</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/security.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Security</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/lsp.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>LSP Support</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible collapsed" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Recipes</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/authentication.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Authentication</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/alternate-layouts.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Alternate Layouts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/blank-layouts.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Blank Layouts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/custom-flash.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Custom Flash Class</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/indexed-forms.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Indexed Form Elements</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/text-field-component.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Text Field Component</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Meta</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/why.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Why?!</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/ai.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>AI Declaration</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><!--]--><!--[--><!--]--></nav></aside><div class="VPContent has-sidebar" id="VPContent" data-v-d8b57b2d data-v-9a6c75ad><div class="VPDoc has-sidebar has-aside" data-v-9a6c75ad data-v-e6f2a212><!--[--><!--]--><div class="container" data-v-e6f2a212><div class="aside" data-v-e6f2a212><div class="aside-curtain" data-v-e6f2a212></div><div class="aside-container" data-v-e6f2a212><div class="aside-content" data-v-e6f2a212><div class="VPDocAside" data-v-e6f2a212 data-v-cb998dce><!--[--><!--]--><!--[--><!--]--><nav aria-labelledby="doc-outline-aria-label" class="VPDocAsideOutline" data-v-cb998dce data-v-f610f197><div class="content" data-v-f610f197><div class="outline-marker" data-v-f610f197></div><div aria-level="2" class="outline-title" id="doc-outline-aria-label" role="heading" data-v-f610f197>On this page</div><ul class="VPDocOutlineItem root" data-v-f610f197 data-v-53c99d69><!--[--><!--]--></ul></div></nav><!--[--><!--]--><div class="spacer" data-v-cb998dce></div><!--[--><!--]--><!----><!--[--><!--]--><!--[--><!--]--></div></div></div></div><div class="content" data-v-e6f2a212><div class="content-container" data-v-e6f2a212><!--[--><!--]--><main class="main" data-v-e6f2a212><div style="position:relative;" class="vp-doc _handlers" data-v-e6f2a212><div><h1 id="handlers-actions" tabindex="-1">Handlers &amp; Actions <a class="header-anchor" href="#handlers-actions" aria-label="Permalink to &quot;Handlers &amp; Actions&quot;">​</a></h1><p>Handlers process form submissions, and <em>actions</em> work similarly to process any arbitrary HTTP request.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>Where a <a href="/pages.html">page</a> renders a web page in HTML, a <em>handler</em> responds to all other HTTP requests. To respond to such HTTP requests, you&#39;d first create a <a href="/routes.html">route</a>, using <code>form</code>, <code>action</code>, or <code>path</code>.</p><h3 id="handler-structure" tabindex="-1">Handler Structure <a class="header-anchor" href="#handler-structure" aria-label="Permalink to &quot;Handler Structure&quot;">​</a></h3><p>A handler&#39;s initializer is subject to <a href="/keyword-injection.html">keyword injection</a>, with the addition of the <code>form:</code> keyword, which, if present, will be an instance of the associated form class, populated with the data in the form submission (not available for <code>path</code> or <code>action</code> routes).</p><p>You must implement <code>handle</code> to process the form. <strong>A handler&#39;s public API, as called by Brut and your tests, is <code>handle!</code></strong>, however you implement <code>handle</code>, which <code>handle!</code> calls.</p><p><code>handle</code>&#39;s return value dictates what will happen next:</p><table tabindex="0"><thead><tr><th>Return Value</th><th>Behavior</th></tr></thead><tbody><tr><td>Instance of a page or component</td><td>That page or component&#39;s HTML is generated and returned. This is not a redirect, but more like <code>render :new</code> in a Rails controller</td></tr><tr><td><a href="/api/Brut/FrontEnd/HttpStatus.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::HttpStatus</code></a></td><td>This HTTP status is returned with no body. Use <code>http_status</code> from <a href="/api/Brut/FrontEnd/HandlingResults.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::HandlingResults</code></a> (included in all handlers) to create an instance from a number</td></tr><tr><td><code>URI</code></td><td>Redirect to the URI. Use <code>redirect_to</code> from <a href="/api/Brut/FrontEnd/HandlingResults.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::HandlingResults</code></a> (included in all handlers) to generate a URI from a page class and parameters</td></tr><tr><td>Two element array with <em>element 0</em> being a <a href="/api/Brut/FrontEnd/HttpStatus.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::HttpStatus</code></a>, and <em>element 1</em> being a page or component instance</td><td>Generates the page or component&#39;s HTML, but sets the given status instead of 200. Useful for Ajax responses where the HTTP status affects client-side behavior</td></tr><tr><td><a href="/api/Brut/FrontEnd/Download.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Download</code></a></td><td>Download a file</td></tr><tr><td><a href="/api/Brut/FrontEnd/GenericResponse.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::GenericResponse</code></a></td><td>wrap any Rack response</td></tr></tbody></table><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>The only way to render something other than HTML is to do so as a <code>GenericResponse</code>, which is basically the low-level Rack API. Brut encourages Ajax responses to be HTML and for you to use the browser&#39;s APIs to interact with that HTML. Brut may make it easier to work with other types of content in the future.</p></div><h3 id="handling-a-form-submission" tabindex="-1">Handling a Form Submission <a class="header-anchor" href="#handling-a-form-submission" aria-label="Permalink to &quot;Handling a Form Submission&quot;">​</a></h3><p>A common pattern when handling a form submisssion is to check for any constraint violations. If there are some, re-generate the HTML for the page containing the form, highlighting the violations. Otherwise, save the data and redirect to another page.</p><p>Here&#39;s how that looks:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
26
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
27
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form</span></span>
26
28
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
27
29
  <span class="line"></span>
28
30
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
31
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # if no client-side violations were submitted</span></span>
29
32
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">@form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">constraint_violations?</span></span>
30
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> authorized_user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">AuthorizedUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">login</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
31
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
32
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> password:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">password</span></span>
33
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
34
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> authorized_user.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">nil?</span></span>
33
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">find</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">name:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
34
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> widget</span></span>
35
35
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server_side_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
36
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
37
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :login_not_found</span></span>
36
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
37
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name_is_taken</span></span>
38
38
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
39
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
40
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">authorized_user</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> authorized_user</span></span>
41
39
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
42
40
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
43
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">constraint_violations?</span></span>
44
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form)</span></span>
45
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
46
- <span class="line highlighted"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> redirect_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DashboardPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
47
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
48
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
49
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>The only way to render something other than HTML is to do so as a <code>GenericResponse</code>, which is basically the low-level Rack API. Brut encourages Ajax responses to be HTML and for you to use the browser&#39;s APIs to interact with that HTML. Brut may make it easier to work with other types of content in the future.</p></div><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Testing handlers requires calling their <em>public API</em>, which is <code>handle!</code>. This is not the method you implement (which is the non-bang <code>handle</code>). The reason is that <code>handle!</code> manages the logic around calling <code>before_handle</code>, which allows your tests to always call <code>handle!</code> and know they are testing how the handler would be used in produciton.</p><p>Each handler spec includes <a href="/api/Brut/SpecSupport/HandlerSupport.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::HandlerSupport</code></a>, which allows you to create production-like flash, clock, and session objects. To assert the results of calling <code>handle!</code>, there are several RSpec matchers you can use to make your tests easier to write.</p><ul><li><code>have_redirected_to</code> will check that the handler redirected to a give URI.</li><li><code>have_rendered</code> will check that the handler rendered a specific page</li><li><code>have_returned_http_status</code> will check that the handler returned an HTTP status</li><li><code>have_constraint_violation</code> will check if a form had a particular constraint violation set on it</li></ul><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;spec_helper&quot;</span></span>
50
41
  <span class="line"></span>
51
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RSpec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">describe</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
52
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> describe </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;#handle!&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
53
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> context </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;when login is not valid&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
54
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;re-renders LoginPage&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
55
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">LoginForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">params:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
56
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;nonexistent@example.com&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
57
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> password:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;not a password&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
58
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> })</span></span>
59
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = described_class.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
60
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
61
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> empty_session </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># empty_session provided by HandlerSupport</span></span>
62
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
63
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(result).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_rendered</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">LoginPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
64
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(form).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :login_not_found</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
65
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
42
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">constraint_violations?</span></span>
43
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form)</span></span>
44
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
45
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">name:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
46
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> quantity:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">quantity</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
47
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> description:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">description</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
48
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> redirect_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">WidgetsPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
66
49
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
67
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> context </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;when login is valid&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
68
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;forward to the DashboardPage&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
69
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># Assume this is set up via FactoryBot</span></span>
70
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;pat@example.com&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
71
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> password:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;1q2w3e4r5t6y7u8i9o&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
50
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
51
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Unlike a Rails controller, the return value of <code>handle</code> controls the behavior. As we saw in <a href="/form-constraints.html">Form Constraints</a>, the HTML generated by <code>NewWidgetPage</code> will show constraint violations, so by returning <code>NewWidgetPage.new(form: @form)</code>, the page has the same form instance, including all the constraint violations, and the visitor will see the problems.</p><h3 id="handling-other-requests" tabindex="-1">Handling Other Requests <a class="header-anchor" href="#handling-other-requests" aria-label="Permalink to &quot;Handling Other Requests&quot;">​</a></h3><p>Non-form submissions work similarly, however there is no form available. If the request could potentially involve a visitor-initiated error, you can use the <a href="/flash-and-session.html">flash</a> to communicate back. The flash is available for injection into the initializer and, assuming your page uses it to show messages, can allow for communication when something is wrong:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> App</span></span>
52
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
72
53
  <span class="line"></span>
73
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">LoginForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">params:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> {</span></span>
74
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;pat@example.com&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
75
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> password:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;1q2w3e4r5t6y7u8i9o&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
76
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> })</span></span>
77
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> session</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = empty_session</span></span>
78
- <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> result</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = described_class.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
79
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
80
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
81
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
82
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(result).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_redirected_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DashboardPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
83
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">authorized_user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">not_to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> eq</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
84
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Session will be explained later</span></span>
85
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
54
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> routes </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
55
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> action </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/delete_widget/:widget_id&quot;</span></span>
56
+ <span class="line"></span>
57
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
58
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
59
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span>
60
+ <span class="line"></span>
61
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DeleteWidgetByIdHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
62
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">widget_id:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">flash:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
63
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @widget_id </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> widget_id</span></span>
64
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @flash </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> flash</span></span>
65
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
66
+ <span class="line"></span>
67
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
68
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">find!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">id:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @widget_id)</span></span>
69
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> widget.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">can_delete?</span></span>
70
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> widget.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">delete</span></span>
71
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @flash.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">notice</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :widget_deleted</span></span>
72
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> redirect_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">WidgetsPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
73
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
74
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @flash.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">alert</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :widget_cannot_be_deleted</span></span>
75
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> WidgetsPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span></span>
86
76
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
87
77
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
88
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>You should avoid having business logic in your handlers. Since handlers bridge the gap between HTTP and your app, their API is naturally simplistic and String-based. The handler should defer to business logic (which can be done by either passing the form object directly, or extracting its data and passing that). Based on the response, the handler will then decide what HTTP response is approriate.</p><p>This means that your handlers will be relatively simple and their tests will as well. It does mean that their tests may require the use of mocks or stubs, but that&#39;s fine. Mocks and stubs exist for a reason.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 5, 2025</em></p><p>None at this time.</p></div></div></main><footer class="VPDocFooter" data-v-e6f2a212 data-v-1bcd8184><!--[--><!--]--><!----><nav class="prev-next" aria-labelledby="doc-footer-aria-label" data-v-1bcd8184><span class="visually-hidden" id="doc-footer-aria-label" data-v-1bcd8184>Pager</span><div class="pager" data-v-1bcd8184><a class="VPLink link pager-link prev" href="/forms.html" data-v-1bcd8184><!--[--><span class="desc" data-v-1bcd8184>Previous page</span><span class="title" data-v-1bcd8184>Forms</span><!--]--></a></div><div class="pager" data-v-1bcd8184><a class="VPLink link pager-link next" href="/components.html" data-v-1bcd8184><!--[--><span class="desc" data-v-1bcd8184>Next page</span><span class="title" data-v-1bcd8184>Components</span><!--]--></a></div></nav></footer><!--[--><!--]--></div></div></div><!--[--><!--]--></div></div><!----><!--[--><!--]--></div></div>
89
- <script>window.__VP_HASH_MAP__=JSON.parse("{\"ai.md\":\"_6HCDL6d\",\"assets.md\":\"D3wunzLx\",\"brut-js.md\":\"o2DAO2s2\",\"business-logic.md\":\"BY4hGy0m\",\"cli.md\":\"RmeA2b0i\",\"components.md\":\"CRUMdRoN\",\"configuration.md\":\"BGHl8oRC\",\"css.md\":\"DJgj2clw\",\"custom-element-tests.md\":\"BrYJQEl3\",\"database-access.md\":\"C7l-Vuvb\",\"database-schema.md\":\"BUjR0VS1\",\"deployment.md\":\"Dbka4OTr\",\"dev-environment.md\":\"GZv6xvi9\",\"doc-conventions.md\":\"-kN3Xo5C\",\"end-to-end-tests.md\":\"yfQHC0b5\",\"flash-and-session.md\":\"BXY8RvT0\",\"forms.md\":\"B-koVgyw\",\"getting-started.md\":\"Ciz82L0m\",\"handlers.md\":\"089DVD3v\",\"hooks.md\":\"C4-moMny\",\"i18n.md\":\"Do9i1qWl\",\"index.md\":\"B28EwVpq\",\"instrumentation.md\":\"a9Pjps4P\",\"javascript.md\":\"GWbhRS51\",\"jobs.md\":\"S-2amAYp\",\"keyword-injection.md\":\"Dt2tKREs\",\"layouts.md\":\"cPnh3NId\",\"lsp.md\":\"Bsu-f6VU\",\"markdown-examples.md\":\"CCFEQO44\",\"middleware.md\":\"Czz_UlJN\",\"not-released.md\":\"BBy28McC\",\"overview.md\":\"C5wlBcR5\",\"pages.md\":\"BE3kfOc5\",\"recipes_authentication.md\":\"CAsXf7hk\",\"routes.md\":\"BMM7peut\",\"security.md\":\"C668yXCi\",\"seed-data.md\":\"BvFZlqIk\",\"space-time-continuum.md\":\"KPUIKysQ\",\"tutorial.md\":\"BnoGjrdK\",\"unit-tests.md\":\"DUGrnLj5\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"Brut RB\",\"description\":\"Documentation for the Brut.RB web framework.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"search\":{\"provider\":\"local\"},\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Overview\",\"link\":\"/overview\"},{\"text\":\"Brut API\",\"link\":\"/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutCSS\",\"link\":\"/brut-css/index.html\",\"target\":\"_self\"}],\"outline\":[2,3],\"sidebar\":[{\"text\":\"Overview\",\"collapsed\":false,\"items\":[{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Concepts\",\"link\":\"/overview\"},{\"text\":\"Documentation Conventions\",\"link\":\"/doc-conventions\"},{\"text\":\"Tutorial\",\"link\":\"/tutorial\"},{\"text\":\"Dev Environment\",\"link\":\"/dev-environment\"},{\"text\":\"AI Declaration\",\"link\":\"/ai\"}]},{\"text\":\"Front-End\",\"collapsed\":false,\"items\":[{\"text\":\"Routes\",\"link\":\"/routes\"},{\"text\":\"Pages\",\"link\":\"/pages\"},{\"text\":\"Layouts\",\"link\":\"/layouts\"},{\"text\":\"Forms\",\"link\":\"/forms\"},{\"text\":\"Handlers and Actions\",\"link\":\"/handlers\"},{\"text\":\"Components\",\"link\":\"/components\"},{\"text\":\"Flash and Session\",\"link\":\"/flash-and-session\"},{\"text\":\"Space/Time Continuum\",\"link\":\"/space-time-continuum\"},{\"text\":\"JavaScript\",\"link\":\"/javascript\"},{\"text\":\"CSS\",\"link\":\"/css\"},{\"text\":\"Assets\",\"link\":\"/assets\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js\"}]},{\"text\":\"Back-End\",\"collapsed\":false,\"items\":[{\"text\":\"Database Schema\",\"link\":\"/database-schema\"},{\"text\":\"Database Access\",\"link\":\"/database-access\"},{\"text\":\"Seed Data\",\"link\":\"/seed-data\"},{\"text\":\"Jobs\",\"link\":\"/jobs\"},{\"text\":\"Business Logic\",\"link\":\"/business-logic\"}]},{\"text\":\"Framework\",\"collapsed\":false,\"items\":[{\"text\":\"Configuration\",\"link\":\"/configuration\"},{\"text\":\"Keyword Injection\",\"link\":\"/keyword-injection\"},{\"text\":\"I18n\",\"link\":\"/i18n\"},{\"text\":\"CLI / Tasks\",\"link\":\"/cli\"},{\"text\":\"Deployment\",\"link\":\"/deployment\"}]},{\"text\":\"Testing\",\"collapsed\":false,\"items\":[{\"text\":\"Unit Tests\",\"link\":\"/unit-tests\"},{\"text\":\"End-to-End Tests\",\"link\":\"/end-to-end-tests\"},{\"text\":\"Testing Custom Elements\",\"link\":\"/custom-element-tests\"}]},{\"text\":\"Advanced Topics\",\"collapsed\":true,\"items\":[{\"text\":\"Route Hooks\",\"link\":\"/hooks\"},{\"text\":\"Middleware\",\"link\":\"/middleware\"},{\"text\":\"Instrumentation\",\"link\":\"/instrumentation\"},{\"text\":\"Security\",\"link\":\"/security\"},{\"text\":\"LSP Support\",\"link\":\"/lsp\"}]},{\"text\":\"Recipes\",\"collapsed\":true,\"items\":[{\"text\":\"Authentication\",\"link\":\"/recipes/authentication\"},{\"text\":\"Form Validations\",\"link\":\"/recipes/form-validations\"},{\"text\":\"Database Migrations\",\"link\":\"/recipes/database-migrations\"},{\"text\":\"Ajax Form Submission\",\"link\":\"/recipes/ajax-form\"},{\"text\":\"Custom Telemetry\",\"link\":\"/recipes/telemetry\"},{\"text\":\"CLI App/Task\",\"link\":\"/recipes/cli-app\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/thirdtank/brut\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
78
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><h3 id="hooks" tabindex="-1">Hooks <a class="header-anchor" href="#hooks" aria-label="Permalink to &quot;Hooks&quot;">​</a></h3><p>A handler&#39;s public API is <code>handle!</code>, because it first calls <code>before_handle</code>. This operates as a before hook, and has the same return values as <code>handle</code>, with the exception of also recognizing <code>nil</code>, which indicates processing should proceed to <code>handle</code>.</p><p>Generally, you don&#39;t need to implement hooksd on a per-handler basis, but may find it useful in a shared super class to implement cross-cutting behavior.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>See <a href="/unit-tests.html">Unit Testing</a> for some basic assumptions and configuration available for all Brut unit tests.</p><p>Handler tests should be straightforward: you create your handler, call <code>handle!</code> (remember, <code>handle!</code> is the public API, and will call your hooks, which you want in a test), then examine the result returned and any ancillary behavior, such as updated database records.</p><p>Some matchers are available to make assertions about <code>handle!</code>&#39;s return value:</p><ul><li><code>have_redirected_to</code> will check that the handler redirected to a give URI. See <a href="/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveRedirectedTo</code></a>.</li><li><code>have_generated</code> will check that the handler generated a specific page or component&#39;s HTML. See <code>&lt;D-f&gt;Brut::SpecSupport::Matchers::HaveGenerated</code>.</li><li><code>have_returned_http_status</code> will check that the handler returned an HTTP status. See <a href="/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveReturnedHttpStatus</code></a>.</li><li><code>have_constraint_violation</code> will check if a form had a particular constraint violation set on it. See <a href="/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveConstraintViolation</code></a>.</li></ul><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><h3 id="you-don-t-always-need-resourceful-or-restful-routes" tabindex="-1">You Don&#39;t Always Need Resourceful or RESTful Routes <a class="header-anchor" href="#you-don-t-always-need-resourceful-or-restful-routes" aria-label="Permalink to &quot;You Don&#39;t Always Need Resourceful or RESTful Routes&quot;">​</a></h3><p>For any code where the browser is performing a submission, use either <code>form</code> or <code>action</code> to declare your route. These will both use an HTTP <code>POST</code> to your server and handler. In the example above, we had a <code>POST</code> to <code>/delete_widget/:widget_id</code>, and not, say, a <code>DELETE</code> to it.</p><p>The main reason is that a browser can only submit to a server using <code>GET</code> or <code>POST</code>, so there&#39;s little value in &quot;tunneling&quot; another verb of POST. It doesn&#39;t really matter. And, even though you may use Ajax to submit such data, having it degrade to normal browser-based HTTP is a good practice.</p><p>For API-like calls where a browser will never directly interact with the route, and it would only be via a server-to-server call, RESTful routes makes sense. But they don&#39;t need to be the default.</p><h3 id="avoid-business-logic-in-handlers" tabindex="-1">Avoid Business Logic in Handlers <a class="header-anchor" href="#avoid-business-logic-in-handlers" aria-label="Permalink to &quot;Avoid Business Logic in Handlers&quot;">​</a></h3><p>Since handlers bridge the gap between HTTP and your app, their API is naturally simplistic and String-based. The handler should defer to business logic (which can be done by either passing the form object directly, or extracting its data and passing that). Based on the response, the handler will then decide what HTTP response is approriate.</p><p>This means that your handlers will be relatively simple and their tests will as well. It does mean that their tests may require the use of mocks or stubs, but that&#39;s fine. Mocks and stubs exist for a reason.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 5, 2025</em></p><p>None at this time.</p></div></div></main><footer class="VPDocFooter" data-v-e6f2a212 data-v-1bcd8184><!--[--><!--]--><!----><nav class="prev-next" aria-labelledby="doc-footer-aria-label" data-v-1bcd8184><span class="visually-hidden" id="doc-footer-aria-label" data-v-1bcd8184>Pager</span><div class="pager" data-v-1bcd8184><a class="VPLink link pager-link prev" href="/form-constraints.html" data-v-1bcd8184><!--[--><span class="desc" data-v-1bcd8184>Previous page</span><span class="title" data-v-1bcd8184>Form Constraints</span><!--]--></a></div><div class="pager" data-v-1bcd8184><a class="VPLink link pager-link next" href="/components.html" data-v-1bcd8184><!--[--><span class="desc" data-v-1bcd8184>Next page</span><span class="title" data-v-1bcd8184>Components</span><!--]--></a></div></nav></footer><!--[--><!--]--></div></div></div><!--[--><!--]--></div></div><!----><!--[--><!--]--></div></div>
79
+ <script>window.__VP_HASH_MAP__=JSON.parse("{\"ai.md\":\"Cy9GWnER\",\"assets.md\":\"7C3HWkga\",\"brut-js.md\":\"B4GYxQVw\",\"business-logic.md\":\"BY4hGy0m\",\"cli.md\":\"CjsktgFz\",\"components.md\":\"DatoNgFo\",\"configuration.md\":\"DeyhpqEx\",\"css.md\":\"CltvJqAa\",\"custom-element-tests.md\":\"B_rbta32\",\"database-access.md\":\"gnluu54N\",\"database-schema.md\":\"CSYk6E6v\",\"deployment.md\":\"BLseERGV\",\"dev-environment.md\":\"BroAOLhF\",\"dir-structure.md\":\"CWir1pic\",\"doc-conventions.md\":\"BzmSrTEW\",\"end-to-end-tests.md\":\"DzqRpZ43\",\"features.md\":\"DPFXsy0z\",\"flash-and-session.md\":\"nPvUpnUx\",\"form-constraints.md\":\"x5tNpTTI\",\"forms.md\":\"C2Dizvzq\",\"getting-started.md\":\"C93e0odB\",\"handlers.md\":\"Chyri6KA\",\"hooks.md\":\"Jmb5VOLA\",\"i18n.md\":\"xQhiGo1G\",\"index.md\":\"CAMqGBJE\",\"instrumentation.md\":\"BgcaGVYH\",\"javascript.md\":\"DzrMxUmI\",\"jobs.md\":\"S-2amAYp\",\"keyword-injection.md\":\"95Zgh2eN\",\"layouts.md\":\"CJGDFY-m\",\"lsp.md\":\"Dn1rIiW0\",\"markdown-examples.md\":\"CCFEQO44\",\"middleware.md\":\"Czz_UlJN\",\"not-released.md\":\"BBy28McC\",\"overview.md\":\"Bdq4qt3L\",\"pages.md\":\"B7Hc-i6H\",\"recipes_alternate-layouts.md\":\"BwEytl59\",\"recipes_authentication.md\":\"Dzvi_g69\",\"recipes_blank-layouts.md\":\"fyAUJyJR\",\"recipes_custom-flash.md\":\"CrQbI5eH\",\"recipes_indexed-forms.md\":\"CstYyOSo\",\"recipes_text-field-component.md\":\"H4wLAK0Z\",\"routes.md\":\"B8kfUPHU\",\"security.md\":\"C0G_AZR-\",\"seed-data.md\":\"BvFZlqIk\",\"space-time-continuum.md\":\"xl44xDos\",\"tutorial.md\":\"a4a0eVOy\",\"unit-tests.md\":\"DUGrnLj5\",\"why.md\":\"C-hk5xgJ\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"Brut RB\",\"description\":\"Documentation for the Brut.RB web framework.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"search\":{\"provider\":\"local\"},\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Overview\",\"link\":\"/overview\"},{\"text\":\"Brut API\",\"link\":\"/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutCSS\",\"link\":\"/brut-css/index.html\",\"target\":\"_self\"}],\"outline\":[2,3],\"sidebar\":[{\"text\":\"Overview\",\"collapsed\":false,\"items\":[{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Concepts\",\"link\":\"/overview\"},{\"text\":\"Features\",\"link\":\"/features\"},{\"text\":\"Directory Structure\",\"link\":\"/dir-structure\"},{\"text\":\"Dev Environment\",\"link\":\"/dev-environment\"},{\"text\":\"Tutorial\",\"link\":\"/tutorial\"},{\"text\":\"Documentation Conventions\",\"link\":\"/doc-conventions\"}]},{\"text\":\"Front-End\",\"collapsed\":false,\"items\":[{\"text\":\"Routes\",\"link\":\"/routes\"},{\"text\":\"Pages\",\"link\":\"/pages\"},{\"text\":\"Layouts\",\"link\":\"/layouts\"},{\"text\":\"Forms\",\"link\":\"/forms\"},{\"text\":\"Form Constraints\",\"link\":\"/form-constraints\"},{\"text\":\"Handlers and Actions\",\"link\":\"/handlers\"},{\"text\":\"Components\",\"link\":\"/components\"},{\"text\":\"Flash and Session\",\"link\":\"/flash-and-session\"},{\"text\":\"Space/Time Continuum\",\"link\":\"/space-time-continuum\"},{\"text\":\"JavaScript\",\"link\":\"/javascript\"},{\"text\":\"CSS\",\"link\":\"/css\"},{\"text\":\"Assets\",\"link\":\"/assets\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js\"}]},{\"text\":\"Back-End\",\"collapsed\":false,\"items\":[{\"text\":\"Database Schema\",\"link\":\"/database-schema\"},{\"text\":\"Database Access\",\"link\":\"/database-access\"},{\"text\":\"Seed Data\",\"link\":\"/seed-data\"},{\"text\":\"Jobs\",\"link\":\"/jobs\"},{\"text\":\"Business Logic\",\"link\":\"/business-logic\"}]},{\"text\":\"Framework\",\"collapsed\":false,\"items\":[{\"text\":\"Configuration\",\"link\":\"/configuration\"},{\"text\":\"Keyword Injection\",\"link\":\"/keyword-injection\"},{\"text\":\"I18n\",\"link\":\"/i18n\"},{\"text\":\"CLI / Tasks\",\"link\":\"/cli\"},{\"text\":\"Deployment\",\"link\":\"/deployment\"}]},{\"text\":\"Testing\",\"collapsed\":false,\"items\":[{\"text\":\"Unit Tests\",\"link\":\"/unit-tests\"},{\"text\":\"End-to-End Tests\",\"link\":\"/end-to-end-tests\"},{\"text\":\"Testing Custom Elements\",\"link\":\"/custom-element-tests\"}]},{\"text\":\"Advanced Topics\",\"collapsed\":true,\"items\":[{\"text\":\"Route Hooks\",\"link\":\"/hooks\"},{\"text\":\"Middleware\",\"link\":\"/middleware\"},{\"text\":\"Instrumentation\",\"link\":\"/instrumentation\"},{\"text\":\"Security\",\"link\":\"/security\"},{\"text\":\"LSP Support\",\"link\":\"/lsp\"}]},{\"text\":\"Recipes\",\"collapsed\":true,\"items\":[{\"text\":\"Authentication\",\"link\":\"/recipes/authentication\"},{\"text\":\"Alternate Layouts\",\"link\":\"/recipes/alternate-layouts\"},{\"text\":\"Blank Layouts\",\"link\":\"/recipes/blank-layouts\"},{\"text\":\"Custom Flash Class\",\"link\":\"/recipes/custom-flash\"},{\"text\":\"Indexed Form Elements\",\"link\":\"/recipes/indexed-forms\"},{\"text\":\"Text Field Component\",\"link\":\"/recipes/text-field-component\"}]},{\"text\":\"Meta\",\"collapsed\":false,\"items\":[{\"text\":\"Why?!\",\"link\":\"/why\"},{\"text\":\"AI Declaration\",\"link\":\"/ai\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/thirdtank/brut\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
90
80
 
91
81
  </body>
92
82
  </html>
data/docs/hashmap.json CHANGED
@@ -1 +1 @@
1
- {"ai.md":"_6HCDL6d","assets.md":"D3wunzLx","brut-js.md":"o2DAO2s2","business-logic.md":"BY4hGy0m","cli.md":"RmeA2b0i","components.md":"CRUMdRoN","configuration.md":"BGHl8oRC","css.md":"DJgj2clw","custom-element-tests.md":"BrYJQEl3","database-access.md":"C7l-Vuvb","database-schema.md":"BUjR0VS1","deployment.md":"Dbka4OTr","dev-environment.md":"GZv6xvi9","doc-conventions.md":"-kN3Xo5C","end-to-end-tests.md":"yfQHC0b5","flash-and-session.md":"BXY8RvT0","forms.md":"B-koVgyw","getting-started.md":"Ciz82L0m","handlers.md":"089DVD3v","hooks.md":"C4-moMny","i18n.md":"Do9i1qWl","index.md":"B28EwVpq","instrumentation.md":"a9Pjps4P","javascript.md":"GWbhRS51","jobs.md":"S-2amAYp","keyword-injection.md":"Dt2tKREs","layouts.md":"cPnh3NId","lsp.md":"Bsu-f6VU","markdown-examples.md":"CCFEQO44","middleware.md":"Czz_UlJN","not-released.md":"BBy28McC","overview.md":"C5wlBcR5","pages.md":"BE3kfOc5","recipes_authentication.md":"CAsXf7hk","routes.md":"BMM7peut","security.md":"C668yXCi","seed-data.md":"BvFZlqIk","space-time-continuum.md":"KPUIKysQ","tutorial.md":"BnoGjrdK","unit-tests.md":"DUGrnLj5"}
1
+ {"ai.md":"Cy9GWnER","assets.md":"7C3HWkga","brut-js.md":"B4GYxQVw","business-logic.md":"BY4hGy0m","cli.md":"CjsktgFz","components.md":"DatoNgFo","configuration.md":"DeyhpqEx","css.md":"CltvJqAa","custom-element-tests.md":"B_rbta32","database-access.md":"gnluu54N","database-schema.md":"CSYk6E6v","deployment.md":"BLseERGV","dev-environment.md":"BroAOLhF","dir-structure.md":"CWir1pic","doc-conventions.md":"BzmSrTEW","end-to-end-tests.md":"DzqRpZ43","features.md":"DPFXsy0z","flash-and-session.md":"nPvUpnUx","form-constraints.md":"x5tNpTTI","forms.md":"C2Dizvzq","getting-started.md":"C93e0odB","handlers.md":"Chyri6KA","hooks.md":"Jmb5VOLA","i18n.md":"xQhiGo1G","index.md":"CAMqGBJE","instrumentation.md":"BgcaGVYH","javascript.md":"DzrMxUmI","jobs.md":"S-2amAYp","keyword-injection.md":"95Zgh2eN","layouts.md":"CJGDFY-m","lsp.md":"Dn1rIiW0","markdown-examples.md":"CCFEQO44","middleware.md":"Czz_UlJN","not-released.md":"BBy28McC","overview.md":"Bdq4qt3L","pages.md":"B7Hc-i6H","recipes_alternate-layouts.md":"BwEytl59","recipes_authentication.md":"Dzvi_g69","recipes_blank-layouts.md":"fyAUJyJR","recipes_custom-flash.md":"CrQbI5eH","recipes_indexed-forms.md":"CstYyOSo","recipes_text-field-component.md":"H4wLAK0Z","routes.md":"B8kfUPHU","security.md":"C0G_AZR-","seed-data.md":"BvFZlqIk","space-time-continuum.md":"xl44xDos","tutorial.md":"a4a0eVOy","unit-tests.md":"DUGrnLj5","why.md":"C-hk5xgJ"}