brut 0.8.0 → 0.9.1

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 (420) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +9 -0
  3. data/CHANGELOG.md +13 -0
  4. data/Dockerfile.dx +8 -1
  5. data/Gemfile.lock +101 -63
  6. data/bin/ci +9 -5
  7. data/bin/docs +1 -1
  8. data/bin/generate-and-run-rubocop +52 -0
  9. data/bin/rubocop +27 -0
  10. data/bin/setup +46 -0
  11. data/bin/test +18 -0
  12. data/brut-css/bin/publish +1 -1
  13. data/brut-css/package-lock.json +2 -2
  14. data/brut-css/package.json +1 -1
  15. data/brut-js/bin/publish +1 -1
  16. data/brut-js/package-lock.json +2 -2
  17. data/brut-js/package.json +1 -1
  18. data/brut-js/specs/ConfirmSubmit.spec.js +32 -1
  19. data/brut-js/src/ConfirmSubmit.js +28 -7
  20. data/brut.gemspec +32 -26
  21. data/brutrb.com/.vitepress/config.mjs +3 -0
  22. data/brutrb.com/database-schema.md +1 -1
  23. data/brutrb.com/images/tutorial/basic-form-with-violations.png +0 -0
  24. data/brutrb.com/images/tutorial/basic-form.png +0 -0
  25. data/brutrb.com/images/tutorial/initial-home-page.png +0 -0
  26. data/brutrb.com/images/tutorial/new-post-editor.png +0 -0
  27. data/brutrb.com/images/tutorial/new-post-home-page.png +0 -0
  28. data/brutrb.com/images/tutorial/styled-form-with-server-side-violations.png +0 -0
  29. data/brutrb.com/images/tutorial/styled-form-with-violations.png +0 -0
  30. data/brutrb.com/images/tutorial/styled-home-page-with-posts.png +0 -0
  31. data/brutrb.com/images/tutorial/styled-home-page.png +0 -0
  32. data/brutrb.com/images/tutorial/welcome-to-brut.png +0 -0
  33. data/brutrb.com/recipes/migrations.md +1 -1
  34. data/brutrb.com/tutorial.md +1728 -3
  35. data/docker-compose.dx.yml +3 -0
  36. data/docs/404.html +2 -2
  37. data/docs/adrs.html +3 -3
  38. data/docs/ai.html +3 -3
  39. data/docs/api/Brut/BackEnd/SeedData.html +2 -2
  40. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +2 -2
  41. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +2 -2
  42. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +2 -2
  43. data/docs/api/Brut/BackEnd/Sidekiq.html +2 -2
  44. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +2 -2
  45. data/docs/api/Brut/BackEnd/Validators.html +2 -2
  46. data/docs/api/Brut/BackEnd.html +2 -2
  47. data/docs/api/Brut/CLI/App.html +2 -2
  48. data/docs/api/Brut/CLI/AppRunner.html +2 -2
  49. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +2 -2
  50. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +2 -2
  51. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +2 -2
  52. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +2 -2
  53. data/docs/api/Brut/CLI/Apps/BuildAssets.html +2 -2
  54. data/docs/api/Brut/CLI/Apps/DB/Create.html +2 -2
  55. data/docs/api/Brut/CLI/Apps/DB/Drop.html +2 -2
  56. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +2 -2
  57. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +2 -2
  58. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +2 -2
  59. data/docs/api/Brut/CLI/Apps/DB/Seed.html +2 -2
  60. data/docs/api/Brut/CLI/Apps/DB/Status.html +2 -2
  61. data/docs/api/Brut/CLI/Apps/DB.html +2 -2
  62. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +2 -2
  63. data/docs/api/Brut/CLI/Apps/DeployBase.html +2 -2
  64. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +3 -3
  65. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +2 -2
  66. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +2 -2
  67. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +2 -2
  68. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +2 -2
  69. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +2 -2
  70. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +2 -2
  71. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +2 -2
  72. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +2 -2
  73. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +2 -2
  74. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +2 -2
  75. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +2 -2
  76. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +3 -3
  77. data/docs/api/Brut/CLI/Apps/Scaffold.html +2 -2
  78. data/docs/api/Brut/CLI/Apps/Test/Audit.html +2 -2
  79. data/docs/api/Brut/CLI/Apps/Test/E2e.html +2 -2
  80. data/docs/api/Brut/CLI/Apps/Test/JS.html +2 -2
  81. data/docs/api/Brut/CLI/Apps/Test/Run.html +2 -2
  82. data/docs/api/Brut/CLI/Apps/Test.html +2 -2
  83. data/docs/api/Brut/CLI/Apps.html +2 -2
  84. data/docs/api/Brut/CLI/Command.html +2 -2
  85. data/docs/api/Brut/CLI/Error.html +2 -2
  86. data/docs/api/Brut/CLI/ExecutionResults/Result.html +2 -2
  87. data/docs/api/Brut/CLI/ExecutionResults.html +2 -2
  88. data/docs/api/Brut/CLI/Executor.html +2 -2
  89. data/docs/api/Brut/CLI/InvalidOption.html +2 -2
  90. data/docs/api/Brut/CLI/Options.html +80 -2
  91. data/docs/api/Brut/CLI/Output.html +2 -2
  92. data/docs/api/Brut/CLI/SystemExecError.html +2 -2
  93. data/docs/api/Brut/CLI.html +2 -2
  94. data/docs/api/Brut/FactoryBot.html +3 -3
  95. data/docs/api/Brut/Framework/App.html +3 -3
  96. data/docs/api/Brut/Framework/Config.html +27 -15
  97. data/docs/api/Brut/Framework/Container.html +2 -2
  98. data/docs/api/Brut/Framework/Error.html +2 -2
  99. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +2 -2
  100. data/docs/api/Brut/Framework/Errors/Bug.html +2 -2
  101. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +2 -2
  102. data/docs/api/Brut/Framework/Errors/MissingParameter.html +2 -2
  103. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +2 -2
  104. data/docs/api/Brut/Framework/Errors/NotFound.html +2 -2
  105. data/docs/api/Brut/Framework/Errors/NotImplemented.html +2 -2
  106. data/docs/api/Brut/Framework/Errors.html +2 -2
  107. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +2 -2
  108. data/docs/api/Brut/Framework/MCP.html +21 -11
  109. data/docs/api/Brut/Framework/ProjectEnvironment.html +2 -2
  110. data/docs/api/Brut/Framework.html +2 -2
  111. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +2 -2
  112. data/docs/api/Brut/FrontEnd/Component/Helpers.html +2 -2
  113. data/docs/api/Brut/FrontEnd/Component.html +2 -2
  114. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +2 -2
  115. data/docs/api/Brut/FrontEnd/Components/FormTag.html +4 -4
  116. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +2 -2
  117. data/docs/api/Brut/FrontEnd/Components/Input.html +2 -2
  118. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +2 -2
  119. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +2 -2
  120. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +2 -2
  121. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +2 -2
  122. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +2 -2
  123. data/docs/api/Brut/FrontEnd/Components/Inputs.html +2 -2
  124. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +2 -2
  125. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +2 -2
  126. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +2 -2
  127. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +2 -2
  128. data/docs/api/Brut/FrontEnd/Components.html +2 -2
  129. data/docs/api/Brut/FrontEnd/CsrfProtector.html +250 -0
  130. data/docs/api/Brut/FrontEnd/Download.html +2 -2
  131. data/docs/api/Brut/FrontEnd/Flash.html +2 -2
  132. data/docs/api/Brut/FrontEnd/Form.html +6 -6
  133. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +2 -2
  134. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +2 -2
  135. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +2 -2
  136. data/docs/api/Brut/FrontEnd/Forms/Input.html +2 -2
  137. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +2 -2
  138. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +2 -2
  139. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +2 -2
  140. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +2 -2
  141. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +2 -2
  142. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +2 -2
  143. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +2 -2
  144. data/docs/api/Brut/FrontEnd/Forms.html +2 -2
  145. data/docs/api/Brut/FrontEnd/GenericResponse.html +2 -2
  146. data/docs/api/Brut/FrontEnd/Handler.html +2 -2
  147. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +2 -2
  148. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +2 -2
  149. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +2 -2
  150. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +2 -2
  151. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +2 -2
  152. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +2 -2
  153. data/docs/api/Brut/FrontEnd/Handlers.html +2 -2
  154. data/docs/api/Brut/FrontEnd/HandlingResults.html +2 -2
  155. data/docs/api/Brut/FrontEnd/HttpMethod.html +2 -2
  156. data/docs/api/Brut/FrontEnd/HttpStatus.html +2 -2
  157. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +2 -2
  158. data/docs/api/Brut/FrontEnd/Layout.html +2 -2
  159. data/docs/api/Brut/FrontEnd/Middleware.html +2 -2
  160. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +2 -2
  161. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +2 -2
  162. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +2 -2
  163. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +2 -2
  164. data/docs/api/Brut/FrontEnd/Middlewares.html +2 -2
  165. data/docs/api/Brut/FrontEnd/Page.html +2 -2
  166. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +2 -2
  167. data/docs/api/Brut/FrontEnd/Pages.html +2 -2
  168. data/docs/api/Brut/FrontEnd/RequestContext.html +2 -2
  169. data/docs/api/Brut/FrontEnd/RouteHook.html +2 -2
  170. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +2 -2
  171. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +2 -2
  172. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +2 -2
  173. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +2 -2
  174. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +2 -2
  175. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +2 -2
  176. data/docs/api/Brut/FrontEnd/RouteHooks.html +2 -2
  177. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +2 -2
  178. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +2 -2
  179. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +2 -2
  180. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +2 -2
  181. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +2 -2
  182. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +2 -2
  183. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +2 -2
  184. data/docs/api/Brut/FrontEnd/Routing/Route.html +2 -2
  185. data/docs/api/Brut/FrontEnd/Routing.html +2 -2
  186. data/docs/api/Brut/FrontEnd/Session.html +2 -2
  187. data/docs/api/Brut/FrontEnd.html +3 -3
  188. data/docs/api/Brut/I18n/BaseMethods.html +3 -3
  189. data/docs/api/Brut/I18n/ForBackEnd.html +2 -2
  190. data/docs/api/Brut/I18n/ForCLI.html +2 -2
  191. data/docs/api/Brut/I18n/ForHTML.html +2 -2
  192. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +2 -2
  193. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +2 -2
  194. data/docs/api/Brut/I18n.html +2 -2
  195. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +2 -2
  196. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +2 -2
  197. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +2 -2
  198. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +2 -2
  199. data/docs/api/Brut/Instrumentation.html +2 -2
  200. data/docs/api/Brut/RubocopConfig.html +237 -0
  201. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +2 -2
  202. data/docs/api/Brut/SinatraHelpers.html +2 -2
  203. data/docs/api/Brut/SpecSupport/ClockSupport.html +2 -2
  204. data/docs/api/Brut/SpecSupport/ComponentSupport.html +7 -13
  205. data/docs/api/Brut/SpecSupport/E2ETestServer.html +2 -2
  206. data/docs/api/Brut/SpecSupport/E2eSupport.html +2 -2
  207. data/docs/api/Brut/SpecSupport/EnhancedNode.html +2 -2
  208. data/docs/api/Brut/SpecSupport/FlashSupport.html +2 -2
  209. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +2 -2
  210. data/docs/api/Brut/SpecSupport/GeneralSupport.html +2 -2
  211. data/docs/api/Brut/SpecSupport/HandlerSupport.html +2 -2
  212. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +2 -2
  213. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +2 -2
  214. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +2 -2
  215. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +2 -2
  216. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +2 -2
  217. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +2 -2
  218. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +2 -2
  219. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +2 -2
  220. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +2 -2
  221. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +2 -2
  222. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +2 -2
  223. data/docs/api/Brut/SpecSupport/Matchers.html +2 -2
  224. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +2 -2
  225. data/docs/api/Brut/SpecSupport/RSpecSetup.html +4 -4
  226. data/docs/api/Brut/SpecSupport/SessionSupport.html +2 -2
  227. data/docs/api/Brut/SpecSupport.html +2 -2
  228. data/docs/api/Brut.html +128 -12
  229. data/docs/api/Clock.html +2 -2
  230. data/docs/api/ModuleName.html +28 -28
  231. data/docs/api/RichString.html +22 -22
  232. data/docs/api/SemanticLogger/Appender/Async.html +2 -2
  233. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +2 -2
  234. data/docs/api/Sequel/Extensions/BrutMigrations.html +2 -2
  235. data/docs/api/Sequel/Extensions.html +2 -2
  236. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +2 -2
  237. data/docs/api/Sequel/Plugins/CreatedAt.html +2 -2
  238. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +2 -2
  239. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +2 -2
  240. data/docs/api/Sequel/Plugins/ExternalId.html +2 -2
  241. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +2 -2
  242. data/docs/api/Sequel/Plugins/FindBang.html +2 -2
  243. data/docs/api/Sequel/Plugins.html +2 -2
  244. data/docs/api/Sequel.html +2 -2
  245. data/docs/api/_index.html +16 -2
  246. data/docs/api/class_list.html +1 -1
  247. data/docs/api/file.README.html +28 -2
  248. data/docs/api/index.html +28 -2
  249. data/docs/api/method_list.html +192 -168
  250. data/docs/api/top-level-namespace.html +2 -2
  251. data/docs/assets/{app.DyQLb4Ot.js → app.BuBdZoUg.js} +1 -1
  252. data/docs/assets/basic-form-with-violations.Cv6Y9-Q_.png +0 -0
  253. data/docs/assets/basic-form.DbHnu0oW.png +0 -0
  254. data/docs/assets/chunks/@localSearchIndexroot.BZ_a3X0T.js +1 -0
  255. data/docs/assets/chunks/{VPLocalSearchBox.T1iA-eJx.js → VPLocalSearchBox.UtnyvkX-.js} +1 -1
  256. data/docs/assets/chunks/{theme.ChwsbWjK.js → theme.DqwvuBEK.js} +2 -2
  257. data/docs/assets/{components.md.DHh-NwKs.js → components.md.Bu80E2Nr.js} +3 -3
  258. data/docs/assets/{configuration.md.D8Wz3oJU.js → configuration.md.CuIxVsSf.js} +1 -1
  259. data/docs/assets/{database-schema.md.C5gXexJi.js → database-schema.md.LpmBPVEU.js} +1 -1
  260. data/docs/assets/{forms.md.BRE85eju.js → forms.md.DnLbzVDa.js} +1 -1
  261. data/docs/assets/{getting-started.md.2ioiTe-B.js → getting-started.md.DdQLmU3C.js} +2 -2
  262. data/docs/assets/initial-home-page.DNIaYmgP.png +0 -0
  263. data/docs/assets/new-post-editor.DrHr-5oh.png +0 -0
  264. data/docs/assets/new-post-home-page.Bm34lyMg.png +0 -0
  265. data/docs/assets/{recipes_migrations.md.DPN3gQE3.js → recipes_migrations.md.CTcnWDJF.js} +1 -1
  266. data/docs/assets/styled-form-with-server-side-violations.Bjxd8Dpv.png +0 -0
  267. data/docs/assets/styled-form-with-violations.Bv_sa9tg.png +0 -0
  268. data/docs/assets/styled-home-page-with-posts.Dd4kG89D.png +0 -0
  269. data/docs/assets/styled-home-page.BzdI7dWz.png +0 -0
  270. data/docs/assets/tutorial.md.C4zR5XPG.js +728 -0
  271. data/docs/assets/tutorial.md.C4zR5XPG.lean.js +1 -0
  272. data/docs/assets/welcome-to-brut.VSWzl17-.png +0 -0
  273. data/docs/assets.html +3 -3
  274. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  275. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  276. data/docs/brut-js/api/Autosubmit.html +1 -1
  277. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  278. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  279. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  280. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  281. data/docs/brut-js/api/BufferedLogger.html +1 -1
  282. data/docs/brut-js/api/ConfirmSubmit.html +18 -10
  283. data/docs/brut-js/api/ConfirmSubmit.js.html +29 -8
  284. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  285. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  286. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  287. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  288. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  289. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  290. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  291. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  292. data/docs/brut-js/api/Form.html +1 -1
  293. data/docs/brut-js/api/Form.js.html +1 -1
  294. data/docs/brut-js/api/I18nTranslation.html +1 -1
  295. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  296. data/docs/brut-js/api/LocaleDetection.html +1 -1
  297. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  298. data/docs/brut-js/api/Logger.html +1 -1
  299. data/docs/brut-js/api/Logger.js.html +1 -1
  300. data/docs/brut-js/api/Message.html +1 -1
  301. data/docs/brut-js/api/Message.js.html +1 -1
  302. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  303. data/docs/brut-js/api/RichString.html +1 -1
  304. data/docs/brut-js/api/RichString.js.html +1 -1
  305. data/docs/brut-js/api/Tabs.html +1 -1
  306. data/docs/brut-js/api/Tabs.js.html +1 -1
  307. data/docs/brut-js/api/Tracing.html +1 -1
  308. data/docs/brut-js/api/Tracing.js.html +1 -1
  309. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  310. data/docs/brut-js/api/external-Performance.html +1 -1
  311. data/docs/brut-js/api/external-Promise.html +1 -1
  312. data/docs/brut-js/api/external-ValidityState.html +1 -1
  313. data/docs/brut-js/api/external-Window.html +1 -1
  314. data/docs/brut-js/api/external-fetch.html +1 -1
  315. data/docs/brut-js/api/global.html +1 -1
  316. data/docs/brut-js/api/index.html +1 -1
  317. data/docs/brut-js/api/index.js.html +1 -1
  318. data/docs/brut-js/api/module-testing.html +1 -1
  319. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  320. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  321. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  322. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  323. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  324. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  325. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  326. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  327. data/docs/brut-js/api/testing_index.js.html +1 -1
  328. data/docs/brut-js.html +3 -3
  329. data/docs/business-logic.html +3 -3
  330. data/docs/cli.html +3 -3
  331. data/docs/components.html +7 -7
  332. data/docs/configuration.html +5 -5
  333. data/docs/css.html +3 -3
  334. data/docs/custom-element-tests.html +3 -3
  335. data/docs/database-access.html +3 -3
  336. data/docs/database-schema.html +5 -5
  337. data/docs/deployment.html +3 -3
  338. data/docs/dev-environment.html +3 -3
  339. data/docs/dir-structure.html +3 -3
  340. data/docs/doc-conventions.html +3 -3
  341. data/docs/end-to-end-tests.html +3 -3
  342. data/docs/features.html +3 -3
  343. data/docs/flash-and-session.html +3 -3
  344. data/docs/form-constraints.html +3 -3
  345. data/docs/forms.html +5 -5
  346. data/docs/getting-started.html +6 -6
  347. data/docs/handlers.html +3 -3
  348. data/docs/hashmap.json +1 -1
  349. data/docs/hooks.html +3 -3
  350. data/docs/i18n.html +3 -3
  351. data/docs/index.html +3 -3
  352. data/docs/instrumentation.html +3 -3
  353. data/docs/javascript.html +3 -3
  354. data/docs/jobs.html +3 -3
  355. data/docs/keyword-injection.html +3 -3
  356. data/docs/layouts.html +3 -3
  357. data/docs/lsp.html +3 -3
  358. data/docs/markdown-examples.html +3 -3
  359. data/docs/middleware.html +3 -3
  360. data/docs/overview.html +3 -3
  361. data/docs/pages.html +3 -3
  362. data/docs/recipes/alternate-layouts.html +3 -3
  363. data/docs/recipes/authentication.html +3 -3
  364. data/docs/recipes/blank-layouts.html +3 -3
  365. data/docs/recipes/custom-flash.html +3 -3
  366. data/docs/recipes/indexed-forms.html +3 -3
  367. data/docs/recipes/migrations.html +5 -5
  368. data/docs/recipes/text-field-component.html +3 -3
  369. data/docs/roadmap.html +3 -3
  370. data/docs/routes.html +3 -3
  371. data/docs/security.html +3 -3
  372. data/docs/seed-data.html +3 -3
  373. data/docs/space-time-continuum.html +3 -3
  374. data/docs/tutorial.html +732 -5
  375. data/docs/unit-tests.html +3 -3
  376. data/docs/why.html +3 -3
  377. data/dx/build +45 -9
  378. data/lib/brut/cli/app.rb +1 -1
  379. data/lib/brut/cli/apps/heroku_container_based_deploy.rb +1 -1
  380. data/lib/brut/cli/apps/scaffold.rb +1 -1
  381. data/lib/brut/cli/options.rb +4 -0
  382. data/lib/brut/factory_bot.rb +1 -1
  383. data/lib/brut/framework/app.rb +1 -1
  384. data/lib/brut/framework/config.rb +18 -12
  385. data/lib/brut/framework/mcp.rb +12 -7
  386. data/lib/brut/front_end/csrf_protector.rb +30 -0
  387. data/lib/brut/front_end/form.rb +4 -4
  388. data/lib/brut/front_end/page.rb +1 -1
  389. data/lib/brut/front_end/routing.rb +1 -1
  390. data/lib/brut/front_end.rb +1 -0
  391. data/lib/brut/i18n/base_methods.rb +1 -1
  392. data/lib/brut/instrumentation/logger_span_exporter.rb +1 -1
  393. data/lib/brut/junk_drawer.rb +3 -1
  394. data/lib/brut/rubocop_config.rb +123 -0
  395. data/lib/brut/spec_support/component_support.rb +0 -3
  396. data/lib/brut/spec_support/rspec_setup.rb +2 -2
  397. data/lib/brut/version.rb +1 -1
  398. data/mkbrut/Gemfile.lock +1 -1
  399. data/mkbrut/dx +1 -0
  400. data/mkbrut/lib/mkbrut/app.rb +1 -1
  401. data/mkbrut/lib/mkbrut/cli.rb +1 -1
  402. data/mkbrut/lib/mkbrut/segments/heroku.rb +1 -1
  403. data/mkbrut/lib/mkbrut/version.rb +1 -1
  404. data/mkbrut/mkbrut.gemspec +3 -1
  405. data/mkbrut/templates/Base/app/config/i18n/en/1_defaults.rb +2 -2
  406. data/mkbrut/templates/Base/app/config/i18n/en/2_app.rb +1 -1
  407. data/mkbrut/templates/Base/bin/setup +1 -1
  408. data/mkbrut/templates/segments/Demo/app/src/front_end/components/flash_component.rb +1 -1
  409. data/specs/brut/junk_drawer.spec.rb +4 -0
  410. metadata +92 -35
  411. data/.rspec +0 -3
  412. data/docs/assets/chunks/@localSearchIndexroot.CmtZyrFA.js +0 -1
  413. data/docs/assets/tutorial.md.BIb7XT6j.js +0 -1
  414. data/docs/assets/tutorial.md.BIb7XT6j.lean.js +0 -1
  415. /data/docs/assets/{components.md.DHh-NwKs.lean.js → components.md.Bu80E2Nr.lean.js} +0 -0
  416. /data/docs/assets/{configuration.md.D8Wz3oJU.lean.js → configuration.md.CuIxVsSf.lean.js} +0 -0
  417. /data/docs/assets/{database-schema.md.C5gXexJi.lean.js → database-schema.md.LpmBPVEU.lean.js} +0 -0
  418. /data/docs/assets/{forms.md.BRE85eju.lean.js → forms.md.DnLbzVDa.lean.js} +0 -0
  419. /data/docs/assets/{getting-started.md.2ioiTe-B.lean.js → getting-started.md.DdQLmU3C.lean.js} +0 -0
  420. /data/docs/assets/{recipes_migrations.md.DPN3gQE3.lean.js → recipes_migrations.md.CTcnWDJF.lean.js} +0 -0
