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
@@ -83,25 +83,24 @@ Docker container for local observability via OpenTelemetry.
83
83
  ```
84
84
  > dx/start
85
85
  ```
86
- 4. Now, "log in" to the container where your app and its tests will run:
86
+ 4. Now, install your aps gems and set it all up:
87
87
 
88
88
  ```
89
- > dx/exec login
90
- ```
91
-
92
- 5. Set everything up:
93
-
94
- ```
95
- inside-container> bin/setup
89
+ > dx/exec bin/setup
96
90
  ```
97
91
 
98
92
  Now, you're ready to go. See [Dev Environemnt](/dev-environment) for details on how
99
93
  this all works.
100
94
 
95
+ > [!NOTE]
96
+ > Instead of running `dx/exec` in front of your commands, you
97
+ > can instead do `dx/exec bash` to "log in" to the running container.
98
+ > You'll have a normal prompt and can issue commands directly from there.
99
+
101
100
  ## Run the App
102
101
 
103
102
  ```
104
- inside-container> bin/dev
103
+ dx/exec bin/dev
105
104
  ```
106
105
 
107
106
  You can now visit your app at `localhost:6502`
@@ -128,10 +127,10 @@ There are a few tests you can run, as well as some checks that you aren't using
128
127
  RubyGems with security vulnerabilities. Run it all now with `bin/ci`:
129
128
 
130
129
  ```
131
- inside-container> bin/dev
130
+ dx/exec bin/ci
132
131
  ```
133
132
 
134
133
  ## Now Build The Rest of Your App 🦉
135
134
 
136
- You can [follow the tutorial](/tutorial), check out the [conceptual overview](/overview), or dive straight into the API docs. You might also want to check out the docs for [LSP Support](/lsp).
135
+ You can [follow the tutorial](/tutorial), check out the [conceptual overview](/overview), or dive straight into the [API docs](/api/index.html). You might also want to check out the docs for [LSP Support](/lsp).
137
136
 
@@ -6,137 +6,161 @@ Handlers process form submissions, and *actions* work similarly to process any a
6
6
 
7
7
  Where a [page](/pages) renders a web page in HTML, a *handler* responds to all other HTTP requests. To respond to such HTTP requests, you'd first create a [route](/routes), using `form`, `action`, or `path`.
8
8
 
9
- ### Declaring Routes
9
+ ### Handler Structure
10
10
 
11
- `form` and `action` are intended to be used when an HTTP form is being submitted. The latter—`action`—is for when your form has no user-editable elements. This is akin to Rails' `button_to` helper, where the contents of the URL contains everything needed to service the request.
11
+ A handler's initializer is subject to [keyword injection](/keyword-injection), with
12
+ the addition of the `form:` keyword, which, if present, will be an instance of the
13
+ associated form class, populated with the data in the form submission (not available for `path` or `action` routes).
12
14
 
13
- `path` is for arbitrary HTTP requests and methods, so to create a webhook that responds to a PUT, you'd use:
15
+ You must implement `handle` to process the form. **A handler's public API, as
16
+ called by Brut and your tests, is `handle!`**, however you implement `handle`, which
17
+ `handle!` calls.
14
18
 
15
- ```ruby
16
- # inside app/src/app.rb
17
- path "/webhooks/stripe", method: :put
18
- ```
19
-
20
- 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 `Webhooks::StripeHandler`.
21
-
22
- ### Implementing Handlers
19
+ `handle`'s return value dictates what will happen next:
23
20
 
24
- 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 [Keyword Injection](/keyword-injection) for the details, noting that your handler can be inejcted with custom objects you've configured in a route hook.
21
+ | Return Value | Behavior |
22
+ |---|---|
23
+ |Instance of a page or component | That page or component's HTML is generated and returned. This is not a redirect, but more like `render :new` in a Rails controller |
24
+ |`Brut::FrontEnd::HttpStatus` | This HTTP status is returned with no body. Use `http_status` from `Brut::FrontEnd::HandlingResults` (included in all handlers) to create an instance from a number |
25
+ | `URI` | Redirect to the URI. Use `redirect_to` from `Brut::FrontEnd::HandlingResults` (included in all handlers) to generate a URI from a page class and parameters |
26
+ | Two element array with *element 0* being a `Brut::FrontEnd::HttpStatus`, and *element 1* being a page or component instance | Generates the page or component's HTML, but sets the given status instead of 200. Useful for Ajax responses where the HTTP status affects client-side behavior |
27
+ | `Brut::FrontEnd::Download` | Download a file |
28
+ | `Brut::FrontEnd::GenericResponse` | wrap any Rack response |
25
29
 
