brut 0.0.28 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (532) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.projections.json +10 -0
  4. data/.rspec +3 -0
  5. data/Dockerfile.dx +32 -14
  6. data/Gemfile.lock +1 -1
  7. data/README.md +23 -2
  8. data/assets/Logo-Square.pxd +0 -0
  9. data/assets/LogoPylon.pxd +0 -0
  10. data/assets/LogoStop.pxd +0 -0
  11. data/assets/LogoTall.pxd +0 -0
  12. data/assets/MetroLogo.graffle +0 -0
  13. data/assets/SocialImage.png +0 -0
  14. data/assets/SocialImage.pxd +0 -0
  15. data/bin/docs +24 -2
  16. data/bin/rspec +27 -0
  17. data/bin/setup +3 -3
  18. data/brutrb.com/.vitepress/config.mjs +45 -8
  19. data/brutrb.com/.vitepress/theme/custom.css +7 -0
  20. data/brutrb.com/.vitepress/theme/style.css +29 -17
  21. data/brutrb.com/ai.md +10 -15
  22. data/brutrb.com/assets.md +2 -9
  23. data/brutrb.com/brut-js.md +12 -2
  24. data/brutrb.com/cli.md +9 -13
  25. data/brutrb.com/components.md +118 -96
  26. data/brutrb.com/configuration.md +3 -4
  27. data/brutrb.com/css.md +2 -2
  28. data/brutrb.com/custom-element-tests.md +3 -4
  29. data/brutrb.com/database-access.md +1 -1
  30. data/brutrb.com/database-schema.md +29 -41
  31. data/brutrb.com/deployment.md +123 -45
  32. data/brutrb.com/dev-environment.md +7 -7
  33. data/brutrb.com/dir-structure.md +120 -0
  34. data/brutrb.com/doc-conventions.md +18 -15
  35. data/brutrb.com/dx +1 -0
  36. data/brutrb.com/end-to-end-tests.md +12 -10
  37. data/brutrb.com/features.md +373 -0
  38. data/brutrb.com/flash-and-session.md +115 -131
  39. data/brutrb.com/form-constraints.md +266 -0
  40. data/brutrb.com/forms.md +140 -765
  41. data/brutrb.com/getting-started.md +10 -11
  42. data/brutrb.com/handlers.md +119 -95
  43. data/brutrb.com/hooks.md +18 -20
  44. data/brutrb.com/i18n.md +6 -4
  45. data/brutrb.com/images/LogoPylon.png +0 -0
  46. data/brutrb.com/images/LogoSquare.png +0 -0
  47. data/brutrb.com/images/LogoStop.png +0 -0
  48. data/brutrb.com/images/LogoTall.png +0 -0
  49. data/brutrb.com/images/OverviewMetro.graffle +0 -0
  50. data/brutrb.com/images/OverviewMetro.png +0 -0
  51. data/brutrb.com/index.md +4 -3
  52. data/brutrb.com/instrumentation.md +7 -10
  53. data/brutrb.com/javascript.md +14 -14
  54. data/brutrb.com/keyword-injection.md +72 -114
  55. data/brutrb.com/layouts.md +20 -52
  56. data/brutrb.com/lsp.md +1 -1
  57. data/brutrb.com/overview.md +35 -377
  58. data/brutrb.com/pages.md +119 -207
  59. data/brutrb.com/public/SocialImage.png +0 -0
  60. data/brutrb.com/public/favicon.ico +0 -0
  61. data/brutrb.com/recipes/alternate-layouts.md +32 -0
  62. data/brutrb.com/recipes/authentication.md +315 -6
  63. data/brutrb.com/recipes/blank-layouts.md +22 -0
  64. data/brutrb.com/recipes/custom-flash.md +51 -0
  65. data/brutrb.com/recipes/indexed-forms.md +149 -0
  66. data/brutrb.com/recipes/text-field-component.md +182 -0
  67. data/brutrb.com/routes.md +56 -82
  68. data/brutrb.com/security.md +0 -3
  69. data/brutrb.com/space-time-continuum.md +8 -12
  70. data/brutrb.com/tutorial.md +1 -1
  71. data/brutrb.com/why.md +19 -0
  72. data/docker-compose.dx.yml +5 -2
  73. data/docs/404.html +8 -3
  74. data/docs/SocialImage.png +0 -0
  75. data/docs/ai.html +11 -6
  76. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  77. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  78. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  79. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  80. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  81. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  82. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  83. data/docs/api/Brut/BackEnd.html +1 -1
  84. data/docs/api/Brut/CLI/App.html +1 -1
  85. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  86. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  87. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  88. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  89. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  90. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  91. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  92. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  93. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  94. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  95. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  96. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  97. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  98. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  99. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  100. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  101. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  102. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  103. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  104. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  105. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  106. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  107. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  108. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  109. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  110. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  111. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  112. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  113. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  114. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  115. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  116. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  117. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  118. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  119. data/docs/api/Brut/CLI/Apps.html +1 -1
  120. data/docs/api/Brut/CLI/Command.html +1 -1
  121. data/docs/api/Brut/CLI/Error.html +1 -1
  122. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  123. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  124. data/docs/api/Brut/CLI/Executor.html +1 -1
  125. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  126. data/docs/api/Brut/CLI/Options.html +1 -1
  127. data/docs/api/Brut/CLI/Output.html +1 -1
  128. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  129. data/docs/api/Brut/CLI.html +1 -1
  130. data/docs/api/Brut/FactoryBot.html +1 -1
  131. data/docs/api/Brut/Framework/App.html +1 -1
  132. data/docs/api/Brut/Framework/Config.html +1 -1
  133. data/docs/api/Brut/Framework/Container.html +1 -1
  134. data/docs/api/Brut/Framework/Error.html +1 -1
  135. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  136. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  137. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  138. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  139. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  140. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  141. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  142. data/docs/api/Brut/Framework/Errors.html +1 -1
  143. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  144. data/docs/api/Brut/Framework/MCP.html +1 -1
  145. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  146. data/docs/api/Brut/Framework.html +1 -1
  147. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  152. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  153. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  154. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  155. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  156. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  157. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
  158. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  162. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  164. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  167. data/docs/api/Brut/FrontEnd/Form.html +9 -11
  168. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +201 -0
  170. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +535 -0
  171. data/docs/api/Brut/FrontEnd/Forms/Input.html +983 -35
  172. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  173. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +29 -19
  174. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +141 -20
  175. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  176. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +141 -20
  177. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  178. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  179. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  180. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  181. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  182. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  183. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  184. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  185. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  186. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  187. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  188. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  189. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  190. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  191. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  192. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  193. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  194. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  195. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  196. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  197. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  198. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  199. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  200. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  201. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  202. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  203. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  204. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  205. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  206. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  207. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  208. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  209. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  210. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  211. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  212. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  213. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  214. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  215. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  216. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  217. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  218. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  219. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  220. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  221. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  222. data/docs/api/Brut/FrontEnd.html +1 -1
  223. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  224. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  225. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  226. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  227. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  228. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  229. data/docs/api/Brut/I18n.html +1 -1
  230. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  231. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  232. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  233. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  234. data/docs/api/Brut/Instrumentation.html +1 -1
  235. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  236. data/docs/api/Brut/SinatraHelpers.html +1 -1
  237. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  238. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  239. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  240. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  241. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  242. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  243. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  244. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  245. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  246. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  247. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  248. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  249. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  250. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  251. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  252. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  253. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  254. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  255. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  256. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  257. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  258. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  259. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  260. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  261. data/docs/api/Brut/SpecSupport.html +1 -1
  262. data/docs/api/Brut.html +1 -1
  263. data/docs/api/Clock.html +1 -1
  264. data/docs/api/RichString.html +1 -1
  265. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  266. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  267. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  268. data/docs/api/Sequel/Extensions.html +1 -1
  269. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  270. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  271. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  272. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  273. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  274. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  275. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  276. data/docs/api/Sequel/Plugins.html +1 -1
  277. data/docs/api/Sequel.html +1 -1
  278. data/docs/api/_index.html +15 -1
  279. data/docs/api/class_list.html +1 -1
  280. data/docs/api/css/full_list.css +2 -1
  281. data/docs/api/css/style.css +14 -13
  282. data/docs/api/file.README.html +22 -3
  283. data/docs/api/index.html +22 -3
  284. data/docs/api/method_list.html +435 -275
  285. data/docs/api/top-level-namespace.html +1 -1
  286. data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
  287. data/docs/assets/OverviewMetro.DUS-5fUZ.png +0 -0
  288. data/docs/assets/{ai.md._6HCDL6d.js → ai.md.Cy9GWnER.js} +1 -1
  289. data/docs/assets/ai.md.Cy9GWnER.lean.js +1 -0
  290. data/docs/assets/{app.BX81XO4N.js → app.ClaS47Ru.js} +1 -1
  291. data/docs/assets/{assets.md.D3wunzLx.js → assets.md.7C3HWkga.js} +3 -3
  292. data/docs/assets/{assets.md.D3wunzLx.lean.js → assets.md.7C3HWkga.lean.js} +1 -1
  293. data/docs/assets/{brut-js.md.o2DAO2s2.js → brut-js.md.B4GYxQVw.js} +1 -1
  294. data/docs/assets/{brut-js.md.o2DAO2s2.lean.js → brut-js.md.B4GYxQVw.lean.js} +1 -1
  295. data/docs/assets/chunks/@localSearchIndexroot.Biqy1A4t.js +1 -0
  296. data/docs/assets/chunks/{VPLocalSearchBox.gABXcTWp.js → VPLocalSearchBox.DtgDfde2.js} +1 -1
  297. data/docs/assets/chunks/{theme.DwUXXAL3.js → theme.B45bvibT.js} +2 -2
  298. data/docs/assets/{cli.md.RmeA2b0i.js → cli.md.CjsktgFz.js} +15 -20
  299. data/docs/assets/components.md.DatoNgFo.js +96 -0
  300. data/docs/assets/{components.md.CRUMdRoN.lean.js → components.md.DatoNgFo.lean.js} +1 -1
  301. data/docs/assets/{configuration.md.BGHl8oRC.js → configuration.md.DeyhpqEx.js} +3 -3
  302. data/docs/assets/{css.md.DJgj2clw.js → css.md.CltvJqAa.js} +3 -3
  303. data/docs/assets/{custom-element-tests.md.BrYJQEl3.js → custom-element-tests.md.B_rbta32.js} +3 -3
  304. data/docs/assets/{database-access.md.C7l-Vuvb.js → database-access.md.gnluu54N.js} +1 -1
  305. data/docs/assets/{database-schema.md.BUjR0VS1.js → database-schema.md.CSYk6E6v.js} +6 -6
  306. data/docs/assets/{database-schema.md.BUjR0VS1.lean.js → database-schema.md.CSYk6E6v.lean.js} +1 -1
  307. data/docs/assets/deployment.md.BLseERGV.js +48 -0
  308. data/docs/assets/deployment.md.BLseERGV.lean.js +1 -0
  309. data/docs/assets/dev-environment.md.BroAOLhF.js +11 -0
  310. data/docs/assets/dir-structure.md.CWir1pic.js +46 -0
  311. data/docs/assets/dir-structure.md.CWir1pic.lean.js +1 -0
  312. data/docs/assets/doc-conventions.md.BzmSrTEW.js +1 -0
  313. data/docs/assets/doc-conventions.md.BzmSrTEW.lean.js +1 -0
  314. data/docs/assets/{end-to-end-tests.md.yfQHC0b5.js → end-to-end-tests.md.DzqRpZ43.js} +5 -3
  315. data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +1 -0
  316. data/docs/assets/features.md.DPFXsy0z.js +154 -0
  317. data/docs/assets/features.md.DPFXsy0z.lean.js +1 -0
  318. data/docs/assets/flash-and-session.md.nPvUpnUx.js +79 -0
  319. data/docs/assets/{flash-and-session.md.BXY8RvT0.lean.js → flash-and-session.md.nPvUpnUx.lean.js} +1 -1
  320. data/docs/assets/form-constraints.md.x5tNpTTI.js +90 -0
  321. data/docs/assets/form-constraints.md.x5tNpTTI.lean.js +1 -0
  322. data/docs/assets/forms.md.C2Dizvzq.js +64 -0
  323. data/docs/assets/forms.md.C2Dizvzq.lean.js +1 -0
  324. data/docs/assets/{getting-started.md.Ciz82L0m.js → getting-started.md.C93e0odB.js} +5 -5
  325. data/docs/assets/{getting-started.md.Ciz82L0m.lean.js → getting-started.md.C93e0odB.lean.js} +1 -1
  326. data/docs/assets/handlers.md.Chyri6KA.js +54 -0
  327. data/docs/assets/handlers.md.Chyri6KA.lean.js +1 -0
  328. data/docs/assets/{hooks.md.C4-moMny.js → hooks.md.Jmb5VOLA.js} +4 -4
  329. data/docs/assets/{hooks.md.C4-moMny.lean.js → hooks.md.Jmb5VOLA.lean.js} +1 -1
  330. data/docs/assets/{i18n.md.Do9i1qWl.js → i18n.md.xQhiGo1G.js} +2 -2
  331. data/docs/assets/{i18n.md.Do9i1qWl.lean.js → i18n.md.xQhiGo1G.lean.js} +1 -1
  332. data/docs/assets/index.md.CAMqGBJE.js +1 -0
  333. data/docs/assets/index.md.CAMqGBJE.lean.js +1 -0
  334. data/docs/assets/{instrumentation.md.a9Pjps4P.js → instrumentation.md.BgcaGVYH.js} +2 -2
  335. data/docs/assets/{instrumentation.md.a9Pjps4P.lean.js → instrumentation.md.BgcaGVYH.lean.js} +1 -1
  336. data/docs/assets/{javascript.md.GWbhRS51.js → javascript.md.DzrMxUmI.js} +7 -7
  337. data/docs/assets/{javascript.md.GWbhRS51.lean.js → javascript.md.DzrMxUmI.lean.js} +1 -1
  338. data/docs/assets/keyword-injection.md.95Zgh2eN.js +21 -0
  339. data/docs/assets/{keyword-injection.md.Dt2tKREs.lean.js → keyword-injection.md.95Zgh2eN.lean.js} +1 -1
  340. data/docs/assets/{layouts.md.cPnh3NId.js → layouts.md.CJGDFY-m.js} +2 -15
  341. data/docs/assets/layouts.md.CJGDFY-m.lean.js +1 -0
  342. data/docs/assets/{lsp.md.Bsu-f6VU.js → lsp.md.Dn1rIiW0.js} +1 -1
  343. data/docs/assets/{lsp.md.Bsu-f6VU.lean.js → lsp.md.Dn1rIiW0.lean.js} +1 -1
  344. data/docs/assets/overview.md.Bdq4qt3L.js +1 -0
  345. data/docs/assets/overview.md.Bdq4qt3L.lean.js +1 -0
  346. data/docs/assets/pages.md.B7Hc-i6H.js +45 -0
  347. data/docs/assets/pages.md.B7Hc-i6H.lean.js +1 -0
  348. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.js +22 -0
  349. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +1 -0
  350. data/docs/assets/recipes_authentication.md.Dzvi_g69.js +156 -0
  351. data/docs/assets/recipes_authentication.md.Dzvi_g69.lean.js +1 -0
  352. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +15 -0
  353. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +1 -0
  354. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.js +26 -0
  355. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +1 -0
  356. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.js +74 -0
  357. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +1 -0
  358. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.js +101 -0
  359. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +1 -0
  360. data/docs/assets/routes.md.B8kfUPHU.js +21 -0
  361. data/docs/assets/{routes.md.BMM7peut.lean.js → routes.md.B8kfUPHU.lean.js} +1 -1
  362. data/docs/assets/{security.md.C668yXCi.js → security.md.C0G_AZR-.js} +1 -1
  363. data/docs/assets/{security.md.C668yXCi.lean.js → security.md.C0G_AZR-.lean.js} +1 -1
  364. data/docs/assets/space-time-continuum.md.xl44xDos.js +1 -0
  365. data/docs/assets/{space-time-continuum.md.KPUIKysQ.lean.js → space-time-continuum.md.xl44xDos.lean.js} +1 -1
  366. data/docs/assets/{style.D73IYGCX.css → style.prAgp4yQ.css} +1 -1
  367. data/docs/assets/tutorial.md.a4a0eVOy.js +1 -0
  368. data/docs/assets/tutorial.md.a4a0eVOy.lean.js +1 -0
  369. data/docs/assets/why.md.C-hk5xgJ.js +1 -0
  370. data/docs/assets/why.md.C-hk5xgJ.lean.js +1 -0
  371. data/docs/assets.html +12 -7
  372. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  373. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  374. data/docs/brut-js/api/Autosubmit.html +1 -1
  375. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  376. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  377. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  378. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  379. data/docs/brut-js/api/BufferedLogger.html +1 -1
  380. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  381. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  382. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  383. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  384. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  385. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  386. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  387. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  388. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  389. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  390. data/docs/brut-js/api/Form.html +1 -1
  391. data/docs/brut-js/api/Form.js.html +1 -1
  392. data/docs/brut-js/api/I18nTranslation.html +1 -1
  393. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  394. data/docs/brut-js/api/LocaleDetection.html +1 -1
  395. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  396. data/docs/brut-js/api/Logger.html +1 -1
  397. data/docs/brut-js/api/Logger.js.html +1 -1
  398. data/docs/brut-js/api/Message.html +1 -1
  399. data/docs/brut-js/api/Message.js.html +1 -1
  400. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  401. data/docs/brut-js/api/RichString.html +1 -1
  402. data/docs/brut-js/api/RichString.js.html +1 -1
  403. data/docs/brut-js/api/Tabs.html +1 -1
  404. data/docs/brut-js/api/Tabs.js.html +1 -1
  405. data/docs/brut-js/api/Tracing.html +1 -1
  406. data/docs/brut-js/api/Tracing.js.html +1 -1
  407. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  408. data/docs/brut-js/api/external-Performance.html +1 -1
  409. data/docs/brut-js/api/external-Promise.html +1 -1
  410. data/docs/brut-js/api/external-ValidityState.html +1 -1
  411. data/docs/brut-js/api/external-Window.html +1 -1
  412. data/docs/brut-js/api/external-fetch.html +1 -1
  413. data/docs/brut-js/api/global.html +1 -1
  414. data/docs/brut-js/api/index.html +1 -1
  415. data/docs/brut-js/api/index.js.html +1 -1
  416. data/docs/brut-js/api/module-testing.html +1 -1
  417. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  418. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  419. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  420. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  421. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  422. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  423. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  424. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  425. data/docs/brut-js/api/testing_index.js.html +1 -1
  426. data/docs/brut-js.html +12 -7
  427. data/docs/business-logic.html +10 -5
  428. data/docs/cli.html +26 -26
  429. data/docs/components.html +61 -64
  430. data/docs/configuration.html +13 -8
  431. data/docs/css.html +14 -9
  432. data/docs/custom-element-tests.html +14 -9
  433. data/docs/database-access.html +12 -7
  434. data/docs/database-schema.html +15 -10
  435. data/docs/deployment.html +58 -6
  436. data/docs/dev-environment.html +12 -7
  437. data/docs/dir-structure.html +74 -0
  438. data/docs/doc-conventions.html +11 -6
  439. data/docs/end-to-end-tests.html +15 -8
  440. data/docs/favicon.ico +0 -0
  441. data/docs/features.html +182 -0
  442. data/docs/flash-and-session.html +73 -82
  443. data/docs/form-constraints.html +118 -0
  444. data/docs/forms.html +57 -367
  445. data/docs/getting-started.html +15 -10
  446. data/docs/handlers.html +51 -61
  447. data/docs/hashmap.json +1 -1
  448. data/docs/hooks.html +14 -9
  449. data/docs/i18n.html +12 -7
  450. data/docs/index.html +11 -6
  451. data/docs/instrumentation.html +12 -7
  452. data/docs/javascript.html +17 -12
  453. data/docs/jobs.html +10 -5
  454. data/docs/keyword-injection.html +22 -21
  455. data/docs/layouts.html +12 -20
  456. data/docs/lsp.html +11 -6
  457. data/docs/markdown-examples.html +10 -5
  458. data/docs/middleware.html +10 -5
  459. data/docs/not-released.html +10 -5
  460. data/docs/overview.html +11 -138
  461. data/docs/pages.html +49 -121
  462. data/docs/recipes/alternate-layouts.html +50 -0
  463. data/docs/recipes/authentication.html +166 -6
  464. data/docs/recipes/blank-layouts.html +43 -0
  465. data/docs/recipes/custom-flash.html +54 -0
  466. data/docs/recipes/indexed-forms.html +102 -0
  467. data/docs/recipes/text-field-component.html +129 -0
  468. data/docs/routes.html +16 -19
  469. data/docs/security.html +11 -6
  470. data/docs/seed-data.html +10 -5
  471. data/docs/space-time-continuum.html +11 -6
  472. data/docs/tutorial.html +11 -6
  473. data/docs/unit-tests.html +10 -5
  474. data/docs/why.html +29 -0
  475. data/dx/bash_customizations +7 -0
  476. data/dx/build +13 -2
  477. data/dx/docker-compose.env +1 -1
  478. data/dx/exec +25 -8
  479. data/lib/brut/front_end/form.rb +8 -8
  480. data/lib/brut/front_end/forms/input.rb +253 -20
  481. data/lib/brut/front_end/forms/input_definition.rb +15 -12
  482. data/lib/brut/front_end/forms/radio_button_group_input.rb +8 -1
  483. data/lib/brut/front_end/forms/select_input.rb +8 -1
  484. data/lib/brut/front_end.rb +1 -0
  485. data/lib/brut/version.rb +1 -1
  486. data/specs/brut/front_end/forms/input.spec.rb +978 -0
  487. data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +54 -0
  488. data/specs/brut/front_end/forms/select_input.spec.rb +54 -0
  489. data/specs/spec_helper.rb +27 -0
  490. data/specs/support/matchers/have_constraint_violation.rb +23 -0
  491. data/specs/support/matchers.rb +5 -0
  492. data/specs/support.rb +3 -0
  493. metadata +141 -77
  494. data/brutrb.com/public/images/logo-300.png +0 -0
  495. data/brutrb.com/public/images/logo.png +0 -0
  496. data/docs/assets/ai.md._6HCDL6d.lean.js +0 -1
  497. data/docs/assets/chunks/@localSearchIndexroot.CoYzciVi.js +0 -1
  498. data/docs/assets/components.md.CRUMdRoN.js +0 -104
  499. data/docs/assets/deployment.md.Dbka4OTr.js +0 -1
  500. data/docs/assets/deployment.md.Dbka4OTr.lean.js +0 -1
  501. data/docs/assets/dev-environment.md.GZv6xvi9.js +0 -11
  502. data/docs/assets/doc-conventions.md.-kN3Xo5C.js +0 -1
  503. data/docs/assets/doc-conventions.md.-kN3Xo5C.lean.js +0 -1
  504. data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +0 -1
  505. data/docs/assets/flash-and-session.md.BXY8RvT0.js +0 -93
  506. data/docs/assets/forms.md.B-koVgyw.js +0 -379
  507. data/docs/assets/forms.md.B-koVgyw.lean.js +0 -1
  508. data/docs/assets/handlers.md.089DVD3v.js +0 -69
  509. data/docs/assets/handlers.md.089DVD3v.lean.js +0 -1
  510. data/docs/assets/index.md.B28EwVpq.js +0 -1
  511. data/docs/assets/index.md.B28EwVpq.lean.js +0 -1
  512. data/docs/assets/keyword-injection.md.Dt2tKREs.js +0 -25
  513. data/docs/assets/layouts.md.cPnh3NId.lean.js +0 -1
  514. data/docs/assets/overview.Da81cB9R.png +0 -0
  515. data/docs/assets/overview.md.C5wlBcR5.js +0 -133
  516. data/docs/assets/overview.md.C5wlBcR5.lean.js +0 -1
  517. data/docs/assets/pages.md.BE3kfOc5.js +0 -122
  518. data/docs/assets/pages.md.BE3kfOc5.lean.js +0 -1
  519. data/docs/assets/recipes_authentication.md.CAsXf7hk.js +0 -1
  520. data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +0 -1
  521. data/docs/assets/routes.md.BMM7peut.js +0 -29
  522. data/docs/assets/space-time-continuum.md.KPUIKysQ.js +0 -1
  523. data/docs/assets/tutorial.md.BnoGjrdK.js +0 -1
  524. data/docs/assets/tutorial.md.BnoGjrdK.lean.js +0 -1
  525. data/docs/images/logo-300.png +0 -0
  526. data/docs/images/logo.png +0 -0
  527. /data/docs/assets/{cli.md.RmeA2b0i.lean.js → cli.md.CjsktgFz.lean.js} +0 -0
  528. /data/docs/assets/{configuration.md.BGHl8oRC.lean.js → configuration.md.DeyhpqEx.lean.js} +0 -0
  529. /data/docs/assets/{css.md.DJgj2clw.lean.js → css.md.CltvJqAa.lean.js} +0 -0
  530. /data/docs/assets/{custom-element-tests.md.BrYJQEl3.lean.js → custom-element-tests.md.B_rbta32.lean.js} +0 -0
  531. /data/docs/assets/{database-access.md.C7l-Vuvb.lean.js → database-access.md.gnluu54N.lean.js} +0 -0
  532. /data/docs/assets/{dev-environment.md.GZv6xvi9.lean.js → dev-environment.md.BroAOLhF.lean.js} +0 -0