data/docs/unit-tests.html CHANGED
@@ -9,8 +9,8 @@
9
9
  <link rel="preload stylesheet" href="/assets/style.B1z60PPQ.css" as="style">
10
10
  <link rel="preload stylesheet" href="/vp-icons.css" as="style">
11
11
 
12
- <script type="module" src="/assets/app.DyQLb4Ot.js"></script>
13
- <link rel="modulepreload" href="/assets/chunks/theme.ChwsbWjK.js">
12
+ <script type="module" src="/assets/app.BuBdZoUg.js"></script>
13
+ <link rel="modulepreload" href="/assets/chunks/theme.DqwvuBEK.js">
14
14
  <link rel="modulepreload" href="/assets/chunks/framework.1L-BeKqY.js">
15
15
  <link rel="modulepreload" href="/assets/unit-tests.md.DUGrnLj5.lean.js">
16
16
  <link rel="icon" href="/favicon.ico">
@@ -35,7 +35,7 @@
35
35
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> FactoryBot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">lint</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> traits:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span></span>
36
36
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
37
37
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This implies that each factory and each trait of that factory can be created without providing any additional attributes. This is <em>critical</em> to sustainable tests over time. If any factory can be created at any time without dependencies, your tests will be easy to write and maintain.</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 9, 2025</em></p></div></div></main><footer class="VPDocFooter" data-v-e6f2a212 data-v-1bcd8184><!--[--><!--]--><!----><nav class="prev-next" aria-labelledby="doc-footer-aria-label" data-v-1bcd8184><span class="visually-hidden" id="doc-footer-aria-label" data-v-1bcd8184>Pager</span><div class="pager" data-v-1bcd8184><a class="VPLink link pager-link prev" href="/deployment.html" data-v-1bcd8184><!--[--><span class="desc" data-v-1bcd8184>Previous page</span><span class="title" data-v-1bcd8184>Deployment</span><!--]--></a></div><div class="pager" data-v-1bcd8184><a class="VPLink link pager-link next" href="/end-to-end-tests.html" data-v-1bcd8184><!--[--><span class="desc" data-v-1bcd8184>Next page</span><span class="title" data-v-1bcd8184>End-to-End Tests</span><!--]--></a></div></nav></footer><!--[--><!--]--></div></div></div><!--[--><!--]--></div></div><!----><!--[--><!--]--></div></div>
38
- <script>window.__VP_HASH_MAP__=JSON.parse("{\"adrs.md\":\"BxjHi9-8\",\"ai.md\":\"Cy9GWnER\",\"assets.md\":\"7C3HWkga\",\"brut-js.md\":\"B4GYxQVw\",\"business-logic.md\":\"BY4hGy0m\",\"cli.md\":\"CjsktgFz\",\"components.md\":\"DHh-NwKs\",\"configuration.md\":\"D8Wz3oJU\",\"css.md\":\"CltvJqAa\",\"custom-element-tests.md\":\"B_rbta32\",\"database-access.md\":\"gnluu54N\",\"database-schema.md\":\"C5gXexJi\",\"deployment.md\":\"BLseERGV\",\"dev-environment.md\":\"DRH2D2-O\",\"dir-structure.md\":\"CWir1pic\",\"doc-conventions.md\":\"DOkAuXlt\",\"end-to-end-tests.md\":\"DzqRpZ43\",\"features.md\":\"DPFXsy0z\",\"flash-and-session.md\":\"nPvUpnUx\",\"form-constraints.md\":\"DK5adCgM\",\"forms.md\":\"BRE85eju\",\"getting-started.md\":\"2ioiTe-B\",\"handlers.md\":\"h84MMB1R\",\"hooks.md\":\"Jmb5VOLA\",\"i18n.md\":\"BAm9t9JJ\",\"index.md\":\"Bn9e0sRJ\",\"instrumentation.md\":\"BgcaGVYH\",\"javascript.md\":\"DzrMxUmI\",\"jobs.md\":\"S-2amAYp\",\"keyword-injection.md\":\"95Zgh2eN\",\"layouts.md\":\"CVGl9xIO\",\"lsp.md\":\"Dn1rIiW0\",\"markdown-examples.md\":\"CCFEQO44\",\"middleware.md\":\"Czz_UlJN\",\"overview.md\":\"DlKiRRG_\",\"pages.md\":\"B7Hc-i6H\",\"recipes_alternate-layouts.md\":\"BwEytl59\",\"recipes_authentication.md\":\"Dzvi_g69\",\"recipes_blank-layouts.md\":\"fyAUJyJR\",\"recipes_custom-flash.md\":\"CrQbI5eH\",\"recipes_indexed-forms.md\":\"CstYyOSo\",\"recipes_migrations.md\":\"DPN3gQE3\",\"recipes_text-field-component.md\":\"H4wLAK0Z\",\"roadmap.md\":\"C6PRi0DX\",\"routes.md\":\"BD6y2i-f\",\"security.md\":\"C0G_AZR-\",\"seed-data.md\":\"BvFZlqIk\",\"space-time-continuum.md\":\"xl44xDos\",\"tutorial.md\":\"BIb7XT6j\",\"unit-tests.md\":\"DUGrnLj5\",\"why.md\":\"C-hk5xgJ\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"Brut RB\",\"description\":\"Documentation for the Brut.RB web framework.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"search\":{\"provider\":\"local\"},\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Overview\",\"link\":\"/overview\"},{\"text\":\"Brut API\",\"link\":\"/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutCSS\",\"link\":\"/brut-css/index.html\",\"target\":\"_self\"}],\"outline\":[2,3],\"sidebar\":[{\"text\":\"Overview\",\"collapsed\":false,\"items\":[{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Concepts\",\"link\":\"/overview\"},{\"text\":\"Features\",\"link\":\"/features\"},{\"text\":\"Directory Structure\",\"link\":\"/dir-structure\"},{\"text\":\"Dev Environment\",\"link\":\"/dev-environment\"},{\"text\":\"Tutorial\",\"link\":\"/tutorial\"},{\"text\":\"Documentation Conventions\",\"link\":\"/doc-conventions\"}]},{\"text\":\"Front-End\",\"collapsed\":false,\"items\":[{\"text\":\"Routes\",\"link\":\"/routes\"},{\"text\":\"Pages\",\"link\":\"/pages\"},{\"text\":\"Layouts\",\"link\":\"/layouts\"},{\"text\":\"Forms\",\"link\":\"/forms\"},{\"text\":\"Form Constraints\",\"link\":\"/form-constraints\"},{\"text\":\"Handlers and Actions\",\"link\":\"/handlers\"},{\"text\":\"Components\",\"link\":\"/components\"},{\"text\":\"Flash and Session\",\"link\":\"/flash-and-session\"},{\"text\":\"Space/Time Continuum\",\"link\":\"/space-time-continuum\"},{\"text\":\"JavaScript\",\"link\":\"/javascript\"},{\"text\":\"CSS\",\"link\":\"/css\"},{\"text\":\"Assets\",\"link\":\"/assets\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js\"}]},{\"text\":\"Back-End\",\"collapsed\":false,\"items\":[{\"text\":\"Database Schema\",\"link\":\"/database-schema\"},{\"text\":\"Database Access\",\"link\":\"/database-access\"},{\"text\":\"Seed Data\",\"link\":\"/seed-data\"},{\"text\":\"Jobs\",\"link\":\"/jobs\"},{\"text\":\"Business Logic\",\"link\":\"/business-logic\"}]},{\"text\":\"Framework\",\"collapsed\":false,\"items\":[{\"text\":\"Configuration\",\"link\":\"/configuration\"},{\"text\":\"Keyword Injection\",\"link\":\"/keyword-injection\"},{\"text\":\"I18n\",\"link\":\"/i18n\"},{\"text\":\"CLI / Tasks\",\"link\":\"/cli\"},{\"text\":\"Deployment\",\"link\":\"/deployment\"}]},{\"text\":\"Testing\",\"collapsed\":false,\"items\":[{\"text\":\"Unit Tests\",\"link\":\"/unit-tests\"},{\"text\":\"End-to-End Tests\",\"link\":\"/end-to-end-tests\"},{\"text\":\"Testing Custom Elements\",\"link\":\"/custom-element-tests\"}]},{\"text\":\"Advanced Topics\",\"collapsed\":true,\"items\":[{\"text\":\"Route Hooks\",\"link\":\"/hooks\"},{\"text\":\"Middleware\",\"link\":\"/middleware\"},{\"text\":\"Instrumentation\",\"link\":\"/instrumentation\"},{\"text\":\"Security\",\"link\":\"/security\"},{\"text\":\"LSP Support\",\"link\":\"/lsp\"}]},{\"text\":\"Recipes\",\"collapsed\":true,\"items\":[{\"text\":\"Migration Basics\",\"link\":\"/recipes/migrations\"},{\"text\":\"Authentication\",\"link\":\"/recipes/authentication\"},{\"text\":\"Alternate Layouts\",\"link\":\"/recipes/alternate-layouts\"},{\"text\":\"Blank Layouts\",\"link\":\"/recipes/blank-layouts\"},{\"text\":\"Custom Flash Class\",\"link\":\"/recipes/custom-flash\"},{\"text\":\"Indexed Form Elements\",\"link\":\"/recipes/indexed-forms\"},{\"text\":\"Text Field Component\",\"link\":\"/recipes/text-field-component\"}]},{\"text\":\"Meta\",\"collapsed\":false,\"items\":[{\"text\":\"Why?!\",\"link\":\"/why\"},{\"text\":\"ADRs\",\"link\":\"/adrs\"},{\"text\":\"Roadmap to 1.0\",\"link\":\"/roadmap\"},{\"text\":\"AI Declaration\",\"link\":\"/ai\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/thirdtank/brut\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
38
+ <script>window.__VP_HASH_MAP__=JSON.parse("{\"adrs.md\":\"BxjHi9-8\",\"ai.md\":\"Cy9GWnER\",\"assets.md\":\"7C3HWkga\",\"brut-js.md\":\"B4GYxQVw\",\"business-logic.md\":\"BY4hGy0m\",\"cli.md\":\"CjsktgFz\",\"components.md\":\"Bu80E2Nr\",\"configuration.md\":\"CuIxVsSf\",\"css.md\":\"CltvJqAa\",\"custom-element-tests.md\":\"B_rbta32\",\"database-access.md\":\"gnluu54N\",\"database-schema.md\":\"LpmBPVEU\",\"deployment.md\":\"BLseERGV\",\"dev-environment.md\":\"DRH2D2-O\",\"dir-structure.md\":\"CWir1pic\",\"doc-conventions.md\":\"DOkAuXlt\",\"end-to-end-tests.md\":\"DzqRpZ43\",\"features.md\":\"DPFXsy0z\",\"flash-and-session.md\":\"nPvUpnUx\",\"form-constraints.md\":\"DK5adCgM\",\"forms.md\":\"DnLbzVDa\",\"getting-started.md\":\"DdQLmU3C\",\"handlers.md\":\"h84MMB1R\",\"hooks.md\":\"Jmb5VOLA\",\"i18n.md\":\"BAm9t9JJ\",\"index.md\":\"Bn9e0sRJ\",\"instrumentation.md\":\"BgcaGVYH\",\"javascript.md\":\"DzrMxUmI\",\"jobs.md\":\"S-2amAYp\",\"keyword-injection.md\":\"95Zgh2eN\",\"layouts.md\":\"CVGl9xIO\",\"lsp.md\":\"Dn1rIiW0\",\"markdown-examples.md\":\"CCFEQO44\",\"middleware.md\":\"Czz_UlJN\",\"overview.md\":\"DlKiRRG_\",\"pages.md\":\"B7Hc-i6H\",\"recipes_alternate-layouts.md\":\"BwEytl59\",\"recipes_authentication.md\":\"Dzvi_g69\",\"recipes_blank-layouts.md\":\"fyAUJyJR\",\"recipes_custom-flash.md\":\"CrQbI5eH\",\"recipes_indexed-forms.md\":\"CstYyOSo\",\"recipes_migrations.md\":\"CTcnWDJF\",\"recipes_text-field-component.md\":\"H4wLAK0Z\",\"roadmap.md\":\"C6PRi0DX\",\"routes.md\":\"BD6y2i-f\",\"security.md\":\"C0G_AZR-\",\"seed-data.md\":\"BvFZlqIk\",\"space-time-continuum.md\":\"xl44xDos\",\"tutorial.md\":\"C4zR5XPG\",\"unit-tests.md\":\"DUGrnLj5\",\"why.md\":\"C-hk5xgJ\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"Brut RB\",\"description\":\"Documentation for the Brut.RB web framework.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"search\":{\"provider\":\"local\"},\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Overview\",\"link\":\"/overview\"},{\"text\":\"Brut API\",\"link\":\"/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutCSS\",\"link\":\"/brut-css/index.html\",\"target\":\"_self\"}],\"outline\":[2,3],\"sidebar\":[{\"text\":\"Overview\",\"collapsed\":false,\"items\":[{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Concepts\",\"link\":\"/overview\"},{\"text\":\"Features\",\"link\":\"/features\"},{\"text\":\"Directory Structure\",\"link\":\"/dir-structure\"},{\"text\":\"Dev Environment\",\"link\":\"/dev-environment\"},{\"text\":\"Tutorial\",\"link\":\"/tutorial\"},{\"text\":\"Documentation Conventions\",\"link\":\"/doc-conventions\"}]},{\"text\":\"Front-End\",\"collapsed\":false,\"items\":[{\"text\":\"Routes\",\"link\":\"/routes\"},{\"text\":\"Pages\",\"link\":\"/pages\"},{\"text\":\"Layouts\",\"link\":\"/layouts\"},{\"text\":\"Forms\",\"link\":\"/forms\"},{\"text\":\"Form Constraints\",\"link\":\"/form-constraints\"},{\"text\":\"Handlers and Actions\",\"link\":\"/handlers\"},{\"text\":\"Components\",\"link\":\"/components\"},{\"text\":\"Flash and Session\",\"link\":\"/flash-and-session\"},{\"text\":\"Space/Time Continuum\",\"link\":\"/space-time-continuum\"},{\"text\":\"JavaScript\",\"link\":\"/javascript\"},{\"text\":\"CSS\",\"link\":\"/css\"},{\"text\":\"Assets\",\"link\":\"/assets\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js\"}]},{\"text\":\"Back-End\",\"collapsed\":false,\"items\":[{\"text\":\"Database Schema\",\"link\":\"/database-schema\"},{\"text\":\"Database Access\",\"link\":\"/database-access\"},{\"text\":\"Seed Data\",\"link\":\"/seed-data\"},{\"text\":\"Jobs\",\"link\":\"/jobs\"},{\"text\":\"Business Logic\",\"link\":\"/business-logic\"}]},{\"text\":\"Framework\",\"collapsed\":false,\"items\":[{\"text\":\"Configuration\",\"link\":\"/configuration\"},{\"text\":\"Keyword Injection\",\"link\":\"/keyword-injection\"},{\"text\":\"I18n\",\"link\":\"/i18n\"},{\"text\":\"CLI / Tasks\",\"link\":\"/cli\"},{\"text\":\"Deployment\",\"link\":\"/deployment\"}]},{\"text\":\"Testing\",\"collapsed\":false,\"items\":[{\"text\":\"Unit Tests\",\"link\":\"/unit-tests\"},{\"text\":\"End-to-End Tests\",\"link\":\"/end-to-end-tests\"},{\"text\":\"Testing Custom Elements\",\"link\":\"/custom-element-tests\"}]},{\"text\":\"Advanced Topics\",\"collapsed\":true,\"items\":[{\"text\":\"Route Hooks\",\"link\":\"/hooks\"},{\"text\":\"Middleware\",\"link\":\"/middleware\"},{\"text\":\"Instrumentation\",\"link\":\"/instrumentation\"},{\"text\":\"Security\",\"link\":\"/security\"},{\"text\":\"LSP Support\",\"link\":\"/lsp\"}]},{\"text\":\"Recipes\",\"collapsed\":true,\"items\":[{\"text\":\"Migration Basics\",\"link\":\"/recipes/migrations\"},{\"text\":\"Authentication\",\"link\":\"/recipes/authentication\"},{\"text\":\"Alternate Layouts\",\"link\":\"/recipes/alternate-layouts\"},{\"text\":\"Blank Layouts\",\"link\":\"/recipes/blank-layouts\"},{\"text\":\"Custom Flash Class\",\"link\":\"/recipes/custom-flash\"},{\"text\":\"Indexed Form Elements\",\"link\":\"/recipes/indexed-forms\"},{\"text\":\"Text Field Component\",\"link\":\"/recipes/text-field-component\"}]},{\"text\":\"Meta\",\"collapsed\":false,\"items\":[{\"text\":\"Why?!\",\"link\":\"/why\"},{\"text\":\"ADRs\",\"link\":\"/adrs\"},{\"text\":\"Roadmap to 1.0\",\"link\":\"/roadmap\"},{\"text\":\"AI Declaration\",\"link\":\"/ai\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/thirdtank/brut\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
39
39
 
40
40
  </body>
41
41
  </html>
data/docs/why.html CHANGED
@@ -9,8 +9,8 @@
9
9
  <link rel="preload stylesheet" href="/assets/style.B1z60PPQ.css" as="style">
10
10
  <link rel="preload stylesheet" href="/vp-icons.css" as="style">
11
11
 
12
- <script type="module" src="/assets/app.DyQLb4Ot.js"></script>
13
- <link rel="modulepreload" href="/assets/chunks/theme.ChwsbWjK.js">
12
+ <script type="module" src="/assets/app.BuBdZoUg.js"></script>
13
+ <link rel="modulepreload" href="/assets/chunks/theme.DqwvuBEK.js">
14
14
  <link rel="modulepreload" href="/assets/chunks/framework.1L-BeKqY.js">
15
15
  <link rel="modulepreload" href="/assets/why.md.C-hk5xgJ.lean.js">
16
16
  <link rel="icon" href="/favicon.ico">
@@ -23,7 +23,7 @@
23
23
  </head>
24
24
  <body>
25
25
  <div id="app"><div class="Layout" data-v-d8b57b2d><!--[--><!--]--><!--[--><span tabindex="-1" data-v-fcbfc0e0></span><a href="#VPContent" class="VPSkipLink visually-hidden" data-v-fcbfc0e0>Skip to content</a><!--]--><!----><header class="VPNav" data-v-d8b57b2d data-v-7ad780c2><div class="VPNavBar" data-v-7ad780c2 data-v-9fd4d1dd><div class="wrapper" data-v-9fd4d1dd><div class="container" data-v-9fd4d1dd><div class="title" data-v-9fd4d1dd><div class="VPNavBarTitle has-sidebar" data-v-9fd4d1dd data-v-9f43907a><a class="title" href="/" data-v-9f43907a><!--[--><!--]--><!----><span data-v-9f43907a>Brut RB</span><!--[--><!--]--></a></div></div><div class="content" data-v-9fd4d1dd><div class="content-body" data-v-9fd4d1dd><!--[--><!--]--><div class="VPNavBarSearch search" data-v-9fd4d1dd><!--[--><!----><div id="local-search"><button type="button" class="DocSearch DocSearch-Button" aria-label="Search"><span class="DocSearch-Button-Container"><span class="vp-icon DocSearch-Search-Icon"></span><span class="DocSearch-Button-Placeholder">Search</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button></div><!--]--></div><nav aria-labelledby="main-nav-aria-label" class="VPNavBarMenu menu" data-v-9fd4d1dd data-v-afb2845e><span id="main-nav-aria-label" class="visually-hidden" data-v-afb2845e> Main Navigation </span><!--[--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Home</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/getting-started.html" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Getting Started</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/overview.html" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Overview</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/api/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>Brut API</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/brut-js/api/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>BrutJS</span><!--]--></a><!--]--><!--[--><a class="VPLink link VPNavBarMenuLink" href="/brut-css/index.html" target="_self" tabindex="0" data-v-afb2845e data-v-815115f5><!--[--><span data-v-815115f5>BrutCSS</span><!--]--></a><!--]--><!--]--></nav><!----><div class="VPNavBarAppearance appearance" data-v-9fd4d1dd data-v-3f90c1a5><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title aria-checked="false" data-v-3f90c1a5 data-v-be9742d9 data-v-b4ccac88><span class="check" data-v-b4ccac88><span class="icon" data-v-b4ccac88><!--[--><span class="vpi-sun sun" data-v-be9742d9></span><span class="vpi-moon moon" data-v-be9742d9></span><!--]--></span></span></button></div><div class="VPSocialLinks VPNavBarSocialLinks social-links" data-v-9fd4d1dd data-v-ef6192dc data-v-e71e869c><!--[--><a class="VPSocialLink no-icon" href="https://github.com/thirdtank/brut" aria-label="github" target="_blank" rel="noopener" data-v-e71e869c data-v-60a9a2d3><span class="vpi-social-github"></span></a><!--]--></div><div class="VPFlyout VPNavBarExtra extra" data-v-9fd4d1dd data-v-f953d92f data-v-bfe7971f><button type="button" class="button" aria-haspopup="true" aria-expanded="false" aria-label="extra navigation" data-v-bfe7971f><span class="vpi-more-horizontal icon" data-v-bfe7971f></span></button><div class="menu" data-v-bfe7971f><div class="VPMenu" data-v-bfe7971f data-v-20ed86d6><!----><!--[--><!--[--><!----><div class="group" data-v-f953d92f><div class="item appearance" data-v-f953d92f><p class="label" data-v-f953d92f>Appearance</p><div class="appearance-action" data-v-f953d92f><button class="VPSwitch VPSwitchAppearance" type="button" role="switch" title aria-checked="false" data-v-f953d92f data-v-be9742d9 data-v-b4ccac88><span class="check" data-v-b4ccac88><span class="icon" data-v-b4ccac88><!--[--><span class="vpi-sun sun" data-v-be9742d9></span><span class="vpi-moon moon" data-v-be9742d9></span><!--]--></span></span></button></div></div></div><div class="group" data-v-f953d92f><div class="item social-links" data-v-f953d92f><div class="VPSocialLinks social-links-list" data-v-f953d92f data-v-e71e869c><!--[--><a class="VPSocialLink no-icon" href="https://github.com/thirdtank/brut" aria-label="github" target="_blank" rel="noopener" data-v-e71e869c data-v-60a9a2d3><span class="vpi-social-github"></span></a><!--]--></div></div></div><!--]--><!--]--></div></div></div><!--[--><!--]--><button type="button" class="VPNavBarHamburger hamburger" aria-label="mobile navigation" aria-expanded="false" aria-controls="VPNavScreen" data-v-9fd4d1dd data-v-6bee1efd><span class="container" data-v-6bee1efd><span class="top" data-v-6bee1efd></span><span class="middle" data-v-6bee1efd></span><span class="bottom" data-v-6bee1efd></span></span></button></div></div></div></div><div class="divider" data-v-9fd4d1dd><div class="divider-line" data-v-9fd4d1dd></div></div></div><!----></header><div class="VPLocalNav has-sidebar empty" data-v-d8b57b2d data-v-2488c25a><div class="container" data-v-2488c25a><button class="menu" aria-expanded="false" aria-controls="VPSidebarNav" data-v-2488c25a><span class="vpi-align-left menu-icon" data-v-2488c25a></span><span class="menu-text" data-v-2488c25a>Menu</span></button><div class="VPLocalNavOutlineDropdown" style="--vp-vh:0px;" data-v-2488c25a data-v-6b867909><button data-v-6b867909>Return to top</button><!----></div></div></div><aside class="VPSidebar" data-v-d8b57b2d data-v-42c4c606><div class="curtain" data-v-42c4c606></div><nav class="nav" id="VPSidebarNav" aria-labelledby="sidebar-aria-label" tabindex="-1" data-v-42c4c606><span class="visually-hidden" id="sidebar-aria-label" data-v-42c4c606> Sidebar Navigation </span><!--[--><!--]--><!--[--><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Overview</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/getting-started.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Getting Started</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/overview.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Concepts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/features.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Features</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/dir-structure.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Directory Structure</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/dev-environment.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Dev Environment</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/tutorial.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Tutorial</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/doc-conventions.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Documentation Conventions</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Front-End</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/routes.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Routes</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/pages.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Pages</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/layouts.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Layouts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/forms.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Forms</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/form-constraints.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Form Constraints</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/handlers.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Handlers and Actions</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/components.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Components</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/flash-and-session.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Flash and Session</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/space-time-continuum.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Space/Time Continuum</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/javascript.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>JavaScript</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/css.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>CSS</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/assets.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Assets</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/brut-js.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>BrutJS</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Back-End</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/database-schema.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Database Schema</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/database-access.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Database Access</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/seed-data.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Seed Data</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/jobs.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Jobs</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/business-logic.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Business Logic</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Framework</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/configuration.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Configuration</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/keyword-injection.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Keyword Injection</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/i18n.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>I18n</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/cli.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>CLI / Tasks</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/deployment.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Deployment</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Testing</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/unit-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Unit Tests</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/end-to-end-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>End-to-End Tests</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/custom-element-tests.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Testing Custom Elements</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible collapsed" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Advanced Topics</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/hooks.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Route Hooks</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/middleware.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Middleware</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/instrumentation.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Instrumentation</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/security.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Security</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/lsp.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>LSP Support</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible collapsed" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Recipes</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/migrations.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Migration Basics</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/authentication.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Authentication</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/alternate-layouts.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Alternate Layouts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/blank-layouts.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Blank Layouts</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/custom-flash.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Custom Flash Class</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/indexed-forms.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Indexed Form Elements</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/recipes/text-field-component.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Text Field Component</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><div class="no-transition group" data-v-51288d80><section class="VPSidebarItem level-0 collapsible has-active" data-v-51288d80 data-v-0009425e><div class="item" role="button" tabindex="0" data-v-0009425e><div class="indicator" data-v-0009425e></div><h2 class="text" data-v-0009425e>Meta</h2><div class="caret" role="button" aria-label="toggle section" tabindex="0" data-v-0009425e><span class="vpi-chevron-right caret-icon" data-v-0009425e></span></div></div><div class="items" data-v-0009425e><!--[--><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/why.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Why?!</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/adrs.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>ADRs</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/roadmap.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>Roadmap to 1.0</p><!--]--></a><!----></div><!----></div><div class="VPSidebarItem level-1 is-link" data-v-0009425e data-v-0009425e><div class="item" data-v-0009425e><div class="indicator" data-v-0009425e></div><a class="VPLink link link" href="/ai.html" data-v-0009425e><!--[--><p class="text" data-v-0009425e>AI Declaration</p><!--]--></a><!----></div><!----></div><!--]--></div></section></div><!--]--><!--[--><!--]--></nav></aside><div class="VPContent has-sidebar" id="VPContent" data-v-d8b57b2d data-v-9a6c75ad><div class="VPDoc has-sidebar has-aside" data-v-9a6c75ad data-v-e6f2a212><!--[--><!--]--><div class="container" data-v-e6f2a212><div class="aside" data-v-e6f2a212><div class="aside-curtain" data-v-e6f2a212></div><div class="aside-container" data-v-e6f2a212><div class="aside-content" data-v-e6f2a212><div class="VPDocAside" data-v-e6f2a212 data-v-cb998dce><!--[--><!--]--><!--[--><!--]--><nav aria-labelledby="doc-outline-aria-label" class="VPDocAsideOutline" data-v-cb998dce data-v-f610f197><div class="content" data-v-f610f197><div class="outline-marker" data-v-f610f197></div><div aria-level="2" class="outline-title" id="doc-outline-aria-label" role="heading" data-v-f610f197>On this page</div><ul class="VPDocOutlineItem root" data-v-f610f197 data-v-53c99d69><!--[--><!--]--></ul></div></nav><!--[--><!--]--><div class="spacer" data-v-cb998dce></div><!--[--><!--]--><!----><!--[--><!--]--><!--[--><!--]--></div></div></div></div><div class="content" data-v-e6f2a212><div class="content-container" data-v-e6f2a212><!--[--><!--]--><main class="main" data-v-e6f2a212><div style="position:relative;" class="vp-doc _why" data-v-e6f2a212><div><h1 id="why-does-brut-exist" tabindex="-1">Why Does Brut Exist? <a class="header-anchor" href="#why-does-brut-exist" aria-label="Permalink to &quot;Why Does Brut Exist?&quot;">​</a></h1><p>I love writing Ruby, but grew tired of writing Rails. Rails is great, and has been great to me over the years. I&#39;ve written a lot of books about it! But the churn and increasing configuration burden made me think: what if we had another way to build web apps in Ruby?</p><p>What if it was totally different, but still focused on being straightforward and simple? What if it had <em>fewer</em> abstractions, <em>less</em> configuration, and not as much <em>stuff</em>?</p><p>My thinking is, you need to know HTML, JavaScript, CSS, SQL, Ruby, HTTP, and a few other things to make a web app. What if we tried to limit the additional abstractions you&#39;d have to learn?</p><p>That&#39;s what Brut is trying to be. Straightfoward, direct abstractions or translations of stuff you already know. The raw web…or at least as raw as it can be.</p></div></div></main><footer class="VPDocFooter" data-v-e6f2a212 data-v-1bcd8184><!--[--><!--]--><!----><nav class="prev-next" aria-labelledby="doc-footer-aria-label" data-v-1bcd8184><span class="visually-hidden" id="doc-footer-aria-label" data-v-1bcd8184>Pager</span><div class="pager" data-v-1bcd8184><a class="VPLink link pager-link prev" href="/recipes/text-field-component.html" data-v-1bcd8184><!--[--><span class="desc" data-v-1bcd8184>Previous page</span><span class="title" data-v-1bcd8184>Text Field Component</span><!--]--></a></div><div class="pager" data-v-1bcd8184><a class="VPLink link pager-link next" href="/adrs.html" data-v-1bcd8184><!--[--><span class="desc" data-v-1bcd8184>Next page</span><span class="title" data-v-1bcd8184>ADRs</span><!--]--></a></div></nav></footer><!--[--><!--]--></div></div></div><!--[--><!--]--></div></div><!----><!--[--><!--]--></div></div>