26
- After the handler is created, it's `before_handle` method is called. If it returns `nil`, `handle` is called to trigger whatever logic the handler needs to trigger.
27
-
28
- Both `handle` and `before_handle` can return a variety of objects that determine what will happen:
30
+ > [!IMPORTANT]
31
+ > The only way to render something other than HTML is to do so as a
32
+ > `GenericResponse`, which is basically the low-level Rack API. Brut
33
+ > encourages Ajax responses to be HTML and for you to use the browser's
34
+ > APIs to interact with that HTML. Brut may make it easier to work
35
+ > with other types of content in the future.
29
36
 
30
- * 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' `render :new` and **not** a `redirect_to(new_widgets_path)`.
31
- * A `Brut::FrontEnd::HttpStatus` which sends that status and an empty body back. This object can be created from
32
- an integer using the `http_status` helper, available to all handlers.
33
- * A `URI`, which will cause a redirect to that URI. You can create the `URI` yourself, or use the helper
34
- `redirect_to`, which accepts a string.
35
- * A two-element array with a page or component as the first element and an `Brut::FrontEnd::HttpStatus` as the
36
- second. This will render that page or component'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.
37
- * A `Brut::FrontEnd::Download`, which encapsulates a file to be downloaded.
38
- * A `Brut::FrontEnd::GenericResponse`, which wraps any Rack response with a defined type.
37
+ ### Handling a Form Submission
39
38
 
40
- `before_handle` may also return `nil` to indicate that `handle` should be called. `handle` may not return `nil`.
39
+ A common pattern when handling a form submisssion is to check for any constraint
40
+ violations. If there are some, re-generate the HTML for the page containing the
41
+ form, highlighting the violations. Otherwise, save the data and redirect to another
42
+ page.
41
43
 
42
- Supposing our `LoginForm` and `LoginHandler` wanted to use a common pattern of re-rendering `LoginPage` on constraint violations, and forwarding on to, say, a `DashboardPage`. Your handler might look like so:
44
+ Here's how that looks:
43
45
 
44
- ```ruby {23-27}
45
- # app/src/front_end/handlers/login_handler.rb
46
- class LoginHandler < AppHandler
47
- def initialize(form:, session:) # We'll discuss the session later
48
- @form = form
49
- @session = session
46
+ ```ruby
47
+ class NewWidgetHandler < AppHandler
48
+ def initialize(form:)
49
+ @form = form
50
50
  end
51
51
 
52
52
  def handle
53
+ # if no client-side violations were submitted
53
54
  if !@form.constraint_violations?
54
- authorized_user = AuthorizedUser.login(
55
- email: form.email,
56
- password: form.password
57
- )
58
- if authorized_user.nil?
55
+ widget = DB::Widget.find(name: form.name)
56
+ if widget
59
57
  @form.server_side_constraint_violation(
60
- input_name: :email,
61
- key: :login_not_found
58
+ input_name: :name,
59
+ key: :name_is_taken
62
60
  )
63
- else
64
- session.authorized_user = authorized_user
65
61
  end
66
62
  end
63
+
67
64
  if @form.constraint_violations?
68
- LoginPage.new(form: @form)
65
+ NewWidgetPage.new(form: @form)
69
66
  else
70
- redirect_to(DashboardPage.routing)
67
+ DB::Widget.create(name: form.name,
68
+ quantity: form.quantity,
69
+ description: form.description)
70
+ redirect_to(WidgetsPage)
71
71
  end
72
72
  end
73
73
  end
