brut 0.0.21 → 0.0.23

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 (689) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +24 -3
  3. data/.nvim.lua +1 -0
  4. data/Dockerfile.dx +12 -3
  5. data/Gemfile.lock +9 -7
  6. data/README.md +0 -7
  7. data/Rakefile +6 -4
  8. data/bin/dev +20 -0
  9. data/bin/docs +27 -0
  10. data/bin/setup +47 -1
  11. data/brut-css/.nvim.lua +1 -0
  12. data/brut-css/README.md +28 -0
  13. data/brut-css/bin/build +31 -0
  14. data/brut-css/bin/dev +1 -0
  15. data/brut-css/bin/docs +15 -0
  16. data/brut-css/bin/setup +5 -0
  17. data/brut-css/config/media-queries-all.css +15 -0
  18. data/brut-css/config/media-queries-minimal.css +5 -0
  19. data/brut-css/config/postcss.config.cjs +7 -0
  20. data/brut-css/config/pseudo-classes-all.css +9 -0
  21. data/brut-css/dx +1 -0
  22. data/brut-css/package-lock.json +3217 -0
  23. data/brut-css/package.json +36 -0
  24. data/brut-css/src/css/appearance.css +145 -0
  25. data/brut-css/src/css/border.css +522 -0
  26. data/brut-css/src/css/colors.css +3502 -0
  27. data/brut-css/src/css/dimensions.css +548 -0
  28. data/brut-css/src/css/flex.css +179 -0
  29. data/brut-css/src/css/index.css +13 -0
  30. data/brut-css/src/css/layout.css +120 -0
  31. data/brut-css/src/css/list.css +41 -0
  32. data/brut-css/src/css/positioning.css +354 -0
  33. data/brut-css/src/css/properties/colors.css +455 -0
  34. data/brut-css/src/css/properties/index.css +3 -0
  35. data/brut-css/src/css/properties/spacing.css +140 -0
  36. data/brut-css/src/css/properties/typography.css +224 -0
  37. data/brut-css/src/css/reset.css +107 -0
  38. data/brut-css/src/css/spacing.css +585 -0
  39. data/brut-css/src/css/typography.css +519 -0
  40. data/brut-css/src/css/utils.css +104 -0
  41. data/brut-css/src/docs/1_getting-started/1_overview.md +46 -0
  42. data/brut-css/src/docs/1_getting-started/2_installation.md +25 -0
  43. data/brut-css/src/docs/1_getting-started/3_core-concepts.md +75 -0
  44. data/brut-css/src/docs/1_getting-started/4_simple-example.md +132 -0
  45. data/brut-css/src/docs/1_getting-started/page.html.ejs +10 -0
  46. data/brut-css/src/docs/2_properties/page.html.ejs +71 -0
  47. data/brut-css/src/docs/3_classes/color-demo.html.ejs +31 -0
  48. data/brut-css/src/docs/3_classes/page.html.ejs +87 -0
  49. data/brut-css/src/docs/4_customization/1_design-system.md +36 -0
  50. data/brut-css/src/docs/4_customization/2_breakpoints.md +75 -0
  51. data/brut-css/src/docs/4_customization/3_pseudo-classes.md +74 -0
  52. data/brut-css/src/docs/4_customization/4_advanced-configuration.md +40 -0
  53. data/brut-css/src/docs/4_customization/page.html.ejs +10 -0
  54. data/brut-css/src/docs/docs.css +98 -0
  55. data/brut-css/src/docs/includes/body-and-header.html.ejs +30 -0
  56. data/brut-css/src/docs/includes/footer-and-rest.html.ejs +9 -0
  57. data/brut-css/src/docs/includes/head.html.ejs +5 -0
  58. data/brut-css/src/docs/includes/nav.html.ejs +10 -0
  59. data/brut-css/src/docs/index.html.ejs +32 -0
  60. data/brut-css/src/docs/prism-twilight.min.css +1 -0
  61. data/brut-css/src/js/Logger.js +71 -0
  62. data/brut-css/src/js/build.js +111 -0
  63. data/brut-css/src/js/cli/CLIArgError.js +7 -0
  64. data/brut-css/src/js/cli/Debug.js +27 -0
  65. data/brut-css/src/js/cli/DocsDir.js +16 -0
  66. data/brut-css/src/js/cli/DocsTemplateSourceDir.js +16 -0
  67. data/brut-css/src/js/cli/InputFile.js +31 -0
  68. data/brut-css/src/js/cli/MediaQueryConfigFile.js +10 -0
  69. data/brut-css/src/js/cli/OutputFile.js +22 -0
  70. data/brut-css/src/js/cli/ParsedArg.js +17 -0
  71. data/brut-css/src/js/cli/PathToBrutCSSRoot.js +19 -0
  72. data/brut-css/src/js/cli/PseudoClassConfigFile.js +11 -0
  73. data/brut-css/src/js/cli.js +108 -0
  74. data/brut-css/src/js/docGenerator.js +467 -0
  75. data/brut-css/src/js/mediaQueryConfigParser.js +98 -0
  76. data/brut-css/src/js/post-css-plugins/addMediaQueriesPlugin.js +49 -0
  77. data/brut-css/src/js/post-css-plugins/addPseudoClassesPlugin.js +42 -0
  78. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Category.js +9 -0
  79. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/DocState.js +185 -0
  80. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Documentable.js +8 -0
  81. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Group.js +7 -0
  82. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/ParsedComment.js +73 -0
  83. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Property.js +9 -0
  84. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyCategory.js +4 -0
  85. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyGroup.js +8 -0
  86. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Rule.js +12 -0
  87. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleCategory.js +4 -0
  88. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleGroup.js +8 -0
  89. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeRef.js +5 -0
  90. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeURL.js +9 -0
  91. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin.js +49 -0
  92. data/brut-css/src/js/post-css-plugins/generateRootCustomPropertiesPlugin.js +45 -0
  93. data/brut-css/src/js/pseudoClassConfigParser.js +145 -0
  94. data/brut-js/.projections.json +10 -0
  95. data/brut-js/README.md +118 -0
  96. data/brut-js/bin/build +10 -0
  97. data/brut-js/bin/ci +5 -0
  98. data/brut-js/bin/setup +5 -0
  99. data/brut-js/docs/README.md +8 -0
  100. data/brut-js/docs/jsdoc-plugins/customElementTag.js +8 -0
  101. data/brut-js/docs/jsdoc-theme/publish.js +692 -0
  102. data/brut-js/docs/jsdoc-theme/static/scripts/linenumber.js +25 -0
  103. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/Apache-License-2.0.txt +202 -0
  104. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/lang-css.js +2 -0
  105. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/prettify.js +28 -0
  106. data/brut-js/docs/jsdoc-theme/static/styles/jsdoc-default.css +327 -0
  107. data/brut-js/docs/jsdoc-theme/static/styles/prettify-jsdoc.css +111 -0
  108. data/brut-js/docs/jsdoc-theme/static/styles/prettify-tomorrow.css +132 -0
  109. data/brut-js/docs/jsdoc-theme/tmpl/augments.tmpl +10 -0
  110. data/brut-js/docs/jsdoc-theme/tmpl/container.tmpl +199 -0
  111. data/brut-js/docs/jsdoc-theme/tmpl/details.tmpl +143 -0
  112. data/brut-js/docs/jsdoc-theme/tmpl/example.tmpl +2 -0
  113. data/brut-js/docs/jsdoc-theme/tmpl/examples.tmpl +13 -0
  114. data/brut-js/docs/jsdoc-theme/tmpl/exceptions.tmpl +32 -0
  115. data/brut-js/docs/jsdoc-theme/tmpl/layout.tmpl +38 -0
  116. data/brut-js/docs/jsdoc-theme/tmpl/mainpage.tmpl +14 -0
  117. data/brut-js/docs/jsdoc-theme/tmpl/members.tmpl +38 -0
  118. data/brut-js/docs/jsdoc-theme/tmpl/method.tmpl +131 -0
  119. data/brut-js/docs/jsdoc-theme/tmpl/modifies.tmpl +14 -0
  120. data/brut-js/docs/jsdoc-theme/tmpl/params.tmpl +131 -0
  121. data/brut-js/docs/jsdoc-theme/tmpl/properties.tmpl +108 -0
  122. data/brut-js/docs/jsdoc-theme/tmpl/returns.tmpl +19 -0
  123. data/brut-js/docs/jsdoc-theme/tmpl/source.tmpl +8 -0
  124. data/brut-js/docs/jsdoc-theme/tmpl/tutorial.tmpl +19 -0
  125. data/brut-js/docs/jsdoc-theme/tmpl/type.tmpl +7 -0
  126. data/brut-js/docs/jsdoc.config.json +23 -0
  127. data/brut-js/docs/package-lock.json +343 -0
  128. data/brut-js/docs/package.json +7 -0
  129. data/brut-js/package-lock.json +2171 -0
  130. data/brut-js/package.json +32 -0
  131. data/brut-js/specs/AjaxSubmit.spec.js +256 -0
  132. data/brut-js/specs/Autosubmit.spec.js +127 -0
  133. data/brut-js/specs/ConfirmSubmit.spec.js +193 -0
  134. data/brut-js/specs/ConstraintViolationMessage.spec.js +33 -0
  135. data/brut-js/specs/ConstraintViolationMessages.spec.js +29 -0
  136. data/brut-js/specs/CopyToClipboard.spec.js +35 -0
  137. data/brut-js/specs/Form.spec.js +181 -0
  138. data/brut-js/specs/I18nTranslation.spec.js +19 -0
  139. data/brut-js/specs/LocaleDetection.spec.js +22 -0
  140. data/brut-js/specs/Message.spec.js +15 -0
  141. data/brut-js/specs/SpecHelper.js +23 -0
  142. data/brut-js/specs/Tabs.spec.js +41 -0
  143. data/brut-js/specs/config/asset_metadata.json +7 -0
  144. data/brut-js/src/AjaxSubmit.js +384 -0
  145. data/brut-js/src/Autosubmit.js +63 -0
  146. data/brut-js/src/BaseCustomElement.js +261 -0
  147. data/brut-js/src/ConfirmSubmit.js +116 -0
  148. data/brut-js/src/ConfirmationDialog.js +143 -0
  149. data/brut-js/src/ConstraintViolationMessage.js +125 -0
  150. data/brut-js/src/ConstraintViolationMessages.js +98 -0
  151. data/brut-js/src/CopyToClipboard.js +96 -0
  152. data/brut-js/src/Form.js +151 -0
  153. data/brut-js/src/I18nTranslation.js +61 -0
  154. data/brut-js/src/LocaleDetection.js +117 -0
  155. data/brut-js/src/Logger.js +90 -0
  156. data/brut-js/src/Message.js +56 -0
  157. data/brut-js/src/RichString.js +113 -0
  158. data/brut-js/src/Tabs.js +168 -0
  159. data/brut-js/src/Tracing.js +247 -0
  160. data/brut-js/src/appForTestingOnly.js +15 -0
  161. data/brut-js/src/index.js +130 -0
  162. data/brut-js/src/testing/AssetMetadata.js +35 -0
  163. data/brut-js/src/testing/AssetMetadataLoader.js +25 -0
  164. data/brut-js/src/testing/CustomElementTest.js +235 -0
  165. data/brut-js/src/testing/DOMCreator.js +45 -0
  166. data/brut-js/src/testing/index.js +48 -0
  167. data/brutrb.com/.vitepress/config.mjs +106 -0
  168. data/brutrb.com/.vitepress/plugins/jsdocLinker.js +34 -0
  169. data/brutrb.com/.vitepress/plugins/rdocLinker.js +18 -0
  170. data/brutrb.com/.vitepress/theme/custom.css +7 -0
  171. data/brutrb.com/.vitepress/theme/index.js +18 -0
  172. data/brutrb.com/.vitepress/theme/style.css +149 -0
  173. data/brutrb.com/ai.md +68 -0
  174. data/brutrb.com/assets.md +138 -0
  175. data/brutrb.com/bin/build +5 -0
  176. data/brutrb.com/bin/deploy +7 -0
  177. data/brutrb.com/bin/dev +5 -0
  178. data/brutrb.com/bin/setup +5 -0
  179. data/brutrb.com/brut-js.md +117 -0
  180. data/brutrb.com/business-logic.md +55 -0
  181. data/brutrb.com/cli.md +278 -0
  182. data/brutrb.com/components.md +243 -0
  183. data/brutrb.com/configuration.md +257 -0
  184. data/brutrb.com/css.md +103 -0
  185. data/brutrb.com/custom-element-tests.md +149 -0
  186. data/brutrb.com/database-access.md +201 -0
  187. data/brutrb.com/database-schema.md +312 -0
  188. data/brutrb.com/deployment.md +66 -0
  189. data/brutrb.com/dev-environment.md +179 -0
  190. data/brutrb.com/doc-conventions.md +39 -0
  191. data/brutrb.com/end-to-end-tests.md +174 -0
  192. data/brutrb.com/flash-and-session.md +224 -0
  193. data/brutrb.com/forms.md +866 -0
  194. data/brutrb.com/getting-started.md +66 -0
  195. data/brutrb.com/handlers.md +153 -0
  196. data/brutrb.com/hooks.md +178 -0
  197. data/brutrb.com/i18n.md +188 -0
  198. data/brutrb.com/images/Makefile +10 -0
  199. data/brutrb.com/images/dev-env-overview.dot +54 -0
  200. data/brutrb.com/images/dev-env-overview.png +0 -0
  201. data/brutrb.com/images/dev-env-protocol.dot +37 -0
  202. data/brutrb.com/images/dev-env-protocol.png +0 -0
  203. data/brutrb.com/images/logo-300.png +0 -0
  204. data/brutrb.com/images/logo.png +0 -0
  205. data/brutrb.com/images/overview.graffle +0 -0
  206. data/brutrb.com/images/overview.png +0 -0
  207. data/brutrb.com/images/spa.dot +19 -0
  208. data/brutrb.com/images/spa.png +0 -0
  209. data/brutrb.com/images/workspace-protocol.dot +44 -0
  210. data/brutrb.com/images/workspace-protocol.png +0 -0
  211. data/brutrb.com/index.md +36 -0
  212. data/brutrb.com/instrumentation.md +183 -0
  213. data/brutrb.com/javascript.md +122 -0
  214. data/brutrb.com/jobs.md +14 -0
  215. data/brutrb.com/keyword-injection.md +237 -0
  216. data/brutrb.com/markdown-examples.md +85 -0
  217. data/brutrb.com/middleware.md +80 -0
  218. data/brutrb.com/not-released.md +5 -0
  219. data/brutrb.com/overview.md +404 -0
  220. data/brutrb.com/package-lock.json +2404 -0
  221. data/brutrb.com/package.json +11 -0
  222. data/brutrb.com/pages.md +378 -0
  223. data/brutrb.com/public/images/logo-300.png +0 -0
  224. data/brutrb.com/public/images/logo.png +0 -0
  225. data/brutrb.com/routes.md +215 -0
  226. data/brutrb.com/security.md +105 -0
  227. data/brutrb.com/seed-data.md +63 -0
  228. data/brutrb.com/space-time-continuum.md +85 -0
  229. data/brutrb.com/tutorial.md +3 -0
  230. data/brutrb.com/unit-tests.md +148 -0
  231. data/docker-compose.dx.yml +6 -3
  232. data/docs/404.html +21 -0
  233. data/docs/CNAME +1 -0
  234. data/docs/ai.html +24 -0
  235. data/docs/api/Brut/BackEnd/SeedData.html +493 -0
  236. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +214 -0
  237. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +125 -0
  238. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +125 -0
  239. data/docs/api/Brut/BackEnd/Sidekiq.html +125 -0
  240. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +414 -0
  241. data/docs/api/Brut/BackEnd/Validators.html +128 -0
  242. data/docs/api/Brut/BackEnd.html +132 -0
  243. data/docs/api/Brut/CLI/App.html +1576 -0
  244. data/docs/api/Brut/CLI/AppRunner.html +491 -0
  245. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +264 -0
  246. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +306 -0
  247. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +262 -0
  248. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +314 -0
  249. data/docs/api/Brut/CLI/Apps/BuildAssets.html +183 -0
  250. data/docs/api/Brut/CLI/Apps/DB/Create.html +365 -0
  251. data/docs/api/Brut/CLI/Apps/DB/Drop.html +357 -0
  252. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +383 -0
  253. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +335 -0
  254. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +329 -0
  255. data/docs/api/Brut/CLI/Apps/DB/Seed.html +347 -0
  256. data/docs/api/Brut/CLI/Apps/DB/Status.html +383 -0
  257. data/docs/api/Brut/CLI/Apps/DB.html +183 -0
  258. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +303 -0
  259. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +512 -0
  260. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +398 -0
  261. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +374 -0
  262. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +410 -0
  263. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +262 -0
  264. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +303 -0
  265. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +480 -0
  266. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +450 -0
  267. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +380 -0
  268. data/docs/api/Brut/CLI/Apps/Scaffold.html +253 -0
  269. data/docs/api/Brut/CLI/Apps/Test/Audit.html +464 -0
  270. data/docs/api/Brut/CLI/Apps/Test/E2e.html +407 -0
  271. data/docs/api/Brut/CLI/Apps/Test/JS.html +262 -0
  272. data/docs/api/Brut/CLI/Apps/Test/Run.html +578 -0
  273. data/docs/api/Brut/CLI/Apps/Test.html +253 -0
  274. data/docs/api/Brut/CLI/Apps.html +125 -0
  275. data/docs/api/Brut/CLI/Command.html +2342 -0
  276. data/docs/api/Brut/CLI/Error.html +139 -0
  277. data/docs/api/Brut/CLI/ExecutionResults/Result.html +664 -0
  278. data/docs/api/Brut/CLI/ExecutionResults.html +675 -0
  279. data/docs/api/Brut/CLI/Executor.html +430 -0
  280. data/docs/api/Brut/CLI/InvalidOption.html +245 -0
  281. data/docs/api/Brut/CLI/Options.html +753 -0
  282. data/docs/api/Brut/CLI/Output.html +699 -0
  283. data/docs/api/Brut/CLI/SystemExecError.html +451 -0
  284. data/docs/api/Brut/CLI.html +263 -0
  285. data/docs/api/Brut/FactoryBot.html +225 -0
  286. data/docs/api/Brut/Framework/App.html +1097 -0
  287. data/docs/api/Brut/Framework/Config.html +1045 -0
  288. data/docs/api/Brut/Framework/Container.html +1379 -0
  289. data/docs/api/Brut/Framework/Error.html +140 -0
  290. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +144 -0
  291. data/docs/api/Brut/Framework/Errors/Bug.html +234 -0
  292. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +257 -0
  293. data/docs/api/Brut/Framework/Errors/MissingParameter.html +273 -0
  294. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +471 -0
  295. data/docs/api/Brut/Framework/Errors/NotFound.html +308 -0
  296. data/docs/api/Brut/Framework/Errors/NotImplemented.html +234 -0
  297. data/docs/api/Brut/Framework/Errors.html +328 -0
  298. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +392 -0
  299. data/docs/api/Brut/Framework/MCP.html +861 -0
  300. data/docs/api/Brut/Framework/ProjectEnvironment.html +648 -0
  301. data/docs/api/Brut/Framework.html +129 -0
  302. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +317 -0
  303. data/docs/api/Brut/FrontEnd/Component/Helpers.html +326 -0
  304. data/docs/api/Brut/FrontEnd/Component.html +365 -0
  305. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +470 -0
  306. data/docs/api/Brut/FrontEnd/Components/FormTag.html +518 -0
  307. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +317 -0
  308. data/docs/api/Brut/FrontEnd/Components/Input.html +195 -0
  309. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +339 -0
  310. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +660 -0
  311. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +417 -0
  312. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +918 -0
  313. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +651 -0
  314. data/docs/api/Brut/FrontEnd/Components/Inputs.html +125 -0
  315. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +367 -0
  316. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +336 -0
  317. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +655 -0
  318. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +352 -0
  319. data/docs/api/Brut/FrontEnd/Components.html +135 -0
  320. data/docs/api/Brut/FrontEnd/Download.html +467 -0
  321. data/docs/api/Brut/FrontEnd/Flash.html +1150 -0
  322. data/docs/api/Brut/FrontEnd/Form.html +1157 -0
  323. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +634 -0
  324. data/docs/api/Brut/FrontEnd/Forms/Input.html +615 -0
  325. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +547 -0
  326. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1318 -0
  327. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +609 -0
  328. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +587 -0
  329. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +613 -0
  330. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +582 -0
  331. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +609 -0
  332. data/docs/api/Brut/FrontEnd/Forms.html +127 -0
  333. data/docs/api/Brut/FrontEnd/GenericResponse.html +377 -0
  334. data/docs/api/Brut/FrontEnd/Handler.html +442 -0
  335. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +318 -0
  336. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +336 -0
  337. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +399 -0
  338. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +354 -0
  339. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +151 -0
  340. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +315 -0
  341. data/docs/api/Brut/FrontEnd/Handlers.html +125 -0
  342. data/docs/api/Brut/FrontEnd/HandlingResults.html +339 -0
  343. data/docs/api/Brut/FrontEnd/HttpMethod.html +661 -0
  344. data/docs/api/Brut/FrontEnd/HttpStatus.html +496 -0
  345. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +284 -0
  346. data/docs/api/Brut/FrontEnd/Layout.html +318 -0
  347. data/docs/api/Brut/FrontEnd/Middleware.html +135 -0
  348. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +288 -0
  349. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +292 -0
  350. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +324 -0
  351. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +372 -0
  352. data/docs/api/Brut/FrontEnd/Middlewares.html +125 -0
  353. data/docs/api/Brut/FrontEnd/Page.html +773 -0
  354. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +797 -0
  355. data/docs/api/Brut/FrontEnd/Pages.html +125 -0
  356. data/docs/api/Brut/FrontEnd/RequestContext.html +1312 -0
  357. data/docs/api/Brut/FrontEnd/RouteHook.html +424 -0
  358. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +242 -0
  359. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +249 -0
  360. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +264 -0
  361. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +261 -0
  362. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +284 -0
  363. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +252 -0
  364. data/docs/api/Brut/FrontEnd/RouteHooks.html +115 -0
  365. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +227 -0
  366. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +305 -0
  367. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +324 -0
  368. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +319 -0
  369. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +315 -0
  370. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +315 -0
  371. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +327 -0
  372. data/docs/api/Brut/FrontEnd/Routing/Route.html +761 -0
  373. data/docs/api/Brut/FrontEnd/Routing.html +927 -0
  374. data/docs/api/Brut/FrontEnd/Session.html +1195 -0
  375. data/docs/api/Brut/FrontEnd.html +134 -0
  376. data/docs/api/Brut/I18n/BaseMethods.html +931 -0
  377. data/docs/api/Brut/I18n/ForBackEnd.html +302 -0
  378. data/docs/api/Brut/I18n/ForCLI.html +302 -0
  379. data/docs/api/Brut/I18n/ForHTML.html +296 -0
  380. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +316 -0
  381. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +930 -0
  382. data/docs/api/Brut/I18n.html +127 -0
  383. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +435 -0
  384. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +286 -0
  385. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +302 -0
  386. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +864 -0
  387. data/docs/api/Brut/Instrumentation.html +126 -0
  388. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +532 -0
  389. data/docs/api/Brut/SinatraHelpers.html +281 -0
  390. data/docs/api/Brut/SpecSupport/ClockSupport.html +383 -0
  391. data/docs/api/Brut/SpecSupport/ComponentSupport.html +502 -0
  392. data/docs/api/Brut/SpecSupport/E2ETestServer.html +503 -0
  393. data/docs/api/Brut/SpecSupport/E2eSupport.html +142 -0
  394. data/docs/api/Brut/SpecSupport/EnhancedNode.html +403 -0
  395. data/docs/api/Brut/SpecSupport/FlashSupport.html +278 -0
  396. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +401 -0
  397. data/docs/api/Brut/SpecSupport/GeneralSupport.html +195 -0
  398. data/docs/api/Brut/SpecSupport/HandlerSupport.html +160 -0
  399. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +553 -0
  400. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +439 -0
  401. data/docs/api/Brut/SpecSupport/Matchers.html +125 -0
  402. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +335 -0
  403. data/docs/api/Brut/SpecSupport/RSpecSetup.html +602 -0
  404. data/docs/api/Brut/SpecSupport/SessionSupport.html +196 -0
  405. data/docs/api/Brut/SpecSupport.html +129 -0
  406. data/docs/api/Brut.html +225 -0
  407. data/docs/api/Clock.html +603 -0
  408. data/docs/api/RichString.html +968 -0
  409. data/docs/api/SemanticLogger/Appender/Async.html +219 -0
  410. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +115 -0
  411. data/docs/api/Sequel/Extensions/BrutMigrations.html +533 -0
  412. data/docs/api/Sequel/Extensions.html +117 -0
  413. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +105 -0
  414. data/docs/api/Sequel/Plugins/CreatedAt.html +125 -0
  415. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +207 -0
  416. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +186 -0
  417. data/docs/api/Sequel/Plugins/ExternalId.html +218 -0
  418. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +202 -0
  419. data/docs/api/Sequel/Plugins/FindBang.html +125 -0
  420. data/docs/api/Sequel/Plugins.html +117 -0
  421. data/docs/api/Sequel.html +117 -0
  422. data/docs/api/_index.html +1553 -0
  423. data/docs/api/class_list.html +54 -0
  424. data/docs/api/css/common.css +1 -0
  425. data/docs/api/css/full_list.css +58 -0
  426. data/docs/api/css/style.css +503 -0
  427. data/docs/api/file.README.html +127 -0
  428. data/docs/api/file_list.html +59 -0
  429. data/docs/api/frames.html +22 -0
  430. data/docs/api/index.html +127 -0
  431. data/docs/api/js/app.js +344 -0
  432. data/docs/api/js/full_list.js +242 -0
  433. data/docs/api/js/jquery.js +4 -0
  434. data/docs/api/method_list.html +3998 -0
  435. data/docs/api/top-level-namespace.html +112 -0
  436. data/docs/assets/ai.md.tZrjP9im.js +1 -0
  437. data/docs/assets/ai.md.tZrjP9im.lean.js +1 -0
  438. data/docs/assets/app.D_yaTITQ.js +1 -0
  439. data/docs/assets/assets.md.D3wunzLx.js +19 -0
  440. data/docs/assets/assets.md.D3wunzLx.lean.js +1 -0
  441. data/docs/assets/brut-js.md.o2DAO2s2.js +12 -0
  442. data/docs/assets/brut-js.md.o2DAO2s2.lean.js +1 -0
  443. data/docs/assets/business-logic.md.BY4hGy0m.js +1 -0
  444. data/docs/assets/business-logic.md.BY4hGy0m.lean.js +1 -0
  445. data/docs/assets/chunks/@localSearchIndexroot.BsN5i0Fi.js +1 -0
  446. data/docs/assets/chunks/VPLocalSearchBox.B2-ZzyTY.js +8 -0
  447. data/docs/assets/chunks/framework.1L-BeKqY.js +18 -0
  448. data/docs/assets/chunks/theme.CfGFVRvE.js +2 -0
  449. data/docs/assets/cli.md.RmeA2b0i.js +127 -0
  450. data/docs/assets/cli.md.RmeA2b0i.lean.js +1 -0
  451. data/docs/assets/components.md.eCttGlN-.js +104 -0
  452. data/docs/assets/components.md.eCttGlN-.lean.js +1 -0
  453. data/docs/assets/configuration.md.BRriU0cL.js +78 -0
  454. data/docs/assets/configuration.md.BRriU0cL.lean.js +1 -0
  455. data/docs/assets/css.md.DJgj2clw.js +21 -0
  456. data/docs/assets/css.md.DJgj2clw.lean.js +1 -0
  457. data/docs/assets/custom-element-tests.md.BrYJQEl3.js +69 -0
  458. data/docs/assets/custom-element-tests.md.BrYJQEl3.lean.js +1 -0
  459. data/docs/assets/database-access.md.C7l-Vuvb.js +63 -0
  460. data/docs/assets/database-access.md.C7l-Vuvb.lean.js +1 -0
  461. data/docs/assets/database-schema.md.BUjR0VS1.js +63 -0
  462. data/docs/assets/database-schema.md.BUjR0VS1.lean.js +1 -0
  463. data/docs/assets/deployment.md.Dbka4OTr.js +1 -0
  464. data/docs/assets/deployment.md.Dbka4OTr.lean.js +1 -0
  465. data/docs/assets/dev-env-overview.Gj7NWM8-.png +0 -0
  466. data/docs/assets/dev-env-protocol.DysDAtnz.png +0 -0
  467. data/docs/assets/dev-environment.md.BNc8AYiK.js +11 -0
  468. data/docs/assets/dev-environment.md.BNc8AYiK.lean.js +1 -0
  469. data/docs/assets/doc-conventions.md.DCfRXXi-.js +1 -0
  470. data/docs/assets/doc-conventions.md.DCfRXXi-.lean.js +1 -0
  471. data/docs/assets/end-to-end-tests.md.yfQHC0b5.js +26 -0
  472. data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +1 -0
  473. data/docs/assets/flash-and-session.md.BXY8RvT0.js +93 -0
  474. data/docs/assets/flash-and-session.md.BXY8RvT0.lean.js +1 -0
  475. data/docs/assets/forms.md.CBTYQ_Cz.js +379 -0
  476. data/docs/assets/forms.md.CBTYQ_Cz.lean.js +1 -0
  477. data/docs/assets/getting-started.md.Bz2s1Vjb.js +2 -0
  478. data/docs/assets/getting-started.md.Bz2s1Vjb.lean.js +1 -0
  479. data/docs/assets/handlers.md.089DVD3v.js +69 -0
  480. data/docs/assets/handlers.md.089DVD3v.lean.js +1 -0
  481. data/docs/assets/hooks.md.C4-moMny.js +80 -0
  482. data/docs/assets/hooks.md.C4-moMny.lean.js +1 -0
  483. data/docs/assets/i18n.md.Do9i1qWl.js +23 -0
  484. data/docs/assets/i18n.md.Do9i1qWl.lean.js +1 -0
  485. data/docs/assets/index.md.B28EwVpq.js +1 -0
  486. data/docs/assets/index.md.B28EwVpq.lean.js +1 -0
  487. data/docs/assets/instrumentation.md.CL6ax7nT.js +35 -0
  488. data/docs/assets/instrumentation.md.CL6ax7nT.lean.js +1 -0
  489. data/docs/assets/javascript.md.GWbhRS51.js +31 -0
  490. data/docs/assets/javascript.md.GWbhRS51.lean.js +1 -0
  491. data/docs/assets/jobs.md.S-2amAYp.js +1 -0
  492. data/docs/assets/jobs.md.S-2amAYp.lean.js +1 -0
  493. data/docs/assets/keyword-injection.md.Dt2tKREs.js +25 -0
  494. data/docs/assets/keyword-injection.md.Dt2tKREs.lean.js +1 -0
  495. data/docs/assets/markdown-examples.md.CCFEQO44.js +33 -0
  496. data/docs/assets/markdown-examples.md.CCFEQO44.lean.js +1 -0
  497. data/docs/assets/middleware.md.Czz_UlJN.js +20 -0
  498. data/docs/assets/middleware.md.Czz_UlJN.lean.js +1 -0
  499. data/docs/assets/not-released.md.BBy28McC.js +1 -0
  500. data/docs/assets/not-released.md.BBy28McC.lean.js +1 -0
  501. data/docs/assets/overview.Da81cB9R.png +0 -0
  502. data/docs/assets/overview.md.CDalkuxV.js +133 -0
  503. data/docs/assets/overview.md.CDalkuxV.lean.js +1 -0
  504. data/docs/assets/pages.md.BE3kfOc5.js +122 -0
  505. data/docs/assets/pages.md.BE3kfOc5.lean.js +1 -0
  506. data/docs/assets/routes.md.BMM7peut.js +29 -0
  507. data/docs/assets/routes.md.BMM7peut.lean.js +1 -0
  508. data/docs/assets/security.md.C668yXCi.js +1 -0
  509. data/docs/assets/security.md.C668yXCi.lean.js +1 -0
  510. data/docs/assets/seed-data.md.BvFZlqIk.js +14 -0
  511. data/docs/assets/seed-data.md.BvFZlqIk.lean.js +1 -0
  512. data/docs/assets/spa.qejUdp-5.png +0 -0
  513. data/docs/assets/space-time-continuum.md.KPUIKysQ.js +1 -0
  514. data/docs/assets/space-time-continuum.md.KPUIKysQ.lean.js +1 -0
  515. data/docs/assets/style.D73IYGCX.css +1 -0
  516. data/docs/assets/tutorial.md.BnoGjrdK.js +1 -0
  517. data/docs/assets/tutorial.md.BnoGjrdK.lean.js +1 -0
  518. data/docs/assets/unit-tests.md.DUGrnLj5.js +13 -0
  519. data/docs/assets/unit-tests.md.DUGrnLj5.lean.js +1 -0
  520. data/docs/assets/workspace-protocol.C0gXsoDb.png +0 -0
  521. data/docs/assets.html +42 -0
  522. data/docs/brut-css/brut.css +1 -0
  523. data/docs/brut-css/brut.max.css +22372 -0
  524. data/docs/brut-css/classes/appearances.html +783 -0
  525. data/docs/brut-css/classes/background-colors.html +3529 -0
  526. data/docs/brut-css/classes/border-colors.html +3529 -0
  527. data/docs/brut-css/classes/borders.html +2293 -0
  528. data/docs/brut-css/classes/dimensions.html +2581 -0
  529. data/docs/brut-css/classes/flex.html +917 -0
  530. data/docs/brut-css/classes/foreground-colors.html +3261 -0
  531. data/docs/brut-css/classes/junk-drawer.html +431 -0
  532. data/docs/brut-css/classes/layout.html +668 -0
  533. data/docs/brut-css/classes/lists.html +331 -0
  534. data/docs/brut-css/classes/positioning.html +1751 -0
  535. data/docs/brut-css/classes/spacings.html +2633 -0
  536. data/docs/brut-css/classes/typography.html +2206 -0
  537. data/docs/brut-css/customization/advanced-configuration.html +204 -0
  538. data/docs/brut-css/customization/breakpoints.html +227 -0
  539. data/docs/brut-css/customization/design-system.html +197 -0
  540. data/docs/brut-css/customization/pseudo-classes.html +228 -0
  541. data/docs/brut-css/docs.css +98 -0
  542. data/docs/brut-css/getting-started/core-concepts.html +234 -0
  543. data/docs/brut-css/getting-started/installation.html +190 -0
  544. data/docs/brut-css/getting-started/overview.html +210 -0
  545. data/docs/brut-css/getting-started/simple-example.html +285 -0
  546. data/docs/brut-css/index.html +193 -0
  547. data/docs/brut-css/prism-twilight.min.css +1 -0
  548. data/docs/brut-css/properties/colors.html +1548 -0
  549. data/docs/brut-css/properties/spacings.html +614 -0
  550. data/docs/brut-css/properties/typography.html +777 -0
  551. data/docs/brut-js/api/AjaxSubmit.html +374 -0
  552. data/docs/brut-js/api/AjaxSubmit.js.html +435 -0
  553. data/docs/brut-js/api/Autosubmit.html +192 -0
  554. data/docs/brut-js/api/Autosubmit.js.html +114 -0
  555. data/docs/brut-js/api/BaseCustomElement.html +1091 -0
  556. data/docs/brut-js/api/BaseCustomElement.js.html +312 -0
  557. data/docs/brut-js/api/BrutCustomElements.html +172 -0
  558. data/docs/brut-js/api/BufferedLogger.html +173 -0
  559. data/docs/brut-js/api/ConfirmSubmit.html +278 -0
  560. data/docs/brut-js/api/ConfirmSubmit.js.html +167 -0
  561. data/docs/brut-js/api/ConfirmationDialog.html +425 -0
  562. data/docs/brut-js/api/ConfirmationDialog.js.html +194 -0
  563. data/docs/brut-js/api/ConstraintViolationMessage.html +448 -0
  564. data/docs/brut-js/api/ConstraintViolationMessage.js.html +176 -0
  565. data/docs/brut-js/api/ConstraintViolationMessages.html +590 -0
  566. data/docs/brut-js/api/ConstraintViolationMessages.js.html +149 -0
  567. data/docs/brut-js/api/CopyToClipboard.html +345 -0
  568. data/docs/brut-js/api/CopyToClipboard.js.html +147 -0
  569. data/docs/brut-js/api/Form.html +294 -0
  570. data/docs/brut-js/api/Form.js.html +202 -0
  571. data/docs/brut-js/api/I18nTranslation.html +409 -0
  572. data/docs/brut-js/api/I18nTranslation.js.html +112 -0
  573. data/docs/brut-js/api/LocaleDetection.html +312 -0
  574. data/docs/brut-js/api/LocaleDetection.js.html +168 -0
  575. data/docs/brut-js/api/Logger.html +702 -0
  576. data/docs/brut-js/api/Logger.js.html +141 -0
  577. data/docs/brut-js/api/Message.html +238 -0
  578. data/docs/brut-js/api/Message.js.html +107 -0
  579. data/docs/brut-js/api/PrefixedLogger.html +369 -0
  580. data/docs/brut-js/api/RichString.html +1049 -0
  581. data/docs/brut-js/api/RichString.js.html +164 -0
  582. data/docs/brut-js/api/Tabs.html +295 -0
  583. data/docs/brut-js/api/Tabs.js.html +219 -0
  584. data/docs/brut-js/api/Tracing.html +277 -0
  585. data/docs/brut-js/api/Tracing.js.html +298 -0
  586. data/docs/brut-js/api/external-CustomElementRegistry.html +140 -0
  587. data/docs/brut-js/api/external-Performance.html +138 -0
  588. data/docs/brut-js/api/external-Promise.html +138 -0
  589. data/docs/brut-js/api/external-ValidityState.html +138 -0
  590. data/docs/brut-js/api/external-Window.html +233 -0
  591. data/docs/brut-js/api/external-fetch.html +138 -0
  592. data/docs/brut-js/api/global.html +400 -0
  593. data/docs/brut-js/api/index.html +168 -0
  594. data/docs/brut-js/api/index.js.html +181 -0
  595. data/docs/brut-js/api/module-testing.html +383 -0
  596. data/docs/brut-js/api/scripts/linenumber.js +25 -0
  597. data/docs/brut-js/api/scripts/prettify/Apache-License-2.0.txt +202 -0
  598. data/docs/brut-js/api/scripts/prettify/lang-css.js +2 -0
  599. data/docs/brut-js/api/scripts/prettify/prettify.js +28 -0
  600. data/docs/brut-js/api/styles/jsdoc-default.css +327 -0
  601. data/docs/brut-js/api/styles/prettify-jsdoc.css +111 -0
  602. data/docs/brut-js/api/styles/prettify-tomorrow.css +132 -0
  603. data/docs/brut-js/api/testing.AssetMetadata.html +172 -0
  604. data/docs/brut-js/api/testing.AssetMetadataLoader.html +171 -0
  605. data/docs/brut-js/api/testing.CustomElementTest.html +679 -0
  606. data/docs/brut-js/api/testing.DOMCreator.html +171 -0
  607. data/docs/brut-js/api/testing_AssetMetadata.js.html +86 -0
  608. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +76 -0
  609. data/docs/brut-js/api/testing_CustomElementTest.js.html +286 -0
  610. data/docs/brut-js/api/testing_DOMCreator.js.html +96 -0
  611. data/docs/brut-js/api/testing_index.js.html +99 -0
  612. data/docs/brut-js.html +35 -0
  613. data/docs/business-logic.html +24 -0
  614. data/docs/cli.html +150 -0
  615. data/docs/components.html +127 -0
  616. data/docs/configuration.html +101 -0
  617. data/docs/css.html +44 -0
  618. data/docs/custom-element-tests.html +92 -0
  619. data/docs/database-access.html +86 -0
  620. data/docs/database-schema.html +86 -0
  621. data/docs/deployment.html +24 -0
  622. data/docs/dev-environment.html +34 -0
  623. data/docs/doc-conventions.html +24 -0
  624. data/docs/end-to-end-tests.html +49 -0
  625. data/docs/flash-and-session.html +116 -0
  626. data/docs/forms.html +402 -0
  627. data/docs/getting-started.html +25 -0
  628. data/docs/handlers.html +92 -0
  629. data/docs/hashmap.json +1 -0
  630. data/docs/hooks.html +103 -0
  631. data/docs/i18n.html +46 -0
  632. data/docs/images/logo-300.png +0 -0
  633. data/docs/images/logo.png +0 -0
  634. data/docs/index.html +24 -0
  635. data/docs/instrumentation.html +58 -0
  636. data/docs/javascript.html +54 -0
  637. data/docs/jobs.html +24 -0
  638. data/docs/keyword-injection.html +48 -0
  639. data/docs/markdown-examples.html +56 -0
  640. data/docs/middleware.html +43 -0
  641. data/docs/not-released.html +24 -0
  642. data/docs/overview.html +156 -0
  643. data/docs/pages.html +145 -0
  644. data/docs/routes.html +52 -0
  645. data/docs/security.html +24 -0
  646. data/docs/seed-data.html +37 -0
  647. data/docs/space-time-continuum.html +24 -0
  648. data/docs/tutorial.html +24 -0
  649. data/docs/unit-tests.html +36 -0
  650. data/docs/vp-icons.css +1 -0
  651. data/lib/brut/cli/app_runner.rb +1 -1
  652. data/lib/brut/cli/apps/test.rb +5 -0
  653. data/lib/brut/framework/mcp.rb +0 -4
  654. data/lib/brut/front_end/component.rb +59 -84
  655. data/lib/brut/front_end/components/constraint_violations.rb +1 -4
  656. data/lib/brut/front_end/components/form_tag.rb +1 -1
  657. data/lib/brut/front_end/components/input.rb +3 -3
  658. data/lib/brut/front_end/components/inputs/csrf_token.rb +1 -1
  659. data/lib/brut/front_end/components/inputs/{text_field.rb → input_tag.rb} +6 -8
  660. data/lib/brut/front_end/components/inputs/radio_button.rb +1 -1
  661. data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +187 -0
  662. data/lib/brut/front_end/components/inputs/{textarea.rb → textarea_tag.rb} +2 -2
  663. data/lib/brut/front_end/components/time_tag.rb +2 -1
  664. data/lib/brut/front_end/form.rb +4 -4
  665. data/lib/brut/front_end/forms/input.rb +2 -1
  666. data/lib/brut/front_end/forms/input_definition.rb +5 -2
  667. data/lib/brut/front_end/forms/radio_button_group_input.rb +2 -1
  668. data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +2 -2
  669. data/lib/brut/front_end/forms/select_input.rb +2 -4
  670. data/lib/brut/front_end/forms/select_input_definition.rb +2 -2
  671. data/lib/brut/front_end/handler.rb +28 -26
  672. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +5 -2
  673. data/lib/brut/front_end/handlers/instrumentation_handler.rb +8 -4
  674. data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -5
  675. data/lib/brut/front_end/handlers/missing_handler.rb +5 -2
  676. data/lib/brut/front_end/page.rb +4 -6
  677. data/lib/brut/front_end/request_context.rb +3 -2
  678. data/lib/brut/i18n/base_methods.rb +136 -82
  679. data/lib/brut/i18n/for_back_end.rb +1 -0
  680. data/lib/brut/i18n/for_cli.rb +1 -0
  681. data/lib/brut/i18n/for_html.rb +32 -4
  682. data/lib/brut/instrumentation/open_telemetry.rb +12 -2
  683. data/lib/brut/sinatra_helpers.rb +10 -3
  684. data/lib/brut/spec_support/component_support.rb +18 -18
  685. data/lib/brut/spec_support/e2e_test_server.rb +3 -0
  686. data/lib/brut/version.rb +1 -1
  687. data/lib/sequel/extensions/brut_migrations.rb +12 -9
  688. metadata +647 -5
  689. data/lib/brut/front_end/components/inputs/select.rb +0 -117