26
- <script>window.__VP_HASH_MAP__=JSON.parse("{\"adrs.md\":\"BxjHi9-8\",\"ai.md\":\"Cy9GWnER\",\"assets.md\":\"7C3HWkga\",\"brut-js.md\":\"B4GYxQVw\",\"business-logic.md\":\"BY4hGy0m\",\"cli.md\":\"CjsktgFz\",\"components.md\":\"DHh-NwKs\",\"configuration.md\":\"D8Wz3oJU\",\"css.md\":\"CltvJqAa\",\"custom-element-tests.md\":\"B_rbta32\",\"database-access.md\":\"gnluu54N\",\"database-schema.md\":\"C5gXexJi\",\"deployment.md\":\"BLseERGV\",\"dev-environment.md\":\"DRH2D2-O\",\"dir-structure.md\":\"CWir1pic\",\"doc-conventions.md\":\"DOkAuXlt\",\"end-to-end-tests.md\":\"DzqRpZ43\",\"features.md\":\"DPFXsy0z\",\"flash-and-session.md\":\"nPvUpnUx\",\"form-constraints.md\":\"DK5adCgM\",\"forms.md\":\"BRE85eju\",\"getting-started.md\":\"2ioiTe-B\",\"handlers.md\":\"h84MMB1R\",\"hooks.md\":\"Jmb5VOLA\",\"i18n.md\":\"BAm9t9JJ\",\"index.md\":\"Bn9e0sRJ\",\"instrumentation.md\":\"BgcaGVYH\",\"javascript.md\":\"DzrMxUmI\",\"jobs.md\":\"S-2amAYp\",\"keyword-injection.md\":\"95Zgh2eN\",\"layouts.md\":\"CVGl9xIO\",\"lsp.md\":\"Dn1rIiW0\",\"markdown-examples.md\":\"CCFEQO44\",\"middleware.md\":\"Czz_UlJN\",\"overview.md\":\"DlKiRRG_\",\"pages.md\":\"B7Hc-i6H\",\"recipes_alternate-layouts.md\":\"BwEytl59\",\"recipes_authentication.md\":\"Dzvi_g69\",\"recipes_blank-layouts.md\":\"fyAUJyJR\",\"recipes_custom-flash.md\":\"CrQbI5eH\",\"recipes_indexed-forms.md\":\"CstYyOSo\",\"recipes_migrations.md\":\"DPN3gQE3\",\"recipes_text-field-component.md\":\"H4wLAK0Z\",\"roadmap.md\":\"C6PRi0DX\",\"routes.md\":\"BD6y2i-f\",\"security.md\":\"C0G_AZR-\",\"seed-data.md\":\"BvFZlqIk\",\"space-time-continuum.md\":\"xl44xDos\",\"tutorial.md\":\"BIb7XT6j\",\"unit-tests.md\":\"DUGrnLj5\",\"why.md\":\"C-hk5xgJ\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"Brut RB\",\"description\":\"Documentation for the Brut.RB web framework.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"search\":{\"provider\":\"local\"},\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Overview\",\"link\":\"/overview\"},{\"text\":\"Brut API\",\"link\":\"/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutCSS\",\"link\":\"/brut-css/index.html\",\"target\":\"_self\"}],\"outline\":[2,3],\"sidebar\":[{\"text\":\"Overview\",\"collapsed\":false,\"items\":[{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Concepts\",\"link\":\"/overview\"},{\"text\":\"Features\",\"link\":\"/features\"},{\"text\":\"Directory Structure\",\"link\":\"/dir-structure\"},{\"text\":\"Dev Environment\",\"link\":\"/dev-environment\"},{\"text\":\"Tutorial\",\"link\":\"/tutorial\"},{\"text\":\"Documentation Conventions\",\"link\":\"/doc-conventions\"}]},{\"text\":\"Front-End\",\"collapsed\":false,\"items\":[{\"text\":\"Routes\",\"link\":\"/routes\"},{\"text\":\"Pages\",\"link\":\"/pages\"},{\"text\":\"Layouts\",\"link\":\"/layouts\"},{\"text\":\"Forms\",\"link\":\"/forms\"},{\"text\":\"Form Constraints\",\"link\":\"/form-constraints\"},{\"text\":\"Handlers and Actions\",\"link\":\"/handlers\"},{\"text\":\"Components\",\"link\":\"/components\"},{\"text\":\"Flash and Session\",\"link\":\"/flash-and-session\"},{\"text\":\"Space/Time Continuum\",\"link\":\"/space-time-continuum\"},{\"text\":\"JavaScript\",\"link\":\"/javascript\"},{\"text\":\"CSS\",\"link\":\"/css\"},{\"text\":\"Assets\",\"link\":\"/assets\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js\"}]},{\"text\":\"Back-End\",\"collapsed\":false,\"items\":[{\"text\":\"Database Schema\",\"link\":\"/database-schema\"},{\"text\":\"Database Access\",\"link\":\"/database-access\"},{\"text\":\"Seed Data\",\"link\":\"/seed-data\"},{\"text\":\"Jobs\",\"link\":\"/jobs\"},{\"text\":\"Business Logic\",\"link\":\"/business-logic\"}]},{\"text\":\"Framework\",\"collapsed\":false,\"items\":[{\"text\":\"Configuration\",\"link\":\"/configuration\"},{\"text\":\"Keyword Injection\",\"link\":\"/keyword-injection\"},{\"text\":\"I18n\",\"link\":\"/i18n\"},{\"text\":\"CLI / Tasks\",\"link\":\"/cli\"},{\"text\":\"Deployment\",\"link\":\"/deployment\"}]},{\"text\":\"Testing\",\"collapsed\":false,\"items\":[{\"text\":\"Unit Tests\",\"link\":\"/unit-tests\"},{\"text\":\"End-to-End Tests\",\"link\":\"/end-to-end-tests\"},{\"text\":\"Testing Custom Elements\",\"link\":\"/custom-element-tests\"}]},{\"text\":\"Advanced Topics\",\"collapsed\":true,\"items\":[{\"text\":\"Route Hooks\",\"link\":\"/hooks\"},{\"text\":\"Middleware\",\"link\":\"/middleware\"},{\"text\":\"Instrumentation\",\"link\":\"/instrumentation\"},{\"text\":\"Security\",\"link\":\"/security\"},{\"text\":\"LSP Support\",\"link\":\"/lsp\"}]},{\"text\":\"Recipes\",\"collapsed\":true,\"items\":[{\"text\":\"Migration Basics\",\"link\":\"/recipes/migrations\"},{\"text\":\"Authentication\",\"link\":\"/recipes/authentication\"},{\"text\":\"Alternate Layouts\",\"link\":\"/recipes/alternate-layouts\"},{\"text\":\"Blank Layouts\",\"link\":\"/recipes/blank-layouts\"},{\"text\":\"Custom Flash Class\",\"link\":\"/recipes/custom-flash\"},{\"text\":\"Indexed Form Elements\",\"link\":\"/recipes/indexed-forms\"},{\"text\":\"Text Field Component\",\"link\":\"/recipes/text-field-component\"}]},{\"text\":\"Meta\",\"collapsed\":false,\"items\":[{\"text\":\"Why?!\",\"link\":\"/why\"},{\"text\":\"ADRs\",\"link\":\"/adrs\"},{\"text\":\"Roadmap to 1.0\",\"link\":\"/roadmap\"},{\"text\":\"AI Declaration\",\"link\":\"/ai\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/thirdtank/brut\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
26
+ <script>window.__VP_HASH_MAP__=JSON.parse("{\"adrs.md\":\"BxjHi9-8\",\"ai.md\":\"Cy9GWnER\",\"assets.md\":\"7C3HWkga\",\"brut-js.md\":\"B4GYxQVw\",\"business-logic.md\":\"BY4hGy0m\",\"cli.md\":\"CjsktgFz\",\"components.md\":\"Bu80E2Nr\",\"configuration.md\":\"CuIxVsSf\",\"css.md\":\"CltvJqAa\",\"custom-element-tests.md\":\"B_rbta32\",\"database-access.md\":\"gnluu54N\",\"database-schema.md\":\"LpmBPVEU\",\"deployment.md\":\"BLseERGV\",\"dev-environment.md\":\"DRH2D2-O\",\"dir-structure.md\":\"CWir1pic\",\"doc-conventions.md\":\"DOkAuXlt\",\"end-to-end-tests.md\":\"DzqRpZ43\",\"features.md\":\"DPFXsy0z\",\"flash-and-session.md\":\"nPvUpnUx\",\"form-constraints.md\":\"DK5adCgM\",\"forms.md\":\"DnLbzVDa\",\"getting-started.md\":\"DdQLmU3C\",\"handlers.md\":\"h84MMB1R\",\"hooks.md\":\"Jmb5VOLA\",\"i18n.md\":\"BAm9t9JJ\",\"index.md\":\"Bn9e0sRJ\",\"instrumentation.md\":\"BgcaGVYH\",\"javascript.md\":\"DzrMxUmI\",\"jobs.md\":\"S-2amAYp\",\"keyword-injection.md\":\"95Zgh2eN\",\"layouts.md\":\"CVGl9xIO\",\"lsp.md\":\"Dn1rIiW0\",\"markdown-examples.md\":\"CCFEQO44\",\"middleware.md\":\"Czz_UlJN\",\"overview.md\":\"DlKiRRG_\",\"pages.md\":\"B7Hc-i6H\",\"recipes_alternate-layouts.md\":\"BwEytl59\",\"recipes_authentication.md\":\"Dzvi_g69\",\"recipes_blank-layouts.md\":\"fyAUJyJR\",\"recipes_custom-flash.md\":\"CrQbI5eH\",\"recipes_indexed-forms.md\":\"CstYyOSo\",\"recipes_migrations.md\":\"CTcnWDJF\",\"recipes_text-field-component.md\":\"H4wLAK0Z\",\"roadmap.md\":\"C6PRi0DX\",\"routes.md\":\"BD6y2i-f\",\"security.md\":\"C0G_AZR-\",\"seed-data.md\":\"BvFZlqIk\",\"space-time-continuum.md\":\"xl44xDos\",\"tutorial.md\":\"C4zR5XPG\",\"unit-tests.md\":\"DUGrnLj5\",\"why.md\":\"C-hk5xgJ\"}");window.__VP_SITE_DATA__=JSON.parse("{\"lang\":\"en-US\",\"dir\":\"ltr\",\"title\":\"Brut RB\",\"description\":\"Documentation for the Brut.RB web framework.\",\"base\":\"/\",\"head\":[],\"router\":{\"prefetchLinks\":true},\"appearance\":true,\"themeConfig\":{\"search\":{\"provider\":\"local\"},\"nav\":[{\"text\":\"Home\",\"link\":\"/\"},{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Overview\",\"link\":\"/overview\"},{\"text\":\"Brut API\",\"link\":\"/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js/api/index.html\",\"target\":\"_self\"},{\"text\":\"BrutCSS\",\"link\":\"/brut-css/index.html\",\"target\":\"_self\"}],\"outline\":[2,3],\"sidebar\":[{\"text\":\"Overview\",\"collapsed\":false,\"items\":[{\"text\":\"Getting Started\",\"link\":\"/getting-started\"},{\"text\":\"Concepts\",\"link\":\"/overview\"},{\"text\":\"Features\",\"link\":\"/features\"},{\"text\":\"Directory Structure\",\"link\":\"/dir-structure\"},{\"text\":\"Dev Environment\",\"link\":\"/dev-environment\"},{\"text\":\"Tutorial\",\"link\":\"/tutorial\"},{\"text\":\"Documentation Conventions\",\"link\":\"/doc-conventions\"}]},{\"text\":\"Front-End\",\"collapsed\":false,\"items\":[{\"text\":\"Routes\",\"link\":\"/routes\"},{\"text\":\"Pages\",\"link\":\"/pages\"},{\"text\":\"Layouts\",\"link\":\"/layouts\"},{\"text\":\"Forms\",\"link\":\"/forms\"},{\"text\":\"Form Constraints\",\"link\":\"/form-constraints\"},{\"text\":\"Handlers and Actions\",\"link\":\"/handlers\"},{\"text\":\"Components\",\"link\":\"/components\"},{\"text\":\"Flash and Session\",\"link\":\"/flash-and-session\"},{\"text\":\"Space/Time Continuum\",\"link\":\"/space-time-continuum\"},{\"text\":\"JavaScript\",\"link\":\"/javascript\"},{\"text\":\"CSS\",\"link\":\"/css\"},{\"text\":\"Assets\",\"link\":\"/assets\"},{\"text\":\"BrutJS\",\"link\":\"/brut-js\"}]},{\"text\":\"Back-End\",\"collapsed\":false,\"items\":[{\"text\":\"Database Schema\",\"link\":\"/database-schema\"},{\"text\":\"Database Access\",\"link\":\"/database-access\"},{\"text\":\"Seed Data\",\"link\":\"/seed-data\"},{\"text\":\"Jobs\",\"link\":\"/jobs\"},{\"text\":\"Business Logic\",\"link\":\"/business-logic\"}]},{\"text\":\"Framework\",\"collapsed\":false,\"items\":[{\"text\":\"Configuration\",\"link\":\"/configuration\"},{\"text\":\"Keyword Injection\",\"link\":\"/keyword-injection\"},{\"text\":\"I18n\",\"link\":\"/i18n\"},{\"text\":\"CLI / Tasks\",\"link\":\"/cli\"},{\"text\":\"Deployment\",\"link\":\"/deployment\"}]},{\"text\":\"Testing\",\"collapsed\":false,\"items\":[{\"text\":\"Unit Tests\",\"link\":\"/unit-tests\"},{\"text\":\"End-to-End Tests\",\"link\":\"/end-to-end-tests\"},{\"text\":\"Testing Custom Elements\",\"link\":\"/custom-element-tests\"}]},{\"text\":\"Advanced Topics\",\"collapsed\":true,\"items\":[{\"text\":\"Route Hooks\",\"link\":\"/hooks\"},{\"text\":\"Middleware\",\"link\":\"/middleware\"},{\"text\":\"Instrumentation\",\"link\":\"/instrumentation\"},{\"text\":\"Security\",\"link\":\"/security\"},{\"text\":\"LSP Support\",\"link\":\"/lsp\"}]},{\"text\":\"Recipes\",\"collapsed\":true,\"items\":[{\"text\":\"Migration Basics\",\"link\":\"/recipes/migrations\"},{\"text\":\"Authentication\",\"link\":\"/recipes/authentication\"},{\"text\":\"Alternate Layouts\",\"link\":\"/recipes/alternate-layouts\"},{\"text\":\"Blank Layouts\",\"link\":\"/recipes/blank-layouts\"},{\"text\":\"Custom Flash Class\",\"link\":\"/recipes/custom-flash\"},{\"text\":\"Indexed Form Elements\",\"link\":\"/recipes/indexed-forms\"},{\"text\":\"Text Field Component\",\"link\":\"/recipes/text-field-component\"}]},{\"text\":\"Meta\",\"collapsed\":false,\"items\":[{\"text\":\"Why?!\",\"link\":\"/why\"},{\"text\":\"ADRs\",\"link\":\"/adrs\"},{\"text\":\"Roadmap to 1.0\",\"link\":\"/roadmap\"},{\"text\":\"AI Declaration\",\"link\":\"/ai\"}]}],\"socialLinks\":[{\"icon\":\"github\",\"link\":\"https://github.com/thirdtank/brut\"}]},\"locales\":{},\"scrollOffset\":134,\"cleanUrls\":false}");</script>
27
27
 
28
28
  </body>
29
29
  </html>
data/dx/build CHANGED
@@ -6,6 +6,41 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${0}" )" > /dev/null 2>&1 && pwd )
6
6
 
