brut 0.0.20 → 0.0.22

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 (728) 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/{doc-src → brutrb.com}/keyword-injection.md +122 -68
  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/back_end/seed_data.rb +19 -2
  652. data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
  653. data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
  654. data/lib/brut/back_end/sidekiq.rb +2 -1
  655. data/lib/brut/back_end/validator.rb +5 -1
  656. data/lib/brut/back_end.rb +4 -2
  657. data/lib/brut/cli/app_runner.rb +1 -1
  658. data/lib/brut/cli/apps/test.rb +5 -0
  659. data/lib/brut/cli.rb +4 -3
  660. data/lib/brut/factory_bot.rb +0 -5
  661. data/lib/brut/framework/app.rb +70 -5
  662. data/lib/brut/framework/config.rb +5 -3
  663. data/lib/brut/framework/container.rb +3 -2
  664. data/lib/brut/framework/errors.rb +12 -4
  665. data/lib/brut/framework/mcp.rb +58 -1
  666. data/lib/brut/framework/project_environment.rb +6 -2
  667. data/lib/brut/framework.rb +1 -1
  668. data/lib/brut/front_end/component.rb +69 -71
  669. data/lib/brut/front_end/components/constraint_violations.rb +1 -4
  670. data/lib/brut/front_end/components/form_tag.rb +1 -1
  671. data/lib/brut/front_end/components/input.rb +3 -3
  672. data/lib/brut/front_end/components/inputs/csrf_token.rb +1 -1
  673. data/lib/brut/front_end/components/inputs/{text_field.rb → input_tag.rb} +7 -9
  674. data/lib/brut/front_end/components/inputs/radio_button.rb +1 -1
  675. data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +187 -0
  676. data/lib/brut/front_end/components/inputs/{textarea.rb → textarea_tag.rb} +2 -2
  677. data/lib/brut/front_end/components/time_tag.rb +2 -1
  678. data/lib/brut/front_end/form.rb +4 -4
  679. data/lib/brut/front_end/forms/input.rb +2 -1
  680. data/lib/brut/front_end/forms/input_definition.rb +5 -2
  681. data/lib/brut/front_end/forms/radio_button_group_input.rb +2 -1
  682. data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +2 -2
  683. data/lib/brut/front_end/forms/select_input.rb +2 -4
  684. data/lib/brut/front_end/forms/select_input_definition.rb +2 -2
  685. data/lib/brut/front_end/handler.rb +28 -26
  686. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +5 -2
  687. data/lib/brut/front_end/handlers/instrumentation_handler.rb +8 -4
  688. data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -5
  689. data/lib/brut/front_end/handlers/missing_handler.rb +5 -2
  690. data/lib/brut/front_end/layout.rb +16 -0
  691. data/lib/brut/front_end/page.rb +52 -29
  692. data/lib/brut/front_end/request_context.rb +3 -2
  693. data/lib/brut/front_end/routing.rb +5 -1
  694. data/lib/brut/front_end.rb +4 -13
  695. data/lib/brut/i18n/base_methods.rb +167 -79
  696. data/lib/brut/i18n/for_back_end.rb +4 -0
  697. data/lib/brut/i18n/for_cli.rb +4 -0
  698. data/lib/brut/i18n/for_html.rb +32 -4
  699. data/lib/brut/i18n/http_accept_language.rb +47 -0
  700. data/lib/brut/instrumentation/open_telemetry.rb +36 -1
  701. data/lib/brut/instrumentation.rb +3 -5
  702. data/lib/brut/sinatra_helpers.rb +11 -3
  703. data/lib/brut/spec_support/component_support.rb +30 -16
  704. data/lib/brut/spec_support/e2e_support.rb +1 -1
  705. data/lib/brut/spec_support/e2e_test_server.rb +3 -0
  706. data/lib/brut/spec_support/general_support.rb +3 -0
  707. data/lib/brut/spec_support/handler_support.rb +6 -1
  708. data/lib/brut/spec_support/matcher.rb +1 -0
  709. data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
  710. data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
  711. data/lib/brut/spec_support/matchers/have_i18n_string.rb +2 -5
  712. data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
  713. data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
  714. data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
  715. data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
  716. data/lib/brut/spec_support.rb +1 -1
  717. data/lib/brut/version.rb +1 -1
  718. data/lib/brut.rb +5 -4
  719. data/lib/sequel/extensions/brut_migrations.rb +1 -1
  720. metadata +648 -13
  721. data/doc-src/architecture.md +0 -102
  722. data/doc-src/assets.md +0 -98
  723. data/doc-src/forms.md +0 -214
  724. data/doc-src/handlers.md +0 -83
  725. data/doc-src/javascript.md +0 -265
  726. data/doc-src/pages.md +0 -210
  727. data/doc-src/route-hooks.md +0 -59
  728. data/lib/brut/front_end/components/inputs/select.rb +0 -117