@@ -0,0 +1,373 @@
1
+ # Quick Tour of Brut's Features
2
+
3
+ ## Pages
4
+
5
+ A [*Page*](/pages) models, well, a web page. It's a class that holds all the data
6
+ necessary to generate its HTML as well as a method called `page_template`, which
7
+ generates the HTML via Phlex.
8
+
9
+ A page's routing is convention-based and starts with a URL:
10
+
11
+ ```ruby{6}
12
+ class App < Brut::Framework::App
13
+ def id = "my-app"
14
+ def organization = "my-org"
15
+
16
+ routes do
17
+ page "/dashboard"
18
+ end
19
+ end
20
+ ```
21
+
22
+ This URL means our page class is expected in `DashboardPage`.
23
+
24
+ ```ruby
25
+ class DashboardPage < AppPage
26
+ def initialize
27
+ @now = Time.now
28
+ end
29
+
30
+ def page_template
31
+ main do
32
+ h1 { "Hello!" }
33
+ h2 do
34
+ plain("It's ")
35
+ time(datetime: l(@now, format: iso_8601)) do
36
+ l(@now, format: date)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ ```
43
+
44
+ This would all produce HTML like so, depending on the value of
45
+
46
+ ```html [/dashboard]
47
+ <main>
48
+ <h1>Hello!</h1>
49
+ <h2>It's
50
+ <time datetime="2025-02-17">
51
+ Monday, Feb 17
52
+ </time>
53
+ </h2>
54
+ </main>
55
+ ```
56
+
57
+ Note that the actual HTML delivered would include the code for a layout.
58
+
59
+ ## Layouts
60
+
61
+ Brut includes the concept of [layouts](/layouts), and they work similar to Rails.
62
+ Layouts are classes, however, and implement the Phlex-standard `view_template`
63
+ method:
64
+
65
+ ```ruby
66
+ class DefaultLayout < Brut::FrontEnd::Layout
67
+ def initialize(page_name:)
68
+ @page_name = page_name
69
+ end
70
+
71
+ def view_template
72
+ doctype
73
+ html(lang: "en") do
74
+ head do
75
+ meta(charset: "utf-8")
76
+ link(rel: "preload", as: "style", href: asset_path("/css/styles.css"))
77
+ link(rel: "stylesheet", href: asset_path("/css/styles.css"))
78
+ script(defer: true, src: asset_path("/js/app.js"))
79
+ title { app_name }
80
+ end
81
+ body do
82
+ yield
83
+ end
84
+ end
85
+ end
86
+ end
87
+ ```
88
+
89
+ This produces this HTML:
90
+
91
+ ```html
92
+ <!DOCTYPE html>
93
+ <html>
94
+ <head>
95
+ <meta charset="utf-8">
96
+ <link rel="preload" as="style" href="/css/styles-«HASH».css">
97
+ <link rel="stylesheet" href="/css/styles-«HASH».css">
98
+ <script defer src="/js/app-«HASH».js">
99
+ <title>My Awesome App</title>
100
+ </head>
101
+ <body>
102
+ <-- page HTML here -->
103
+ </body>
104
+ </html>
105
+ ```
106
+
107
+
108
+ ## Components
109
+
110
+ *Components* are a way to manage the complexity of HTML generation. The are Phlex components, meaning they are a class that implements `view_template`.
111
+
112
+ Here's an example of a flash message component:
113
+
114
+ ```ruby
115
+ # components/flash_component.rb
116
+ class FlashComponent < AppComponent
117
+ def initialize(flash:)
118
+ if flash.notice?
119
+ @message_key = flash.notice
120
+ @role = :info
121
+ elsif flash.alert?
122
+ @message_key = flash.alert
123
+ @role = :alert
124
+ end
125
+ end
126
+
127
+ def any_message? = !@message_key.nil?
128
+
129
+ def view_template
130
+ if any_message?
131
+ div(role: @role) do
132
+ t([ :flash, @message_key ])
133
+ end
134
+ end
135
+ end
136
+ end
137
+ ```
138
+
139
+ You can then use this in any other view using `render`, provided by Phlex.
140
+
141
+ ```ruby
142
+ def page_template
143
+ header do
144
+ render FlashComponent.new(flash:)
145
+ end
146
+ end
147
+ ```
148
+
149
+ ## Forms
150
+
151
+ [*Forms*](/forms) are a major concept like pages, since they are the way a browser
152
+ submits data to the server.
153
+
154
+ In Brut, a form does three things:
155
+
156
+ * Describes the data in the `<form>` tag
157
+ * Implies a route where its data is submitted via HTTP POST
158
+ * Provides access to the submitted data (via an object with methods, not a Hash of Whatever)
159
+
160
+ Like `page`, `form` declares a form's route:
161
+
162
+ ```ruby{2}
163
+ routes do
164
+ form "/login"
165
+ end
166
+ ```
167
+
168
+ Brut is convention-based, so it will expect a class named `LoginForm` to exist. It
169
+ will also expect `LoginHandler` to exist, which is a class that will receive the
170
+ form submission and process it. More on handlers below.
171
+
172
+ `LoginForm` uses class methods to declare its inputs. These class methods mirror
173
+ the various form element tags in HTML (`input`, `select`, etc.), and the methods
174
+ attributes allow you to declare names and client-side constraints:
175
+
176
+ ```ruby [forms/login_form.rb]
177
+ class LoginForm < AppForm
178
+ input :email
179
+ input :password, minlength: 8
180
+ end
181
+ ```
182
+
183
+ An instance of this class can be used to create HTML:
184
+
185
+ ```ruby
186
+ def view_template
187
+ FormTag(for: @form) do
188
+ Inputs::InputTag(form: @form, input_name: :email)
189
+ Inputs::InputTag(form: @form, input_name: :password)
190
+ button { "Login" }
191
+ end
192
+ end
193
+ ```
194
+
195
+ > [!NOTE]
196
+ > We'll explain what `FormTag` and `Inputs::InputTag` are in the [forms
197
+ > section](/forms)
198
+
199
+ This generates this HTML:
200
+
201
+ ```html
202
+ <form action="/login" method="POST">
203
+ <input type="email" name="email" required>
204
+ <input type="password" name="password" required minlength="8">
205
+ <button>Login</button>
206
+ </form>
207
+ ```
208
+
209
+ When the form is submitted, an instance of `LoginForm` is created and made available
210
+ to `LoginHandler`
211
+
212
+ ## Handlers
213
+
214
+ A handler is like a controller in Rails, except it only has one method: `handle`.
215
+ Unlike a Rails controller, a handler class is given its arguments explicitly, and
216
+ `handle`'s return value dictates what will happen next.
217
+
218
+ ```ruby
219
+ class LoginHandler < AppHandler
220
+ def initialize(form:)
221
+ @form = form
222
+ end
223
+ def handle
224
+ if @form.email == "secret@example.com" &&
225
+ @form.password = "sup3rs3cret!"
226
+ redirect_to(DashboardPage)
227
+ else
228
+ form.server_side_constraint_violation(
229
+ input_name: :email,
230
+ key: :no_such_user
231
+ )
232
+ LoginPage.new(form:)
233
+ end
234
+ end
235
+ end
236
+ ```
237
+
238
+ Note that we access the form's values as methods, not by digging into a Hash of
239
+ Whatever. Also note that returning an instance of a page will generate that page's
240
+ HTML, much like Rails' `render :edit` might. Lastly, `redirect_to` is a
241
+ convenience to generate a URL to `DashboardPage`, and ultimately causes `handle` to
242
+ return a `URI`, which Brut interprets as a redirect.
243
+
244
+
245
+ ## JavaScript
246
+
247
+ Brut doesn't include a front-end framework, however you can certainly use one. All
248
+ JavaScript is bundled into a single bundle by [esbuild](https://esbuild.github.io/).
249
+
250
+ Brut includes [BrutJS](/brut-js), which is a collection of autonomous custom
251
+ elements AKA Web Components that provide convenient features like autosubmit, form
252
+ submission confirmation and more:
253
+
254
+ ```ruby{5,7}
255
+ def view_template
256
+ FormTag(for: @form) do
257
+ Inputs::InputTag(form: @form, input_name: :email)
258
+ Inputs::InputTag(form: @form, input_name: :password)
259
+ brut_confirm_submit(message: "Really login? In this economy?!") do
260
+ button { "Login" }
261
+ end
262
+ end
263
+ end
264
+ ```
265
+
266
+ When "Login" is pressed, `window.confirm` will ask if the visitor wants to proceed.
267
+ This custom element can also use a `<dialog>` that you style, and works even better
268
+ if that `<dialog>` makes use of `<brut-confirmation-dialog>`.
269
+
270
+ ## CSS
271
+
272
+ Brut includes [BrutCSS](/css#using-brut-css), which is a lightweight utility-based
273
+ CSS library to let you get started quickly. It's *not* TailwindCSS, nor will it ever
274
+ be.
275
+
276
+ You can replace it with whatever you like easily enough.
277
+
278
+
279
+ ## Database Schema
280
+
281
+ Brut provides access to an SQL database via Sequel. Brut uses Sequel's database schema management, however it is enhanced to
282
+ encourage good practices by default.
283
+
284
+ > [!NOTE]
285
+ > Brut currently *only supports* PostgreSQL. It may support all RDBMSes that Sequel supports, but as of now,
286
+ > it's just Postgres.
287
+
288
+ Consider a `households` table that relates to an `accounts` table.
289
+
290
+ ```ruby
291
+ create_table :households,
292
+ comment: "Family unit managing the data" do
293
+ column :timezone, :text
294
+ column :dinner_time_of_day, :text
295
+ constraint(
296
+ :time_must_be_time,
297
+ %{
298
+ (dinner_time_of_day ~ '^[01][0-9]:[0-5][0-9]$') OR
299
+ (dinner_time_of_day ~ '^2[0-3]:[0-5][0-9]$')
300
+ }
301
+ )
302
+ end
303
+
304
+ create_table :accounts,
305
+ comment: "People or systems who can access this system",
306
+ external_id: true do
307
+
308
+ column :email, :email_address, unique: true
309
+ column :deactivated_at, :timestamptz, null: true
310
+ foreign_key :household_id, :households
311
+ end
312
+ ```
313
+
314
+ This is mostly using [Sequel's built-in migrations API](https://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html). But, a few additional behaviors are happening:
315
+
316
+ * Columns default to `NOT NULL`
317
+ * Tables require comments
318
+ * Foreign keys default to having constraints and indexes
319
+
320
+ There are other quality-of-life features of Brut's migration system, all designed to default to a good practice, with a way to do it
321
+ however you want when needed.
322
+
323
+ ## Database Access
324
+
325
+ Brut uses `Sequel::Model` to access data in your database. To discourage the conflation of "models of database tables" with "models
326
+ of your application's domain", these classes are in the `DB` namespace. Thus, the class `DB::Household` would be able to access the
327
+ `households` table defined above. This frees you up to create a `Household` class to model your domain's logic without being coupled
328
+ to how you store some data in a database.
329
+
330
+ ```ruby
331
+ class DB::Account < AppDataModel
332
+ has_external_id :ac
333
+ many_to_one :household
334
+ end
335
+
336
+ class DB::Household < AppDataModel
337
+ one_to_many :accounts
338
+ end
339
+ ```
340
+
341
+ ## Domain and Business Logic
342
+
343
+ Brut uses Zeitwerk for code loading, so any directories you create will be auto-loaded and refreshed during development. This means that you can create a class named `Household` in `app/src/back_end/domain/household.rb` and it would be loaded. Or, you could create `HouseholdService` in `app/src/back_end/services/household_service.rb` if you like.
344
+
345
+ > [!TIP]
346
+ > Providing a generally-useful abstraction for business or domain logic is not usually feasible.
347
+ > Thus, Brut doesn't provide much beyond Zeitwerk's auto-loading feature. It may provide more
348
+ > assistance in the future, but for now, Brut's approach is to free you from any prescription
349
+ > or moral imperative. Manage your domain and business logic how you see fit. You know your domain
350
+ > and team better than we do.
351
+
352
+ ## Testing
353
+
354
+ Brut provides support for three types of tests:
355
+
356
+ * Unit Tests, using RSpec
357
+ * End-to-end tests, using RSpec and Playwright
358
+ * Custom Element tests, written in JavaScript, using Mocha
359
+
360
+ Since Brut is based on classes, objects, and methods, your unit tests will usually
361
+ be straightforward, however Brut provides helpers to test your Page and Component
362
+ HTML using Nokogiri. FactoryBot is included and configured to manage test data.
363
+
364
+ ## Tasks
365
+
366
+ Brut doesn't use Rake tasks. It uses CLI apps powered by Ruby's `OptionParser`. Brut provides bootstrapping classes to make your own CLIs, as well as some light abstractions to make `OptionParser` a little more ergonomic. Brut's dev and production management CLIs are built using this support.
367
+
368
+ ## Observability
369
+
370
+ Brut has built-in support for [OpenTelemetry](https://opentelemetry.io/). Brut includes configuration for the [otel-desktop-viewer](https://github.com/CtrlSpice/otel-desktop-viewer) or a text-based viewer suitable for development. For production, most observability vendors provide OpenTelemetry ingestion any many have free tiers.
371
+
372
+ Brut does support logging, however you are encouraged to use OpenTelemetry instead.
373
+
@@ -1,205 +1,189 @@
1
1
  # Flash and Session
2
2
 
3
- Brut sessions are stored in cookies, encrypted to prevent tampering. The *flash*, which is a way to temporarily
4
- store small bits of information between page loads, is encoded in the session.
3
+ Brut sessions are stored in cookies, encrypted to prevent tampering. The *flash*, which is a way to temporarily store small bits of information between page loads, is encoded in the session.
5
4
 
6
5
  ## Overview
7
6
 
8
- Unlike Rails, the session and flash are presented to you as objects, not Hashes. By declaring the `session:`
9
- parameter on an initializer, you'll be given the current session for the request as an `AppSession`, which
10
- inherits from `Brut::FrontEnd::Session`. Similarly, declaring `flash:`, you'll get a `Brut::FrontEnd::Flash`.
7
+ Unlike Rails, the session and flash are presented to you as objects, not Hashes of Whatever. By declaring the `session:`
8
+ parameter on an initializer, you'll be given the current session for the request as an `AppSession`, which inherits from `Brut::FrontEnd::Session`. Similarly, declaring `flash:`, you'll get a `Brut::FrontEnd::Flash`.
11
9
 
12
10
  The idea is to use Ruby's type system to describe what data is in the session and flash.
13
11
 
14
12
  ### Session
15
13
 
16
- Brut's session is somewhat richer than you might get from other frameworks. In particular, the session can
17
- provide you:
14
+ Brut's session is somewhat richer than you might get from other frameworks. In particular, the session can provide you:
18
15
 
19
16
  * The current `Brut::I18n::HTTPAcceptLanguage`, which is the visitor's locale. See [I18n](/i18n) for how this
20
17
  works and how to use this value.
21
18
  * The timezone as provided by the browser.
22
19
  * An explicitly-set timezone that may or may not be what the browser provided. See [Space-Time Continuum](/space-time-continuum) for more details.
23
20
 
24
- The session also handles serializing the flash to and from the browser's cookies and can store any arbitrary data
25
- you like via `[]`. You are encouraged to add methods to your app's `AppSession` to make it explicit what you are
26
- storing.
21
+ To access the session, declare it as a keyword argument to your page, handler, or
22
+ global component's intitializer:
27
23
 
28
- Let's see the [route hook](/hooks) from the [pages](/pages) section again.
24
+ ```ruby
25
+ class HomePage < AppPage
26
+ def initialize(session:)
27
+ @session = session
28
+ end
29
+ end
30
+ ```
31
+
32
+ When you create your Brut app, your `AppSession` won't have anything in it, although
33
+ it's a `Brut::FrontEnd::Session`, so you can certainly use `[]` and `[]=` on it.
34
+ However, you are encouraged to declare methods that describe precisely what is in
35
+ the session.
29
36
 
30
- > [!CAUTION]
31
- > This hook is not production-ready. It lacks certain error-handling situations and
32
- > makes an assumption about how the session is managed. It's for demonstration only.
33
- > The [route hooks](/hooks) section has a more
34
- > appropriate example.
37
+ Let's say the currently logged-in visitor is available in the session. Your
38
+ `HomePage` could look like so:
35
39
 
36
40
  ```ruby
37
- class RequireAuthBeforeHook < Brut::FrontEnd::RouteHook
38
- def before(request_context:,session:)
39
- if session.current_user_id
40
- user = DB::User.find(id: session.current_user_id)
41
- if user
42
- request_context[:current_user] = user
41
+ class HomePage < AppPage
42
+ def initialize(session:)
43
+ @session = session
44
+ end
45
+
46
+ def view_template
47
+ h1 do
48
+ if @session.current_visitor
49
+ "Hello #{@session.current_visitor.name}"
50
+ else
51
+ "Hi!"
43
52
  end
44
53
  end
45
54
  end
46
55
  end
47
56
  ```
48
57
 
49
- When this hook executes, `session` will be an `AppSession`, serialized from the browser's cookies. Here's what
50
- that class might look like:
58
+ Let's suppose a `LoginHandler` exists, that can set a value for `current_visitor`:
51
59
 
52
60
  ```ruby
53
- # app/src/front_end/support/app_session.rb
54
- class AppSession < Brut::FrontEnd::Session
55
- def login!(current_user:)
56
- self[:current_user_id] = current_user.id
57
- end
58
-
59
- def logout!
60
- self[:current_user_id] = nil
61
+ class LoginHandler < AppHandler
62
+ def initialize(form:, session:)
63
+ @form = form
64
+ @session = session
61
65
  end
62
66
 
63
- def logged_in?
64
- !!self.current_user_id
67
+ def handle
68
+ visitor = Login.from_form(form:) # assume this exists
69
+ if visitor
70
+ @sesion.login!(visitor:)
71
+ else
72
+ # ...
73
+ end
65
74
  end
66
-
67
- def current_user_id = self[:current_user_id]
68
75
  end
69
76
  ```
70
77
 
71
- The session is a rich object and not just a thin wrapper over a Hash. You could even have the session perform
72
- the lookup in the database:
78
+ `AppSession` would need to look like so:
73
79
 
74
- ```ruby {11,14-16}
75
- # app/src/front_end/support/app_session.rb
80
+ ```ruby
76
81
  class AppSession < Brut::FrontEnd::Session
77
- def login!(current_user:)
78
- self[:current_user_id] = current_user.id
79
- end
80
- def logout!
81
- self[:current_user_id] = nil
82
+ def login!(visitor:)
83
+ self[:current_visitor_id] = visitor.id
82
84
  end
83
85
 
84
- def logged_in?
85
- !!self.current_user
86
- end
87
-
88
- def current_user
89
- DB::User.find(id: self[:current_user_id])
86
+ def current_visitor
87
+ DB::Visitor.find(id: self[:current_visitor_id])
90
88
  end
91
89
  end
92
90
  ```
93
91
 
94
- Now, the hook could call `current_user`:
92
+ Brut encourages your session to be a rich object. You can declare any methods you
93
+ like:
95
94
 
96
- ```ruby {3-5}
97
- class RequireAuthBeforeHook < Brut::FrontEnd::RouteHook
98
- def before(request_context:,session:)
99
- if session.logged_in?
100
- request_context[:current_user] = session.current_user
101
- end
102
- end
95
+ ```ruby
96
+ class AppSession < Brut::FrontEnd::Session
97
+ def logged_in? = !!self.current_visitor
103
98
  end
104
99
  ```
105
100
 
106
- Let's see `LoginHandler` from the [handlers](/handlers) section, to see how to save the current user. Given what
107
- we've learned, the declaration of the `session:` parameter to the initializer means the relevant instance of
108
- `AppSession` will be passed in.
101
+ > [!NOTE]
102
+ > When dealing with auth, you can leverage
103
+ > keyword injection beyond injecting the session. This is
104
+ > discussed in [the auth recipe](/recipes/authentication.md)
109
105
 
110
- ```ruby {20}
111
- # app/src/front_end/handlers/login_handler.rb
112
- class LoginHandler < AppHandler
113
- def initialize(form:, session:)
114
- @form = form
115
- @session = session
116
- end
106
+ ### Flash
117
107
 
118
- def handle
119
- if !@form.constraint_violations?
120
- authorized_user = AuthorizedUser.login(
121
- email: form.email,
122
- password: form.password
123
- )
124
- if authorized_user.nil?
125
- @form.server_side_constraint_violation(
126
- input_name: :email,
127
- key: :login_not_found
128
- )
129
- else
130
- session.login!(current_user: authorized_user.user)
131
- end
132
- end
133
- if @form.constraint_violations?
134
- LoginPage.new(form: @form)
135
- else
136
- redirect_to(DashboardPage.routing)
137
- end
108
+ To access the flash, declare it as a keyword argument to your page, handler, or
109
+ global component's intitializer:
110
+
111
+ ```ruby {2,4}
112
+ class DeleteWidgetByIdHandler < AppHandler
113
+ def initialize(widget_id:, flash:)
114
+ @widget_id = widget_id
115
+ @flash = flash
138
116
  end
139
117
  end
140
118
  ```
141
119
 
142
- Brut will handle saving the updated values in the response so when, in this case, the `DashboardPage` is
143
- rendered, it can see which user is logged in.
144
-
145
- ### Flash
146
-
147
- By default, your app will use Brut's flash class, `Brut::FrontEnd::Flash`. This is because you typically don't
148
- need to enhance the flash. Brut's flash has an "alert" and "notice", and you can use them however you see fit. You can also set arbitrary messages in the flash via `[]`.
120
+ By default, the flash will be a `Brut::FrontEnd::Flash`. While you can set your own
121
+ class, this is less commonly needed, so Brut doesn't provide one by default. Like
122
+ the session, you can use `[]`, but are discouraged from this to avoid Hashes of
123
+ Whatever littering your code.
149
124
 
150
- The contents of the flash only survive one request, so anything you set will be available in that session's next
151
- request, but not after that.
125
+ The default flash provides a `notice` attribute and an `alert` attribute. Their
126
+ values only survive one request, so anything you set will be available in that session's next request, but not after that.
152
127
 
153
- Note that the flash's alert and notice are intended to be I18n keys. You don't have to use them this way, but it
154
- is encouraged. If you pass an array into `alert=` or `notice=`, the elements will be joined to form an I18n key.
128
+ The values are expected to be I18n keys:
155
129
 
156
- You can create your own subclass if you need a richer flash class than the one Brut provides.
157
-
158
- First, create your class. It can be anywhere, but we recommend `app/src/front_end/support/app_flash.rb`:
159
-
160
- ```ruby
161
- # app/src/front_end/support/app_flash.rb
162
- class AppFlash < Brut::FrontEnd::Flash
163
- # For example
164
- def debug=(debug)
165
- self[:debug] = debug
130
+ ```ruby {5,8}
131
+ def handle
132
+ widget = DB::Widget.find!(id: @widget_id)
133
+ if widget.can_delete?
134
+ widget.delete
135
+ @flash.notice = :widget_deleted
136
+ redirect_to(WidgetsPage)
137
+ else
138
+ @flash.alert = :widget_cannot_be_deleted
139
+ WidgetsPage.new
140
+ end
166
141
  end
167
- def debug = self[:debug]
168
- def debug? = !!self.debug
169
142
  end
170
143
  ```
171
144
 
172
- Then, in your `App`, located in `app/src/app.rb`, use `Brut.container.override` to change the class used for the
173
- flash:
174
-
175
- ```ruby
176
- class App < Brut::Framework::App
177
- # ...
178
- def initialize
179
- Brut.container.override(
180
- "flash_class",
181
- AppFlash
182
- )
145
+ This is only enforced by convention, but you should stick to one convention since
146
+ you will likely create a [global component](/components) for the flash:
147
+
148
+ ```ruby {17}
149
+ class FlashComponent < AppComponent
150
+ def initialize(flash:)
151
+ if flash.notice?
152
+ @message_key = flash.notice
153
+ @role = :info
154
+ elsif flash.alert?
155
+ @message_key = flash.alert
156
+ @role = :alert
157
+ end
183
158
  end
184
159
 
185
- # ...
160
+ def any_message? = !@message_key.nil?
161
+
162
+ def view_template
163
+ if any_message?
164
+ div(role: @role) do
165
+ t([ :flash, @message_key ])
166
+ end
167
+ end
168
+ end
186
169
  end
187
170
  ```
188
171
 
189
- Brut's configuration system is discussed in more detail in [Configuration](/configuration).
172
+ See [using your own Flash class](/recipes/custom-flash) to see how to enhance Brut's
173
+ flash with your own logic.
190
174
 
191
175
  ## Testing
192
176
 
193
177
  Testing your session or flash classes may not be super valuable, however they are normal Ruby objects so you can
194
- test them in a conventional way. Both classes treat their internals as a Hash, so you can implement and assert
195
- via the `[]` and `[]=` methods.
178
+ test them in a conventional way. Although you are discouraged from using `[]` and
179
+ `[]=` as the public API of your session or flash, they can be useful for assertions
180
+ or test setup.
196
181
 
197
182
  ## Recommended Practices
198
183
 
199
- While you can use both the flash and the session has a hash of whatever, your are encouraged to avoid this in
200
- your production code. Create well-defined attributes or methods to manipulate these objects using the language
201
- of your domain.
202
-
184
+ Do not treat the session or flash as a Hash of Whatever. Isolate all magic keys to
185
+ the class and provide a rich API. It doesn't take that much effort and will make
186
+ your app way easier to manage.
203
187
 
204
188
  ## Technical Notes
205
189