7
7
  . "${SCRIPT_DIR}/dx.sh.lib"
8
8
 
9
+ read_custom_build_args() {
10
+ build_args_file="${SCRIPT_DIR}/build.args"
11
+ if [ -e "${build_args_file}" ]; then
12
+ for arg in `grep -v '^#' "${build_args_file}"`; do
13
+ BUILD_ARGS+=(--build-arg ${arg})
14
+ done
15
+ fi
16
+ }
17
+
18
+ setup_local_user_build_args() {
19
+ require_command "id"
20
+ require_command "uname"
21
+ user_uid=$(id -u)
22
+ user_gid=$(id -g)
23
+ docker_gid=
24
+ sadly_user_must_be_added_to_root=
25
+ OS=$(uname)
26
+ if [ "${OS}" == "Darwin" ] ; then
27
+ docker_gid=$(stat -f %g /var/run/docker.sock)
28
+ sadly_user_must_be_added_to_root="0,"
29
+ else
30
+ if [ "${OS}" == "Linux" ] ; then
31
+ docker_gid=$(stat -c %g /var/run/docker.sock)
32
+ else
33
+ log "Could not determine OS, which is needed to know how to invoke stat to figure out the group id of /var/run/docker.sock"
34
+ log "Docker commands will not work"
35
+ fi
36
+ fi
37
+ echo user_uid=${user_uid} > "${SCRIPT_DIR}"/build.args
38
+ echo user_gid=${user_gid} >> "${SCRIPT_DIR}"/build.args
39
+ echo docker_gid=${docker_gid} >> "${SCRIPT_DIR}"/build.args
40
+ echo sadly_user_must_be_added_to_root=${sadly_user_must_be_added_to_root} >> "${SCRIPT_DIR}"/build.args
41
+ }
42
+
43
+
9
44
  require_command "docker"