74
74
  ```
75
75
 
76
- > [!IMPORTANT]
77
- > The only way to render something other than HTML is to do so as a
78
- > `GenericResponse`, which is basically the low-level Rack API. Brut
79
- > encourages Ajax responses to be HTML and for you to use the browser's
80
- > APIs to interact with that HTML. Brut may make it easier to work
81
- > with other types of content in the future.
76
+ Unlike a Rails controller, the return value of `handle` controls the behavior. As
77
+ we saw in [Form Constraints](/form-constraints), the HTML generated by
78
+ `NewWidgetPage` will show constraint violations, so by returning
79
+ `NewWidgetPage.new(form: @form)`, the page has the same form instance, including all
80
+ the constraint violations, and the visitor will see the problems.
82
81
 
83
- ## Testing
82
+ ### Handling Other Requests
84
83
 
85
- Testing handlers requires calling their *public API*, which is `handle!`. This is not the method you implement (which is the non-bang `handle`). The reason is that `handle!` manages the logic around calling `before_handle`, which allows your tests to always call `handle!` and know they are testing how the handler would be used in produciton.
84
+ Non-form submissions work similarly, however there is no form available. If the
85
+ request could potentially involve a visitor-initiated error, you can use the
86
+ [flash](/flash-and-session) to communicate back. The flash is available for injection into the
87
+ initializer and, assuming your page uses it to show messages, can allow for
88
+ communication when something is wrong:
86
89
 
87
- Each handler spec includes `Brut::SpecSupport::HandlerSupport`, which allows you to create production-like flash, clock, and session objects. To assert the results of calling `handle!`, there are several RSpec matchers you can use to make your tests easier to write.
90
+ ```ruby
91
+ class App
92
+ # ...
88
93
 
89
- * `have_redirected_to` will check that the handler redirected to a give URI.
90
- * `have_rendered` will check that the handler rendered a specific page
91
- * `have_returned_http_status` will check that the handler returned an HTTP status
92
- * `have_constraint_violation` will check if a form had a particular constraint violation set on it
94
+ routes do
95
+ action "/delete_widget/:widget_id"
93
96
 
94
- ```ruby
95
- require "spec_helper"
96
-
97
- RSpec.describe LoginPage do
98
- describe "#handle!" do
99
- context "when login is not valid" do
100
- it "re-renders LoginPage" do
101
- form = LoginForm.new(params: {
102
- email: "nonexistent@example.com",
103
- password: "not a password",
104
- })
105
- result = described_class.new(
106
- form:,
107
- session: empty_session # empty_session provided by HandlerSupport
108
- )
109
- expect(result).to have_rendered(LoginPage)
110
- expect(form).to have_constraint_violation(:email, key: :login_not_found)
111
- end
112
- end
113
- context "when login is valid" do
114
- it "forward to the DashboardPage" do
115
- user = create(:user, # Assume this is set up via FactoryBot
116
- email: "pat@example.com",
117
- password: "1q2w3e4r5t6y7u8i9o")
118
-
119
- form = LoginForm.new(params: {
120
- email: "pat@example.com",
121
- password: "1q2w3e4r5t6y7u8i9o",
122
- })
123
- session = empty_session
124
- result = described_class.new(
125
- form:,
126
- session:,
127
- )
128
- expect(result).to have_redirected_to(DashboardPage.routing)
129
- expect(session.authorized_user).not_to eq(nil)
130
- # Session will be explained later
131
- end
97
+ # ...
98
+ end
99
+ end
100
+
101
+ class DeleteWidgetByIdHandler < AppHandler
102
+ def initialize(widget_id:, flash:)
103
+ @widget_id = widget_id
104
+ @flash = flash
105
+ end
106
+
107
+ def handle
108
+ widget = DB::Widget.find!(id: @widget_id)
109
+ if widget.can_delete?
110
+ widget.delete
111
+ @flash.notice = :widget_deleted
112
+ redirect_to(WidgetsPage)
113
+ else
114
+ @flash.alert = :widget_cannot_be_deleted
115
+ WidgetsPage.new
132
116
  end
133
117
  end
134
118
  end
135
119
  ```
136
120
 
