brut 0.0.29 → 0.2.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 (517) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +23 -2
  4. data/assets/LogoStop.pxd +0 -0
  5. data/assets/MetroLogo.graffle +0 -0
  6. data/assets/SocialImage.png +0 -0
  7. data/assets/SocialImage.pxd +0 -0
  8. data/brutrb.com/.vitepress/config.mjs +48 -9
  9. data/brutrb.com/.vitepress/theme/style.css +14 -35
  10. data/brutrb.com/adrs.md +15 -0
  11. data/brutrb.com/ai.md +10 -15
  12. data/brutrb.com/assets.md +2 -9
  13. data/brutrb.com/brut-js.md +12 -2
  14. data/brutrb.com/cli.md +9 -13
  15. data/brutrb.com/components.md +118 -96
  16. data/brutrb.com/configuration.md +3 -4
  17. data/brutrb.com/css.md +2 -2
  18. data/brutrb.com/custom-element-tests.md +3 -4
  19. data/brutrb.com/database-access.md +1 -1
  20. data/brutrb.com/database-schema.md +29 -41
  21. data/brutrb.com/dev-environment.md +13 -8
  22. data/brutrb.com/dir-structure.md +120 -0
  23. data/brutrb.com/doc-conventions.md +17 -15
  24. data/brutrb.com/dx +1 -0
  25. data/brutrb.com/end-to-end-tests.md +12 -10
  26. data/brutrb.com/features.md +373 -0
  27. data/brutrb.com/flash-and-session.md +115 -131
  28. data/brutrb.com/form-constraints.md +266 -0
  29. data/brutrb.com/forms.md +140 -765
  30. data/brutrb.com/getting-started.md +10 -11
  31. data/brutrb.com/handlers.md +119 -95
  32. data/brutrb.com/hooks.md +18 -20
  33. data/brutrb.com/i18n.md +6 -4
  34. data/brutrb.com/images/DevEnvironment.graffle +0 -0
  35. data/brutrb.com/images/DevEnvironment.png +0 -0
  36. data/brutrb.com/images/LogoStop.png +0 -0
  37. data/brutrb.com/index.md +0 -3
  38. data/brutrb.com/instrumentation.md +7 -10
  39. data/brutrb.com/javascript.md +14 -14
  40. data/brutrb.com/keyword-injection.md +72 -114
  41. data/brutrb.com/layouts.md +20 -52
  42. data/brutrb.com/lsp.md +1 -1
  43. data/brutrb.com/overview.md +30 -372
  44. data/brutrb.com/pages.md +119 -207
  45. data/brutrb.com/public/SocialImage.png +0 -0
  46. data/brutrb.com/public/favicon.ico +0 -0
  47. data/brutrb.com/recipes/alternate-layouts.md +32 -0
  48. data/brutrb.com/recipes/authentication.md +315 -6
  49. data/brutrb.com/recipes/blank-layouts.md +22 -0
  50. data/brutrb.com/recipes/custom-flash.md +51 -0
  51. data/brutrb.com/recipes/indexed-forms.md +149 -0
  52. data/brutrb.com/recipes/text-field-component.md +182 -0
  53. data/brutrb.com/roadmap.md +57 -0
  54. data/brutrb.com/routes.md +56 -82
  55. data/brutrb.com/security.md +0 -3
  56. data/brutrb.com/space-time-continuum.md +8 -12
  57. data/brutrb.com/tutorial.md +5 -1
  58. data/brutrb.com/why.md +19 -0
  59. data/docs/404.html +8 -3
  60. data/docs/SocialImage.png +0 -0
  61. data/docs/adrs.html +29 -0
  62. data/docs/ai.html +11 -6
  63. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  64. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  65. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  66. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  67. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  68. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  69. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  70. data/docs/api/Brut/BackEnd.html +1 -1
  71. data/docs/api/Brut/CLI/App.html +1 -1
  72. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  73. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  74. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  75. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  76. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  77. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  78. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  79. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  80. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  81. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  82. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  83. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  84. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  85. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  86. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  87. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  88. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  89. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  90. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  91. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  92. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +3 -3
  93. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  94. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  95. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  96. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  97. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +2 -2
  98. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  99. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  100. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  101. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  102. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  103. data/docs/api/Brut/CLI/Apps/Test/JS.html +6 -6
  104. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  105. data/docs/api/Brut/CLI/Apps/Test.html +2 -2
  106. data/docs/api/Brut/CLI/Apps.html +1 -1
  107. data/docs/api/Brut/CLI/Command.html +3 -3
  108. data/docs/api/Brut/CLI/Error.html +1 -1
  109. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  110. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  111. data/docs/api/Brut/CLI/Executor.html +1 -1
  112. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  113. data/docs/api/Brut/CLI/Options.html +1 -1
  114. data/docs/api/Brut/CLI/Output.html +1 -1
  115. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  116. data/docs/api/Brut/CLI.html +1 -1
  117. data/docs/api/Brut/FactoryBot.html +1 -1
  118. data/docs/api/Brut/Framework/App.html +1 -1
  119. data/docs/api/Brut/Framework/Config.html +1 -1
  120. data/docs/api/Brut/Framework/Container.html +1 -1
  121. data/docs/api/Brut/Framework/Error.html +1 -1
  122. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  123. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  124. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  125. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  126. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  127. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  128. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  129. data/docs/api/Brut/Framework/Errors.html +1 -1
  130. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  131. data/docs/api/Brut/Framework/MCP.html +1 -1
  132. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  133. data/docs/api/Brut/Framework.html +1 -1
  134. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  135. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  136. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  137. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  138. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  139. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  140. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  141. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  143. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +3 -3
  145. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  146. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  147. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  152. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  153. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  154. data/docs/api/Brut/FrontEnd/Form.html +9 -11
  155. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  156. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
  157. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
  158. data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +135 -20
  162. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +135 -20
  164. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  167. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  168. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  170. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  171. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  172. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  173. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  174. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  175. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  176. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  177. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  178. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  179. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  180. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  181. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  182. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  183. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  184. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  185. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  186. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  187. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  188. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +2 -2
  189. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  190. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  191. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  192. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  193. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  194. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  195. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  196. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  197. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  198. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  199. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  200. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  201. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  202. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  203. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  204. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  205. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  206. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  207. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  208. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  209. data/docs/api/Brut/FrontEnd.html +1 -1
  210. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  211. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  212. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  213. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  214. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  215. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  216. data/docs/api/Brut/I18n.html +1 -1
  217. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  218. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  219. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  220. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  221. data/docs/api/Brut/Instrumentation.html +1 -1
  222. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  223. data/docs/api/Brut/SinatraHelpers.html +1 -1
  224. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  225. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  226. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  227. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  228. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  229. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  230. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  231. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  232. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  233. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  234. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  235. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  236. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  237. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  238. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  239. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  240. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  241. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  242. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  243. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  244. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  245. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  246. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  247. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  248. data/docs/api/Brut/SpecSupport.html +1 -1
  249. data/docs/api/Brut.html +1 -1
  250. data/docs/api/Clock.html +1 -1
  251. data/docs/api/RichString.html +150 -343
  252. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  253. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  254. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  255. data/docs/api/Sequel/Extensions.html +1 -1
  256. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  257. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  258. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  259. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  260. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  261. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  262. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  263. data/docs/api/Sequel/Plugins.html +1 -1
  264. data/docs/api/Sequel.html +1 -1
  265. data/docs/api/_index.html +5 -5
  266. data/docs/api/class_list.html +1 -1
  267. data/docs/api/file.README.html +22 -3
  268. data/docs/api/index.html +22 -3
  269. data/docs/api/method_list.html +290 -306
  270. data/docs/api/top-level-namespace.html +1 -1
  271. data/docs/assets/DevEnvironment.DaFcVfwP.png +0 -0
  272. data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
  273. data/docs/assets/adrs.md.JRxZ5uYE.js +1 -0
  274. data/docs/assets/adrs.md.JRxZ5uYE.lean.js +1 -0
  275. data/docs/assets/{ai.md._6HCDL6d.js → ai.md.Cy9GWnER.js} +1 -1
  276. data/docs/assets/ai.md.Cy9GWnER.lean.js +1 -0
  277. data/docs/assets/{app.BhrfSt68.js → app.Dm3x-DQc.js} +1 -1
  278. data/docs/assets/{assets.md.D3wunzLx.js → assets.md.7C3HWkga.js} +3 -3
  279. data/docs/assets/{assets.md.D3wunzLx.lean.js → assets.md.7C3HWkga.lean.js} +1 -1
  280. data/docs/assets/{brut-js.md.o2DAO2s2.js → brut-js.md.B4GYxQVw.js} +1 -1
  281. data/docs/assets/{brut-js.md.o2DAO2s2.lean.js → brut-js.md.B4GYxQVw.lean.js} +1 -1
  282. data/docs/assets/chunks/@localSearchIndexroot.BqRrkR00.js +1 -0
  283. data/docs/assets/chunks/{VPLocalSearchBox.Dpot_2H4.js → VPLocalSearchBox.DL6bnqee.js} +1 -1
  284. data/docs/assets/chunks/{theme.N2SNVLgU.js → theme.BXdlf6e8.js} +2 -2
  285. data/docs/assets/{cli.md.RmeA2b0i.js → cli.md.CjsktgFz.js} +15 -20
  286. data/docs/assets/components.md.Pg_Lo35G.js +96 -0
  287. data/docs/assets/{components.md.CRUMdRoN.lean.js → components.md.Pg_Lo35G.lean.js} +1 -1
  288. data/docs/assets/{configuration.md.LG-zIBww.js → configuration.md.BfeGnEci.js} +3 -3
  289. data/docs/assets/{css.md.DJgj2clw.js → css.md.CltvJqAa.js} +3 -3
  290. data/docs/assets/{custom-element-tests.md.BrYJQEl3.js → custom-element-tests.md.B_rbta32.js} +3 -3
  291. data/docs/assets/{database-access.md.C7l-Vuvb.js → database-access.md.gnluu54N.js} +1 -1
  292. data/docs/assets/{database-schema.md.BUjR0VS1.js → database-schema.md.CSYk6E6v.js} +6 -6
  293. data/docs/assets/{database-schema.md.BUjR0VS1.lean.js → database-schema.md.CSYk6E6v.lean.js} +1 -1
  294. data/docs/assets/dev-environment.md.Dy6EldaM.js +16 -0
  295. data/docs/assets/dev-environment.md.Dy6EldaM.lean.js +1 -0
  296. data/docs/assets/dir-structure.md.CWir1pic.js +46 -0
  297. data/docs/assets/dir-structure.md.CWir1pic.lean.js +1 -0
  298. data/docs/assets/doc-conventions.md.DOkAuXlt.js +1 -0
  299. data/docs/assets/doc-conventions.md.DOkAuXlt.lean.js +1 -0
  300. data/docs/assets/{end-to-end-tests.md.yfQHC0b5.js → end-to-end-tests.md.DzqRpZ43.js} +5 -3
  301. data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +1 -0
  302. data/docs/assets/features.md.DPFXsy0z.js +154 -0
  303. data/docs/assets/features.md.DPFXsy0z.lean.js +1 -0
  304. data/docs/assets/flash-and-session.md.nPvUpnUx.js +79 -0
  305. data/docs/assets/{flash-and-session.md.BXY8RvT0.lean.js → flash-and-session.md.nPvUpnUx.lean.js} +1 -1
  306. data/docs/assets/form-constraints.md.x5tNpTTI.js +90 -0
  307. data/docs/assets/form-constraints.md.x5tNpTTI.lean.js +1 -0
  308. data/docs/assets/forms.md.BQZlCwvi.js +64 -0
  309. data/docs/assets/forms.md.BQZlCwvi.lean.js +1 -0
  310. data/docs/assets/{getting-started.md.Dj0qtZI2.js → getting-started.md.BcXnNuD6.js} +5 -5
  311. data/docs/assets/{getting-started.md.Dj0qtZI2.lean.js → getting-started.md.BcXnNuD6.lean.js} +1 -1
  312. data/docs/assets/handlers.md.Chyri6KA.js +54 -0
  313. data/docs/assets/handlers.md.Chyri6KA.lean.js +1 -0
  314. data/docs/assets/{hooks.md.C4-moMny.js → hooks.md.Jmb5VOLA.js} +4 -4
  315. data/docs/assets/{hooks.md.C4-moMny.lean.js → hooks.md.Jmb5VOLA.lean.js} +1 -1
  316. data/docs/assets/{i18n.md.Do9i1qWl.js → i18n.md.xQhiGo1G.js} +2 -2
  317. data/docs/assets/{i18n.md.Do9i1qWl.lean.js → i18n.md.xQhiGo1G.lean.js} +1 -1
  318. data/docs/assets/index.md.Bn9e0sRJ.js +1 -0
  319. data/docs/assets/index.md.Bn9e0sRJ.lean.js +1 -0
  320. data/docs/assets/{instrumentation.md.a9Pjps4P.js → instrumentation.md.BgcaGVYH.js} +2 -2
  321. data/docs/assets/{instrumentation.md.a9Pjps4P.lean.js → instrumentation.md.BgcaGVYH.lean.js} +1 -1
  322. data/docs/assets/{javascript.md.GWbhRS51.js → javascript.md.DzrMxUmI.js} +7 -7
  323. data/docs/assets/{javascript.md.GWbhRS51.lean.js → javascript.md.DzrMxUmI.lean.js} +1 -1
  324. data/docs/assets/keyword-injection.md.95Zgh2eN.js +21 -0
  325. data/docs/assets/{keyword-injection.md.Dt2tKREs.lean.js → keyword-injection.md.95Zgh2eN.lean.js} +1 -1
  326. data/docs/assets/{layouts.md.cPnh3NId.js → layouts.md.CJGDFY-m.js} +2 -15
  327. data/docs/assets/layouts.md.CJGDFY-m.lean.js +1 -0
  328. data/docs/assets/{lsp.md.Bsu-f6VU.js → lsp.md.Dn1rIiW0.js} +1 -1
  329. data/docs/assets/{lsp.md.Bsu-f6VU.lean.js → lsp.md.Dn1rIiW0.lean.js} +1 -1
  330. data/docs/assets/overview.md.iMnwLO4x.js +1 -0
  331. data/docs/assets/overview.md.iMnwLO4x.lean.js +1 -0
  332. data/docs/assets/pages.md.B7Hc-i6H.js +45 -0
  333. data/docs/assets/pages.md.B7Hc-i6H.lean.js +1 -0
  334. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.js +22 -0
  335. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +1 -0
  336. data/docs/assets/recipes_authentication.md.Dzvi_g69.js +156 -0
  337. data/docs/assets/recipes_authentication.md.Dzvi_g69.lean.js +1 -0
  338. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +15 -0
  339. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +1 -0
  340. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.js +26 -0
  341. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +1 -0
  342. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.js +74 -0
  343. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +1 -0
  344. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.js +101 -0
  345. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +1 -0
  346. data/docs/assets/roadmap.md.C6PRi0DX.js +1 -0
  347. data/docs/assets/roadmap.md.C6PRi0DX.lean.js +1 -0
  348. data/docs/assets/routes.md.B8kfUPHU.js +21 -0
  349. data/docs/assets/{routes.md.BMM7peut.lean.js → routes.md.B8kfUPHU.lean.js} +1 -1
  350. data/docs/assets/{security.md.C668yXCi.js → security.md.C0G_AZR-.js} +1 -1
  351. data/docs/assets/{security.md.C668yXCi.lean.js → security.md.C0G_AZR-.lean.js} +1 -1
  352. data/docs/assets/space-time-continuum.md.xl44xDos.js +1 -0
  353. data/docs/assets/{space-time-continuum.md.KPUIKysQ.lean.js → space-time-continuum.md.xl44xDos.lean.js} +1 -1
  354. data/docs/assets/{style.B2o1L9eN.css → style.B1z60PPQ.css} +1 -1
  355. data/docs/assets/tutorial.md.BYXj4cOu.js +1 -0
  356. data/docs/assets/tutorial.md.BYXj4cOu.lean.js +1 -0
  357. data/docs/assets/why.md.C-hk5xgJ.js +1 -0
  358. data/docs/assets/why.md.C-hk5xgJ.lean.js +1 -0
  359. data/docs/assets.html +12 -7
  360. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  361. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  362. data/docs/brut-js/api/Autosubmit.html +1 -1
  363. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  364. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  365. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  366. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  367. data/docs/brut-js/api/BufferedLogger.html +1 -1
  368. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  369. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  370. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  371. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  372. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  373. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  374. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  375. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  376. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  377. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  378. data/docs/brut-js/api/Form.html +1 -1
  379. data/docs/brut-js/api/Form.js.html +1 -1
  380. data/docs/brut-js/api/I18nTranslation.html +1 -1
  381. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  382. data/docs/brut-js/api/LocaleDetection.html +1 -1
  383. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  384. data/docs/brut-js/api/Logger.html +1 -1
  385. data/docs/brut-js/api/Logger.js.html +1 -1
  386. data/docs/brut-js/api/Message.html +1 -1
  387. data/docs/brut-js/api/Message.js.html +1 -1
  388. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  389. data/docs/brut-js/api/RichString.html +1 -1
  390. data/docs/brut-js/api/RichString.js.html +1 -1
  391. data/docs/brut-js/api/Tabs.html +1 -1
  392. data/docs/brut-js/api/Tabs.js.html +1 -1
  393. data/docs/brut-js/api/Tracing.html +1 -1
  394. data/docs/brut-js/api/Tracing.js.html +1 -1
  395. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  396. data/docs/brut-js/api/external-Performance.html +1 -1
  397. data/docs/brut-js/api/external-Promise.html +1 -1
  398. data/docs/brut-js/api/external-ValidityState.html +1 -1
  399. data/docs/brut-js/api/external-Window.html +1 -1
  400. data/docs/brut-js/api/external-fetch.html +1 -1
  401. data/docs/brut-js/api/global.html +1 -1
  402. data/docs/brut-js/api/index.html +1 -1
  403. data/docs/brut-js/api/index.js.html +1 -1
  404. data/docs/brut-js/api/module-testing.html +1 -1
  405. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  406. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  407. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  408. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  409. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  410. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  411. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  412. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  413. data/docs/brut-js/api/testing_index.js.html +1 -1
  414. data/docs/brut-js.html +12 -7
  415. data/docs/business-logic.html +10 -5
  416. data/docs/cli.html +26 -26
  417. data/docs/components.html +61 -64
  418. data/docs/configuration.html +13 -8
  419. data/docs/css.html +14 -9
  420. data/docs/custom-element-tests.html +14 -9
  421. data/docs/database-access.html +12 -7
  422. data/docs/database-schema.html +15 -10
  423. data/docs/deployment.html +10 -5
  424. data/docs/dev-environment.html +17 -7
  425. data/docs/dir-structure.html +74 -0
  426. data/docs/doc-conventions.html +11 -6
  427. data/docs/end-to-end-tests.html +15 -8
  428. data/docs/favicon.ico +0 -0
  429. data/docs/features.html +182 -0
  430. data/docs/flash-and-session.html +73 -82
  431. data/docs/form-constraints.html +118 -0
  432. data/docs/forms.html +57 -367
  433. data/docs/getting-started.html +15 -10
  434. data/docs/handlers.html +51 -61
  435. data/docs/hashmap.json +1 -1
  436. data/docs/hooks.html +14 -9
  437. data/docs/i18n.html +12 -7
  438. data/docs/index.html +11 -6
  439. data/docs/instrumentation.html +12 -7
  440. data/docs/javascript.html +17 -12
  441. data/docs/jobs.html +10 -5
  442. data/docs/keyword-injection.html +22 -21
  443. data/docs/layouts.html +12 -20
  444. data/docs/lsp.html +11 -6
  445. data/docs/markdown-examples.html +10 -5
  446. data/docs/middleware.html +10 -5
  447. data/docs/overview.html +11 -138
  448. data/docs/pages.html +49 -121
  449. data/docs/recipes/alternate-layouts.html +50 -0
  450. data/docs/recipes/authentication.html +166 -6
  451. data/docs/recipes/blank-layouts.html +43 -0
  452. data/docs/recipes/custom-flash.html +54 -0
  453. data/docs/recipes/indexed-forms.html +102 -0
  454. data/docs/recipes/text-field-component.html +129 -0
  455. data/docs/roadmap.html +29 -0
  456. data/docs/routes.html +16 -19
  457. data/docs/security.html +11 -6
  458. data/docs/seed-data.html +10 -5
  459. data/docs/space-time-continuum.html +11 -6
  460. data/docs/tutorial.html +11 -6
  461. data/docs/unit-tests.html +10 -5
  462. data/docs/why.html +29 -0
  463. data/lib/brut/cli/apps/test.rb +1 -1
  464. data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +2 -2
  465. data/lib/brut/front_end/form.rb +8 -8
  466. data/lib/brut/front_end/forms/radio_button_group_input.rb +8 -1
  467. data/lib/brut/front_end/forms/select_input.rb +8 -1
  468. data/lib/brut/junk_drawer.rb +48 -9
  469. data/lib/brut/version.rb +1 -1
  470. data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +54 -0
  471. data/specs/brut/front_end/forms/select_input.spec.rb +54 -0
  472. data/specs/brut/junk_drawer.spec.rb +75 -0
  473. metadata +129 -82
  474. data/brutrb.com/images/logo-300.png +0 -0
  475. data/brutrb.com/images/logo.png +0 -0
  476. data/brutrb.com/not-released.md +0 -5
  477. data/brutrb.com/public/images/logo-300.png +0 -0
  478. data/brutrb.com/public/images/logo.png +0 -0
  479. data/docs/assets/LogoStop.X8x-4riz.png +0 -0
  480. data/docs/assets/ai.md._6HCDL6d.lean.js +0 -1
  481. data/docs/assets/chunks/@localSearchIndexroot.CeRAdP1K.js +0 -1
  482. data/docs/assets/components.md.CRUMdRoN.js +0 -104
  483. data/docs/assets/dev-env-overview.Gj7NWM8-.png +0 -0
  484. data/docs/assets/dev-environment.md.GZv6xvi9.js +0 -11
  485. data/docs/assets/dev-environment.md.GZv6xvi9.lean.js +0 -1
  486. data/docs/assets/doc-conventions.md.-kN3Xo5C.js +0 -1
  487. data/docs/assets/doc-conventions.md.-kN3Xo5C.lean.js +0 -1
  488. data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +0 -1
  489. data/docs/assets/flash-and-session.md.BXY8RvT0.js +0 -93
  490. data/docs/assets/forms.md.B-koVgyw.js +0 -379
  491. data/docs/assets/forms.md.B-koVgyw.lean.js +0 -1
  492. data/docs/assets/handlers.md.089DVD3v.js +0 -69
  493. data/docs/assets/handlers.md.089DVD3v.lean.js +0 -1
  494. data/docs/assets/index.md.CuBB-BdM.js +0 -1
  495. data/docs/assets/index.md.CuBB-BdM.lean.js +0 -1
  496. data/docs/assets/keyword-injection.md.Dt2tKREs.js +0 -25
  497. data/docs/assets/layouts.md.cPnh3NId.lean.js +0 -1
  498. data/docs/assets/not-released.md.BBy28McC.js +0 -1
  499. data/docs/assets/not-released.md.BBy28McC.lean.js +0 -1
  500. data/docs/assets/overview.md.DVKRM8zl.js +0 -133
  501. data/docs/assets/overview.md.DVKRM8zl.lean.js +0 -1
  502. data/docs/assets/pages.md.BE3kfOc5.js +0 -122
  503. data/docs/assets/pages.md.BE3kfOc5.lean.js +0 -1
  504. data/docs/assets/recipes_authentication.md.CAsXf7hk.js +0 -1
  505. data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +0 -1
  506. data/docs/assets/routes.md.BMM7peut.js +0 -29
  507. data/docs/assets/space-time-continuum.md.KPUIKysQ.js +0 -1
  508. data/docs/assets/tutorial.md.BnoGjrdK.js +0 -1
  509. data/docs/assets/tutorial.md.BnoGjrdK.lean.js +0 -1
  510. data/docs/images/logo-300.png +0 -0
  511. data/docs/images/logo.png +0 -0
  512. data/docs/not-released.html +0 -24
  513. /data/docs/assets/{cli.md.RmeA2b0i.lean.js → cli.md.CjsktgFz.lean.js} +0 -0
  514. /data/docs/assets/{configuration.md.LG-zIBww.lean.js → configuration.md.BfeGnEci.lean.js} +0 -0
  515. /data/docs/assets/{css.md.DJgj2clw.lean.js → css.md.CltvJqAa.lean.js} +0 -0
  516. /data/docs/assets/{custom-element-tests.md.BrYJQEl3.lean.js → custom-element-tests.md.B_rbta32.lean.js} +0 -0
  517. /data/docs/assets/{database-access.md.C7l-Vuvb.lean.js → database-access.md.gnluu54N.lean.js} +0 -0