10
45
  load_docker_compose_env
11
46
 
@@ -15,23 +50,24 @@ if ! exec_hook_if_exists "build.pre" Dockerfile.dx "${IMAGE}"; then
15
50
  log "build.pre failed"
16
51
  exit 1
17
52
  fi
18
- build_args_file="${SCRIPT_DIR}/build.args"
19
53
  BUILD_ARGS=()
20
- if [ -e "${build_args_file}" ]; then
21
- for arg in `grep -v '^#' "${build_args_file}"`; do
22
- BUILD_ARGS+=(--build-arg ${arg})
23
- done
24
- fi
54
+
55
+ setup_local_user_build_args
56
+ read_custom_build_args
25
57
 
26
58
  docker build \
27
59
  --file Dockerfile.dx \
28
- --progress plain \
29
- --tag "${IMAGE}" ${BUILD_ARGS[@]} \
60
+ --tag "${IMAGE}" \
61
+ ${BUILD_ARGS[@]} \
30
62
  ./
31
63
 
32
- exec_hook_if_exists "build.post" Dockerfile.dx "${IMAGE}"
64
+ if ! exec_hook_if_exists "build.post" Dockerfile.dx "${IMAGE}"; then
65
+ log "build.pre failed"
66
+ exit 1
67
+ fi
33
68
 