121
+ ### Hooks
122
+
123
+ A handler's public API is `handle!`, because it first calls `before_handle`. This
124
+ operates as a before hook, and has the same return values as `handle`, with the
125
+ exception of also recognizing `nil`, which indicates processing should proceed to
126
+ `handle`.
127
+
128
+ Generally, you don't need to implement hooksd on a per-handler basis, but may find
129
+ it useful in a shared super class to implement cross-cutting behavior.
130
+
131
+ ## Testing
132
+
133
+ See [Unit Testing](/unit-tests) for some basic assumptions and configuration available for all Brut unit tests.
134
+
135
+ Handler tests should be straightforward: you create your handler, call `handle!`
136
+ (remember, `handle!` is the public API, and will call your hooks, which you want in
137
+ a test), then examine the result returned and any ancillary behavior, such as
138
+ updated database records.
139
+
140
+ Some matchers are available to make assertions about `handle!`'s return value:
141
+
142
+ * `have_redirected_to` will check that the handler redirected to a give URI. See `Brut::SpecSupport::Matchers::HaveRedirectedTo`.
143
+ * `have_generated` will check that the handler generated a specific page or component's HTML. See `<D-f>Brut::SpecSupport::Matchers::HaveGenerated`.
144
+ * `have_returned_http_status` will check that the handler returned an HTTP status. See `Brut::SpecSupport::Matchers::HaveReturnedHttpStatus`.
145
+ * `have_constraint_violation` will check if a form had a particular constraint violation set on it. See `Brut::SpecSupport::Matchers::HaveConstraintViolation`.
146
+
137
147
  ## Recommended Practices
138
148
 
139
- 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.
149
+ ### You Don't Always Need Resourceful or RESTful Routes
150
+
151
+ For any code where the browser is performing a submission, use either `form` or
152
+ `action` to declare your route. These will both use an HTTP `POST` to your server and handler. In the example above, we had a `POST` to `/delete_widget/:widget_id`, and not, say, a `DELETE` to it.
153
+
154
+ The main reason is that a browser can only submit to a server using `GET` or `POST`, so there's little value in "tunneling" another verb of POST. It doesn't really matter. And, even though you may use Ajax to submit such data, having it degrade to normal browser-based HTTP is a good practice.
155
+
156
+ For API-like calls where a browser will never directly interact with the route, and
157
+ it would only be via a server-to-server call, RESTful routes makes sense. But they
158
+ don't need to be the default.
159
+
160
+ ### Avoid Business Logic in Handlers
161
+
162
+
163
+ 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.
140
164
 
141
165
  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's fine. Mocks and stubs exist for a reason.
142
166
 
data/brutrb.com/hooks.md CHANGED
@@ -6,11 +6,10 @@ hooks can happen before a page or handler is called, or after.
6
6
  ## Overview
7
7
 
8
8
  We've seen examples thusfar of using a route hook to place the authenticated user or account into the
9
- request context for later injection into pages or handlers. Brut uses route hooks to locale detection and
10
- for content security policies.
9
+ request context for later injection into pages or handlers. Brut uses route hooks for locale detection and for content security policies.
11
10
 
12
- At its core, a before hook is a class that extends `Brut::FrontEnd::RouteHook` and implements `before`
13
- and an after hook implements `after`. Both `before` and `after` can be [injected](/keyword-injection) with request-time information.
11
+ At its core, a *before* hook is a class that extends `Brut::FrontEnd::RouteHook` and implements `before`
12
+ and an *after* hook implements `after`. Both `before` and `after` can be [injected](/keyword-injection) with request-time information.
14
13
 
15
14
  To register a hook, you'd call `before` or `after` in your `App`:
16
15
 
@@ -28,16 +27,11 @@ end
28
27
 
29
28
  The value can be a string or symbol, but should not be the class itself, as this can mess with load order.
30
29
 
31
- The code for `RequireAuthBeforeHook` we saw before was marked with a red warning that it wasn't production ready. Let's
32
- see what a more realistic hook would look like.
30
+ Let's implement a realistic hook that checks for authenticated users. Our hook will
31
+ detect if a user is logged in. If not, we'll redirect to a login page.
33
32
 