@@ -1 +0,0 @@
1
- import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const g=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),e={name:"forms.md"};function l(h,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t("",164)]))}const E=i(e,[["render",l]]);export{g as __pageData,E as default};
@@ -1,69 +0,0 @@
1
- import{_ as i,c as a,o as e,ag as n}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Handlers & Actions","description":"","frontmatter":{},"headers":[],"relativePath":"handlers.md","filePath":"handlers.md"}'),t={name:"handlers.md"};function h(l,s,p,r,k,d){return e(),a("div",null,s[0]||(s[0]=[n(`<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>
2
- <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>
3
- <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>
4
- <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>
5
- <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>
6
- <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>
7
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
8
- <span class="line"></span>
9
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
10
- <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>
11
- <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>
12
- <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>
13
- <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>
14
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
15
- <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>
16
- <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>
17
- <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>
18
- <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>
19
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
20
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
21
- <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>
22
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
23
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
24
- <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>
25
- <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>
26
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
27
- <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>
28
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
29
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
30
- <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>
31
- <span class="line"></span>
32
- <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>
33
- <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>
34
- <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>
35
- <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>
36
- <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>
37
- <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>
38
- <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>
39
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> })</span></span>
40
- <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>
41
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
42
- <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>
43
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
44
- <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>
45
- <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>
46
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
47
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
48
- <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>
49
- <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>
50
- <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>
51
- <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>
52
- <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>
53
- <span class="line"></span>
54
- <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>
55
- <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>
56
- <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>
57
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> })</span></span>
58
- <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>
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;">,</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_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>
64
- <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>
65
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # Session will be explained later</span></span>
66
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
67
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
68
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
69
- <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>`,30)]))}const E=i(t,[["render",h]]);export{c as __pageData,E as default};
@@ -1 +0,0 @@
1
- import{_ as i,c as a,o as e,ag as n}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Handlers & Actions","description":"","frontmatter":{},"headers":[],"relativePath":"handlers.md","filePath":"handlers.md"}'),t={name:"handlers.md"};function h(l,s,p,r,k,d){return e(),a("div",null,s[0]||(s[0]=[n("",30)]))}const E=i(t,[["render",h]]);export{c as __pageData,E as default};
@@ -1 +0,0 @@
1
- import{_ as a,c as o,o as n,j as t}from"./chunks/framework.1L-BeKqY.js";const s="/assets/LogoStop.X8x-4riz.png",g=JSON.parse(`{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Brut RB","text":"Raw Ruby Web Apps","tagline":"Standards-based, No-nonsense, HTML-first, Low Ceremony","ximage":{"src":"/images/LogoTall.png","alt":"A Ruby gemstone embedded into a concrete, brutalist building"},"actions":[{"theme":"brand","text":"Brut is Not Yet Released","link":"/not-released"},{"theme":"brand","text":"Getting Started","link":"/getting-started"},{"theme":"alt","text":"Conceptual Overview","link":"/overview"}]},"xfeatures":[{"title":"Standards-Based","icon":"📄","details":"Brut leverages HTML, HTTP, SQL, and the Ruby standard library to let you write apps using standards you already know…or could quickly learn"},{"title":"Convention-Oriented","icon":"🎚️","details":"In a Brut app, there's usually just one way to do something. Learn things once, and you won't forget how your app works."},{"title":"Objects and Methods","icon":"💻","details":"Author classes to create objects on which to call methods. Nothing fancy."},{"title":"Builds on Community Libraries","icon":"🏘️","details":"Sequel, Phlex, I18n, RSpec. They do it best"}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}`),i={name:"index.md"};function r(d,e,l,c,u,p){return n(),o("div",null,e[0]||(e[0]=[t("p",null,[t("img",{src:s,alt:"logo"})],-1)]))}const h=a(i,[["render",r]]);export{g as __pageData,h as default};
@@ -1 +0,0 @@
1
- import{_ as a,c as o,o as n,j as t}from"./chunks/framework.1L-BeKqY.js";const s="/assets/LogoStop.X8x-4riz.png",g=JSON.parse(`{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Brut RB","text":"Raw Ruby Web Apps","tagline":"Standards-based, No-nonsense, HTML-first, Low Ceremony","ximage":{"src":"/images/LogoTall.png","alt":"A Ruby gemstone embedded into a concrete, brutalist building"},"actions":[{"theme":"brand","text":"Brut is Not Yet Released","link":"/not-released"},{"theme":"brand","text":"Getting Started","link":"/getting-started"},{"theme":"alt","text":"Conceptual Overview","link":"/overview"}]},"xfeatures":[{"title":"Standards-Based","icon":"📄","details":"Brut leverages HTML, HTTP, SQL, and the Ruby standard library to let you write apps using standards you already know…or could quickly learn"},{"title":"Convention-Oriented","icon":"🎚️","details":"In a Brut app, there's usually just one way to do something. Learn things once, and you won't forget how your app works."},{"title":"Objects and Methods","icon":"💻","details":"Author classes to create objects on which to call methods. Nothing fancy."},{"title":"Builds on Community Libraries","icon":"🏘️","details":"Sequel, Phlex, I18n, RSpec. They do it best"}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}`),i={name:"index.md"};function r(d,e,l,c,u,p){return n(),o("div",null,e[0]||(e[0]=[t("p",null,[t("img",{src:s,alt:"logo"})],-1)]))}const h=a(i,[["render",r]]);export{g as __pageData,h as default};
@@ -1,25 +0,0 @@
1
- import{_ as i,c as s,o as a,ag as t}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Keyword Injection","description":"","frontmatter":{},"headers":[],"relativePath":"keyword-injection.md","filePath":"keyword-injection.md"}'),n={name:"keyword-injection.md"};function o(l,e,h,r,d,p){return a(),s("div",null,e[0]||(e[0]=[t(`<h1 id="keyword-injection" tabindex="-1">Keyword Injection <a class="header-anchor" href="#keyword-injection" aria-label="Permalink to &quot;Keyword Injection&quot;">​</a></h1><p>Brut is desiged around classes and objects, as compared to modules and DSLs. Almost everything you do when building your app is to create a class that has an initializer and implements one or more methods. But, these initalizers and methods often need information from the request that Brut is managing.</p><p>In a basic Rack or Sinatra app, you would access this information via Rack&#39;s API, which is essentially a <code>Hash</code> of whatever. It&#39;s error-prone and requires consulting documentation, source code, or runtime information to figure out what&#39;s stored where.</p><p>Brut can instead inject these values explicitly into the classes of yours it creates. It does this based on the names of keyword arguments declared by your class&#39; intializer or a template method Brut will call.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>For example, a <a href="/pages.html">Page</a> requires you to implement an initializer. That initializer&#39;s keyword arguments define what information is needed. Brut provides that information when it creates the object. This is a form of dependency injection and it can simplify your code if used effectively.</p><p>Consider this route:</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:#24292E;--shiki-dark:#E1E4E8;">page </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/widgets/:id&quot;</span></span></code></pre></div><p>Brut will expect to find <code>WidgetsByIdPage</code>. Your initializer can declare <code>id:</code> as a keyword arg and this will be passed when the class is created:</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;"> WidgetsByIdPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
2
- <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;">id:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
3
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @widget </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</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;">(id)</span></span>
4
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
5
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>If the page requires access to the session, it can declare that:</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;"> WidgetsByIdPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
6
- <span class="line highlighted"><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;">id:</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>
7
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @widget </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</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;">(id)</span></span>
8
- <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @current_user </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">current_user</span></span>
9
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
10
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Because <code>session:</code> is a required argument, Brut cannot instantiate the page without it, so it will always be passed in and availbale.</p><h3 id="standard-injectible-information" tabindex="-1">Standard Injectible Information <a class="header-anchor" href="#standard-injectible-information" aria-label="Permalink to &quot;Standard Injectible Information&quot;">​</a></h3><p>In any request, the following information is available to be injected:</p><ul><li><code>session:</code> - An instance of your app&#39;s <a href="/api/Brut/FrontEnd/Session.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Session</code></a> subclass for the current visitor&#39;s session.</li><li><code>flash:</code> - An instance of your app&#39;s <a href="/api/Brut/FrontEnd/Flash.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Flash</code></a> subclass.</li><li><code>xhr:</code> - true if this was an Ajax request.</li><li><code>body:</code> - the body submitted, if any.</li><li><code>csrf_token:</code> - The current CSRF token.</li><li><code>clock:</code> - A <code>Clock</code> to be used to access the current time in the visitor&#39;s time zone.</li><li><code>http_*</code> - any parameter that starts with <code>http_</code> is assumed to be for an HTTP header. For example, <code>http_accept_language</code> would be given the value for the &quot;Accept-Language&quot; header. See the section on HTTP headers below.</li><li><code>env:</code> - The Rack env. This is discouraged, but available if you can&#39;t get what you want directly</li></ul><p>Depending on the context, other information is available:</p><ul><li><code>form:</code> - If a form was submitted, this is the <a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a> subclass containing the data. See <a href="/forms.html">Forms</a>.</li><li>Any query string paramter - Note that if these conflict with existing Brut values, the behavior is undefined. Name your query string parameters carefully. These should have default values or your page won&#39;t work if they are omitted.</li><li>Any route parameter - These should not have default values, since they are required for Brut to match the route.</li></ul><p>A <a href="/api/Brut/FrontEnd/RouteHook.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::RouteHook</code></a> is slightly different. Only the following data is available to be injected:</p><ul><li><code>request_context:</code> - The current request context, thought it may be <code>nil</code> depending on when the hook runs</li><li><code>session:</code> - An instance of your app&#39;s <a href="/api/Brut/FrontEnd/Session.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Session</code></a> subclass for the current visitor&#39;s session.</li><li><code>request:</code> - The Rack request</li><li><code>response:</code> - The Rack response</li><li><code>env:</code> - The Rack env.</li></ul><h3 id="http-headers" tabindex="-1">HTTP Headers <a class="header-anchor" href="#http-headers" aria-label="Permalink to &quot;HTTP Headers&quot;">​</a></h3><p>Since any header can be sent with a request, Brut allows you to access them, including non-standard ones. Rack (which is based on CGI), provides access to all HTTP headers in the <code>env</code> by taking the header name, replacing dashes (&quot;-&quot;) with underscores (&quot;_&quot;), and prepending <code>http_</code> to the name, then uppercasing it. Thus, &quot;User-Agent&quot; becomes <code>HTTP_USER_AGENT</code>.</p><p>Because Ruby parameters and variables must start with a lower-case letter, Brut uses the lowercased version of the Rack/CGI variable. Thus, to receive the &quot;User-Agent&quot;, you would declare the keyword parameter <code>http_user_agent</code>.</p><p>Further, because headers come from the client and may not be under your control, the value that is actually injected depends on a few things:</p><ul><li>If your keyword arg is required, i.e. there is no default value: <ul><li>If the header was not provided, <code>nil</code> is injected.</li><li>If the header <em>was</em> provided, it&#39;s value is injected, even if it&#39;s the empty string.</li></ul></li><li>If your keyword arg is optional, i.e. it has a default value <ul><li>If the header was not provided, no value is injected, and your code will receive the default value.</li><li>If the header <em>was</em> provided, it&#39;s value is injected, even if it&#39;s the empty string.</li></ul></li></ul><h3 id="ordering-and-disambiguation" tabindex="-1">Ordering and Disambiguation <a class="header-anchor" href="#ordering-and-disambiguation" aria-label="Permalink to &quot;Ordering and Disambiguation&quot;">​</a></h3><p>You are discouraged from using builtin keys for your own data or request parameters. For example, you should not have a query string parameter named <code>env</code> as this conflicts with the builtin <code>env</code> that Brut will inject.</p><p>Since you can inject your own data (see below), you are free to corrupt the request context. Please don&#39;t do this. Brut may actively prevent this in the future.</p><p>You can also use the request context to put your own data that can be injected.</p><h3 id="injecting-custom-data" tabindex="-1">Injecting Custom Data <a class="header-anchor" href="#injecting-custom-data" aria-label="Permalink to &quot;Injecting Custom Data&quot;">​</a></h3><p>The correct place to inject your own data into the request is in a <a href="/hooks.html">before hook</a>. When you configure a before hook, it will run after Brut&#39;s internal <a href="/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::RouteHooks::SetupRequestContext</code></a>, which ensures the request context exists and is ready for use.</p><p>For example, here is how you might inject the currently logged-in account based on the session:</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;"> AuthBeforeHook</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RouteHook</span></span>
11
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> before</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">request_context:</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>
12
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">authenticated_account</span></span>
13
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> request_context[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:authenticated_account</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">authenticated_account</span></span>
14
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
15
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> continue</span></span>
16
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
17
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Note that the value is only injected if it exists. It&#39;s important not to inject <code>nil</code> for values that don&#39;t exist.</p><p>You may be thinking that this particular example is unnecessary. You could simply inject <code>session:</code> and call <code>session.authenticated_account</code>:</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;"> DashboardPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
18
- <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;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
19
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @widgets </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">authenticated_account</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">widgets</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # e.g.</span></span>
20
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
21
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>If <code>DashboardPage</code> requires an authenticated account, by only injecting the session, you&#39;ll need to handle the case where <code>session.authenticated_account</code> is <code>nil</code>. Instead, if you configure the <code>AuthBeforeHook</code> as above, then inject <code>authenticated_account</code>, you avoid the need for this logic:</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;"> DashboardPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
22
- <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;">authenticated_account:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
23
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @widgets </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> authenticated_account.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">widgets</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # e.g.</span></span>
24
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
25
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Because <code>AuthBeforeHook</code> never injects <code>nil</code>, <code>DashboardPage</code> can rely on <code>authenticated_account</code> always being present. Further, if a visitor tried to access <code>/dashboard_page</code> without having been authenticated, Brut would be unable to create an instance of <code>DashboardPage</code> and generate an error.</p><h3 id="nil-and-empty-strings" tabindex="-1"><code>nil</code> and Empty Strings <a class="header-anchor" href="#nil-and-empty-strings" aria-label="Permalink to &quot;\`nil\` and Empty Strings&quot;">​</a></h3><p>When a keyword argument has no default value, Brut will require that value to exist and be available for injection. If the keyword is not one of the canned always-available values, it will look in the request context, then in the query string.</p><p>If the request has the keyword as a key, <em>it will inject whatever value it finds, including <code>nil</code></em>. In general, you should avoid injecting <code>nil</code> when you actually intend to not have a value.</p><p>For example, the <code>AuthBeforeHook</code> above, you could implement it 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:#24292E;--shiki-dark:#E1E4E8;">request_context[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:authenticated_account</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">authenticated_account</span></span></code></pre></div><p>The problem is that if the visitor is not logged in, the <code>:authenticated_account</code> <em>will</em> have a value, and that value will be <code>nil</code>. This is almost certainly not what you want.</p><p>For query string parameters, the HTTP spec says that they are strings. Thus, if a query string parameter is present in ther request URL, it will <em>always</em> have a value and <em>never</em> be <code>nil</code>. If the paramter doesn&#39;t have a value after the <code>=</code> (e.g. for <code>foo</code> in <code>?foo=&amp;bar=quux</code>), the value will be the empty string.</p><p>This means you must write code to explicitly handle the cases you care about.</p><h3 id="when-values-aren-t-available" tabindex="-1">When Values Aren&#39;t Available <a class="header-anchor" href="#when-values-aren-t-available" aria-label="Permalink to &quot;When Values Aren&#39;t Available&quot;">​</a></h3><p>When a value is not available for injection, and the keyword doesn&#39;t provide a default, Brut will raise an error. This is because such a situation represents a design error.</p><p>For example, the <code>DashboardPage</code> above requires an <code>authenticated_account</code>. Your app should never route a logged-out visitor to that page. This allows the <code>DashboardPage</code> to avoid having to check for <code>nil</code> and figure out what to do.</p><p>This is most relevant for query string parameters, since they can be easily manipulated by the visitor in their browser. Query string parameters should always have a default value, even if it&#39;s <code>nil</code>.</p><p><em>Path</em> parameters (like <code>:id</code> in <code>WidgetsByIdPage</code>) should <em>never</em> have a default value as their absence means a different URL was requested. For example, <code>/widgets</code> would trigger a <code>WidgetsPage</code>. <em>Only</em> if the <code>:id</code> path parameter is present would the <code>WidgetsByIdPage</code> be triggered, so it&#39;s safe to omit the default value for <code>id:</code> (and pointless to include one).</p><p>See <a href="/hooks.html">route hooks</a>.</p><h3 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h3><p>Brut will not create your classes in a test. Instead, you must pass in the values you want. There are various helpers in <a href="/api/Brut/SpecSupport.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport</code></a> to create blank or empty versions of the special classes.</p><p>In particular, A basic <code>request_context</code> is setup per test and injected into the Thread local storage. This means that if your test should trigger a codepath that <em>does</em> cause Brut to use keyword injection, useful values will be injected.</p><p>For your tests, however, you should pass in directly what you need:</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:#E36209;--shiki-dark:#FFAB70;">page</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">WidgetsByIdPage</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;">id:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> widget.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">id</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;"> empty_session)</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>Consider a method 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:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> create_widget</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;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">organization:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">quantity:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span></code></pre></div><p>Outside of Brut, the way to interpret this arguments is as follows:</p><ul><li><code>name</code> is required</li><li><code>organization</code> is optional</li><li><code>quantity</code> has a default value of 10 if not provided</li></ul><p>Any method or intializer that will be keyword-injected should be designed with this in mind. Thus, the following guidelines will be helpful in managing your app:</p><ul><li><strong>Choose arguments based on the needs of the class:</strong><ul><li>If a value is optional, default it to either <code>nil</code> or a symbol that indicates what happens when the value is omitted</li><li>If an optional value has a default, use that (this should be rare for pages, handlers, components, and hooks)</li><li>Otherwise, do not provide a default for the keyword</li></ul></li><li><strong>Design for non-<code>nil</code> values instead of allowing <code>nil</code> and checking for it</strong><ul><li>If a page needs, say, the currently logged-in user, set that up as injectible with no default.</li><li>If a codepath creates that page without the logged-in user, you will get a very obvious error and can figure out how it happened. Your page&#39;s code doesn&#39;t need to figure out what to do with <code>nil</code></li></ul></li><li><strong>Do not inject <code>nil</code> into the request context.</strong> When your code requires a value for a keyword, you want to rely on that value being non-nil. Thus, avoid injecting <code>nil</code> into the request context. Brut will allow it as a sort-of escape hatch, but you should design your app to avoid it</li><li><strong>Be careful injecting global data.</strong> The request context instance is per request, but you could certainly put global data into it. For example, you may put an initialized API client into the request context as a convieniece. <strong>Be careful</strong> because your app is multi-threaded. Any object that is not scoped to the request must be thread-safe.</li></ul><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 7, 2025</em></p><p>Keyword injection is currently implemented in a few places and not available via public API. It could be useful as an API and it will be exposed at some point. For now, it&#39;s only available for Brut-managed classes as documented here.</p>`,69)]))}const k=i(n,[["render",o]]);export{u as __pageData,k as default};
@@ -1 +0,0 @@
1
- import{_ as i,c as a,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const y=JSON.parse('{"title":"Layouts","description":"","frontmatter":{},"headers":[],"relativePath":"layouts.md","filePath":"layouts.md"}'),e={name:"layouts.md"};function l(h,s,p,k,r,o){return t(),a("div",null,s[0]||(s[0]=[n("",26)]))}const c=i(e,[["render",l]]);export{y as __pageData,c as default};
@@ -1 +0,0 @@
1
- import{_ as a,c as s,o as r,j as e,a as o}from"./chunks/framework.1L-BeKqY.js";const p=JSON.parse('{"title":"Brut is Not Released Software","description":"","frontmatter":{},"headers":[],"relativePath":"not-released.md","filePath":"not-released.md"}'),n={name:"not-released.md"};function i(l,t,d,c,u,f){return r(),s("div",null,t[0]||(t[0]=[e("h1",{id:"brut-is-not-released-software",tabindex:"-1"},[o("Brut is Not Released Software "),e("a",{class:"header-anchor",href:"#brut-is-not-released-software","aria-label":'Permalink to "Brut is Not Released Software"'},"​")],-1),e("div",{class:"warning custom-block github-alert"},[e("p",{class:"custom-block-title"},"WARNING"),e("p",null,"Brut is not considered released. It is still in development and some of what is in this doc site is inaccurate.")],-1)]))}const h=a(n,[["render",i]]);export{p as __pageData,h as default};
@@ -1 +0,0 @@
1
- import{_ as a,c as s,o as r,j as e,a as o}from"./chunks/framework.1L-BeKqY.js";const p=JSON.parse('{"title":"Brut is Not Released Software","description":"","frontmatter":{},"headers":[],"relativePath":"not-released.md","filePath":"not-released.md"}'),n={name:"not-released.md"};function i(l,t,d,c,u,f){return r(),s("div",null,t[0]||(t[0]=[e("h1",{id:"brut-is-not-released-software",tabindex:"-1"},[o("Brut is Not Released Software "),e("a",{class:"header-anchor",href:"#brut-is-not-released-software","aria-label":'Permalink to "Brut is Not Released Software"'},"​")],-1),e("div",{class:"warning custom-block github-alert"},[e("p",{class:"custom-block-title"},"WARNING"),e("p",null,"Brut is not considered released. It is still in development and some of what is in this doc site is inaccurate.")],-1)]))}const h=a(n,[["render",i]]);export{p as __pageData,h as default};
@@ -1,133 +0,0 @@
1
- import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.1L-BeKqY.js";const n="/assets/OverviewMetro.DUS-5fUZ.png",g=JSON.parse('{"title":"Conceptual Overview","description":"","frontmatter":{},"headers":[],"relativePath":"overview.md","filePath":"overview.md"}'),l={name:"overview.md"};function h(p,s,r,o,d,k){return e(),a("div",null,s[0]||(s[0]=[t('<h1 id="conceptual-overview" tabindex="-1">Conceptual Overview <a class="header-anchor" href="#conceptual-overview" aria-label="Permalink to &quot;Conceptual Overview&quot;">​</a></h1><p>Brut is a web framework that provides the ability to receive HTTP requests and respond to them. It includes facilities for generating HTML, interacting with a database, managing assets and more. Pretty much everything you need, though not much that you don&#39;t.</p><p>Brut&#39;s approach and design are built on three core values:</p><ul><li><strong>Leverage Standards</strong> - The web platform is great, and Brut wants you to use it.</li><li><strong>There&#39;s One Best Way To Do It</strong> - Flexibility leads to chaos.</li><li><strong>Simple over Easy</strong> - Verbose code that can be quickly understood beats impenetrable compact DSLs every day.</li></ul><p>As such, Brut&#39;s API and concepts are intended to mirror concepts that exist in the domain of building web sites. For example, when you go to a URL, you are viewing a web page. In Brut, a page is rendered by using a subclass of <a href="/api/Brut/FrontEnd/Page.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Page</code></a>, called a <em>page</em>.</p><p>Brut also avoids creating abstractions on top of existing standards you already need to know to build websites. For example, instead of creating a resource/verb abstraction on top of submitting forms over HTTP, Brut instead has you implement a <a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a> that describes the form&#39;s inputs—just like you&#39;d have in HTML.</p><h2 id="basic-elements-of-a-brut-powered-app" tabindex="-1">Basic Elements of a Brut-Powered App <a class="header-anchor" href="#basic-elements-of-a-brut-powered-app" aria-label="Permalink to &quot;Basic Elements of a Brut-Powered App&quot;">​</a></h2><p>Below is a diagram showing the high level parts of a Brut app. It shows four important terms with respect to how Brut is organized:</p><ul><li><em>Client</em> or <em>Client Side</em> is the web browser (or HTTP client). This is where CSS is applied to HTML and where JavaScript is executed. HTTP requests are initiated here.</li><li><em>Server</em> or <em>Server Side</em> is where any code not in the browser runs. In Brut, this includes HTML generation, SQL queries, and everything in between.</li><li><em>Front End</em> is the code that deals with producing your user interface or HTTP API. A lot of this code runs on the <em>server side</em>, however it exists to provide a user interface of some sort.</li><li><em>Back End</em> is the code that deals with everything else, such as accessing a database, executing business logic, or managing background jobs.</li></ul><p><img src="'+n+`" alt="Architectural Overview"></p><ul><li><strong>Visitor</strong> is someone visiting your web site or app.</li><li><strong>Browser</strong> is, well, a web browser</li><li><a href="/pages.html"><strong>Pages</strong></a> generate web pages, which is what happens when a browser&#39;s UI navigates to a URL.</li><li><a href="/forms.html"><strong>Forms</strong></a> describe the inputs of an HTML <code>&lt;form&gt;</code> element, and hold a form&#39;s submitted data for server-side processing. Browser submit forms to the server.</li><li><a href="/components.html"><strong>Components</strong></a> generate HTML fragments and are used to generate the HTML of a page or for re-use across pages.</li><li><a href="/handlers.html"><strong>Handlers</strong></a> receive non-GET HTTP requests from the browser, notably form submissions.</li><li><a href="/javascript.html"><strong>JavaScript</strong></a> and <a href="/assets.html"><strong>Assets</strong></a> (including <a href="/css.html">CSS</a>) are bundled on the server and sent to the client.</li><li><a href="/business-logic.html"><strong>Domain Logic</strong></a> as where your business and domain logic lives and can be implemented however you like.</li><li><a href="/database-access.html"><strong>DB Models</strong></a> are objects that provide access to your database.</li><li><strong>Relational Database</strong> is your database, where data is stored.</li></ul><h2 id="brut-is-not-a-resource-oriented-mvc-framework" tabindex="-1">Brut is Not a Resource-Oriented MVC Framework. <a class="header-anchor" href="#brut-is-not-a-resource-oriented-mvc-framework" aria-label="Permalink to &quot;Brut is Not a Resource-Oriented MVC Framework.&quot;">​</a></h2><p>You will note that Brut is <em>not</em> an MVC framework. Rather than creating an often confusing abstraction on top of HTTP, browsers, and HTML, Brut provides a more direct set of primitives.</p><p>Further, Brut is not <em>resource-oriented</em>. While HTTP does include the concept of resources and verbs to operate on those resources, in the context of building a web application, these two abstractions cause more problems than they solve.</p><p>Although Brut can can certainly respond to any URL and any verb, the core set of abstractions mirror the observed behavior of a web browser: <em>Pages</em> generate HTML (with the help of <em>Components</em>). <em>Forms</em> describe data to collect from the user, which is submitted to <em>Handlers</em> for processing by the back-end. Ajax requests (and arbitrary HTTP requests) can also be responded-to by <em>Handlers</em>.</p><p>In practice, this means that you do not have to perform mental gymnastics to decide exactly what verb and/or resource best represents the use-case you are trying to build. When your partners want to build an &quot;account management page&quot;, you will be able to implement this with a class named <code>AccountManagementPage</code>. When discussing enhancements to the &quot;user settings form&quot;, you will making changes to the <code>UserSettingsForm</code> and <code>UserSettingsHandler</code>. If that form is on the page everyone calls the &quot;preferences page&quot;, you&#39;ll be dealing with <code>PreferencesPage</code>, and not &quot;the index method of the <code>UserSettingsController</code>.</p><p>Let&#39;s go one step deeper to see how these primitives work.</p><h2 id="quick-tour-of-brut-s-primitives" tabindex="-1">Quick Tour of Brut&#39;s Primitives <a class="header-anchor" href="#quick-tour-of-brut-s-primitives" aria-label="Permalink to &quot;Quick Tour of Brut&#39;s Primitives&quot;">​</a></h2><h3 id="pages" tabindex="-1">Pages <a class="header-anchor" href="#pages" aria-label="Permalink to &quot;Pages&quot;">​</a></h3><p>The <em>Page</em> is the best example of Brut&#39;s value system. When you fetch a URL in a web browser, that is referred to as a web page. Thus, in Brut, a route accessed via an HTTP <code>GET</code> is managed by an instance of a <em>page class</em>.</p><p>Instead of a routing system where you must map http-like verbs to resource, Brut&#39;s routes are more direct. For a page, you&#39;d use the <code>page</code> method (we&#39;ll explain more what <code>class App</code> and <code>routes do</code> are doing later):</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 style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Framework</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">App</span></span>
2
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;my-app&quot;</span></span>
3
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> organization</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;my-org&quot;</span></span>
4
- <span class="line"></span>
5
- <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> routes </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
6
- <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> page </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/dashboard&quot;</span></span>
7
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
8
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Brut is convention-based, so when this route is requested by the browser, Brut will instantiate the class <code>DashboardPage</code> to handle the request. The page is an enhanced Phlex component that supports layouts. You implement <code>page_template</code> and make calls to Phlex&#39;s API to generate your page&#39;s HTML.</p><p>You will write an initializer (using keyword arguments) that describes all the data your page needs in order to generate its HTML. Brut will instantiate your page class into an object and use Phlex&#39;s API to generate HTML.</p><p>There is great variety in what your initializer can be given by Brut. For example, if we want to show the current time as well as respond to the query-string parameter &quot;compact&quot;, we&#39;d write our class like so:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group--qXMx" id="tab-KZvr-oY" checked><label data-title="pages/dashboard_page.rb" for="tab-KZvr-oY">pages/dashboard_page.rb</label><input type="radio" name="group--qXMx" id="tab-cgtxeqq"><label data-title="layouts/default_layout.rb" for="tab-cgtxeqq">layouts/default_layout.rb</label></div><div class="blocks"><div class="language-ruby vp-adaptive-theme active"><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;"> DashboardPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
9
- <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;">clock:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">compact:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
10
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @now </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> clock.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">now</span></span>
11
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @compact </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> compact </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">!=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;true&quot;</span></span>
12
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
13
- <span class="line"></span>
14
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
15
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> main </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
16
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> h1 { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Hello!&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
17
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> h2 </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
18
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> plain</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;It&#39;s &quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
19
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> time</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">datetime:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> l</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(@now, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">format:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> iso_8601)) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
20
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> l</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(@now, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">format:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> date)</span></span>
21
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
22
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
23
- <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;">@compact</span></span>
24
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Do you know where your web framework is?&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
25
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
26
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
27
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
28
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><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;"> DefaultLayout</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Layout</span></span>
29
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> view_template</span></span>
30
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> doctype</span></span>
31
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> html </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
32
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> head </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
33
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> title { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;My Awesome Site&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
34
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> body </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
35
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> yield</span></span>
36
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
37
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
38
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
39
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
40
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div></div></div><p>This would all produce HTML like so:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-eDHyu" id="tab-f9HHgvF" checked><label data-title="/dashboard" for="tab-f9HHgvF">/dashboard</label><input type="radio" name="group-eDHyu" id="tab-LFQcgeQ"><label data-title="/dashboard?compact=true" for="tab-LFQcgeQ">/dashboard?compact=true</label></div><div class="blocks"><div class="language-html vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;!</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">DOCTYPE</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
41
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
42
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">head</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
43
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;My Awesome Site&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
44
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">head</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
45
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
46
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">main</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
47
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;Hello!&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
48
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">h2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;It&#39;s</span></span>
49
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">time</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> datetime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;2025-02-17&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
50
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> Monday, Feb 17</span></span>
51
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">time</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
52
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">h2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
53
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
54
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> Do you know where your web framework is?</span></span>
55
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">p</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
56
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">main</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
57
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
58
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;!</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">DOCTYPE</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
59
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
60
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">head</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
61
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;My Awesome Site&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
62
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">head</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
63
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
64
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">main</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
65
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;Hello!&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
66
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">h2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;It&#39;s</span></span>
67
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">time</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> datetime</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;2025-02-17&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
68
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> Monday, Feb 17</span></span>
69
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">time</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
70
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">h2</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
71
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">main</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
72
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">body</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
73
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">html</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div></div></div><h3 id="components" tabindex="-1">Components <a class="header-anchor" href="#components" aria-label="Permalink to &quot;Components&quot;">​</a></h3><p><em>Components</em> are a way to manage the complexity of HTML generation. A component is exactly like a page: it&#39;s Phlex Component that powers dynamic HTML generation. The only difference is that a page has a <em>layout</em>, whereas a component does not.</p><p>In the example below, a flash is passed into <code>FlashMessage</code> by Brut and <code>t</code> translates a string. More on both of those later. Note the components use Phlex API more directly by asking you to implement <code>view_template</code>. Brut tries to defer or mimic APIs of standard libraries classes rather than create its own wrappers unless there is a compelling reason.</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;"># components/flash_message.rb</span></span>
74
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> FlashMessage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppComponent</span></span>
75
- <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;">flash:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
76
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> flash.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">notice?</span></span>
77
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @message_key </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> flash.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">notice</span></span>
78
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @role </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :info</span></span>
79
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> elsif</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> flash.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">alert?</span></span>
80
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @message_key </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> flash.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">alert</span></span>
81
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @role </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :alert</span></span>
82
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
83
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
84
- <span class="line"></span>
85
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> any_message?</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">@message_key.nil?</span></span>
86
- <span class="line"></span>
87
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> view_template</span></span>
88
- <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;">@message_key.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">nil?</span></span>
89
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">role:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @role) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
90
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> t</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;">, @message_key ])</span></span>
91
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
92
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
93
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
94
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>You can then use this in any other view using <code>render</code>, provided by Phlex.</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;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
95
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> header </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
96
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> render </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FlashMessage</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;">flash:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
97
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
98
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><h3 id="forms" tabindex="-1">Forms <a class="header-anchor" href="#forms" aria-label="Permalink to &quot;Forms&quot;">​</a></h3><p>To allow data to be submitted from the browser, you&#39;d use an HTML <code>&lt;form&gt;</code> that contains many <code>&lt;inputs&gt;</code>, as has been the case since the birth of the web. To declare a form that will be submitted, use <code>form</code>:</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:#24292E;--shiki-dark:#E1E4E8;"> routes </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
99
- <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/login&quot;</span></span>
100
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span></code></pre></div><p>Brut is convention-based, so it will expect a class named <code>LoginForm</code> to exist to hold a programmatic description of the form. This description allows the form&#39;s data to be managed during the submission process. That form is passed to an instance of <code>LoginHandler</code>, which contains the logic for processing the submission.</p><p>The idea behind form classes is to avoid dealing with Hashes containing magical strings or symbols and instead deal with a more strictly defined type. This means, among other benefits, you don&#39;t have maintain a separate list of allowed parameters. Your form defines them and is used to generate HTML, so it&#39;s all just one list of parameters. Forms aren&#39;t that fancy, though. Just as HTML <code>FormData</code> is string keys and string values, so it is with Brut forms.</p><p>To define the inputs, class methods are used in the form class&#39; definition. When the form is submitted <code>LoginHandler#handle</code> is called, and passed an instance of the form. The form has methods to access the inputs&#39; values</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-N0YMR" id="tab-p_t9M_a" checked><label data-title="forms/login_form.rb" for="tab-p_t9M_a">forms/login_form.rb</label><input type="radio" name="group-N0YMR" id="tab--3a9l7j"><label data-title="handlers/login_handler.rb" for="tab--3a9l7j">handlers/login_handler.rb</label></div><div class="blocks"><div class="language-ruby vp-adaptive-theme active"><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;"> LoginForm</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppForm</span></span>
101
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:email</span></span>
102
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:password</span></span>
103
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><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;"> LoginHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
104
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</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>
105
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # form.email =&gt; value from &lt;input name=email&gt;</span></span>
106
- <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # form.password =&gt; value from &lt;input name=password&gt;</span></span>
107
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
108
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div></div></div><p>A handler is like a controller in Rails, except it only handles one action. The <code>handle</code> method&#39;s return value indicates what should happen. For example, you may want to re-validate the client-side constraints. If any have been violated, you&#39;d want to re-generate the HTML for the <code>LoginPage</code>. If everything looks good, you&#39;ll redirect to the <code>DashboardPage</code>:</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;"> LoginHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
109
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</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>
110
- <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>
111
- <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;">)</span></span>
112
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
113
- <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>
114
- <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
115
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
116
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><h3 id="javascript-and-css" tabindex="-1">JavaScript and CSS <a class="header-anchor" href="#javascript-and-css" aria-label="Permalink to &quot;JavaScript and CSS&quot;">​</a></h3><p>Brut does not include a front-end framework, however it doesn&#39;t prevent you from using one. Brut <em>does</em> include a configuration of esbuild that will serve you well for most situations. esbuild is also configured to bundle and manage your CSS. This, coupled with recent advancements in CSS means that you don&#39;t need something like SASS. You can use standard <code>@import</code> statements to manage your CSS across multiple files and use CSS nesting to namespace your classes.</p><p>Brut provides a JavaScript <em>library</em> called BrutJS. BrutJS is mostly a set of custom elements that act as HTML Web Components, progressively enhancing markup you manage and style with common behaviors. Some of these components are used to provide localized messaging for client-side constraint violations. You can use these with any other client-side framework, or just use them on their own. You can also skip using them entirely and provide your own solution.</p><h3 id="database-schema" tabindex="-1">Database Schema <a class="header-anchor" href="#database-schema" aria-label="Permalink to &quot;Database Schema&quot;">​</a></h3><p>Brut provides access to an SQL database via Sequel. Brut uses Sequel&#39;s database schema management, however it is enhanced to encourage good practices by default.</p><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>Brut currently <em>only supports</em> PostgreSQL. It may support all RDBMSes that Sequel supports, but as of now, it&#39;s just Postgres.</p></div><p>Consider a <code>households</code> table that relates to an <code>accounts</code> table.</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:#24292E;--shiki-dark:#E1E4E8;">create_table </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:households</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
117
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Family unit managing the data&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
118
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:timezone</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:text</span></span>
119
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:dinner_time_of_day</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:text</span></span>
120
- <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> constraint</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
121
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :time_must_be_time</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
122
- <span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> %{</span></span>
123
- <span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> (dinner_time_of_day ~ &#39;^[01][0-9]:[0-5][0-9]$&#39;) OR</span></span>
124
- <span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> (dinner_time_of_day ~ &#39;^2[0-3]:[0-5][0-9]$&#39;)</span></span>
125
- <span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> }</span></span>
126
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
127
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span>
128
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">create_table </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:accounts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
129
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;People or systems who can access this system&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">external_id:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
130
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </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;">:email_address</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">unique:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span></span>
131
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:deactivated_at</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:timestamptz</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">null:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span></span>
132
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> foreign_key </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:household_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:households</span></span>
133
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This is mostly using <a href="https://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html" target="_blank" rel="noreferrer">Sequel&#39;s built-in migrations API</a>. But, a few additional behaviors are happening:</p><ul><li>Columns default to <code>NOT NULL</code></li><li>Tables require comments</li><li>Foreign keys default to having constraints and indexes</li></ul><p>There are other quality-of-life features of Brut&#39;s migration system, all designed to default to a good practice, with a way to do it however you want when needed.</p><h3 id="database-access" tabindex="-1">Database Access <a class="header-anchor" href="#database-access" aria-label="Permalink to &quot;Database Access&quot;">​</a></h3><p>Brut uses <code>Sequel::Model</code> to access data in your database. To discourage the conflation of &quot;models of database tables&quot; with &quot;models of your application&#39;s domain&quot;, these classes are in the <code>DB</code> namespace. Thus, the class <code>DB::Household</code> would be able to access the <code>households</code> table defined above. This frees you up to create a <code>Household</code> class to model your domain&#39;s logic without being coupled to how you store some data in a database.</p><h3 id="domain-and-business-logic" tabindex="-1">Domain and Business Logic <a class="header-anchor" href="#domain-and-business-logic" aria-label="Permalink to &quot;Domain and Business Logic&quot;">​</a></h3><p>Brut uses Zeitwerk for code loading, so any directories you create will be auto-loaded and refreshed during development. This means that you can create a class named <code>Household</code> in <code>app/src/back_end/domain/household.rb</code> and it would be loaded. Or, you could create <code>HouseholdService</code> in <code>app/src/back_end/services/household_service.rb</code> if you like.</p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>Providing a generally-useful abstraction for business or domain logic is not usually feasible. Thus, Brut doesn&#39;t provide much beyond Zeitwerk&#39;s auto-loading feature. It may provide more assistance in the future, but for now, Brut&#39;s approach is to free you from any prescription or moral imperative. Manage your domain and business logic how you see fit. You know your domain and team better than we do.</p></div><h3 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h3><p>Brut provides support for three types of tests:</p><ul><li>Unit Tests, using RSpec</li><li>End-to-end tests, using RSpec and Playwright</li><li>Custom Element tests, written in JavaScript, using Mocha</li></ul><p>Since almost every class you create is a plain Ruby class, testing the classes is usually straightforward. That said, it&#39;s often better to test Pages and Components through their generated HTML. Brut provides help to do that, based on Nokogiri.</p><h3 id="tasks" tabindex="-1">Tasks <a class="header-anchor" href="#tasks" aria-label="Permalink to &quot;Tasks&quot;">​</a></h3><p>Brut doesn&#39;t use Rake tasks. It uses CLI apps powered by Ruby&#39;s <code>OptionParser</code>. Brut provides bootstrapping classes to make your own CLIs, as well as some light abstractions to make <code>OptionParser</code> a little more ergonomic. Brut&#39;s dev and production management CLIs are built using this support.</p><h3 id="observability" tabindex="-1">Observability <a class="header-anchor" href="#observability" aria-label="Permalink to &quot;Observability&quot;">​</a></h3><p>Brut has built-in support for OpenTelemetry. Brut includes configuration for the otel-desktop-viewer or a text-based viewere suitable for develompent. For production, most observability vendors provide OpenTelemetry ingestion any many have free tiers.</p><p>Brut does support logging, however you are encouraged to use OpenTelemetry instead.</p><h2 id="directory-structure" tabindex="-1">Directory Structure <a class="header-anchor" href="#directory-structure" aria-label="Permalink to &quot;Directory Structure&quot;">​</a></h2><p>At the top level:</p><table tabindex="0"><thead><tr><th>Directory</th><th>Purpose</th></tr></thead><tbody><tr><td><code>app/</code></td><td>Contains all configuration and source code specific to your app</td></tr><tr><td><code>bin/</code></td><td>Contains tasks and other CLIs to do development of your app, such as <code>bin/test</code></td></tr><tr><td><code>dx/</code></td><td>Contains scripts to manage your development environment</td></tr><tr><td><code>specs/</code></td><td>Contains all tests</td></tr></tbody></table><p>Inside <code>app</code>/</p><table tabindex="0"><thead><tr><th>Directory</th><th>Purpose</th></tr></thead><tbody><tr><td><code>bootstrap.rb</code></td><td>A ruby file that sets up your app and ensures everything is <code>require</code>d in the right way.</td></tr><tr><td><code>config/</code></td><td>Configuration for your app, such as localizations and translations. Brut tries very hard to make sure there is no YAML in here at all. YAML is not good for you.</td></tr><tr><td><code>public/</code></td><td>Root of public assets served by the app.</td></tr><tr><td><code>src/</code></td><td>All source code for your app</td></tr></tbody></table><p>Inside <code>app/src</code></p><table tabindex="0"><thead><tr><th>Directory</th><th>Purpose</th></tr></thead><tbody><tr><td><code>app.rb</code></td><td>The core of your app, mostly configuration, such as routes, hooks, middleware, etc.</td></tr><tr><td><code>back_end/</code></td><td>Back end classes for your app including database schema, DB models, seed data, and your domain logic</td></tr><tr><td><code>cli/</code></td><td>Any CLIs or tasks for your app</td></tr><tr><td><code>front_end/</code></td><td>The front-end for your app, including pages, components, forms, handlers, JavaScript, and assets</td></tr></tbody></table><p>Inside <code>app/src/back_end</code></p><table tabindex="0"><thead><tr><th>Directory</th><th>Purpose</th></tr></thead><tbody><tr><td><code>data_models/app_data_model.rb</code></td><td>Base class for all DB model classes</td></tr><tr><td><code>data_models/db</code></td><td>DB model classes</td></tr><tr><td><code>data_models/db.rb</code></td><td>Namespace module for DB model classes</td></tr><tr><td><code>data_models/migrations</code></td><td>Database schema migrations</td></tr><tr><td><code>data_models/seed</code></td><td>Seed data used for local development</td></tr></tbody></table><p>Inside <code>app/src/front_end</code></p><table tabindex="0"><thead><tr><th>Directory</th><th>Purpose</th></tr></thead><tbody><tr><td><code>components/</code></td><td>Component classes</td></tr><tr><td><code>css/</code></td><td>CSS, managed by esbuild and <code>bin/build-assets</code></td></tr><tr><td><code>fonts/</code></td><td>Custom fonts, managed by esbuild and <code>bin/build-assets</code></td></tr><tr><td><code>forms/</code></td><td>Form classes</td></tr><tr><td><code>handlers/</code></td><td>Handler classes</td></tr><tr><td><code>images/</code></td><td>Images, copied to <code>app/public</code> by <code>bin/build-assets</code></td></tr><tr><td><code>js/</code></td><td>JavaScript, managed by esbuild and <code>bin/build-assets</code></td></tr><tr><td><code>layouts/</code></td><td>Layout classes</td></tr><tr><td><code>middlewares/</code></td><td>Rack Middleware, if any</td></tr><tr><td><code>pages/</code></td><td>Page classes</td></tr><tr><td><code>route_hooks/</code></td><td>Route hooks, if any</td></tr><tr><td><code>support/</code></td><td>General support classes/junk drawer.</td></tr><tr><td><code>svgs/</code></td><td>SVGs you want to render inline</td></tr></tbody></table>`,79)]))}const E=i(l,[["render",h]]);export{g as __pageData,E as default};
@@ -1 +0,0 @@
1
- import{_ as i,c as a,o as e,ag as t}from"./chunks/framework.1L-BeKqY.js";const n="/assets/OverviewMetro.DUS-5fUZ.png",g=JSON.parse('{"title":"Conceptual Overview","description":"","frontmatter":{},"headers":[],"relativePath":"overview.md","filePath":"overview.md"}'),l={name:"overview.md"};function h(p,s,r,o,d,k){return e(),a("div",null,s[0]||(s[0]=[t("",79)]))}const E=i(l,[["render",h]]);export{g as __pageData,E as default};