34
69
  log "🌈" "Your Docker image has been built tagged '${IMAGE}'"
35
70
  log "🔄" "You can now run dx/start to start it up, though you may need to stop it first with Ctrl-C"
36
71
 
37
72
  # vim: ft=bash
73
+
data/lib/brut/cli/app.rb CHANGED
@@ -56,7 +56,7 @@ class Brut::CLI::App
56
56
  # @!visibility private
57
57
  def self.env_vars
58
58
  @env_vars ||= {
59
- "BRUT_CLI_RAISE_ON_ERROR" => "if set, shows backtrace on errors"
59
+ "BRUT_CLI_RAISE_ON_ERROR" => "if set, shows backtrace on errors",
60
60
  }
61
61
  end
62
62
 
@@ -76,7 +76,7 @@ Manages a deploy process based on using Heroku's Container Registry. See
76
76
  image_name:,
77
77
  dockerfile: "deploy/Dockerfile.#{name}",
78
78
  heroku_image_name: "registry.heroku.com/#{heroku_app_name}/#{name}",
79
- }
79
+ },
80
80
  ]
81
81
  }.to_h
82
82
 
@@ -51,7 +51,7 @@ class Brut::CLI::Apps::Scaffold < Brut::CLI::App
51
51
  files_to_test_files.each do |source,destination|