@@ -0,0 +1,11 @@
1
+ {
2
+ "devDependencies": {
3
+ "vitepress": "^1.6.3",
4
+ "vue": "^3.5.13"
5
+ },
6
+ "scripts": {
7
+ "docs:dev": "vitepress dev --port 8086 --host=0.0.0.0",
8
+ "docs:build": "vitepress build",
9
+ "docs:preview": "vitepress preview --port 8086 --host=0.0.0.0"
10
+ }
11
+ }
@@ -0,0 +1,378 @@
1
+ # Pages
2
+
3
+ The core abstraction of Brut is the core concept of the web: the web page.
4
+
5
+ A web page is fetched by the browser using an HTTP `GET` request to a URL. When that happens, Brut instantiates an object of a *page class* and uses its `page_template` method to generate its HTML (using calls to Phlex's API).
6
+
7
+ ## Overview
8
+
9
+ You can create everything you need for a page by using `bin/scaffold`:
10
+
11
+ ```shell
12
+ > bin/scaffold page /new-widgets
13
+ ```
14
+
15
+ You can use `--dry-run` to see what it will do:
16
+
17
+ ```shell
18
+ > bin/scaffold --dry-run /new-widgets
19
+ bin/scaffold --dry-run page /new-widgets
20
+ [ bin/scaffold ] app/src/app.rb
21
+ [ bin/scaffold ] will contain:
22
+
23
+ page "/new-widgets"
24
+
25
+ [ bin/scaffold ] app/src/front_end/pages/new_widgets_page.rb
26
+ [ bin/scaffold ] will contain:
27
+
28
+ class NewWidgetsPage < AppPage
29
+ def initialize # add needed arguments here
30
+ end
31
+
32
+ def page_template
33
+ h1 { "Your page is ready" }
34
+ end
35
+ end
36
+
37
+ [ bin/scaffold ] specs/front_end/pages/new_widgets_page.spec.rb
38
+ [ bin/scaffold ] will contain:
39
+
40
+ require "spec_helper"
41
+
42
+ RSpec.describe NewWidgetsPage do
43
+ it "should have tests" do
44
+ expect(true).to eq(false)
45
+ end
46
+ end
47
+
48
+ [ bin/scaffold ] app/config/i18n/en/2_app.rb
49
+ [ bin/scaffold ] will contain:
50
+
51
+ "NewWidgetsPage": {
52
+ title: "New widgets page",
53
+ },
54
+
55
+ [ bin/scaffold ] Page source is in app/src/front_end/pages/new_widgets_page.rb
56
+ [ bin/scaffold ] Page HTML template is in app/src/front_end/pages/new_widgets_page.html.erb
57
+ [ bin/scaffold ] Page test is in specs/front_end/pages/new_widgets_page.spec.rb
58
+ [ bin/scaffold ] Added title to app/config/i18n/en/2_app.rb
59
+ [ bin/scaffold ] Added route to app/src/app.rb
60
+ ```
61
+
62
+ You can, of course, edit `app.rb` and create the classes yourself.
63
+
64
+ > [!WARNING]
65
+ > Adding a `page` route without the corresponding class may not always
66
+ > work, since Brut may try to load the class. Brut does its best
67
+ > to avoid problems, but you should create your route and classes
68
+ > all at once
69
+
70
+ > [!IMPORTANT]
71
+ > Brut cannot currently reload new routes, so you must
72
+ > restart your dev server when you modify or add routes.
73
+
74
+ ### Creating a Page
75
+
76
+ Page classes are expected to be in `app/src/front_end/pages`, named conventionally the way Zeitwerk would expect. For example, `Admin::WidgetsByIdPage` would be expected in `app/src/front_end/pages/admin/widgets_by_id_page.rb`.
77
+
78
+ A page class must be a subclass of `Brut::FrontEnd::Page`, however in practice it will be a subclass of `AppPage` in your app, which is a subclass of `Brut::FrontEnd::Page`. All Brut components have an app-specific base class to allow sharing of logic, if needed.
79
+
80
+ Brut will create the instance of the page class, passing in the keyword
81
+ arguments the initializer specifies (see [Keyword Injection](/keyword-injection)). In particular, any placeholders in the route will be passed-in to the initializer. This is why those placeholders must be valid Ruby keyword argument names.
82
+
83
+ For example, `Admin::WidgetsByIdPage` and its template might look like so:
84
+
85
+ ```ruby
86
+ # pages/admin/widgets_by_id_page.rb
87
+ class Admin::WidgetsByIdPage < AppPage
88
+ def initialize(id:)
89
+ @widget = DB::Widget.find!(id:)
90
+ end
91
+
92
+ private attr_reader :widget
93
+
94
+ def page_template
95
+ h1 { widget.name }
96
+ h2 { widget.status }
97
+ end
98
+ end
99
+ ```
100
+
101
+ Note that `Admin::WidgetsByIdPage` is a normal Ruby class, so you could implement `#widget` as a method, and lazy-load the widget:
102
+
103
+ ```ruby {13}
104
+ class Admin::WidgetsByIdPage < AppPage
105
+ def initialize(id:)
106
+ @widget_id = id
107
+ end
108
+
109
+ def page_template
110
+ h1 { widget.name }
111
+ h2 { widget.status }
112
+ end
113
+
114
+ private
115
+
116
+ def widget = DB::Widget.find!(id: @widget_id)
117
+
118
+ end
119
+ ```
120
+
121
+ A page's initializer can also accept other parameters, provided by Brut.
122
+
123
+ ### Arguments Available to Initializer
124
+
125
+ Brut's [keyword injection](/keyword-injection) is used to create the instance of your page. You can have Brut inject what you need by
126
+ specifying keyword arguments.
127
+
128
+ | Value | Type | Description |
129
+ |-------|------|-------------|
130
+ `session:` | `Brut::FrontEnd::Session` (or your app's subclass) | The current session, even if it's empty. See [Flash and Session](/flash-and-session)|
131
+ `flash:` | `Brut::FrontEnd::Flash` (or your app's subclass) | The current flash, even if it's empty. See [Flash and Session](/flash-and-session) |
132
+ `xhr:` | `true` or `false` | true if this was an Ajax request|
133
+ `csrf_token:` | `String`| The current CSRF token. |
134
+ `clock:` | `Clock` | Used when you need to access the current date and time, potentially accounting for time zones. See [Space/Time Continuum](/space-time-continuum)|
135
+ `http_*` | `String` or `nil` | Any parameter that starts with `http_` is assumed to be for an HTTP header. For example, `http_accept_language` would be given the value for the "Accept-Language" header. See [HTTP Headers](/keyword-injection#http-headers) |
136
+ `env:` | `Hash` | The Rack env. You are discouraged from using this directly in your pages, but if you need it, it's available. |
137
+ Placeholders | `String` | Any placeholder value from the route definition |
138
+ Any query string paramter | `String` | the value given is always a string.
139
+ Any object placed into the request context | `Object` | Values you place into the request context. See below for an example.
140
+
141
+ Thus, if `Admin::WidgetsByIdPage` responds to the `detail_level` query string parameter, needs access to the current time, wants to
142
+ check a value from the session, and responded to the completely made-up header "X-Be-Nice", the initializer would look like so:
143
+
144
+ ```ruby
145
+ def initialize(id:,
146
+ session:,
147
+ clock:,
148
+ http_x_be_nice:,
149
+ detail_level: nil)
150
+ ```
151
+
152
+ > [!CAUTION]
153
+ > Keyword arguments for query string parameters **must** have default values or Brut will be unable to instantiate your page class
154
+ > when they are omitted.
155
+
156
+ > [!NOTE]
157
+ > Omitting a default for an HTTP header is OK, but you should know what the behavior is. See [the HTTP Headers
158
+ > section](/keyword-injection#http-headers) for details.
159
+
160
+ ### Hooks
161
+
162
+ Occasionally, you want to prevent a page from rendering after the visitor has been routed to it. A common
163
+ reason for this could be a lack of authorization by that visitor to view the page.
164
+
165
+ `before_generate` achieves this. If your page class implements it, it will be called after the page is
166
+ initialized, but before the template creationg process starts. Depending on what `before_generate`
167
+ returns, the visitor may be redirected, an error could be sent, or HTML generation may proceed as normal.
168
+
169
+ The return value of `before_generate` determines what will happen:
170
+
171
+ * `URI` - the visitor will be redirected to the given URI. Instead of creating a `URI`, you may use the method `redirect_to`, which
172
+ accepts a page and its parameters.
173
+ * `Brut::FrontEnd::HttpStatus` - the page will not be rendered and this status will be returned. You may use `http_status` to create
174
+ an `HttpStatus` from a number.
175
+ * `Brut::FrontEnd::GenericResponse` - a typed wrapper around the standard Rack response.
176
+ * Anything else - page rendering will proceed as usual.
177
+
178
+ ## Testing
179
+
180
+ See [Unit Testing](/unit-tests) for some basic assumptions and configuration available for all Brut unit tests.
181
+
182
+ Since pages are Plain Ole Ruby Objects, you could test them using conventional means. However, since the ultimate behavior of a
183
+ page is to produce HTML based on its template, it's recommended that your page tests generate HTML and you make assertions about the page's behavior by examining that HTML.
184
+
185
+ Brut provides convenience methods for this, based on Nokogiri. With them, you should be able to access elements of your page using
186
+ the same sorts of CSS selectors you'd use with `document.querySelector` to debug your app in a browser.
187
+
188
+ ### `generate_and_parse` Parses the Generated HTML
189
+
190
+ Brut uses RSpec, so when a page test is detected, Brut will include `Brut::SpecSupport::ComponentSupport`, which provides useful methods and includes other modules you'll need to make testing more straightforward.
191
+
192
+ The main method you'll use is `generate_and_parse`, which accepts an instance of your page and returns a
193
+ `Brut::SpecSupport::EnhancedNode`, which is a delegate to a Nokogiri node.
194
+
195
+ Below, we use the method `e!`, which is provided by `EnhancedNode`. This works just like Nokogiri's `css`, except
196
+ that requires exactly one element to match the selector. If not, the test fails. This allows a more compact test
197
+ when you know there should only be one element matching the selector you've provided.
198
+
199
+ ```ruby
200
+ RSpec.describe CompanyByCompanyId::LocationsByLocationIdPage do
201
+ describe "render" do
202
+ it "shows the company name and location address" do
203
+ company = create(:company) # You must implement
204
+ location = create(:location) # You must implement
205
+
206
+ page = described_class.new(company_id: company.id.to_s,
207
+ location_id: location.id.to_s)
208
+
209
+ parsed_html = generate_and_parse(page)
210
+
211
+ h1 = parsed_html.e!("h1")
212
+ h2 = parsed_html.e!("h2")
213
+
214
+ expect(h1.text).to include(company.name)
215
+ expect(h2.text).to include(location.address)
216
+ end
217
+ end
218
+ end
219
+ ```
220
+
221
+ `e` (without a bang/`!`) is also provided, which will allow zero or one elements to match the selector (i.e. it only fails if there is more than one match). `e` and `e!` are key methods that allow the use of CSS selectors to be usable in your tests.
222
+
223
+ See `Brut::SpecSupport::ClockSupport`, `Brut::SpecSupport::FlashSupport`, and `Brut::SpecSupport::SessionSupport` for additional methods you can use to make it easier to work with clocks, flashes, and sessions, respectively.
224
+
225
+ ### `generate_result` Tests `before_generate`
226
+
227
+ If your page uses `before_generate`, when you call `generate_and_parse`, it will fail unless the page generated
228
+ HTML. In those cases, you can use `generate_result`, which will return what `before_generate` returned, unless
229
+ it returned `nil`, in which case it will return the unparsed HTML.
230
+
231
+ ```ruby {4,10,12}
232
+ RSpec.describe CompanyByCompanyId::LocationsByLocationIdPage do
233
+ describe "render" do
234
+ it "redirects back to the home page for expired companies" do
235
+ company = create(:company, :expired) # You must implement
236
+ location = create(:location) # You must implement
237
+
238
+ page = described_class.new(company_id: company.id.to_s,
239
+ location_id: location.id.to_s)
240
+
241
+ result = generate_result(page)
242
+
243
+ expect(result).to have_redirected_to(HomePage)
244
+
245
+ end
246
+ end
247
+ end
248
+ ```
249
+
250
+ `have_redirected_to` is a matcher provided by Brut. `have_returned_http_status` is also available to assert that
251
+ `before_generate` returned an HTTP status. The reason to use these matchers and `generate_result` instead of
252
+ calling `before_generate` directly is that you want to use the page in a test the way it's used in your app. You
253
+ will also get higher-quality test failure messages.
254
+
255
+ ## Recommended Practices
256
+
257
+ You can build your pages however you like, but here are some tips that will make your app more sustainable and
258
+ easier to work with.
259
+
260
+ ### Instance variables (ivars) are fine.
261
+
262
+ Since `page_template` is a method of your class, it has access to your instance variables (ivars). Feel free to
263
+ use them directly. Only create `attr_reader` implementations if a subclass should be expected to override
264
+ something or you want something lazily evaluated. Make them private. Your page's API is just the method `page_template`.
265
+
266
+ ### Don't set ivars in `before_generate`
267
+
268
+ It's Ruby and you can do whatever you want, but your page class will be easier to understand and test if you set up necessary state in
269
+ your initializer. Memoization is fine, but don't have your `before_generate` set up additional state if you can avoid it. As we'll see
270
+ below, you won't need to use `before_generate` as a failsafe check on authorization.
271
+
272
+ ### Leverage Keyword Injection
273
+
274
+ The list of available data for injection above will always be available to your page, with the exception of query string parameters. The real power comes when you learn how to [inject your own data](/keyword-injection#injecting-custom-data) into the request context.
275
+
276
+ Let's take a common example of a page that require that a visitor be logged in. While your app will have logic to avoid routing a logged-out visitor to any of those pages, it may seem like a good practice to add a failsafe check inside the logic of the page requiring login. This is very common in Rails and might look like so:
277
+
278
+ ```ruby{2}
279
+ class WidgetsController < ApplicationController
280
+ before_action :require_login!
281
+
282
+ # ...
283
+ end
284
+ ```
285
+
286
+ `before_action` is the failsafe - in case someone hacks a URL to find this page, or there is a bug in your app where unauthorized visitors are sent to this page, the `before_action` prevents the page from working.
287
+
288
+ In Brut, you could mimic this behavior using `before_generate`, however this isn't necessary. Instead, you can take advantage of keyword injection.
289
+
290
+ Consider this implementation of `WidgetsByIdPage`:
291
+
292
+ ```ruby
293
+ class WidgetsByIdPage < AppPage
294
+ def initialize(id:, current_user:)
295
+ # ...
296
+ end
297
+ end
298
+ ```
299
+
300
+ `id:` is injected because it is a route placeholder. `current_user:` however, is completely custom to our app. We can arrange to
301
+ have it injected. We'll create a [Route Hook](/hooks) to do this.
302
+
303
+ > [!CAUTION]
304
+ > This hook is not production-ready. It lacks certain error-handling code and
305
+ > makes an assumption about how the session is managed. It's for demonstration only.
306
+ > The [route hooks](/hooks) section has a more
307
+ > appropriate example.
308
+
309
+ ```ruby{6}
310
+ class RequireAuthBeforeHook < Brut::FrontEnd::RouteHook
311
+ def before(request_context:,session:)
312
+ if session.current_user_id
313
+ user = DB::User.find(id: session.current_user_id)
314
+ if user
315
+ request_context[:current_user] = user
316
+ end
317
+ end
318
+ end
319
+ end
320
+ ```
321
+
322
+ Before any route is handled, this before hook is run and passed the `Brut::FrontEnd::RequestContext`. This is where all the
323
+ injectible values live. `request_context[:current_user] = user` makes `user` available to be injected into a page or handler.
324
+
325
+ What this means is that when a visitor is not logged in, there will be no injectible value for `:current_user`. Brut will not be able
326
+ to instantiate `WidgetsByIdPage`, and an error is generated. It is literally impossible to route a logged-out visitor to that page.
327
+
328
+ In practice, this means that any page that requires a logged-in visitor will specify the `current_user:` keyword argument, and **not provide a default value**. You are still required to make sure no one routes a logged-out visitor to a page requiring authentication, but now you don't have to remember to add logic to each page that requires login—you bake it into the page class' type.
329
+
330
+ ### In Tests, It's Fine to Locate Elements Via CSS Selectors
331
+
332
+ Your page's job is to produce HTML. To check if it's doing that, it makes sense to manipulate that HTML using standard, battle-tested
333
+ techniques like CSS selectors. This creates consonance between your in-browser debugging and your test suite.
334
+
335
+ It also makes it much more obvious what's wrong if something is not where you expect it to be.
336
+
337
+ ### That Said, Avoid Test-Specific Attributes or Classes
338
+
339
+ When you have a lot of `<div>` elements, it can be tempting to use attributes like `data-testid` on the elements you want to find in
340
+ your tests. You can often avoid this if you use semantic markup and proper ARIA roles. For example, a Flash message is likely
341
+ something you'd put in a `role="status"` or `role="alert"`, so you don't need `data-flash` or `class="flash"` in order to find it in a
342
+ test.
343
+
344
+ Custom Elements can also be helpful here, as that may be how you choose to manage your client-side behavior.
345
+
346
+ ## Technical Notes
347
+
348
+ > [!IMPORTANT]
349
+ > Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
350
+ > internals, the source code is always more correct.
351
+
352
+ _Last Updated May 4, 2025_
353
+
354
+ ### Page Internal API
355
+
356
+ A Page's core API is the method `handle!`, which can return an HTML-safe string, `URI`, or Rack response.
357
+ Developers should avoid overriding this method, as it also handles the logic related to calling `before_generate`
358
+ as well as the logic required to make layouts work.
359
+
360
+ This is why we recommend using `Brut::SpecSupport::ComponentSupport#generate_and_parse` or `Brut::SpecSupport::ComponentSupport#generate_result` in a tests. *They* call `handle!`, thus ensuring your `before_generate` method will be called and that your page class will behave in a test the way it would in production.
361
+
362
+ ### Layouts
363
+
364
+ Pages do not have to have a layout. You can override Phlex's `view_template` and produce HTML that will not be
365
+ wrapped in any Layout. It may be a better idea to create a `BlankLayout` class to avoid this, but it's up to
366
+ you.
367
+
368
+ ### Helpers in Templates
369
+
370
+ `Brut::FrontEnd::Page` is a subclass of `Brut::FrontEnd::Component`, so all your pages will have access to the helpers included there. This is how, for example, `t` can be called to perform translations, or `time_tag` can be used to create a `<time>` HTML element.
371
+
372
+ If you wish to add helpers to be used in more than one page, you can either add the method to a common base class like `AppPage`, or create a module and `include` it.
373
+
374
+ ### So You Don't Like Phlex?
375
+
376
+ Brut did initially use ERB, but the initial Brut-powered apps ended up having an all-too-common mess of HTML, Ruby, and angle brackets. It really sucked. Phlex seems pretty solid and is a very lightweight abstraction over HTML. It keeps everything in Ruby, but still maintains consonance to what you see in your browser.
377
+
378
+ Support for ERB, Slim, or HAML, is not planned ever.
Binary file
@@ -0,0 +1,215 @@
1
+ # Routes
2
+
3
+ The primary function of a web framework like Brut is to map URLs requested by the browser or an HTTP client and invoke code based on
4
+ them.
5
+
6
+ Brut has a fairly simple routing system. It's not desgined to be flexible—it's designed to make the most common cases you
7
+ will need as straigthforward as possible.
8
+
9
+ ## Overview
10
+
11
+ ### Route Syntax
12
+
13
+ A route is a string that contains the path part of a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL). *Segments* of the
14
+ path (i.e. the stuff between each forward slash `/`) can be either *static* or a *placeholder*. The route is given as a parameter to
15
+ a method that indicates the purpose of the route (e.g. `page`), and these two factors determine the name of the class that will
16
+ handle requests to that route.
17
+
18
+ Specifically:
19
+
20
+ * Only the [pathname](https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname) of a request may be specified.
21
+ * All routes must start with a slash
22
+ * The segements of the pathname may be static or placeholders. Placeholders must be a valid Ruby keyword argument prepended with a
23
+ colon.
24
+ * Routes may not start with a placeholder.
25
+
26
+ Some examples:
27
+
28
+ ```
29
+ "/dash_board"
30
+ "/widgets/:id"
31
+ "/company/:company_id/locations/:location_id"
32
+ "/"
33
+ ```
34
+
35
+ ### Specifying Routes
36
+
37
+ As mentioned above, routes are passed to methods that determine their purpose. There are currently four types of routes, and thus
38
+ four possible methods you would use to configure them:
39
+
40
+ |Method|Purpose| HTTP Method | More Info |
41
+ |------|-------|-------------|-----------|
42
+ |`page` | Specifies a web page at that route | `GET` | [Pages](/pages) |
43
+ |`form` | Indicates a form will exist and post its form data to this route | `POST` | [Forms](/pages) |
44
+ |`action` | Indicates a form with no form data will exist and post to this route | `POST` | [Handlers](/handlers) |
45
+ |`path` | This route will respond to an arbitrary HTTP method, which must be specified as an additional parameter | Any | [Handlers](/handlers) |
46
+
47
+ Brut is designed around generating HTML. HTML provides the ability to navigate to new web pages via `GET`, or submit data to the
48
+ server from a `<form>` via `POST`. That is why three of the four methods are focused on these use-cases.
49
+
50
+ To specify routes, you can call these methods inside the `routes do` block of your `App` class, located in `app/src/app.rb`:
51
+
52
+ ```ruby{6-9} [app/src/app.rb]
53
+ class App < Brut::Framework::App
54
+ def id = "my-app"
55
+ def organization = "my-org"
56
+
57
+ routes do
58
+ page "/widgets/:id"
59
+ form "/new_widget"
60
+ action "/archive_widget/:id"
61
+ path "/widget_payment_received", method: :put
62
+ end
63
+ end
64
+ ```
65
+
66
+ > [!NOTE]
67
+ > Brut does not use an abstraction like resources to manage the routes of your web app.
68
+ > Few non-programmers know what a resource is, so the routing API is designed to match
69
+ > concepts a non-programmer can observe or identify, like URLs, forms, and pages.
70
+
71
+ ### Connecting Routes to Code
72
+
73
+ Brut is convention-based, so the routes you specify, and the method you pass them to, determine the class that will handle the
74
+ request. For `page` routes, Brut will locate a page class (see [Pages](/pages)), which will be used to
75
+ render the web page. All other routes will be managed by a handler (see [Handlers](/handlers)), which are somewhat like a controller
76
+ in Rails, but with only a single method.
77
+
78
+ The name of the class is determined as follows:
79
+
80
+ * Static segments of the pathname are mapped to namespaces or a class based on converting the path segment to camel-case. For example `new_widget` becomes `NewWidget`.
81
+ * The final static segment in the path represents a class name. All other static segments represent modules in which the final class is namespaced
82
+ - If the route is for a page, `Page` is appended to the class name.
83
+ - If the route is for a form, there are two classes in play, one appended with `Form` and one with `Handler`.
84
+ - If the route has no form and is just a handler, `Handler` is appended to the class name.
85
+ * Placeholder segments are attached to the previous static segment, augmenting its name:
86
+ - The placeholder is camel-cased
87
+ - The placeholder is prefixed with `By` for `page` routes and `With` for all other routes
88
+ - the prefixed-placeholder is appended to the previous module or class name, e.g. `WidgetsById`
89
+ * These are now connected to form a valid Ruby class name.
90
+ * The route `/` is special and always maps to `HomePage`.
91
+
92
+ The examples in the previous section demonstrate how this works:
93
+
94
+ | Route | Class name |
95
+ |-------|------------|
96
+ | `page "/widgets/:id"` | `WidgetsByIdPage` |
97
+ | `form "/new_widget"` | `NewWidgetForm` and `NewWidgetHandler`
98
+ | `action "/archive_widget/:id"` | `ArchiveWidgetByIdHandler`
99
+ | `path "/widget_payment_received", method: :put` | `WidgetPaymentReceivedHandler`
100
+
101
+ Note that deeply nested routes that contain several placeholders will work, and create complicated classnames.
102
+
103
+ ```ruby
104
+ page "/company/:company_id/location/:location_id"
105
+ # => CompanyByCompanyId::LocationByLocationIdPage
106
+ ```
107
+
108
+ > [!TIP]
109
+ > If you don't like long complicated names, deeply-nested namespaces, and long directory names, name your routes accordingly.
110
+
111
+ ### Creating URIs from Routes
112
+
113
+ Because each route is associated with a class, you can use the class to create the route, including any placeholders and query string
114
+ parameters.
115
+
116
+ The most direct way to do this is with the `routing` method available on each page or handler class:
117
+
118
+ ```ruby
119
+ > WidgetsByIdPage.routing(id: 42)
120
+ # => /widgets/42
121
+ > WidgetsByIdPage.routing(id: 42, compact: true)
122
+ # => /widgets/42?compact=true
123
+ > ArchiveWidgetByIdHandler.routing(id: 42)
124
+ # => /archive_widget/42
125
+ ```
126
+
127
+ If you fail to provide the required parameters, `routing` will raise a `Brut::Framework::Errors::MissingParameter` with a message
128
+ explaining the problem.
129
+
130
+ ```ruby
131
+ > begin
132
+ WidgetsByIdPage.routing
133
+ rescue Brut::Framework::Errors::MissingParameter => ex
134
+ puts.ex.message
135
+ end
136
+ # => Parameter 'id' was not available. Received params: no params.
137
+ # :id was used as a path parameter for
138
+ # WidgetsByIdPage (path '/widgets/:id')
139
+ ```
140
+
141
+ `routing` is how you create links to other pages:
142
+
143
+ ```erb
144
+ <a href="<%= DashBoardPage.routing %>">
145
+ Go to Dashboard
146
+ </a>
147
+ ```
148
+
149
+ > [!NOTE]
150
+ > You can use `routing` to create `<form>` actions, but `form_tag`, which we'll discuss in [Forms](/forms), can do this for you.
151
+
152
+ The `routing` method isn't an abstraction around routes. It's more of a strongly-typed translation. This means when you change
153
+ something, your app won't route to non-existent routes—it'll blow up with a helpful error.
154
+
155
+ For example, if you decided that `/dash_board/` should've been called `/account_home`, you would change the value in `app.rb`, then
156
+ rename the class. At this point, any code that routes to `DashboardPage.routing` will raise a `NameError`. With sufficient test coverage, you can address everywhere you see the `NameError` and be confident you have changed the name and route successfully.
157
+
158
+ ## Testing
159
+
160
+ Routes are configuration, so you do not need to test them. Your end-to-end tests will ensure your links and form actions are working, and your page tests will ensure any routes they generate in HTML are valid.
161
+
162
+ ## Recommended Practices
163
+
164
+ Brut does not provide flexibility with routes. For example, you cannot specify an optional placeholder. While this may change, Brut
165
+ is designed to isolate logic to classes like pages, forms, hooks, middlewares, or handlers. Brut does not want logic to exist at the
166
+ routing layer.
167
+
168
+ Beyond these technical limitations, here are some recommendations regarding routes.
169
+
170
+ ### Routes Should be Named for Concepts Anyone Can Understand
171
+
172
+ You don't need your routes to be the names of models or database tables. If you have an account management page that allows modifying data in a table called `user_preferences`, but everyone just calls it "the account management page", the route should be `/account_management`.
173
+
174
+ Although routes are primarily for programmers to manage, there's no reason not to name them using the terms everyone involved in your
175
+ app uses. This is part of the reason Brut inserts `By` or `With` when there is a placeholder. It allows you to have a page for all
176
+ widgets—the "widgets page"—and a page for a specific widget by id—the "widgets by id page".
177
+
178
+ ### Prefer Shallow Routes with a Single Placeholder
179
+
180
+ The more path segments your route has, and the more placeholders it is, the longer your class name will be and the more you lose the
181
+ connection to reality. The "company by company id location by location id page" doesn't exactly roll off the tongue.
182
+
183
+ Life will be easier if you can choose names and routes that have a single placeholder. Multiple path segments can be useful for
184
+ namespacing.
185
+
186
+ ### Placeholders Identify Things, Query Strings Search for Things
187
+
188
+ You could certainly have a `/widgets` route, and then look at a query string parameter named `id` to know what widget to show. This
189
+ is likely not what you want. If a route should always identify a specific thing in your back-end, it should have a placeholder where
190
+ that thing's identifier goes.
191
+
192
+ If a route allows searching for things with multiple optional critiera, a query string is more appropriate. This is the HTTP spec, so
193
+ if you follow its guidelines, you'll be fine.
194
+
195
+ ### Pluralization Is Up to You
196
+
197
+ The rules Brut uses to determine the class names to handle routes do not rely on pluralization. You can have a `/widget` route and a `/widgets` route, if that makes sense to your domain and team. They are both handled by the same set of underlying rules.
198
+
199
+ ## Technical Notes
200
+
201
+ > [!IMPORTANT]
202
+ > Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
203
+ > internals, the source code is always more correct.
204
+
205
+ _Last Updated Feb 23, 2025_
206
+
207
+ Brut stores all configured routes in a `Brut::FrontEnd::Routing` object.
208
+ This means that all metadata about a route is available. You are not intended to interact with this class, but you will note that in
209
+ certain circumstances, the `Brut::FrontEnd::Routing::Route` can be injected into your class.
210
+
211
+ Brut uses this metadata to create route handlers with Sinatra. While Brut may not always use Sinatra under the covers, it does as of
212
+ the writing, so when you call `page "/widgets"`, Brut will call `get "/widgets" do` and pass a block to Sinatra to find the class to
213
+ handle the reqest, create an instance of it, call a method on it, and return the response.
214
+
215
+