34
- The purpose of `RequireAuthBeforeHook` is to detect if a user is logged in. If they are, all is well. If they are not,
35
- we want to redirect them to a login page unless the page they've requested is allowed for logged-out users.
36
-
37
- Let's suppose that the home page (`/`) and any page starting with `/auth/` are allowed for logged-out users (`/auth/` being pages related to logging in).
38
-
39
- Brut also reserves some routes for its own, and we want those to be allowed to logged-out users as well. Brut sets
40
- `"brut.owned_path"` in the Rack environment if the requested URL is one it is managing.
33
+ Of course, the login page will need to be accessible without logging in. We also
34
+ don't want Brut-owned paths to require login, either.
41
35
 
42
36
  `before` will need access to the request context, session, Rack request, and Rack environment:
43
37
 
@@ -51,8 +45,7 @@ class RequireAuthBeforeHook < Brut::FrontEnd::RouteHook
51
45
  end
52
46
  ```
53
47
 
54
- We'll use the Rack request's `path_info` to check for allowed routes, and the aforementioned `env["brut.owned_path"]` to
55
- check for a Brut-owned path:
48
+ We'll use the Rack request's `path_info` to check for allowed routes. Brut will set `"brut.owned_path"` in the Rack environment for any path that it owns. We can check that to allow access to those paths.
56
49
 
57
50
  ```ruby {4-6}
58
51
  # app/src/front_end/route_hooks/require_auth_before_hook.rb
@@ -154,9 +147,7 @@ end
154
147
 
155
148
  ## Testing
156
149
 
157
- Route hooks are normal classes, you could test them as you would a handler or other class. This may be
158
- advisable for complex hooks, however it may be more realistic to test their behavior through end-to-end
159
- tests as this will ensure they are configured correctly in the context of the app.
150
+ Route hooks are normal classes, you could test them as you would a handler or other class. This may be advisable for complex hooks, however it may be more realistic to test their behavior through end-to-end tests as this will ensure they are configured correctly in the context of the app.
160
151
 
161
152
  ## Recommended Practices
162
153
 
@@ -174,5 +165,12 @@ For page- or use-case-specific behavior, it may be better to put the logic in a
174
165
 
175
166
  _Last Updated June 12, 2025_
176
167
 
177
- Route hooks and Middlewares do not share implementations, however they are similar in concept. These
178
- concepts may be unified in the future.
168
+ Route hooks and Middlewares do not share implementations, however they are similar in concept. These concepts may be unified in the future.
169
+
170
+ Hooks are applied in `Brut::Framework::MCP` usiung Sinatra's hooks mechanism. While
171
+ Brut may not always be based on Sinatra, it is now. You should not rely on it.
172
+
173
+ Lastly, there is some dissonance in how keyword injection works. Pages and Handlers
174
+ have initializer injection, while hooks use method injection. This may change -
175
+ Hooks may be re-designed to use initializer injection, and even changed so that
176
+ before and after hooks have different base classes.
data/brutrb.com/i18n.md CHANGED
@@ -167,14 +167,16 @@ t("cv.be.required") # => This field is required
167
167
 
168
168
  ## Testing
169
169
 
170
- In tests, you can call `t` and `l` to examine values as needed.
170
+ In tests, you can call `t` and `l` to examine values as needed. You may find the
171
+ `have_i18n_string` matcher usefult to check generated HTML for I18n values (see `Brut::SpecSupport::Matchers::HaveI18nString`).
171
172
 
172
173
  > [!WARNING]
173
- > This aspect of Brut could use improvement. It likely will not work for non-English locales.
174
+ > Brut hardcodes English for tests, which you may not want. This will be addressed
175
+ > in the future.
174
176
 
175
177
  ## Recommended Practices
176
178
 
177
- Could use help here - The implementation feels like a bare minium
179
+ None at this time, however Brut's I18n has not been battle-tested.
178
180
 
179
181
 
180
182
  ## Technical Notes
@@ -185,4 +187,4 @@ Could use help here - The implementation feels like a bare minium
185
187
 
186
188
  _Last Updated May 7, 2025_
187
189
 
188
- TBD
190
+ None at this time.
Binary file
data/brutrb.com/index.md CHANGED
@@ -10,9 +10,6 @@ hero:
10
10
  src: /images/LogoTall.png