52
52
  result = Prism.parse_file(source.to_s)
53
53
  if !result
54
- raise "For some reason Prism did not parse #{source.to_s}"
54
+ raise "For some reason Prism did not parse #{source}"
55
55
  end
56
56
  classes = find_classes(result.value).map { |(module_nodes,class_node)|
57
57
  (module_nodes.map(&:constant_path).map(&:full_name).map(&:to_s) + [class_node.constant_path.full_name.to_s]).compact.join("::")
@@ -74,4 +74,8 @@ class Brut::CLI::Options
74
74
  value
75
75
  end
76
76
  end
77
+ def respond_to_missing?(sym, include_private = false)
78
+ # XXX: Maybe this should not
79
+ true
80
+ end
77
81
  end
@@ -7,7 +7,7 @@ class Brut::FactoryBot
7
7
  def setup!
8
8
  Faker::Config.locale = :en
9
9
  FactoryBot.definition_file_paths = [
10
- Brut.container.app_specs_dir / "factories"
10
+ Brut.container.app_specs_dir / "factories",
11
11
  ]
12
12
  FactoryBot.define do
13
13
  to_create { |instance| instance.save }
@@ -89,7 +89,7 @@ class Brut::Framework::App
89
89
  raise ArgumentError, "Your error handler block may only accept exception: and http_status_code: as required keyword parameters. The following parameters were found:\n #{messages.join("\n ")}"
90
90
  end
91
91
  if @error_blocks[condition]
92
- raise ArgumentError, "You have already configured error handling for condition '#{condition.to_s}'"
92
+ raise ArgumentError, "You have already configured error handling for condition '#{condition}'"
93
93
  end
94
94
  @error_blocks[condition] = block
95
95
  end
@@ -319,25 +319,31 @@ class Brut::Framework::Config
319
319
  allow_app_override: true,
320
320
  )