Binary file
Binary file
Binary file
@@ -0,0 +1,19 @@
1
+ digraph G {
2
+
3
+ Node[fontname="Helvetica"]
4
+ Edge[fontname="Baskerville"]
5
+
6
+ ShouldBeSPA -> SessionMoreThan10Minutes
7
+ SessionMoreThan10Minutes -> No [label="No"]
8
+ SessionMoreThan10Minutes -> TenUpdatesPrimaryData [label="Yes"]
9
+ TenUpdatesPrimaryData -> No [label="No"]
10
+ TenUpdatesPrimaryData -> Maybe [label="Yes"]
11
+
12
+ ShouldBeSPA [ shape=rect label="Should this App\nby a Single-Page-App (SPA)?"]
13
+ No [ shape=octagon]
14
+ Maybe [ shape=circle]
15
+ SessionMoreThan10Minutes [ shape=diamond label="Sessions last\nlonger than\n10 minutes?\n"]
16
+ TenUpdatesPrimaryData [ shape=diamond label="More than 10\nupdates to primary\nuser data?"]
17
+
18
+ { rank=same; SessionMoreThan10Minutes; No }
19
+ }
Binary file
@@ -0,0 +1,44 @@
1
+ digraph G {
2
+
3
+ rankdir="LR"
4
+ nodesep=0.55
5
+ compound=true
6
+ node[shape=box fontname=avenir]
7
+
8
+ Setup[label=<
9
+ <FONT face="avenir">Setup</FONT>
10
+ <br/>
11
+ <FONT face="Courier New">bin/setup</FONT>
12
+ >]
13
+ Run[label=<
14
+ <FONT face="avenir">Run</FONT>
15
+ <br/>
16
+ <FONT face="Courier New">bin/dev</FONT>
17
+ >]
18
+ Test[label=<
19
+ <FONT face="avenir">Test</FONT>
20
+ <br/>
21
+ <FONT face="Courier New">bin/test</FONT>
22
+ >]
23
+ CI[label=<
24
+ <FONT face="avenir">Test</FONT>
25
+ <br/>
26
+ <FONT face="Courier New">bin/ci</FONT>
27
+ >]
28
+ Code[label=<
29
+ <FONT face="avenir">Code</FONT>
30
+ <br/>
31
+ <FONT face="Baskerville">Write Code</FONT>
32
+ <br/>
33
+ <FONT face="Courier New">bin/db</FONT>
34
+ <br/>
35
+ <FONT face="Courier New">bin/scaffold</FONT>
36
+ >]
37
+ Setup -> Run
38
+ Setup -> Code
39
+ Run -> Code
40
+ Code -> Test
41
+ Test -> Code
42
+ Test -> CI
43
+ { rank=same; Run; Code; }
44
+ }
@@ -0,0 +1,36 @@
1
+ ---
2
+ # https://vitepress.dev/reference/default-theme-home-page
3
+ layout: home
4
+
5
+ hero:
6
+ name: "Brut RB"
7
+ text: "Raw Ruby Web Apps"
8
+ tagline: Standards-based, No-nonsense, HTML-first, Low Ceremony
9
+ image:
10
+ src: /images/logo.png
11
+ alt: "A Ruby gemstone embedded into a concrete, brutalist building"
12
+ actions:
13
+ - theme: brand
14
+ text: "Brut is Not Yet Released"
15
+ link: /not-released
16
+ - theme: brand
17
+ text: "Getting Started"
18
+ link: /getting-started
19
+ - theme: alt
20
+ text: Conceptual Overview
21
+ link: /overview
22
+
23
+ features:
24
+ - title: Standards-Based
25
+ icon: 📄
26
+ details: "Brut leverages HTML, HTTP, SQL, and the Ruby standard library to let you write apps using standards you already know…or could quickly learn"
27
+ - title: Convention-Oriented
28
+ icon: 🎚️
29
+ details: "In a Brut app, there's usually just one way to do something. Learn things once, and you won't forget how your app works."
30
+ - title: Objects and Methods
31
+ icon: 💻
32
+ details: "Author classes to create objects on which to call methods. Nothing fancy."
33
+ - title: Builds on Community Libraries
34
+ icon: 🏘️
35
+ details: "Sequel, Phlex, I18n, RSpec. They do it best"
36
+ ---
@@ -0,0 +1,183 @@
1
+ # Instrumentation and Observability
2
+
3
+ Brut has built-in support for OpenTelemetry, which is an open standard used by many observability vendors
4
+ to allow you to understand the behavior of your app in production. Brut also includes a configuration for
5
+ the [otel-desktop-viewer](https://github.com/CtrlSpice/otel-desktop-viewer/), which allows you to see
6
+ instrumentation in development.
7
+
8
+ ## Overview
9
+
10
+ ### Why Instrument?
11
+
12
+ In production, you'll need to know what your app is doing and how well it's working. Historically, logs
13
+ can provide this information in a roundabout way. Over the last many years, Application Performance
14
+ Monitoring (APM) vendors like New Relic and Data Dog allowed developers to see much richer detail about
15
+ how an app is working.
16
+
17
+ You could see, for example, the 95th percentil of your dashboard controller's performance, or the top 10
18
+ slowest SQL statements your app is executing. OpenTelemetry attempts to unify the API used to communicate
19
+ this information from your app to your chosen vendor, and most vendors support it.
20
+
21
+ Instrumentation, then, is a way to record what your app is doing, how long its taking, and perhaps even
22
+ why it's doing what it's doing, down to a very specific level. If properly configured, you could examine
23
+ the performance of the app for a particular user on a particular day.
24
+
25
+ ### Setting up Instrumentation
26
+
27
+ Brut automatically sets up OpenTelemetry (OTel) tracing. The primary interface you will use is
28
+ `Brut::Instrumentation::OpenTelemetry`, which is available via `Brut.container.instrumentation`. We'll
29
+ discuss that in a moment.
30
+
31
+ To configure the specifics of where the traces wil go, the OTel gem uses environment variables:
32
+
33
+ | Variable | Value | Purpose |
34
+ |--------------------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
35
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | Depends on environment | Where to send the tracers. This is provided by your vendor, but is `http://otel-desktop-viewer:4318` in development |
36
+ | `OTEL_EXPORTER_OTLP_HEADERS` | Depends on vendor | Your vendor may ask you to set this. It often contains identifying information or API keys |
37
+ | `OTEL_EXPORTER_OTLP_PROTOCOL` | http/protobuf | Your vendor may request a different protocol, but protobuf is common and supported by otel-desktop-viewer |
38
+ | `OTEL_LOG_LEVEL` | debug | Useful when setting everything up to understand why things aren't working if they aren't working |
39
+ | `OTEL_RUBY_BSP_START_THREAD_ON_BOOT` | false | Deals with esoteric issues with Puma. See [this GitHub issue](https://github.com/open-telemetry/opentelemetry-ruby/issues/462) for the details.
40
+ | `OTEL_SERVICE_NAME` | Your app's `id` from `App` | Identifiers your app's name to the vendor |
41
+ | `OTEL_TRACES_EXPORTER` | otlp | Configures the class inside the OTel gem that will export the instrumentation to the vendor. If you omit this, Brut will log the instrumentation to the console |
42
+
43
+ When you created your Brut app, your `.env.development` and `.env.test` should have values for all these
44
+ environment variables that will send instrumentation to the otel-desktop-viewer that was also configured.
45
+
46
+ If you run your app using `bin/dev` and use the app for a bit, then go to `http://localhost:8000`, you
47
+ will see the otel-desktop-viewer UI and can browser the spans and traces sent by Brut.
48
+
49
+
50
+ ### What is Instrumented By Default
51
+
52
+ Brut attempts to automatically instrument useful things so you don't have to do anything to start getting
53
+ data. Brut will attempt to conform to standard semantics for HTTP requests and SQL statements.
54
+
55
+ Here is a non-exhaustive list of what Brut automatically instruments:
56
+
57
+ * How long each page or handler request takes
58
+ * CLI execution time
59
+ * Time to rebuild the schema for tests
60
+ * Time to run tests
61
+ * Time to apply migrations
62
+ * Time spent inside a route hook
63
+ * The locale detected from the browser
64
+ * The layout class used when rendering a page
65
+ * If a requested path is owned by Brut or not
66
+ * Ignored parameters on all form submissions
67
+ * How long reloading takes in development
68
+ * CSP reporting results
69
+
70
+ ### Adding Your Own Instrumentation
71
+
72
+ You can add instrumentation in a few ways:
73
+
74
+ * *Spans* record a block of code. They are shown as a sub-span if one is already in effect. When you
75
+ create a span, that means it will be shown in the context of the HTTP request.
76
+ * *Attributes* can be added to the current span to provide more context about what is happening. For
77
+ example, the HTTP request method is an attribute of the span used for the HTTP request. These attributes
78
+ allow for sophisticated querying in the vendor's UI.
79
+ * *Events* record things that happen and metadata about that thing. These are like log statements. They
80
+ are associated with the span you are in when you add the event.
81
+
82
+ These can all be added via `Brut.container.instrumentation`, which is a
83
+ `Brut::Instrumentation::OpenTelemetry` instance.
84
+
85
+ These methods are available:
86
+
87
+ * `span(name,**attributes,&block)` - Create a new span around the block yielded.
88
+ * `add_attributes(attributes)` - Add attributes to the current span. These will be prefixed with your
89
+ app's prefix so it's clear in the observability UI that they are for your app and not standard.
90
+ * `add_event(name,**attributes)` - Add an event with optional attributes for the current span.
91
+ * `record_exception(ex,attributes=nil)` - Record an exception that was caught.
92
+ * `record_and_reraise_exception!(ex,attributes=nil)` - Record an exception and raise it.
93
+
94
+ Suppose you want to instrument `RequireAuthBeforeHook` from the [hooks](/hooks) documentation. Although
95
+ the hook's `before` method is instrumented by Brut already, let's add some metadata to that span, and also
96
+ add a span around the login check.
97
+
98
+ ```ruby {11-16,18,20,23-26}
99
+ # app/src/front_end/route_hooks/require_auth_before_hook.rb
100
+ class RequireAuthBeforeHook < Brut::FrontEnd::RouteHook
101
+ def before(request_context:,session:,request:,env:)
102
+ is_home_page = request.path_info.match(/^\/?$/)
103
+ is_auth_route = request.path_info.match?(/^\/auth\//)
104
+ is_brut_owned_path = env["brut.owned_path"]
105
+
106
+ requires_login = !is_home_page &&
107
+ !is_auth_route &&
108
+ !is_brut_owned_path
109
+ Brut.container.instrumentation.add_attributes(
110
+ requires_login:,
111
+ is_home_page:,
112
+ is_auth_route:,
113
+ is_brut_owned_path:
114
+ )
115
+
116
+ Brut.container.instrumentation.span("login-check") do |span|
117
+ if session.logged_in?
118
+ span.add_attributes(logged_in: true)
119
+ request_context[:authenticated_account] = session.authenticated_account
120
+ requires_login = false
121
+ else
122
+ span.add_attributes(logged_in: false)
123
+ end
124
+ end
125
+
126
+ if requires_login
127
+ redirect_to(Auth::LoginPage)
128
+ else
129
+ continue
130
+ end
131
+
132
+ end
133
+ end
134
+ ```
135
+
136
+ Now, for every request someone makes to our app, we will see a span for the `RequireAuthBeforeHook`, and
137
+ inside that span, we'll see the attributes we added as well as a sub-span representing the login check
138
+ (which itself will have an attribute about the user's logged-in status).
139
+
140
+ ### Client-Side Observability
141
+
142
+
143
+ The class `Brut::FrontEnd::Handlers::InstrumentationHandler` is set up to receive information from the
144
+ client-side to provide insights about client-side behavior as part of a server-side request. Brut
145
+ attempts to join up any client-side instrumentation to the request that served it.
146
+
147
+ It does this `Brut::FrontEnd::Components::Traceparent` component, which is included in your default layout
148
+ when you created your Brut app. This creates a `<meta>` tag containing standardized information used to
149
+ connect the client-side behavior to the server-side request.
150
+
151
+ The Brut custom element `<brut-tracing>` uses this information, along with statistics from the browser, to
152
+ send a custom payload back to Brut at the route `/__brut/instrumentation`, which is handled by the
153
+ aforementioned `InstrumentationHandler`.
154
+
155
+ You should then see client-side tracing information as a sub-span of your HTTP request. The information
156
+ available depends on the browser, and some browsers don't send much.
157
+
158
+ ## Testing
159
+
160
+ Generally you don't want to test instrumentation unless it's highly complex and critical to the app's
161
+ ability to be maintained. Ideally, your end-to-end tests will cover all the instrumentation code you
162
+ write so you can be sure that none of that causes a problem.
163
+
164
+ ## Recommended Practices
165
+
166
+ Entire books and conferences exist on how to properly instrument your app. Our suggestion is to take what
167
+ you have by default and add additional instrumentation only to solve specific problems or identify
168
+ specific issues.
169
+
170
+ ## Technical Notes
171
+
172
+ > [!IMPORTANT]
173
+ > Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
174
+ > internals, the source code is always more correct.
175
+
176
+ _Last Updated June 12, 2025_
177
+
178
+ Brut does not have plans to support non-OTel instrumentation, nor does it have plans to provide hooks to
179
+ use proprietary formats. This could change of course.
180
+
181
+ The client-side portion of this is highly customized. The Otel open source code for the client side is
182
+ massive and hugely complex, so Brut decided to try to produce something simple and straightforward as a
183
+ start. This can and will evolve over time.
@@ -0,0 +1,122 @@
1
+ # JavaScript
2
+
3
+ Brut provides basic bundling using [esbuild](https://esbuild.github.io/). Brut does not support nor prevent the
4
+ use of any front-end framework. Brut does, however, include [BrutJS](/brut-js), a lightweight library of HTML custom
5
+ elements and utility code. These elements can provide a fair bit of front-end functionality using progressive
6
+ enhancement without the need for a framework.
7
+
8
+ ## Overview
9
+
10
+ All your app's JavaScript lives in `app/src/front_end/js`, or in modules you bring in via `package.json`. Brut
11
+ will *bundle* all of that up into a single `.js` file that is served up with your app. Brut does this by using
12
+ esbuild, a stable and standardized tool for bundling JavaScript.
13
+
14
+ The way esbuild works is to be given an *entry point* that requires, or transitively requires, all of your
15
+ JavaScript by using ES6 modules. `app/src/front_end/js/index.js` is the entry point for your app.
16
+
17
+ For example, if you have a `Widget` class that uses a `Status` class, and you also use the third party library
18
+ "foobar", here is how all the files would look.
19
+
20
+ First, `package.json` (in your app's root) would include `"foobar"` (and it must set `"type"` to `"module"`):
21
+
22
+ ```json {2,5}
23
+ {
24
+ "name": "your-app",
25
+ "type": "module",
26
+ "license": "UNLICENSED",
27
+ "dependencies": {
28
+ "foobar": "^0.0.11"
29
+ },
30
+ "devDependencies": {
31
+ "chokidar-cli": "^3.0.0",
32
+ "esbuild": "^0.20.2",
33
+ "jsdom": "^25.0.1",
34
+ "mocha": "^10.7.3",
35
+ "playwright": "^1.50.1"
36
+ },
37
+ }
38
+ ```
39
+
40
+ Next, `app/src/front_end/js/index.js` would import both `"foobar"` and `"Widget"`:
41
+
42
+ ```javascript
43
+ import { foobar } from "foobar"
44
+ import Widget from "./Widget"
45
+
46
+ // ...
47
+ ```
48
+
49
+ Notice that "foobar", since it's brought in as a third party dependency, is imported without a `./`. Be careful
50
+ here! Every third party library has a different syntax for how to import whatever it is or does. Consult the
51
+ documentation of each third party library you wish to import.
52
+
53
+ The second `import` uses a `./` because it's importing a file in `app/src/front_end/js` namely `Widget.js`. Be
54
+ careful here, too, as you must be sure to `export` the right thing. Here's what `app/src/front_end/js/Widget.js`
55
+ might look like:
56
+
57
+ ```javascript
58
+ import Status from "./extra/Status"
59
+
60
+ class Widget {
61
+ status = new Status()
62
+ }
63
+
64
+ export default Widget
65
+ ```
66
+
67
+ Note that we import `Status` here. Unlike Ruby, ES6 modules requires each class that references a class to
68
+ import it explicitly. Also notice that we do `export default Widget`, which allows `import Widget` to work.
69
+
70
+ Finally, `app/src/front_end/extra/Status.js` looks like so:
71
+
72
+ ```javascript
73
+ class Status {
74
+ }
75
+ export default Status
76
+ ```
77
+
78
+ When `bin/build-assets` runs, esbuild will use `app/src/front_end/js/index.js` as its *entry point*, and will
79
+ bundle both `Widget.js` and the "foobar" library. When it bundles `Widget.js`, it will see that it imports
80
+ `extra/Status.js` and bundle that, too.
81
+
82
+ This bundle can be included in your app by ensuring this is in your layout:
83
+
84
+ ```ruby {5}
85
+ def view_template
86
+ doctype
87
+ html(lang: "en") do
88
+ head do
89
+ script(defer: true, src: asset_path("/js/app.js"))
90
+ # ...
91
+ ```
92
+
93
+ The `asset_path` helper takes a logical path—`/js/app.js`—and returns the actual path the browser can use. More
94
+ details on this can be found in [assets](/assets).
95
+
96
+ ## Testing
97
+
98
+ Client-side behavior is best tested with end-to-end tests, however you can simplify your end-to-end tests by
99
+ creating unit tests of your custom elements. BrutJS provides support for this. TBD LINK.
100
+
101
+ ## Recommended Practices
102
+
103
+ Brut encourages you to use HTML custom elements as progressive enhancements over server-generated views. This
104
+ sort of client-side code will age well. The toolchain and dependencies are minimal, so you will not have to
105
+ worry too much about code written this way.
106
+
107
+ It *will* be lower level and more verbose than existing frameworks. We would argue that it is not significantly
108
+ more difficult and the sustainability is worth it.
109
+
110
+ ## Technical Notes
111
+
112
+ > [!IMPORTANT]
113
+ > Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
114
+ > internals, the source code is always more correct.
115
+
116
+ _Last Updated May 7, 2025_
117
+
118
+ Currently, Brut only supports a single entry point and bundle. This could be easily made more flexible if there
119
+ is a desire to finely tweak the JavaScript loaded on specific pages.
120
+
121
+ Brut also does not expose any esbuild configuration. This could be provided in the future, but for now, it is
122
+ hard-coded.
@@ -0,0 +1,14 @@
1
+ # Background Jobs
2
+
3
+ Brut provides little direct support for background jobs. Currently, Brut recommends Sidekiq, since it is
4
+ battle-tested, well-supported, and open source.
5
+
6
+ When you set up your Brut app, it should ask if you want Sidekiq support and add the necessary configuraiton.
7
+
8
+ It will expect jobs in `app/src/back_end/jobs`.
9
+
10
+ > [!WARNING]
11
+ > The way Sidekiq is configured with Brut is effective and reliable, but it is complex. It currently
12
+ > involves several moving parts to make it work properly. This will be an area for improvement.
13
+
14
+
@@ -0,0 +1,237 @@
1
+ # Keyword Injection
2
+
3
+ Brut is desiged around classes and objects, as compared to modules and DSLs. Almost everything you do when building your app is to create a class that has an initializer and implements one or more methods. But, these initalizers and methods often need information from the request that Brut is managing.
4
+
5
+ In a basic Rack or Sinatra app, you would access this information via Rack's API, which is essentially a `Hash` of whatever. It's error-prone and requires consulting documentation, source code, or runtime information to figure out what's stored where.
6
+
7
+ Brut can instead inject these values explicitly into the classes of yours it creates. It does this based on the
8
+ names of keyword arguments declared by your class' intializer or a template method Brut will call.
9
+
10
+ ## Overview
11
+
12
+ For example, a [Page](/pages) requires you to implement an initializer. That initializer's keyword arguments define what
13
+ information is needed. Brut provides that information when it creates the object. This is a form of dependency injection and it can simplify your code if used effectively.
14
+
15
+ Consider this route:
16
+
17
+ ```ruby
18
+ page "/widgets/:id"
19
+ ```
20
+
21
+ Brut will expect to find `WidgetsByIdPage`. Your initializer can declare `id:` as a keyword arg and this will be passed when the
22
+ class is created:
23
+
24
+ ```ruby
25
+ class WidgetsByIdPage < AppPage
26
+ def initialize(id:)
27
+ @widget = DB::Widget.find(id)
28
+ end
29
+ end
30
+ ```
31
+
32
+ If the page requires access to the session, it can declare that:
33
+
34
+ ```ruby {2,4}
35
+ class WidgetsByIdPage < AppPage
36
+ def initialize(id:, session:)
37
+ @widget = DB::Widget.find(id)
38
+ @current_user = session.current_user
39
+ end
40
+ end
41
+ ```
42
+
43
+ Because `session:` is a required argument, Brut cannot instantiate the page without it, so it will always be
44
+ passed in and availbale.
45
+
46
+ ### Standard Injectible Information
47
+
48
+ In any request, the following information is available to be injected:
49
+
50
+ * `session:` - An instance of your app's `Brut::FrontEnd::Session` subclass for the current visitor's session.
51
+ * `flash:` - An instance of your app's `Brut::FrontEnd::Flash` subclass.
52
+ * `xhr:` - true if this was an Ajax request.
53
+ * `body:` - the body submitted, if any.
54
+ * `csrf_token:` - The current CSRF token.
55
+ * `clock:` - A `Clock` to be used to access the current time in the visitor's time zone.
56
+ * `http_*` - any parameter that starts with `http_` is assumed to be for an HTTP header. For example, `http_accept_language` would be
57
+ given the value for the "Accept-Language" header. See the section on HTTP headers below.
58
+ * `env:` - The Rack env. This is discouraged, but available if you can't get what you want directly
59
+
60
+ Depending on the context, other information is available:
61
+
62
+ * `form:` - If a form was submitted, this is the `Brut::FrontEnd::Form` subclass containing the data. See [Forms](/forms).
63
+ * Any query string paramter - Note that if these conflict with existing Brut values, the behavior is undefined. Name your query string parameters carefully. These should have default values or your page won't work if they are omitted.
64
+ * Any route parameter - These should not have default values, since they are required for Brut to match the route.
65
+
66
+ A `Brut::FrontEnd::RouteHook` is slightly different. Only the following data is available to be injected:
67
+
68
+ * `request_context:` - The current request context, thought it may be `nil` depending on when the hook runs
69
+ * `session:` - An instance of your app's `Brut::FrontEnd::Session` subclass for the current visitor's session.
70
+ * `request:` - The Rack request
71
+ * `response:` - The Rack response
72
+ * `env:` - The Rack env.
73
+
74
+ ### HTTP Headers
75
+
76
+ Since any header can be sent with a request, Brut allows you to access them, including non-standard ones. Rack (which is based on CGI), provides access to all HTTP headers in the `env` by taking the header name, replacing dashes ("-") with underscores ("\_"), and prepending `http_` to the name, then uppercasing it. Thus, "User-Agent" becomes `HTTP_USER_AGENT`.
77
+
78
+ Because Ruby parameters and variables must start with a lower-case letter, Brut uses the lowercased version of the Rack/CGI variable.
79
+ Thus, to receive the "User-Agent", you would declare the keyword parameter `http_user_agent`.
80
+
81
+ Further, because headers come from the client and may not be under your control, the value that is actually injected depends on a few
82
+ things:
83
+
84
+ * If your keyword arg is required, i.e. there is no default value:
85
+ - If the header was not provided, `nil` is injected.
86
+ - If the header *was* provided, it's value is injected, even if it's the empty string.
87
+ * If your keyword arg is optional, i.e. it has a default value
88
+ - If the header was not provided, no value is injected, and your code will receive the default value.
89
+ - If the header *was* provided, it's value is injected, even if it's the empty string.
90
+
91
+ ### Ordering and Disambiguation
92
+
93
+ You are discouraged from using builtin keys for your own data or request parameters. For example, you should not have a query string
94
+ parameter named `env` as this conflicts with the builtin `env` that Brut will inject.
95
+
96
+ Since you can inject your own data (see below), you are free to corrupt the request context. Please don't do this. Brut may actively
97
+ prevent this in the future.
98
+
99
+ You can also use the request context to put your own data that can be injected.
100
+
101
+ ### Injecting Custom Data
102
+
103
+ The correct place to inject your own data into the request is in a [before hook](/hooks). When you
104
+ configure a before hook, it will run after Brut's internal
105
+ `Brut::FrontEnd::RouteHooks::SetupRequestContext`, which ensures the request context exists and is ready
106
+ for use.
107
+
108
+ For example, here is how you might inject the currently logged-in account based on the session:
109
+
110
+ ```ruby
111
+ class AuthBeforeHook < Brut::FrontEnd::RouteHook
112
+ def before(request_context:,session:)
113
+ if session.authenticated_account
114
+ request_context[:authenticated_account] = session.authenticated_account
115
+ end
116
+ continue
117
+ end
118
+ end
119
+ ```
120
+
121
+ Note that the value is only injected if it exists. It's important not to inject `nil` for values that
122
+ don't exist.
123
+
124
+ You may be thinking that this particular example is unnecessary. You could simply inject `session:` and
125
+ call `session.authenticated_account`:
126
+
127
+ ```ruby
128
+ class DashboardPage < AppPage
129
+ def initialize(session:)
130
+ @widgets = session.authenticated_account.widgets # e.g.
131
+ end
132
+ end
133
+ ```
134
+
135
+ If `DashboardPage` requires an authenticated account, by only injecting the session, you'll need to handle the case where `session.authenticated_account` is `nil`. Instead, if you configure the `AuthBeforeHook` as above, then inject `authenticated_account`, you avoid the need for this logic:
136
+
137
+ ```ruby
138
+ class DashboardPage < AppPage
139
+ def initialize(authenticated_account:)
140
+ @widgets = authenticated_account.widgets # e.g.
141
+ end
142
+ end
143
+ ```
144
+
145
+ Because `AuthBeforeHook` never injects `nil`, `DashboardPage` can rely on `authenticated_account` always being present. Further, if a visitor tried to access `/dashboard_page` without having been authenticated, Brut would be unable to create an instance of `DashboardPage` and generate an error.
146
+
147
+ ### `nil` and Empty Strings
148
+
149
+ When a keyword argument has no default value, Brut will require that value to exist and be available for injection. If the keyword is
150
+ not one of the canned always-available values, it will look in the request context, then in the query string.
151
+
152
+ If the request has the keyword as a key, *it will inject whatever value it finds, including `nil`*. In general, you should avoid
153
+ injecting `nil` when you actually intend to not have a value.
154
+
155
+ For example, the `AuthBeforeHook` above, you could implement it like so:
156
+
157
+ ```ruby
158
+ request_context[:authenticated_account] = session.authenticated_account
159
+ ```
160
+
161
+ The problem is that if the visitor is not logged in, the `:authenticated_account` *will* have a value, and that value will be `nil`. This is almost certainly not what you want.
162
+
163
+ For query string parameters, the HTTP spec says that they are strings. Thus, if a query string parameter is present in ther request
164
+ URL, it will *always* have a value and *never* be `nil`. If the paramter doesn't have a value after the `=` (e.g. for `foo` in `?foo=&bar=quux`), the value will be the empty string.
165
+
166
+ This means you must write code to explicitly handle the cases you care about.
167
+
168
+ ### When Values Aren't Available
169
+
170
+ When a value is not available for injection, and the keyword doesn't provide a default, Brut will raise an error. This is because
171
+ such a situation represents a design error.
172
+
173
+ For example, the `DashboardPage` above requires an `authenticated_account`. Your app should never route a logged-out visitor to that
174
+ page. This allows the `DashboardPage` to avoid having to check for `nil` and figure out what to do.
175
+
176
+ This is most relevant for query string parameters, since they can be easily manipulated by the visitor in their browser. Query string
177
+ parameters should always have a default value, even if it's `nil`.
178
+
179
+ *Path* parameters (like `:id` in `WidgetsByIdPage`) should *never* have a default value as their absence means a different URL was
180
+ requested. For example, `/widgets` would trigger a `WidgetsPage`. *Only* if the `:id` path parameter is present would the
181
+ `WidgetsByIdPage` be triggered, so it's safe to omit the default value for `id:` (and pointless to include one).
182
+
183
+ See [route hooks](/hooks).
184
+
185
+ ### Testing
186
+
187
+ Brut will not create your classes in a test. Instead, you must pass in the values you want. There are various
188
+ helpers in `Brut::SpecSupport` to create blank or empty versions of the special classes.
189
+
190
+ In particular, A basic `request_context` is setup per test and injected into the Thread local storage. This means
191
+ that if your test should trigger a codepath that *does* cause Brut to use keyword injection, useful values will
192
+ be injected.
193
+
194
+ For your tests, however, you should pass in directly what you need:
195
+
196
+ ```ruby
197
+ page = WidgetsByIdPage.new(id: widget.id, session: empty_session)
198
+ ```
199
+
200
+ ## Recommended Practices
201
+
202
+ Consider a method like so:
203
+
204
+ ``` ruby
205
+ def create_widget(name:, organization: nil, quantity: 10)
206
+ ```
207
+
208
+ Outside of Brut, the way to interpret this arguments is as follows:
209
+
210
+ * `name` is required
211
+ * `organization` is optional
212
+ * `quantity` has a default value of 10 if not provided
213
+
214
+ Any method or intializer that will be keyword-injected should be designed with this in mind. Thus, the following guidelines will be helpful in managing your app:
215
+
216
+ * **Choose arguments based on the needs of the class:**
217
+ - If a value is optional, default it to either `nil` or a symbol that indicates what happens when the value is omitted
218
+ - If an optional value has a default, use that (this should be rare for pages, handlers, components, and hooks)
219
+ - Otherwise, do not provide a default for the keyword
220
+ * **Design for non-`nil` values instead of allowing `nil` and checking for it**
221
+ - If a page needs, say, the currently logged-in user, set that up as injectible with no default.
222
+ - If a codepath creates that page without the logged-in user, you will get a very obvious error and
223
+ can figure out how it happened. Your page's code doesn't need to figure out what to do with `nil`
224
+ * **Do not inject `nil` into the request context.** When your code requires a value for a keyword, you want to rely on that value being non-nil. Thus, avoid injecting `nil` into the request context. Brut will allow it as a sort-of escape hatch, but you should design your app to avoid it
225
+ * **Be careful injecting global data.** The request context instance is per request, but you could certainly put global data into it. For example, you may put an initialized API client into the request context as a convieniece. **Be careful** because your app is multi-threaded. Any object that is not scoped to the request must be thread-safe.
226
+
227
+ ## Technical Notes
228
+
229
+ > [!IMPORTANT]
230
+ > Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
231
+ > internals, the source code is always more correct.
232
+
233
+ _Last Updated May 7, 2025_
234
+
235
+ Keyword injection is currently implemented in a few places and not available via public API. It could be useful
236
+ as an API and it will be exposed at some point. For now, it's only available for Brut-managed classes as
237
+ documented here.