11
11
  alt: "A Ruby gemstone embedded into a concrete, brutalist building"
12
12
  actions:
13
- - theme: brand
14
- text: "Brut is Not Yet Released"
15
- link: /not-released
16
13
  - theme: brand
17
14
  text: "Getting Started"
18
15
  link: /getting-started
@@ -28,7 +28,7 @@ Brut automatically sets up OpenTelemetry (OTel) tracing. The primary interface
28
28
  `Brut::Instrumentation::OpenTelemetry`, which is available via `Brut.container.instrumentation`. We'll
29
29
  discuss that in a moment.
30
30
 
31
- To configure the specifics of where the traces wil go, the OTel gem uses environment variables:
31
+ To configure the specifics of where the traces will go, the OTel gem uses environment variables:
32
32
 
33
33
  | Variable | Value | Purpose |
34
34
  |--------------------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -44,7 +44,7 @@ When you created your Brut app, your `.env.development` and `.env.test` should h
44
44
  environment variables that will send instrumentation to the otel-desktop-viewer that was also configured.
45
45
 
46
46
  If you run your app using `bin/dev` and use the app for a bit, then go to `http://localhost:8000`, you
47
- will see the otel-desktop-viewer UI and can browser the spans and traces sent by Brut.
47
+ will see the otel-desktop-viewer UI and can browse the spans and traces sent by Brut.
48
48
 
49
49
 
50
50
  ### What is Instrumented By Default
@@ -54,7 +54,7 @@ data. Brut will attempt to conform to standard semantics for HTTP requests and
54
54
 
55
55
  Here is a non-exhaustive list of what Brut automatically instruments:
56
56
 
57
- * How long each page or handler request takes
57
+ * How long each page or handler request takes, broken down by components.
58
58
  * CLI execution time
59
59
  * Time to rebuild the schema for tests
60
60
  * Time to run tests
@@ -75,7 +75,7 @@ Here is a non-exhaustive list of what Brut automatically instruments:
75
75
  > the actual values inserted or used in `WHERE` clauses.
76
76
  > While you should not be putting sensitive data into your database,
77
77
  > be warned that this is happening. There are plans to improve this
78
- > to be more flexible and reduce the Schance of sensitive data
78
+ > to be more flexible and reduce the chance of sensitive data
79
79
  > being sent in traces.
80
80
 
81
81
  ### Adding Your Own Instrumentation
@@ -155,16 +155,14 @@ The class `Brut::FrontEnd::Handlers::InstrumentationHandler` is set up to receiv
155
155
  client-side to provide insights about client-side behavior as part of a server-side request. Brut
156
156
  attempts to join up any client-side instrumentation to the request that served it.
157
157
 
158
- It does this `Brut::FrontEnd::Components::Traceparent` component, which is included in your default layout
159
- when you created your Brut app. This creates a `<meta>` tag containing standardized information used to
158
+ It does this via the `Brut::FrontEnd::Components::Traceparent` component, which is included in your default layout when you created your Brut app. This creates a `<meta>` tag containing standardized information used to
160
159
  connect the client-side behavior to the server-side request.
161
160
 
162
161
  The Brut custom element `<brut-tracing>` uses this information, along with statistics from the browser, to
163
162
  send a custom payload back to Brut at the route `/__brut/instrumentation`, which is handled by the
164
163
  aforementioned `InstrumentationHandler`.
165
164
 
166
- You should then see client-side tracing information as a sub-span of your HTTP request. The information
167
- available depends on the browser, and some browsers don't send much.
165
+ You should then see client-side tracing information as a sub-span of your HTTP request. The information available depends on the browser, and some browsers don't send much. Also keep in mind that clock drift is real and while client-side timings are accurate, the timestamps will not be.
168
166
 
169
167
  ## Testing
170
168
 
@@ -187,8 +185,7 @@ specific issues.
187
185
  _Last Updated June 12, 2025_
188
186
 
189
187
 
190
- Brut does not have plans to support non-OTel instrumentation, nor does it have plans to provide hooks to
191
- use proprietary formats. This could change of course.
188
+ Brut does not have plans to support non-OTel instrumentation, nor does it have plans to provide hooks to use proprietary formats.
192
189
 