321
321
 
322
+ c.store(
323
+ "csrf_protector",
324
+ Brut::FrontEnd::CsrfProtector,
325
+ "Object to allow custom logic related to CSRF protection",
326
+ allow_app_override: true
327
+ ) do
328
+ Brut::FrontEnd::CsrfProtector.new
329
+ end
330
+
322
331
  c.store(
323
332
  "semantic_logger_appenders",
324
333
  { Hash => "if only one appender is needed", Array => "to configure multiple appenders" },
325
334
  "List of appenders to be configured for SemanticLogger",
326
335
  allow_app_override: true
327
336
  ) do |project_env,log_dir|
328
- appenders = if project_env.development?
329
- [
330
- { formatter: :color, io: $stdout },
331
- { file_name: (log_dir / "development.log").to_s },
332
- ]
333
- end
334
- if appenders.nil?
335
- appenders = { file_name: (log_dir / "#{project_env}.log").to_s }
336
- end
337
- if appenders.nil?
338
- appenders = { io: $stdout }
337
+ if project_env.development?
338
+ [
339
+ { formatter: :color, io: $stdout },
340
+ { file_name: (log_dir / "development.log").to_s },
341
+ ]
342
+ elsif project_env.test?
343
+ { file_name: (log_dir / "test.log").to_s }
344
+ else
345
+ { io: $stdout }
339
346
  end
340
- appenders
341
347
  end
342
348
 
343
349
  c.store(
@@ -131,9 +131,9 @@ class Brut::Framework::MCP
131
131
  exception: ex)
132
132
  rescue => ex2
133
133
  $stderr.puts "While handling an error recording an exception, we get another error from SemanticLogger." + [
134
- [ :original_exception, exception, ].join(": "),
135
- [ :exception_from_recording_exception, ex, ].join(": "),
136
- [ :exception_from_semantic_logger, ex2 ].join(": "),
134
+ [ :original_exception, exception ].join(": "),
135
+ [ :exception_from_recording_exception, ex ].join(": "),
136
+ [ :exception_from_semantic_logger, ex2 ].join(": "),
137
137
 
138
138
  ].join(", ")
139
139
  end
@@ -186,10 +186,15 @@ class Brut::Framework::MCP
186
186
  Rack::Protection::AuthenticityToken,
187
187
  [
188
188
  {
189
- allow_if: ->(env) { env["brut.owned_path"] },
189
+ allow_if: ->(env) {
190
+ brut_owned_path = env["brut.owned_path"]
191
+ app_allowed = Brut.container.csrf_protector.tap {|_| puts _.class.name }.allowed?(env)
192
+
193
+ brut_owned_path || app_allowed
194
+ },
190
195
  message: message,
191
- }
192
- ]
196
+ },
197
+ ],
193
198
  ],
194
199
  ]
195
200
  if Brut.container.auto_reload_classes?
@@ -275,7 +280,7 @@ class Brut::Framework::MCP
275
280
  in TrueClass
276
281
  nil
277
282
  else
278
- raise NoMatchingPatternError, "Result from #{method} hook #{klass}'s #{method} method was a #{result.class} (#{result.to_s} as a string), which cannot be used to understand the response to generate. Return nil or true if processing should proceed"
283
+ raise NoMatchingPatternError, "Result from #{method} hook #{klass}'s #{method} method was a #{result.class} (#{result} as a string), which cannot be used to understand the response to generate. Return nil or true if processing should proceed"
279
284
  end
280
285
  end
281
286
  end
@@ -0,0 +1,30 @@
1
+ # Base for custom logic around CSRF protection. Brut configures `Rack::Protection::AuthenticityToken` for all requests, and
2
+ # this happens early in the request. The idea is that no real POST should be missing a CSRF token. That said, there are times
3
+ # when it must be skipped, such as for webhooks. In that case, you can extend this class and configure it via
4
+ # `Brut.container.override("csrf_protector", YourCustomCsrfProtector.new)` in your `App` class' initializer.
5
+ #
6
+ # @example
7
+ # class CsrfProtector < Brut::FrontEnd::CsrfProtector
8
+ # def allowed?(env)
9
+ # !!env["PATH_INFO"].to_s.match?(/^\/webhooks\//)
10
+ # end
11
+ # end
12
+ # # Then, in app.rb
13
+ # class App < Brut::Framework::App
14
+ # def id = "some-id"
15
+ # def organization = "some-org"
16
+ #
17
+ # def initialize
18
+ # Brut.container.override("csrf_protector") do
19
+ # CsrfProtector.new
20
+ # end
21
+ #
22
+ # # ...
23
+ #
24
+ class Brut::FrontEnd::CsrfProtector
25
+
26
+ # Return true if the request should be allowed without a CSRF token. This implementation returns false.
27
+ def allowed?(env)
28
+ false
29
+ end
30
+ end
@@ -73,7 +73,7 @@ class Brut::FrontEnd::Form
73
73
  }
74
74
  else
75
75
  [
76
- input_definition.make_input(value:, index: nil)
76
+ input_definition.make_input(value:, index: nil),
77
77
  ]
78
78
  end
79
79
 
@@ -109,7 +109,7 @@ class Brut::FrontEnd::Form
109
109
  # @return [Brut::FrontEnd::Forms::Input]
110
110
  def inputs(input_name)
111
111
  @inputs.fetch(input_name.to_s)
112
- rescue KeyError => ex
112
+ rescue KeyError
113
113
  raise Brut::Framework::Errors::Bug, "Form does not define the input '#{input_name}'. You must add this to your form. Found these inputs: #{@inputs.keys.join(', ')}"
114
114
  end
115
115
 
@@ -164,8 +164,8 @@ class Brut::FrontEnd::Form
164
164
  true
165
165
  end
166
166
  },
167
- index
168
- ]
167
+ index,
168
+ ],
169
169
  ]
170
170
  end
171
171
  }.compact
@@ -86,7 +86,7 @@ private
86
86
  layout_class = Module.const_get(
87
87
  layout_class = RichString.new([
88
88
  self.layout,
89
- "layout"
89
+ "layout",
90
90
  ].join("_")).camelize
91
91
  )
92
92
  Brut.container.instrumentation.add_prefixed_attributes("brut", layout_class: layout_class)
@@ -276,7 +276,7 @@ private
276
276
  }
277
277
  part_names[-1] += suffix
278
278
  begin
279
- part_names.inject(Module) { |mod,path_element|
279
+ part_names.reduce(Module) { |mod,path_element|
280
280
  mod.const_get(path_element,mod == Module)
281
281
  }
282
282
  rescue NameError => ex
@@ -9,6 +9,7 @@ module Brut::FrontEnd
9
9
  autoload(:AssetPathResolver, "brut/front_end/asset_path_resolver")
10
10
  autoload(:Component, "brut/front_end/component")
11
11
  autoload(:Components, "brut/front_end/component")
12
+ autoload(:CsrfProtector, "brut/front_end/csrf_protector")
12
13
  autoload(:Download, "brut/front_end/download")
13
14
  autoload(:Flash, "brut/front_end/flash")
14
15
  autoload(:Form, "brut/front_end/form")
@@ -211,7 +211,7 @@ module Brut::I18n::BaseMethods
211
211
  block_contents = safe(capture(&block))
212
212
  interpolated_values[:block] = block_contents
213
213
  end
214
- t_direct(keys_to_check,**interpolated_values.merge(key_given: key))
214
+ t_direct(keys_to_check,**interpolated_values, key_given: key)
215
215
  rescue I18n::MissingInterpolationArgument => ex
216
216
  if ex.key.to_s == "block"
217
217
  raise ArgumentError,"One of the keys #{key.join(", ")} contained a %{block} interpolation value: '#{ex.string}'. This means you must yield a block to `t`"
@@ -58,7 +58,7 @@ private
58
58
  (span.events || []).each do |event|
59
59
  event_message = (" " * (indent + 2)) + "event:#{event.name}"
60
60
  event_params = {
61
- timing: ((event.timestamp - previous_timestamp)/1_000.0).to_i/1_000.0
61
+ timing: ((event.timestamp - previous_timestamp)/1_000.0).to_i/1_000.0,
62
62
  }.merge(event.attributes).merge(synthetic_attributes)
63
63
  SemanticLogger[self.class].info(event_message,event_params)
64
64
  previous_timestamp = event.timestamp
@@ -96,7 +96,9 @@ class RichString
96
96
  }.
97
97
  join.gsub("-","_").
98
98
  gsub(/[_]+/,"_").
99
- gsub(/^_+/,"").
99
+ split(/::/).
100
+ map { |it| it.gsub(/^_+/,"") }.
101
+ join("/").
100
102
  gsub(/_+$/,"")
101
103
  )
102
104
  end