193
190
  The client-side portion of this is highly customized. The Otel open source code for the client side is
194
191
  massive and hugely complex, so Brut decided to try to produce something simple and straightforward as a
@@ -1,9 +1,9 @@
1
1
  # JavaScript
2
2
 
3
- Brut provides basic bundling using [esbuild](https://esbuild.github.io/). Brut does not support nor prevent the
4
- use of any front-end framework. Brut does, however, include [BrutJS](/brut-js), a lightweight library of HTML custom
5
- elements and utility code. These elements can provide a fair bit of front-end functionality using progressive
6
- enhancement without the need for a framework.
3
+ Brut provides basic bundling using [esbuild](https://esbuild.github.io/). You can
4
+ use any front-end framework with Brut, but you don't have to use one.
5
+
6
+ Brut provides [BrutJS](/brut-js), which is a lightweight library of HTML custom elements and utility code. These elements can provide a fair bit of front-end functionality using progressive enhancement without the need for a framework.
7
7
 
8
8
  ## Overview
9
9
 
@@ -19,7 +19,7 @@ For example, if you have a `Widget` class that uses a `Status` class, and you al
19
19
 
20
20
  First, `package.json` (in your app's root) would include `"foobar"` (and it must set `"type"` to `"module"`):
21
21
 
22
- ```json {2,5}
22
+ ```json {3,6}
23
23
  {
24
24
  "name": "your-app",
25
25
  "type": "module",
@@ -50,7 +50,7 @@ Notice that "foobar", since it's brought in as a third party dependency, is impo
50
50
  here! Every third party library has a different syntax for how to import whatever it is or does. Consult the
51
51
  documentation of each third party library you wish to import.
52
52
 
53
- The second `import` uses a `./` because it's importing a file in `app/src/front_end/js` namely `Widget.js`. Be
53
+ The second `import` uses a `./` because it's importing a file in `app/src/front_end/js`, namely `Widget.js`. Be
54
54
  careful here, too, as you must be sure to `export` the right thing. Here's what `app/src/front_end/js/Widget.js`
55
55
  might look like:
56
56
 
@@ -96,16 +96,13 @@ details on this can be found in [assets](/assets).
96
96
  ## Testing
97
97
 
98
98
  Client-side behavior is best tested with end-to-end tests, however you can simplify your end-to-end tests by
99
- creating unit tests of your custom elements. BrutJS provides support for this. TBD LINK.
99
+ creating unit tests of your custom elements. [BrutJS provides limited support for this](/brut-js/api/module-testing.html)
100
100
 
101
101
  ## Recommended Practices
102
102
 
103
- Brut encourages you to use HTML custom elements as progressive enhancements over server-generated views. This
104
- sort of client-side code will age well. The toolchain and dependencies are minimal, so you will not have to
105
- worry too much about code written this way.
103
+ Brut encourages you to use HTML custom elements as progressive enhancements over server-generated views. This sort of client-side code will age well. The toolchain and dependencies are minimal, so you will not have to worry too much about code written this way.
106
104
 
107
- It *will* be lower level and more verbose than existing frameworks. We would argue that it is not significantly
108
- more difficult and the sustainability is worth it.
105
+ It *will* be lower level and more verbose than existing frameworks. We would argue that it is not significantly more difficult and the sustainability is worth it.
109
106
 
110
107
  ## Technical Notes
111
108
 
@@ -118,5 +115,8 @@ _Last Updated May 7, 2025_
118
115
  Currently, Brut only supports a single entry point and bundle. This could be easily made more flexible if there
119
116
  is a desire to finely tweak the JavaScript loaded on specific pages.
120
117
 
121
- Brut also does not expose any esbuild configuration. This could be provided in the future, but for now, it is
122
- hard-coded.
118
+ Brut also does not expose any esbuild configuration. This could be provided in the future, but for now, it is hard-coded.
119
+
120
+ Brut may provide more direct support for import maps, but as of now, import maps are
121
+ not widely used outside of Rails, and tend to cause a lot of problems, especially if
122
+ you aren't able to field an HTTP/2 web server (or even know what that is).