brut 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1104) hide show
  1. checksums.yaml +4 -4
  2. data/exe/brut +34 -0
  3. data/lib/brut/cli/apps/build_assets.rb +78 -48
  4. data/lib/brut/cli/apps/db.rb +168 -202
  5. data/lib/brut/cli/apps/deploy.rb +291 -0
  6. data/lib/brut/cli/apps/heroku_container_based_deploy.rb +6 -0
  7. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/add_segment.rb +5 -5
  8. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/add_segment_options.rb +1 -1
  9. data/lib/brut/cli/apps/new/app.rb +240 -0
  10. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/app_id.rb +1 -1
  11. data/lib/brut/cli/apps/new/app_name.rb +29 -0
  12. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/base.rb +9 -6
  13. data/lib/brut/cli/apps/new/erb_binding_delegate.rb +23 -0
  14. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/internet_identifier.rb +5 -5
  15. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/invalid_identifier.rb +1 -1
  16. data/{mkbrut/lib/mkbrut/app.rb → lib/brut/cli/apps/new/old_app.rb} +8 -11
  17. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/add_css_import.rb +1 -1
  18. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/add_i18n_message.rb +1 -1
  19. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/add_method.rb +1 -1
  20. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/append_to_file.rb +1 -1
  21. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/base_op.rb +3 -3
  22. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/copy_file.rb +1 -1
  23. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/insert_code_in_method.rb +1 -1
  24. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/insert_into_file.rb +1 -1
  25. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/insert_route.rb +1 -1
  26. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/mkdir.rb +1 -1
  27. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/prism_parsing_op.rb +1 -1
  28. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/render_template.rb +1 -1
  29. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/ops/skip_file.rb +1 -1
  30. data/lib/brut/cli/apps/new/ops.rb +17 -0
  31. data/lib/brut/cli/apps/new/organization.rb +5 -0
  32. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/prefix.rb +1 -1
  33. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/segments/bare_bones.rb +12 -11
  34. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/segments/demo.rb +16 -15
  35. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/segments/heroku.rb +9 -5
  36. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/segments/sidekiq.rb +44 -21
  37. data/lib/brut/cli/apps/new/segments.rb +8 -0
  38. data/lib/brut/cli/apps/new/version.rb +3 -0
  39. data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/versions.rb +2 -2
  40. data/lib/brut/cli/apps/new.rb +26 -0
  41. data/lib/brut/cli/apps/scaffold.rb +150 -141
  42. data/lib/brut/cli/apps/test.rb +92 -68
  43. data/lib/brut/cli/commands/base_command.rb +174 -0
  44. data/lib/brut/cli/commands/compound_command.rb +29 -0
  45. data/lib/brut/cli/commands/execution_context.rb +32 -0
  46. data/lib/brut/cli/commands/help.rb +26 -0
  47. data/lib/brut/cli/commands/output_error.rb +13 -0
  48. data/lib/brut/cli/commands/raise_error.rb +11 -0
  49. data/lib/brut/cli/commands.rb +8 -0
  50. data/lib/brut/cli/execute_result.rb +39 -0
  51. data/lib/brut/cli/executor.rb +9 -4
  52. data/lib/brut/cli/output.rb +13 -0
  53. data/lib/brut/cli/parsed_command_line.rb +143 -0
  54. data/lib/brut/cli/runner.rb +124 -0
  55. data/lib/brut/cli.rb +7 -29
  56. data/lib/brut/framework/container.rb +1 -1
  57. data/lib/brut/framework/mcp.rb +59 -13
  58. data/lib/brut/framework/project_environment.rb +3 -1
  59. data/lib/brut/junk_drawer.rb +3 -1
  60. data/lib/brut/spec_support/cli_command_support.rb +45 -0
  61. data/lib/brut/spec_support/e2e_test_server.rb +3 -0
  62. data/lib/brut/spec_support/general_support.rb +1 -1
  63. data/lib/brut/spec_support/matchers/have_executed.rb +35 -0
  64. data/lib/brut/spec_support/rspec_setup.rb +4 -8
  65. data/lib/brut/spec_support.rb +1 -0
  66. data/lib/brut/tui/markup_string.rb +2 -0
  67. data/lib/brut/tui/script/events/command_std_out.rb +3 -2
  68. data/lib/brut/tui/script/exec_step.rb +11 -4
  69. data/lib/brut/tui/script/puts_subscriber.rb +4 -4
  70. data/lib/brut/tui/script.rb +7 -3
  71. data/lib/brut/tui/terminal_theme.rb +15 -11
  72. data/lib/brut/version.rb +1 -1
  73. data/templates/Base/.env.development.local +2 -0
  74. data/templates/Base/bin/ci +42 -0
  75. data/{mkbrut/templates → templates}/Base/bin/release +2 -2
  76. data/templates/Base/bin/setup +174 -0
  77. data/{mkbrut/templates → templates}/Base/bin/watch-and-build-assets +1 -1
  78. data/{mkbrut/templates → templates}/Base/dx/docker-compose.env.erb +1 -1
  79. data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/css/fonts.css +1 -1
  80. data/{mkbrut/templates → templates}/segments/Heroku/deploy/Dockerfile +2 -2
  81. data/templates/segments/Heroku/deploy/docker_config.rb +30 -0
  82. metadata +191 -1055
  83. data/.gitignore +0 -61
  84. data/.projections.json +0 -10
  85. data/CHANGELOG.md +0 -172
  86. data/CODE_OF_CONDUCT.txt +0 -99
  87. data/Dockerfile.dx +0 -82
  88. data/Gemfile +0 -6
  89. data/Gemfile.lock +0 -246
  90. data/LICENSE.txt +0 -370
  91. data/README.md +0 -90
  92. data/Rakefile +0 -25
  93. data/assets/Logo-Square.pxd +0 -0
  94. data/assets/LogoPylon.pxd +0 -0
  95. data/assets/LogoStop.pxd +0 -0
  96. data/assets/LogoTall.pxd +0 -0
  97. data/assets/MetroIcon.graffle +0 -0
  98. data/assets/MetroLogo.graffle +0 -0
  99. data/assets/SocialImage.png +0 -0
  100. data/assets/SocialImage.pxd +0 -0
  101. data/assets/YouTubeThumb.pxd +0 -0
  102. data/bin/bin_kit.rb +0 -51
  103. data/bin/build +0 -86
  104. data/bin/ci +0 -40
  105. data/bin/dev +0 -20
  106. data/bin/docs +0 -86
  107. data/bin/generate-and-run-rubocop +0 -52
  108. data/bin/new-version +0 -8
  109. data/bin/publish +0 -61
  110. data/bin/rake +0 -27
  111. data/bin/rspec +0 -27
  112. data/bin/rubocop +0 -27
  113. data/bin/setup +0 -252
  114. data/bin/test +0 -18
  115. data/brut-css/.nvim.lua +0 -1
  116. data/brut-css/README.md +0 -28
  117. data/brut-css/bin/build +0 -50
  118. data/brut-css/bin/ci +0 -19
  119. data/brut-css/bin/dev +0 -1
  120. data/brut-css/bin/docs +0 -34
  121. data/brut-css/bin/publish +0 -21
  122. data/brut-css/bin/setup +0 -6
  123. data/brut-css/config/media-queries-all.css +0 -15
  124. data/brut-css/config/media-queries-minimal.css +0 -5
  125. data/brut-css/config/postcss.config.cjs +0 -7
  126. data/brut-css/config/pseudo-classes-all.css +0 -9
  127. data/brut-css/dx +0 -1
  128. data/brut-css/package-lock.json +0 -3165
  129. data/brut-css/package.json +0 -36
  130. data/brut-css/src/css/appearance.css +0 -145
  131. data/brut-css/src/css/border.css +0 -522
  132. data/brut-css/src/css/colors.css +0 -3502
  133. data/brut-css/src/css/dimensions.css +0 -548
  134. data/brut-css/src/css/flex.css +0 -179
  135. data/brut-css/src/css/index.css +0 -13
  136. data/brut-css/src/css/layout.css +0 -120
  137. data/brut-css/src/css/list.css +0 -41
  138. data/brut-css/src/css/positioning.css +0 -354
  139. data/brut-css/src/css/properties/colors.css +0 -455
  140. data/brut-css/src/css/properties/index.css +0 -3
  141. data/brut-css/src/css/properties/spacing.css +0 -140
  142. data/brut-css/src/css/properties/typography.css +0 -224
  143. data/brut-css/src/css/reset.css +0 -107
  144. data/brut-css/src/css/spacing.css +0 -585
  145. data/brut-css/src/css/typography.css +0 -519
  146. data/brut-css/src/css/utils.css +0 -104
  147. data/brut-css/src/docs/1_getting-started/1_overview.md +0 -46
  148. data/brut-css/src/docs/1_getting-started/2_installation.md +0 -25
  149. data/brut-css/src/docs/1_getting-started/3_core-concepts.md +0 -75
  150. data/brut-css/src/docs/1_getting-started/4_simple-example.md +0 -132
  151. data/brut-css/src/docs/1_getting-started/page.html.ejs +0 -10
  152. data/brut-css/src/docs/2_properties/page.html.ejs +0 -71
  153. data/brut-css/src/docs/3_classes/color-demo.html.ejs +0 -31
  154. data/brut-css/src/docs/3_classes/page.html.ejs +0 -87
  155. data/brut-css/src/docs/4_customization/1_design-system.md +0 -36
  156. data/brut-css/src/docs/4_customization/2_breakpoints.md +0 -75
  157. data/brut-css/src/docs/4_customization/3_pseudo-classes.md +0 -74
  158. data/brut-css/src/docs/4_customization/4_advanced-configuration.md +0 -40
  159. data/brut-css/src/docs/4_customization/page.html.ejs +0 -10
  160. data/brut-css/src/docs/docs.css +0 -98
  161. data/brut-css/src/docs/includes/body-and-header.html.ejs +0 -30
  162. data/brut-css/src/docs/includes/footer-and-rest.html.ejs +0 -9
  163. data/brut-css/src/docs/includes/head.html.ejs +0 -5
  164. data/brut-css/src/docs/includes/nav.html.ejs +0 -10
  165. data/brut-css/src/docs/index.html.ejs +0 -32
  166. data/brut-css/src/docs/prism-twilight.min.css +0 -1
  167. data/brut-css/src/js/Logger.js +0 -71
  168. data/brut-css/src/js/build.js +0 -111
  169. data/brut-css/src/js/cli/CLIArgError.js +0 -7
  170. data/brut-css/src/js/cli/Debug.js +0 -27
  171. data/brut-css/src/js/cli/DocsDir.js +0 -16
  172. data/brut-css/src/js/cli/DocsTemplateSourceDir.js +0 -16
  173. data/brut-css/src/js/cli/InputFile.js +0 -31
  174. data/brut-css/src/js/cli/MediaQueryConfigFile.js +0 -10
  175. data/brut-css/src/js/cli/OutputFile.js +0 -22
  176. data/brut-css/src/js/cli/ParsedArg.js +0 -17
  177. data/brut-css/src/js/cli/PathToBrutCSSRoot.js +0 -19
  178. data/brut-css/src/js/cli/PseudoClassConfigFile.js +0 -11
  179. data/brut-css/src/js/cli.js +0 -108
  180. data/brut-css/src/js/docGenerator.js +0 -467
  181. data/brut-css/src/js/mediaQueryConfigParser.js +0 -98
  182. data/brut-css/src/js/post-css-plugins/addMediaQueriesPlugin.js +0 -49
  183. data/brut-css/src/js/post-css-plugins/addPseudoClassesPlugin.js +0 -42
  184. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Category.js +0 -9
  185. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/DocState.js +0 -185
  186. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Documentable.js +0 -8
  187. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Group.js +0 -7
  188. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/ParsedComment.js +0 -73
  189. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Property.js +0 -9
  190. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyCategory.js +0 -4
  191. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyGroup.js +0 -8
  192. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Rule.js +0 -12
  193. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleCategory.js +0 -4
  194. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleGroup.js +0 -8
  195. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeRef.js +0 -5
  196. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeURL.js +0 -9
  197. data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin.js +0 -49
  198. data/brut-css/src/js/post-css-plugins/generateRootCustomPropertiesPlugin.js +0 -45
  199. data/brut-css/src/js/pseudoClassConfigParser.js +0 -145
  200. data/brut-js/.projections.json +0 -10
  201. data/brut-js/README.md +0 -118
  202. data/brut-js/bin/build +0 -19
  203. data/brut-js/bin/ci +0 -5
  204. data/brut-js/bin/docs +0 -25
  205. data/brut-js/bin/publish +0 -21
  206. data/brut-js/bin/setup +0 -6
  207. data/brut-js/docs/README.md +0 -8
  208. data/brut-js/docs/jsdoc-plugins/customElementTag.js +0 -8
  209. data/brut-js/docs/jsdoc-theme/publish.js +0 -692
  210. data/brut-js/docs/jsdoc-theme/static/scripts/linenumber.js +0 -25
  211. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/Apache-License-2.0.txt +0 -202
  212. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/lang-css.js +0 -2
  213. data/brut-js/docs/jsdoc-theme/static/scripts/prettify/prettify.js +0 -28
  214. data/brut-js/docs/jsdoc-theme/static/styles/jsdoc-default.css +0 -327
  215. data/brut-js/docs/jsdoc-theme/static/styles/prettify-jsdoc.css +0 -111
  216. data/brut-js/docs/jsdoc-theme/static/styles/prettify-tomorrow.css +0 -132
  217. data/brut-js/docs/jsdoc-theme/tmpl/augments.tmpl +0 -10
  218. data/brut-js/docs/jsdoc-theme/tmpl/container.tmpl +0 -199
  219. data/brut-js/docs/jsdoc-theme/tmpl/details.tmpl +0 -143
  220. data/brut-js/docs/jsdoc-theme/tmpl/example.tmpl +0 -2
  221. data/brut-js/docs/jsdoc-theme/tmpl/examples.tmpl +0 -13
  222. data/brut-js/docs/jsdoc-theme/tmpl/exceptions.tmpl +0 -32
  223. data/brut-js/docs/jsdoc-theme/tmpl/layout.tmpl +0 -38
  224. data/brut-js/docs/jsdoc-theme/tmpl/mainpage.tmpl +0 -14
  225. data/brut-js/docs/jsdoc-theme/tmpl/members.tmpl +0 -38
  226. data/brut-js/docs/jsdoc-theme/tmpl/method.tmpl +0 -131
  227. data/brut-js/docs/jsdoc-theme/tmpl/modifies.tmpl +0 -14
  228. data/brut-js/docs/jsdoc-theme/tmpl/params.tmpl +0 -131
  229. data/brut-js/docs/jsdoc-theme/tmpl/properties.tmpl +0 -108
  230. data/brut-js/docs/jsdoc-theme/tmpl/returns.tmpl +0 -19
  231. data/brut-js/docs/jsdoc-theme/tmpl/source.tmpl +0 -8
  232. data/brut-js/docs/jsdoc-theme/tmpl/tutorial.tmpl +0 -19
  233. data/brut-js/docs/jsdoc-theme/tmpl/type.tmpl +0 -7
  234. data/brut-js/docs/jsdoc.config.json +0 -23
  235. data/brut-js/docs/package-lock.json +0 -343
  236. data/brut-js/docs/package.json +0 -7
  237. data/brut-js/dx +0 -1
  238. data/brut-js/package-lock.json +0 -2210
  239. data/brut-js/package.json +0 -36
  240. data/brut-js/specs/AjaxSubmit.spec.js +0 -453
  241. data/brut-js/specs/Autosubmit.spec.js +0 -127
  242. data/brut-js/specs/ConfirmSubmit.spec.js +0 -224
  243. data/brut-js/specs/ConstraintViolationMessage.spec.js +0 -33
  244. data/brut-js/specs/ConstraintViolationMessages.spec.js +0 -32
  245. data/brut-js/specs/CopyToClipboard.spec.js +0 -35
  246. data/brut-js/specs/Form.spec.js +0 -137
  247. data/brut-js/specs/I18nTranslation.spec.js +0 -19
  248. data/brut-js/specs/LocaleDetection.spec.js +0 -22
  249. data/brut-js/specs/Message.spec.js +0 -15
  250. data/brut-js/specs/SpecHelper.js +0 -23
  251. data/brut-js/specs/Tabs.spec.js +0 -41
  252. data/brut-js/specs/Toast.spec.js +0 -34
  253. data/brut-js/specs/config/asset_metadata.json +0 -7
  254. data/brut-js/src/AjaxSubmit.js +0 -499
  255. data/brut-js/src/Autosubmit.js +0 -63
  256. data/brut-js/src/BaseCustomElement.js +0 -261
  257. data/brut-js/src/ConfirmSubmit.js +0 -137
  258. data/brut-js/src/ConfirmationDialog.js +0 -143
  259. data/brut-js/src/ConstraintViolationMessage.js +0 -140
  260. data/brut-js/src/ConstraintViolationMessages.js +0 -98
  261. data/brut-js/src/CopyToClipboard.js +0 -96
  262. data/brut-js/src/Form.js +0 -147
  263. data/brut-js/src/I18nTranslation.js +0 -64
  264. data/brut-js/src/LocaleDetection.js +0 -117
  265. data/brut-js/src/Logger.js +0 -90
  266. data/brut-js/src/Message.js +0 -62
  267. data/brut-js/src/RichString.js +0 -116
  268. data/brut-js/src/Tabs.js +0 -168
  269. data/brut-js/src/Toast.js +0 -102
  270. data/brut-js/src/Tracing.js +0 -247
  271. data/brut-js/src/appForTestingOnly.js +0 -15
  272. data/brut-js/src/index.js +0 -133
  273. data/brut-js/src/testing/AssetMetadata.js +0 -35
  274. data/brut-js/src/testing/AssetMetadataLoader.js +0 -25
  275. data/brut-js/src/testing/CustomElementTest.js +0 -235
  276. data/brut-js/src/testing/DOMCreator.js +0 -45
  277. data/brut-js/src/testing/index.js +0 -48
  278. data/brut.gemspec +0 -73
  279. data/brutrb.com/.vitepress/config.mjs +0 -164
  280. data/brutrb.com/.vitepress/plugins/jsdocLinker.js +0 -34
  281. data/brutrb.com/.vitepress/plugins/rdocLinker.js +0 -18
  282. data/brutrb.com/.vitepress/theme/custom.css +0 -14
  283. data/brutrb.com/.vitepress/theme/index.js +0 -18
  284. data/brutrb.com/.vitepress/theme/style.css +0 -139
  285. data/brutrb.com/adrs.md +0 -16
  286. data/brutrb.com/ai.md +0 -68
  287. data/brutrb.com/assets.md +0 -131
  288. data/brutrb.com/bin/build +0 -5
  289. data/brutrb.com/bin/deploy +0 -7
  290. data/brutrb.com/bin/dev +0 -5
  291. data/brutrb.com/bin/setup +0 -6
  292. data/brutrb.com/brut-js.md +0 -128
  293. data/brutrb.com/business-logic.md +0 -55
  294. data/brutrb.com/cli.md +0 -274
  295. data/brutrb.com/components.md +0 -265
  296. data/brutrb.com/configuration.md +0 -256
  297. data/brutrb.com/css.md +0 -103
  298. data/brutrb.com/custom-element-tests.md +0 -148
  299. data/brutrb.com/database-access.md +0 -201
  300. data/brutrb.com/database-schema.md +0 -320
  301. data/brutrb.com/deployment.md +0 -158
  302. data/brutrb.com/dev-environment.md +0 -186
  303. data/brutrb.com/dir-structure.md +0 -120
  304. data/brutrb.com/doc-conventions.md +0 -41
  305. data/brutrb.com/dx +0 -1
  306. data/brutrb.com/end-to-end-tests.md +0 -176
  307. data/brutrb.com/features.md +0 -373
  308. data/brutrb.com/flash-and-session.md +0 -208
  309. data/brutrb.com/form-constraints.md +0 -266
  310. data/brutrb.com/forms.md +0 -238
  311. data/brutrb.com/getting-started.md +0 -142
  312. data/brutrb.com/handlers.md +0 -177
  313. data/brutrb.com/hooks.md +0 -176
  314. data/brutrb.com/i18n.md +0 -190
  315. data/brutrb.com/images/DevEnvironment.graffle +0 -0
  316. data/brutrb.com/images/DevEnvironment.png +0 -0
  317. data/brutrb.com/images/LogoSquare.png +0 -0
  318. data/brutrb.com/images/LogoStop.png +0 -0
  319. data/brutrb.com/images/LogoTall.png +0 -0
  320. data/brutrb.com/images/Makefile +0 -10
  321. data/brutrb.com/images/OverviewMetro.graffle +0 -0
  322. data/brutrb.com/images/OverviewMetro.png +0 -0
  323. data/brutrb.com/images/dev-env-overview.dot +0 -54
  324. data/brutrb.com/images/dev-env-overview.png +0 -0
  325. data/brutrb.com/images/dev-env-protocol.dot +0 -37
  326. data/brutrb.com/images/dev-env-protocol.png +0 -0
  327. data/brutrb.com/images/overview.graffle +0 -0
  328. data/brutrb.com/images/overview.png +0 -0
  329. data/brutrb.com/images/spa.dot +0 -19
  330. data/brutrb.com/images/spa.png +0 -0
  331. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser-element-styled.png +0 -0
  332. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser-element.png +0 -0
  333. data/brutrb.com/images/tutorial/02-confirmation-dialog-browser.png +0 -0
  334. data/brutrb.com/images/tutorial/02-confirmation-flow.graffle +0 -0
  335. data/brutrb.com/images/tutorial/02-confirmation-flow.png +0 -0
  336. data/brutrb.com/images/tutorial/basic-form-with-violations.png +0 -0
  337. data/brutrb.com/images/tutorial/basic-form.png +0 -0
  338. data/brutrb.com/images/tutorial/initial-home-page.png +0 -0
  339. data/brutrb.com/images/tutorial/new-post-editor.png +0 -0
  340. data/brutrb.com/images/tutorial/new-post-home-page.png +0 -0
  341. data/brutrb.com/images/tutorial/styled-form-with-server-side-violations.png +0 -0
  342. data/brutrb.com/images/tutorial/styled-form-with-violations.png +0 -0
  343. data/brutrb.com/images/tutorial/styled-home-page-with-posts.png +0 -0
  344. data/brutrb.com/images/tutorial/styled-home-page.png +0 -0
  345. data/brutrb.com/images/tutorial/welcome-to-brut.png +0 -0
  346. data/brutrb.com/images/workspace-protocol.dot +0 -44
  347. data/brutrb.com/images/workspace-protocol.png +0 -0
  348. data/brutrb.com/index.md +0 -34
  349. data/brutrb.com/instrumentation.md +0 -331
  350. data/brutrb.com/javascript.md +0 -122
  351. data/brutrb.com/jobs.md +0 -114
  352. data/brutrb.com/keyword-injection.md +0 -195
  353. data/brutrb.com/layouts.md +0 -156
  354. data/brutrb.com/lsp.md +0 -23
  355. data/brutrb.com/markdown-examples.md +0 -85
  356. data/brutrb.com/middleware.md +0 -80
  357. data/brutrb.com/overview.md +0 -68
  358. data/brutrb.com/package-lock.json +0 -2451
  359. data/brutrb.com/package.json +0 -11
  360. data/brutrb.com/pages.md +0 -290
  361. data/brutrb.com/public/SocialImage.png +0 -0
  362. data/brutrb.com/public/favicon.ico +0 -0
  363. data/brutrb.com/recipes/alternate-layouts.md +0 -32
  364. data/brutrb.com/recipes/authentication.md +0 -336
  365. data/brutrb.com/recipes/custom-flash.md +0 -51
  366. data/brutrb.com/recipes/dev-env-secrets.md +0 -87
  367. data/brutrb.com/recipes/form-errors.md +0 -148
  368. data/brutrb.com/recipes/indexed-forms.md +0 -149
  369. data/brutrb.com/recipes/migrations.md +0 -210
  370. data/brutrb.com/recipes/text-field-component.md +0 -182
  371. data/brutrb.com/roadmap.md +0 -52
  372. data/brutrb.com/routes.md +0 -189
  373. data/brutrb.com/security.md +0 -102
  374. data/brutrb.com/seed-data.md +0 -63
  375. data/brutrb.com/space-time-continuum.md +0 -81
  376. data/brutrb.com/tutorial.md +0 -138
  377. data/brutrb.com/tutorials/01-intro.md +0 -1654
  378. data/brutrb.com/tutorials/02-dialog.md +0 -569
  379. data/brutrb.com/unit-tests.md +0 -148
  380. data/brutrb.com/why.md +0 -19
  381. data/docker-compose.dx.yml +0 -25
  382. data/docs/404.html +0 -26
  383. data/docs/CNAME +0 -1
  384. data/docs/SocialImage.png +0 -0
  385. data/docs/adrs.html +0 -29
  386. data/docs/ai.html +0 -29
  387. data/docs/api/Brut/BackEnd/SeedData.html +0 -493
  388. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +0 -214
  389. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +0 -125
  390. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +0 -125
  391. data/docs/api/Brut/BackEnd/Sidekiq.html +0 -125
  392. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +0 -414
  393. data/docs/api/Brut/BackEnd/Validators.html +0 -128
  394. data/docs/api/Brut/BackEnd.html +0 -132
  395. data/docs/api/Brut/CLI/App.html +0 -1601
  396. data/docs/api/Brut/CLI/AppRunner.html +0 -491
  397. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +0 -264
  398. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +0 -306
  399. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +0 -262
  400. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +0 -314
  401. data/docs/api/Brut/CLI/Apps/BuildAssets.html +0 -183
  402. data/docs/api/Brut/CLI/Apps/DB/Create.html +0 -365
  403. data/docs/api/Brut/CLI/Apps/DB/Drop.html +0 -357
  404. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +0 -389
  405. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +0 -339
  406. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +0 -329
  407. data/docs/api/Brut/CLI/Apps/DB/Seed.html +0 -347
  408. data/docs/api/Brut/CLI/Apps/DB/Status.html +0 -383
  409. data/docs/api/Brut/CLI/Apps/DB.html +0 -183
  410. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +0 -270
  411. data/docs/api/Brut/CLI/Apps/DeployBase.html +0 -257
  412. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +0 -587
  413. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +0 -196
  414. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +0 -303
  415. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +0 -508
  416. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +0 -398
  417. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +0 -374
  418. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +0 -384
  419. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +0 -410
  420. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +0 -262
  421. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +0 -303
  422. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +0 -480
  423. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +0 -450
  424. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +0 -380
  425. data/docs/api/Brut/CLI/Apps/Scaffold.html +0 -253
  426. data/docs/api/Brut/CLI/Apps/Test/Audit.html +0 -474
  427. data/docs/api/Brut/CLI/Apps/Test/E2e.html +0 -407
  428. data/docs/api/Brut/CLI/Apps/Test/JS.html +0 -262
  429. data/docs/api/Brut/CLI/Apps/Test/Run.html +0 -578
  430. data/docs/api/Brut/CLI/Apps/Test.html +0 -253
  431. data/docs/api/Brut/CLI/Apps.html +0 -125
  432. data/docs/api/Brut/CLI/Command.html +0 -2425
  433. data/docs/api/Brut/CLI/Error.html +0 -139
  434. data/docs/api/Brut/CLI/ExecutionResults/Result.html +0 -664
  435. data/docs/api/Brut/CLI/ExecutionResults.html +0 -675
  436. data/docs/api/Brut/CLI/Executor.html +0 -561
  437. data/docs/api/Brut/CLI/InvalidOption.html +0 -245
  438. data/docs/api/Brut/CLI/Options.html +0 -880
  439. data/docs/api/Brut/CLI/Output.html +0 -699
  440. data/docs/api/Brut/CLI/SystemExecError.html +0 -451
  441. data/docs/api/Brut/CLI.html +0 -263
  442. data/docs/api/Brut/FactoryBot.html +0 -225
  443. data/docs/api/Brut/Framework/App.html +0 -1097
  444. data/docs/api/Brut/Framework/Config.html +0 -1071
  445. data/docs/api/Brut/Framework/Container.html +0 -1464
  446. data/docs/api/Brut/Framework/Error.html +0 -140
  447. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +0 -232
  448. data/docs/api/Brut/Framework/Errors/Bug.html +0 -234
  449. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +0 -257
  450. data/docs/api/Brut/Framework/Errors/MissingParameter.html +0 -273
  451. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +0 -471
  452. data/docs/api/Brut/Framework/Errors/NotFound.html +0 -308
  453. data/docs/api/Brut/Framework/Errors/NotImplemented.html +0 -234
  454. data/docs/api/Brut/Framework/Errors.html +0 -351
  455. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +0 -392
  456. data/docs/api/Brut/Framework/MCP.html +0 -871
  457. data/docs/api/Brut/Framework/ProjectEnvironment.html +0 -648
  458. data/docs/api/Brut/Framework.html +0 -129
  459. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +0 -317
  460. data/docs/api/Brut/FrontEnd/Component/Helpers.html +0 -420
  461. data/docs/api/Brut/FrontEnd/Component.html +0 -434
  462. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +0 -491
  463. data/docs/api/Brut/FrontEnd/Components/FormTag.html +0 -526
  464. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +0 -313
  465. data/docs/api/Brut/FrontEnd/Components/Input.html +0 -195
  466. data/docs/api/Brut/FrontEnd/Components/Inputs/ButtonTag.html +0 -447
  467. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +0 -339
  468. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +0 -568
  469. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +0 -419
  470. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +0 -610
  471. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +0 -534
  472. data/docs/api/Brut/FrontEnd/Components/Inputs.html +0 -125
  473. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +0 -367
  474. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +0 -355
  475. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +0 -655
  476. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +0 -352
  477. data/docs/api/Brut/FrontEnd/Components.html +0 -156
  478. data/docs/api/Brut/FrontEnd/CsrfProtector.html +0 -250
  479. data/docs/api/Brut/FrontEnd/Download.html +0 -467
  480. data/docs/api/Brut/FrontEnd/Flash.html +0 -1150
  481. data/docs/api/Brut/FrontEnd/Form.html +0 -1227
  482. data/docs/api/Brut/FrontEnd/Forms/Button.html +0 -331
  483. data/docs/api/Brut/FrontEnd/Forms/ButtonInputDefinition.html +0 -537
  484. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +0 -590
  485. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +0 -201
  486. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +0 -535
  487. data/docs/api/Brut/FrontEnd/Forms/Input.html +0 -1567
  488. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +0 -635
  489. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +0 -1336
  490. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +0 -730
  491. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +0 -587
  492. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +0 -734
  493. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +0 -582
  494. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +0 -659
  495. data/docs/api/Brut/FrontEnd/Forms.html +0 -127
  496. data/docs/api/Brut/FrontEnd/GenericResponse.html +0 -377
  497. data/docs/api/Brut/FrontEnd/Handler.html +0 -442
  498. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +0 -318
  499. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +0 -336
  500. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +0 -399
  501. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +0 -354
  502. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +0 -151
  503. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +0 -315
  504. data/docs/api/Brut/FrontEnd/Handlers.html +0 -125
  505. data/docs/api/Brut/FrontEnd/HandlingResults.html +0 -339
  506. data/docs/api/Brut/FrontEnd/HttpMethod.html +0 -661
  507. data/docs/api/Brut/FrontEnd/HttpStatus.html +0 -496
  508. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +0 -284
  509. data/docs/api/Brut/FrontEnd/Layout.html +0 -486
  510. data/docs/api/Brut/FrontEnd/Middleware.html +0 -135
  511. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +0 -288
  512. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +0 -292
  513. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +0 -324
  514. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +0 -376
  515. data/docs/api/Brut/FrontEnd/Middlewares.html +0 -125
  516. data/docs/api/Brut/FrontEnd/Page.html +0 -781
  517. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +0 -797
  518. data/docs/api/Brut/FrontEnd/Pages.html +0 -125
  519. data/docs/api/Brut/FrontEnd/RequestContext.html +0 -1312
  520. data/docs/api/Brut/FrontEnd/RouteHook.html +0 -424
  521. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +0 -242
  522. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +0 -249
  523. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +0 -264
  524. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +0 -261
  525. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +0 -284
  526. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +0 -252
  527. data/docs/api/Brut/FrontEnd/RouteHooks.html +0 -115
  528. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +0 -227
  529. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +0 -305
  530. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +0 -324
  531. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +0 -319
  532. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +0 -315
  533. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +0 -315
  534. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +0 -327
  535. data/docs/api/Brut/FrontEnd/Routing/Route.html +0 -761
  536. data/docs/api/Brut/FrontEnd/Routing.html +0 -927
  537. data/docs/api/Brut/FrontEnd/Session.html +0 -1195
  538. data/docs/api/Brut/FrontEnd.html +0 -134
  539. data/docs/api/Brut/I18n/BaseMethods.html +0 -931
  540. data/docs/api/Brut/I18n/ForBackEnd.html +0 -302
  541. data/docs/api/Brut/I18n/ForCLI.html +0 -302
  542. data/docs/api/Brut/I18n/ForHTML.html +0 -296
  543. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +0 -316
  544. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +0 -930
  545. data/docs/api/Brut/I18n.html +0 -127
  546. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +0 -435
  547. data/docs/api/Brut/Instrumentation/Methods/ClassMethods.html +0 -596
  548. data/docs/api/Brut/Instrumentation/Methods.html +0 -173
  549. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +0 -286
  550. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +0 -302
  551. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +0 -866
  552. data/docs/api/Brut/Instrumentation.html +0 -128
  553. data/docs/api/Brut/RubocopConfig.html +0 -237
  554. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +0 -534
  555. data/docs/api/Brut/SinatraHelpers.html +0 -281
  556. data/docs/api/Brut/SpecSupport/ClockSupport.html +0 -383
  557. data/docs/api/Brut/SpecSupport/ComponentSupport.html +0 -496
  558. data/docs/api/Brut/SpecSupport/E2ETestServer.html +0 -503
  559. data/docs/api/Brut/SpecSupport/E2eSupport.html +0 -142
  560. data/docs/api/Brut/SpecSupport/EnhancedNode.html +0 -403
  561. data/docs/api/Brut/SpecSupport/FlashSupport.html +0 -278
  562. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +0 -401
  563. data/docs/api/Brut/SpecSupport/GeneralSupport.html +0 -195
  564. data/docs/api/Brut/SpecSupport/HandlerSupport.html +0 -160
  565. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +0 -142
  566. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +0 -142
  567. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +0 -155
  568. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +0 -583
  569. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +0 -149
  570. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +0 -466
  571. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +0 -149
  572. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +0 -149
  573. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +0 -165
  574. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +0 -158
  575. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +0 -156
  576. data/docs/api/Brut/SpecSupport/Matchers.html +0 -125
  577. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +0 -335
  578. data/docs/api/Brut/SpecSupport/RSpecSetup.html +0 -637
  579. data/docs/api/Brut/SpecSupport/SessionSupport.html +0 -196
  580. data/docs/api/Brut/SpecSupport.html +0 -129
  581. data/docs/api/Brut/TUI/AnsiEscapeCode/Mod.html +0 -409
  582. data/docs/api/Brut/TUI/AnsiEscapeCode.html +0 -426
  583. data/docs/api/Brut/TUI/EventLoop/Deque.html +0 -531
  584. data/docs/api/Brut/TUI/EventLoop.html +0 -676
  585. data/docs/api/Brut/TUI/Events/BaseEvent.html +0 -449
  586. data/docs/api/Brut/TUI/Events/EventBus.html +0 -485
  587. data/docs/api/Brut/TUI/Events/EventLoopStarted.html +0 -211
  588. data/docs/api/Brut/TUI/Events/Exception.html +0 -523
  589. data/docs/api/Brut/TUI/Events/Tick.html +0 -294
  590. data/docs/api/Brut/TUI/Events.html +0 -131
  591. data/docs/api/Brut/TUI/MarkupString.html +0 -537
  592. data/docs/api/Brut/TUI/Script/BlockStep.html +0 -300
  593. data/docs/api/Brut/TUI/Script/Events/CommandExecutionFailed.html +0 -252
  594. data/docs/api/Brut/TUI/Script/Events/CommandExecutionSucceeded.html +0 -163
  595. data/docs/api/Brut/TUI/Script/Events/CommandStdErr.html +0 -163
  596. data/docs/api/Brut/TUI/Script/Events/CommandStdOut.html +0 -300
  597. data/docs/api/Brut/TUI/Script/Events/ExecutingCommand.html +0 -298
  598. data/docs/api/Brut/TUI/Script/Events/Message.html +0 -345
  599. data/docs/api/Brut/TUI/Script/Events/PhaseCompleted.html +0 -229
  600. data/docs/api/Brut/TUI/Script/Events/PhaseStarted.html +0 -350
  601. data/docs/api/Brut/TUI/Script/Events/ScriptCompleted.html +0 -282
  602. data/docs/api/Brut/TUI/Script/Events/ScriptStarted.html +0 -343
  603. data/docs/api/Brut/TUI/Script/Events/StepCompleted.html +0 -163
  604. data/docs/api/Brut/TUI/Script/Events/StepStarted.html +0 -346
  605. data/docs/api/Brut/TUI/Script/Events.html +0 -115
  606. data/docs/api/Brut/TUI/Script/ExecStep/ProcessStatusFailed.html +0 -210
  607. data/docs/api/Brut/TUI/Script/ExecStep.html +0 -493
  608. data/docs/api/Brut/TUI/Script/LoggingSubscriber.html +0 -914
  609. data/docs/api/Brut/TUI/Script/PutsSubscriber.html +0 -783
  610. data/docs/api/Brut/TUI/Script/Step.html +0 -313
  611. data/docs/api/Brut/TUI/Script.html +0 -1250
  612. data/docs/api/Brut/TUI/Terminal.html +0 -593
  613. data/docs/api/Brut/TUI/TerminalTheme.html +0 -1403
  614. data/docs/api/Brut/TUI/Themes/Dark.html +0 -706
  615. data/docs/api/Brut/TUI/Themes/Light.html +0 -804
  616. data/docs/api/Brut/TUI/Themes/None.html +0 -218
  617. data/docs/api/Brut/TUI/Themes.html +0 -115
  618. data/docs/api/Brut/TUI.html +0 -129
  619. data/docs/api/Brut.html +0 -341
  620. data/docs/api/Clock.html +0 -603
  621. data/docs/api/ModuleName.html +0 -595
  622. data/docs/api/RichString.html +0 -775
  623. data/docs/api/SemanticLogger/Appender/Async.html +0 -219
  624. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +0 -119
  625. data/docs/api/Sequel/Extensions/BrutMigrations.html +0 -541
  626. data/docs/api/Sequel/Extensions.html +0 -117
  627. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +0 -105
  628. data/docs/api/Sequel/Plugins/CreatedAt.html +0 -125
  629. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +0 -207
  630. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +0 -186
  631. data/docs/api/Sequel/Plugins/ExternalId.html +0 -218
  632. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +0 -202
  633. data/docs/api/Sequel/Plugins/FindBang.html +0 -125
  634. data/docs/api/Sequel/Plugins.html +0 -117
  635. data/docs/api/Sequel.html +0 -117
  636. data/docs/api/SpecSupport/Matchers/BeABug.html +0 -143
  637. data/docs/api/_index.html +0 -1964
  638. data/docs/api/class_list.html +0 -54
  639. data/docs/api/css/common.css +0 -1
  640. data/docs/api/css/full_list.css +0 -59
  641. data/docs/api/css/style.css +0 -504
  642. data/docs/api/file.README.html +0 -172
  643. data/docs/api/file_list.html +0 -59
  644. data/docs/api/frames.html +0 -22
  645. data/docs/api/index.html +0 -172
  646. data/docs/api/js/app.js +0 -344
  647. data/docs/api/js/full_list.js +0 -242
  648. data/docs/api/js/jquery.js +0 -4
  649. data/docs/api/method_list.html +0 -5542
  650. data/docs/api/top-level-namespace.html +0 -112
  651. data/docs/assets/02-confirmation-dialog-browser-element-styled.3NEGM20-.png +0 -0
  652. data/docs/assets/02-confirmation-dialog-browser-element.DPsf0xUW.png +0 -0
  653. data/docs/assets/02-confirmation-dialog-browser.DH8ALFO4.png +0 -0
  654. data/docs/assets/02-confirmation-flow.D9gZ0S5U.png +0 -0
  655. data/docs/assets/DevEnvironment.DaFcVfwP.png +0 -0
  656. data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
  657. data/docs/assets/OverviewMetro.DUS-5fUZ.png +0 -0
  658. data/docs/assets/adrs.md.YglbWtQe.js +0 -1
  659. data/docs/assets/adrs.md.YglbWtQe.lean.js +0 -1
  660. data/docs/assets/ai.md.ChLnvDAX.js +0 -1
  661. data/docs/assets/ai.md.ChLnvDAX.lean.js +0 -1
  662. data/docs/assets/app.B8jAEB7R.js +0 -1
  663. data/docs/assets/assets.md.BEF6Oz6K.js +0 -19
  664. data/docs/assets/assets.md.BEF6Oz6K.lean.js +0 -1
  665. data/docs/assets/basic-form-with-violations.Cv6Y9-Q_.png +0 -0
  666. data/docs/assets/basic-form.DbHnu0oW.png +0 -0
  667. data/docs/assets/brut-js.md.BMz0X1Rz.js +0 -12
  668. data/docs/assets/brut-js.md.BMz0X1Rz.lean.js +0 -1
  669. data/docs/assets/business-logic.md.DbuaOYGU.js +0 -1
  670. data/docs/assets/business-logic.md.DbuaOYGU.lean.js +0 -1
  671. data/docs/assets/chunks/@localSearchIndexroot.DJ8mocCj.js +0 -1
  672. data/docs/assets/chunks/VPLocalSearchBox.gF-Po_fz.js +0 -8
  673. data/docs/assets/chunks/framework.C4nOkCZI.js +0 -18
  674. data/docs/assets/chunks/theme.BjPAOJkz.js +0 -2
  675. data/docs/assets/cli.md.DDMar_51.js +0 -122
  676. data/docs/assets/cli.md.DDMar_51.lean.js +0 -1
  677. data/docs/assets/components.md.Ber8UBM0.js +0 -96
  678. data/docs/assets/components.md.Ber8UBM0.lean.js +0 -1
  679. data/docs/assets/configuration.md.DrJ6YVoZ.js +0 -78
  680. data/docs/assets/configuration.md.DrJ6YVoZ.lean.js +0 -1
  681. data/docs/assets/css.md.K5rOCOQY.js +0 -21
  682. data/docs/assets/css.md.K5rOCOQY.lean.js +0 -1
  683. data/docs/assets/custom-element-tests.md.DiLe-eFw.js +0 -69
  684. data/docs/assets/custom-element-tests.md.DiLe-eFw.lean.js +0 -1
  685. data/docs/assets/database-access.md.Dc8l2Plf.js +0 -63
  686. data/docs/assets/database-access.md.Dc8l2Plf.lean.js +0 -1
  687. data/docs/assets/database-schema.md.BJ_JhXmO.js +0 -70
  688. data/docs/assets/database-schema.md.BJ_JhXmO.lean.js +0 -1
  689. data/docs/assets/deployment.md.CHTx2eTR.js +0 -55
  690. data/docs/assets/deployment.md.CHTx2eTR.lean.js +0 -1
  691. data/docs/assets/dev-env-protocol.DysDAtnz.png +0 -0
  692. data/docs/assets/dev-environment.md.B1S9p5ZK.js +0 -16
  693. data/docs/assets/dev-environment.md.B1S9p5ZK.lean.js +0 -1
  694. data/docs/assets/dir-structure.md.D1T2kGwj.js +0 -46
  695. data/docs/assets/dir-structure.md.D1T2kGwj.lean.js +0 -1
  696. data/docs/assets/doc-conventions.md.CDnWaEFg.js +0 -1
  697. data/docs/assets/doc-conventions.md.CDnWaEFg.lean.js +0 -1
  698. data/docs/assets/end-to-end-tests.md.BJJdNDYL.js +0 -28
  699. data/docs/assets/end-to-end-tests.md.BJJdNDYL.lean.js +0 -1
  700. data/docs/assets/features.md.BDWxnyNO.js +0 -154
  701. data/docs/assets/features.md.BDWxnyNO.lean.js +0 -1
  702. data/docs/assets/flash-and-session.md.CUsMxoNl.js +0 -79
  703. data/docs/assets/flash-and-session.md.CUsMxoNl.lean.js +0 -1
  704. data/docs/assets/form-constraints.md.KlfXSKm2.js +0 -90
  705. data/docs/assets/form-constraints.md.KlfXSKm2.lean.js +0 -1
  706. data/docs/assets/forms.md.RK0zkhm0.js +0 -64
  707. data/docs/assets/forms.md.RK0zkhm0.lean.js +0 -1
  708. data/docs/assets/getting-started.md.CGJ44juQ.js +0 -31
  709. data/docs/assets/getting-started.md.CGJ44juQ.lean.js +0 -1
  710. data/docs/assets/handlers.md.C5tUwmmo.js +0 -54
  711. data/docs/assets/handlers.md.C5tUwmmo.lean.js +0 -1
  712. data/docs/assets/hooks.md.CoiYCKRc.js +0 -80
  713. data/docs/assets/hooks.md.CoiYCKRc.lean.js +0 -1
  714. data/docs/assets/i18n.md.DxkCKhUw.js +0 -23
  715. data/docs/assets/i18n.md.DxkCKhUw.lean.js +0 -1
  716. data/docs/assets/index.md.DnphWyQd.js +0 -1
  717. data/docs/assets/index.md.DnphWyQd.lean.js +0 -1
  718. data/docs/assets/initial-home-page.DNIaYmgP.png +0 -0
  719. data/docs/assets/instrumentation.md.BcxjC4jd.js +0 -90
  720. data/docs/assets/instrumentation.md.BcxjC4jd.lean.js +0 -1
  721. data/docs/assets/javascript.md.D6fxhaQb.js +0 -31
  722. data/docs/assets/javascript.md.D6fxhaQb.lean.js +0 -1
  723. data/docs/assets/jobs.md.Bi3qb3v6.js +0 -25
  724. data/docs/assets/jobs.md.Bi3qb3v6.lean.js +0 -1
  725. data/docs/assets/keyword-injection.md.CqLnnzIz.js +0 -21
  726. data/docs/assets/keyword-injection.md.CqLnnzIz.lean.js +0 -1
  727. data/docs/assets/layouts.md.HEbeK7Jr.js +0 -68
  728. data/docs/assets/layouts.md.HEbeK7Jr.lean.js +0 -1
  729. data/docs/assets/lsp.md.bE9dW8n9.js +0 -1
  730. data/docs/assets/lsp.md.bE9dW8n9.lean.js +0 -1
  731. data/docs/assets/markdown-examples.md.BPmtHlc-.js +0 -33
  732. data/docs/assets/markdown-examples.md.BPmtHlc-.lean.js +0 -1
  733. data/docs/assets/middleware.md.BhOIsg59.js +0 -20
  734. data/docs/assets/middleware.md.BhOIsg59.lean.js +0 -1
  735. data/docs/assets/new-post-editor.DrHr-5oh.png +0 -0
  736. data/docs/assets/new-post-home-page.Bm34lyMg.png +0 -0
  737. data/docs/assets/overview.md.BpWAgPFH.js +0 -1
  738. data/docs/assets/overview.md.BpWAgPFH.lean.js +0 -1
  739. data/docs/assets/pages.md.B3sQXpEd.js +0 -45
  740. data/docs/assets/pages.md.B3sQXpEd.lean.js +0 -1
  741. data/docs/assets/recipes_alternate-layouts.md.C1QzVkA7.js +0 -22
  742. data/docs/assets/recipes_alternate-layouts.md.C1QzVkA7.lean.js +0 -1
  743. data/docs/assets/recipes_authentication.md.CyvoIW82.js +0 -157
  744. data/docs/assets/recipes_authentication.md.CyvoIW82.lean.js +0 -1
  745. data/docs/assets/recipes_custom-flash.md.6gFqf2uL.js +0 -26
  746. data/docs/assets/recipes_custom-flash.md.6gFqf2uL.lean.js +0 -1
  747. data/docs/assets/recipes_dev-env-secrets.md.DC_jVY9U.js +0 -12
  748. data/docs/assets/recipes_dev-env-secrets.md.DC_jVY9U.lean.js +0 -1
  749. data/docs/assets/recipes_form-errors.md.B5ptSzMO.js +0 -66
  750. data/docs/assets/recipes_form-errors.md.B5ptSzMO.lean.js +0 -1
  751. data/docs/assets/recipes_indexed-forms.md.BYYQGW2C.js +0 -74
  752. data/docs/assets/recipes_indexed-forms.md.BYYQGW2C.lean.js +0 -1
  753. data/docs/assets/recipes_migrations.md.Cid7-3cu.js +0 -97
  754. data/docs/assets/recipes_migrations.md.Cid7-3cu.lean.js +0 -1
  755. data/docs/assets/recipes_text-field-component.md.VhOsCtKI.js +0 -101
  756. data/docs/assets/recipes_text-field-component.md.VhOsCtKI.lean.js +0 -1
  757. data/docs/assets/roadmap.md.DqC1Y7Zt.js +0 -1
  758. data/docs/assets/roadmap.md.DqC1Y7Zt.lean.js +0 -1
  759. data/docs/assets/routes.md.C1dgIBtD.js +0 -21
  760. data/docs/assets/routes.md.C1dgIBtD.lean.js +0 -1
  761. data/docs/assets/security.md.Jn4SY1uK.js +0 -1
  762. data/docs/assets/security.md.Jn4SY1uK.lean.js +0 -1
  763. data/docs/assets/seed-data.md.UZW0WxYN.js +0 -14
  764. data/docs/assets/seed-data.md.UZW0WxYN.lean.js +0 -1
  765. data/docs/assets/spa.qejUdp-5.png +0 -0
  766. data/docs/assets/space-time-continuum.md.D9rYGDFH.js +0 -1
  767. data/docs/assets/space-time-continuum.md.D9rYGDFH.lean.js +0 -1
  768. data/docs/assets/style.B1z60PPQ.css +0 -1
  769. data/docs/assets/styled-form-with-server-side-violations.Bjxd8Dpv.png +0 -0
  770. data/docs/assets/styled-form-with-violations.Bv_sa9tg.png +0 -0
  771. data/docs/assets/styled-home-page-with-posts.Dd4kG89D.png +0 -0
  772. data/docs/assets/styled-home-page.BzdI7dWz.png +0 -0
  773. data/docs/assets/tutorial.md.BX6f6l00.js +0 -27
  774. data/docs/assets/tutorial.md.BX6f6l00.lean.js +0 -1
  775. data/docs/assets/tutorials_01-intro.md.CzZ3kpF_.js +0 -708
  776. data/docs/assets/tutorials_01-intro.md.CzZ3kpF_.lean.js +0 -1
  777. data/docs/assets/tutorials_02-dialog.md.DE5WfCXI.js +0 -274
  778. data/docs/assets/tutorials_02-dialog.md.DE5WfCXI.lean.js +0 -1
  779. data/docs/assets/unit-tests.md.vDsdBbO_.js +0 -13
  780. data/docs/assets/unit-tests.md.vDsdBbO_.lean.js +0 -1
  781. data/docs/assets/welcome-to-brut.VSWzl17-.png +0 -0
  782. data/docs/assets/why.md.4WpxdrQ2.js +0 -1
  783. data/docs/assets/why.md.4WpxdrQ2.lean.js +0 -1
  784. data/docs/assets/workspace-protocol.C0gXsoDb.png +0 -0
  785. data/docs/assets.html +0 -47
  786. data/docs/brut-css/brut.css +0 -1
  787. data/docs/brut-css/brut.max.css +0 -22372
  788. data/docs/brut-css/classes/appearances.html +0 -783
  789. data/docs/brut-css/classes/background-colors.html +0 -3529
  790. data/docs/brut-css/classes/border-colors.html +0 -3529
  791. data/docs/brut-css/classes/borders.html +0 -2293
  792. data/docs/brut-css/classes/dimensions.html +0 -2581
  793. data/docs/brut-css/classes/flex.html +0 -917
  794. data/docs/brut-css/classes/foreground-colors.html +0 -3261
  795. data/docs/brut-css/classes/junk-drawer.html +0 -431
  796. data/docs/brut-css/classes/layout.html +0 -668
  797. data/docs/brut-css/classes/lists.html +0 -331
  798. data/docs/brut-css/classes/positioning.html +0 -1751
  799. data/docs/brut-css/classes/spacings.html +0 -2633
  800. data/docs/brut-css/classes/typography.html +0 -2206
  801. data/docs/brut-css/customization/advanced-configuration.html +0 -204
  802. data/docs/brut-css/customization/breakpoints.html +0 -227
  803. data/docs/brut-css/customization/design-system.html +0 -197
  804. data/docs/brut-css/customization/pseudo-classes.html +0 -228
  805. data/docs/brut-css/docs.css +0 -98
  806. data/docs/brut-css/getting-started/core-concepts.html +0 -234
  807. data/docs/brut-css/getting-started/installation.html +0 -190
  808. data/docs/brut-css/getting-started/overview.html +0 -210
  809. data/docs/brut-css/getting-started/simple-example.html +0 -285
  810. data/docs/brut-css/index.html +0 -193
  811. data/docs/brut-css/prism-twilight.min.css +0 -1
  812. data/docs/brut-css/properties/colors.html +0 -1548
  813. data/docs/brut-css/properties/spacings.html +0 -614
  814. data/docs/brut-css/properties/typography.html +0 -777
  815. data/docs/brut-js/api/AjaxSubmit.html +0 -452
  816. data/docs/brut-js/api/AjaxSubmit.js.html +0 -550
  817. data/docs/brut-js/api/Autosubmit.html +0 -192
  818. data/docs/brut-js/api/Autosubmit.js.html +0 -114
  819. data/docs/brut-js/api/BaseCustomElement.html +0 -1091
  820. data/docs/brut-js/api/BaseCustomElement.js.html +0 -312
  821. data/docs/brut-js/api/BrutCustomElements.html +0 -172
  822. data/docs/brut-js/api/BufferedLogger.html +0 -173
  823. data/docs/brut-js/api/ConfirmSubmit.html +0 -286
  824. data/docs/brut-js/api/ConfirmSubmit.js.html +0 -188
  825. data/docs/brut-js/api/ConfirmationDialog.html +0 -425
  826. data/docs/brut-js/api/ConfirmationDialog.js.html +0 -194
  827. data/docs/brut-js/api/ConstraintViolationMessage.html +0 -498
  828. data/docs/brut-js/api/ConstraintViolationMessage.js.html +0 -191
  829. data/docs/brut-js/api/ConstraintViolationMessages.html +0 -590
  830. data/docs/brut-js/api/ConstraintViolationMessages.js.html +0 -149
  831. data/docs/brut-js/api/CopyToClipboard.html +0 -345
  832. data/docs/brut-js/api/CopyToClipboard.js.html +0 -147
  833. data/docs/brut-js/api/Form.html +0 -291
  834. data/docs/brut-js/api/Form.js.html +0 -198
  835. data/docs/brut-js/api/I18nTranslation.html +0 -409
  836. data/docs/brut-js/api/I18nTranslation.js.html +0 -115
  837. data/docs/brut-js/api/LocaleDetection.html +0 -312
  838. data/docs/brut-js/api/LocaleDetection.js.html +0 -168
  839. data/docs/brut-js/api/Logger.html +0 -702
  840. data/docs/brut-js/api/Logger.js.html +0 -141
  841. data/docs/brut-js/api/Message.html +0 -238
  842. data/docs/brut-js/api/Message.js.html +0 -113
  843. data/docs/brut-js/api/PrefixedLogger.html +0 -369
  844. data/docs/brut-js/api/RichString.html +0 -1049
  845. data/docs/brut-js/api/RichString.js.html +0 -167
  846. data/docs/brut-js/api/Tabs.html +0 -295
  847. data/docs/brut-js/api/Tabs.js.html +0 -219
  848. data/docs/brut-js/api/Toast.html +0 -270
  849. data/docs/brut-js/api/Toast.js.html +0 -153
  850. data/docs/brut-js/api/Tracing.html +0 -277
  851. data/docs/brut-js/api/Tracing.js.html +0 -298
  852. data/docs/brut-js/api/external-CustomElementRegistry.html +0 -140
  853. data/docs/brut-js/api/external-Performance.html +0 -138
  854. data/docs/brut-js/api/external-Promise.html +0 -138
  855. data/docs/brut-js/api/external-ValidityState.html +0 -138
  856. data/docs/brut-js/api/external-Window.html +0 -233
  857. data/docs/brut-js/api/external-fetch.html +0 -138
  858. data/docs/brut-js/api/global.html +0 -400
  859. data/docs/brut-js/api/index.html +0 -168
  860. data/docs/brut-js/api/index.js.html +0 -184
  861. data/docs/brut-js/api/module-testing.html +0 -383
  862. data/docs/brut-js/api/scripts/linenumber.js +0 -25
  863. data/docs/brut-js/api/scripts/prettify/Apache-License-2.0.txt +0 -202
  864. data/docs/brut-js/api/scripts/prettify/lang-css.js +0 -2
  865. data/docs/brut-js/api/scripts/prettify/prettify.js +0 -28
  866. data/docs/brut-js/api/styles/jsdoc-default.css +0 -327
  867. data/docs/brut-js/api/styles/prettify-jsdoc.css +0 -111
  868. data/docs/brut-js/api/styles/prettify-tomorrow.css +0 -132
  869. data/docs/brut-js/api/testing.AssetMetadata.html +0 -172
  870. data/docs/brut-js/api/testing.AssetMetadataLoader.html +0 -171
  871. data/docs/brut-js/api/testing.CustomElementTest.html +0 -679
  872. data/docs/brut-js/api/testing.DOMCreator.html +0 -171
  873. data/docs/brut-js/api/testing_AssetMetadata.js.html +0 -86
  874. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +0 -76
  875. data/docs/brut-js/api/testing_CustomElementTest.js.html +0 -286
  876. data/docs/brut-js/api/testing_DOMCreator.js.html +0 -96
  877. data/docs/brut-js/api/testing_index.js.html +0 -99
  878. data/docs/brut-js.html +0 -40
  879. data/docs/business-logic.html +0 -29
  880. data/docs/cli.html +0 -150
  881. data/docs/components.html +0 -124
  882. data/docs/configuration.html +0 -106
  883. data/docs/css.html +0 -49
  884. data/docs/custom-element-tests.html +0 -97
  885. data/docs/database-access.html +0 -91
  886. data/docs/database-schema.html +0 -98
  887. data/docs/deployment.html +0 -83
  888. data/docs/dev-environment.html +0 -44
  889. data/docs/dir-structure.html +0 -74
  890. data/docs/doc-conventions.html +0 -29
  891. data/docs/end-to-end-tests.html +0 -56
  892. data/docs/favicon.ico +0 -0
  893. data/docs/features.html +0 -182
  894. data/docs/flash-and-session.html +0 -107
  895. data/docs/form-constraints.html +0 -118
  896. data/docs/forms.html +0 -92
  897. data/docs/getting-started.html +0 -59
  898. data/docs/handlers.html +0 -82
  899. data/docs/hashmap.json +0 -1
  900. data/docs/hooks.html +0 -108
  901. data/docs/i18n.html +0 -51
  902. data/docs/index.html +0 -29
  903. data/docs/instrumentation.html +0 -118
  904. data/docs/javascript.html +0 -59
  905. data/docs/jobs.html +0 -53
  906. data/docs/keyword-injection.html +0 -49
  907. data/docs/layouts.html +0 -96
  908. data/docs/lsp.html +0 -29
  909. data/docs/markdown-examples.html +0 -61
  910. data/docs/middleware.html +0 -48
  911. data/docs/overview.html +0 -29
  912. data/docs/pages.html +0 -73
  913. data/docs/recipes/alternate-layouts.html +0 -50
  914. data/docs/recipes/authentication.html +0 -185
  915. data/docs/recipes/custom-flash.html +0 -54
  916. data/docs/recipes/dev-env-secrets.html +0 -40
  917. data/docs/recipes/form-errors.html +0 -94
  918. data/docs/recipes/indexed-forms.html +0 -102
  919. data/docs/recipes/migrations.html +0 -125
  920. data/docs/recipes/text-field-component.html +0 -129
  921. data/docs/roadmap.html +0 -29
  922. data/docs/routes.html +0 -49
  923. data/docs/security.html +0 -29
  924. data/docs/seed-data.html +0 -42
  925. data/docs/space-time-continuum.html +0 -29
  926. data/docs/tutorial.html +0 -55
  927. data/docs/tutorials/01-intro.html +0 -736
  928. data/docs/tutorials/02-dialog.html +0 -302
  929. data/docs/unit-tests.html +0 -41
  930. data/docs/vp-icons.css +0 -1
  931. data/docs/why.html +0 -29
  932. data/docs-todo.md +0 -32
  933. data/dx/bash_customizations +0 -6
  934. data/dx/build +0 -73
  935. data/dx/build.pre +0 -15
  936. data/dx/docker-compose.env +0 -22
  937. data/dx/dx.sh.lib +0 -24
  938. data/dx/exec +0 -75
  939. data/dx/setupkit.sh.lib +0 -144
  940. data/dx/show-help-in-app-container-then-wait.sh +0 -38
  941. data/lib/brut/cli/app.rb +0 -238
  942. data/lib/brut/cli/app_runner.rb +0 -252
  943. data/lib/brut/cli/command.rb +0 -258
  944. data/lib/brut/cli/execution_results.rb +0 -119
  945. data/lib/brut/front_end/layouts/_internal.html.erb +0 -68
  946. data/lib/brut/front_end/pages/_missing_page.html.erb +0 -17
  947. data/mkbrut/.gitignore +0 -16
  948. data/mkbrut/CODE_OF_CONDUCT.txt +0 -100
  949. data/mkbrut/Gemfile +0 -3
  950. data/mkbrut/Gemfile.lock +0 -20
  951. data/mkbrut/LICENSE.txt +0 -370
  952. data/mkbrut/README.md +0 -145
  953. data/mkbrut/Rakefile +0 -2
  954. data/mkbrut/bin/build +0 -36
  955. data/mkbrut/bin/ci +0 -19
  956. data/mkbrut/bin/docs +0 -19
  957. data/mkbrut/bin/publish +0 -129
  958. data/mkbrut/bin/rake +0 -16
  959. data/mkbrut/bin/setup +0 -30
  960. data/mkbrut/brut-welcome.png +0 -0
  961. data/mkbrut/deploy/.dockerignore +0 -2
  962. data/mkbrut/deploy/Dockerfile +0 -25
  963. data/mkbrut/dx +0 -1
  964. data/mkbrut/exe/mkbrut +0 -5
  965. data/mkbrut/lib/mkbrut/app_name.rb +0 -29
  966. data/mkbrut/lib/mkbrut/app_options.rb +0 -36
  967. data/mkbrut/lib/mkbrut/cli.rb +0 -189
  968. data/mkbrut/lib/mkbrut/erb_binding_delegate.rb +0 -20
  969. data/mkbrut/lib/mkbrut/ops.rb +0 -17
  970. data/mkbrut/lib/mkbrut/organization.rb +0 -5
  971. data/mkbrut/lib/mkbrut/segments.rb +0 -8
  972. data/mkbrut/lib/mkbrut/version.rb +0 -3
  973. data/mkbrut/lib/mkbrut.rb +0 -20
  974. data/mkbrut/mkbrut.gemspec +0 -34
  975. data/mkbrut/templates/Base/app/src/front_end/images/LogoPylon.png +0 -0
  976. data/mkbrut/templates/Base/bin/build-assets +0 -7
  977. data/mkbrut/templates/Base/bin/ci +0 -39
  978. data/mkbrut/templates/Base/bin/db +0 -9
  979. data/mkbrut/templates/Base/bin/scaffold +0 -9
  980. data/mkbrut/templates/Base/bin/setup +0 -287
  981. data/mkbrut/templates/Base/bin/test +0 -9
  982. data/mkbrut/templates/Base/bin/test-server +0 -29
  983. data/mkbrut/templates/Base/dx/prune +0 -19
  984. data/mkbrut/templates/Base/dx/start +0 -30
  985. data/mkbrut/templates/Base/dx/stop +0 -23
  986. data/mkbrut/templates/segments/Heroku/deploy/heroku_config.rb +0 -27
  987. data/specs/brut/front_end/forms/input.spec.rb +0 -978
  988. data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +0 -54
  989. data/specs/brut/front_end/forms/select_input.spec.rb +0 -54
  990. data/specs/brut/instrumentation/methods.spec.rb +0 -399
  991. data/specs/brut/junk_drawer.spec.rb +0 -79
  992. data/specs/brut/tui/ansi_escape_code.spec.rb +0 -30
  993. data/specs/brut/tui/event_loop.spec.rb +0 -70
  994. data/specs/brut/tui/events/base_event.spec.rb +0 -26
  995. data/specs/brut/tui/events/event_bus.spec.rb +0 -141
  996. data/specs/brut/tui/events/exception.spec.rb +0 -19
  997. data/specs/brut/tui/events/test_event.rb +0 -5
  998. data/specs/spec_helper.rb +0 -31
  999. data/specs/support/matchers/have_constraint_violation.rb +0 -23
  1000. data/specs/support/matchers.rb +0 -5
  1001. data/specs/support.rb +0 -3
  1002. /data/{mkbrut/lib/mkbrut → lib/brut/cli/apps/new}/prefixed_io.rb +0 -0
  1003. /data/{mkbrut/templates → templates}/Base/.dockerignore +0 -0
  1004. /data/{mkbrut/templates → templates}/Base/.env.development.erb +0 -0
  1005. /data/{mkbrut/templates → templates}/Base/.env.test.erb +0 -0
  1006. /data/{mkbrut/templates → templates}/Base/.gitignore +0 -0
  1007. /data/{mkbrut/templates → templates}/Base/.projections.json +0 -0
  1008. /data/{mkbrut/templates → templates}/Base/Dockerfile.dx +0 -0
  1009. /data/{mkbrut/templates → templates}/Base/Gemfile.erb +0 -0
  1010. /data/{mkbrut/templates → templates}/Base/Procfile.development +0 -0
  1011. /data/{mkbrut/templates → templates}/Base/Procfile.test +0 -0
  1012. /data/{mkbrut/templates → templates}/Base/README.md +0 -0
  1013. /data/{mkbrut/templates → templates}/Base/README.md.erb +0 -0
  1014. /data/{mkbrut/templates → templates}/Base/app/bootstrap.rb +0 -0
  1015. /data/{mkbrut/templates → templates}/Base/app/config/i18n/en/1_defaults.rb +0 -0
  1016. /data/{mkbrut/templates → templates}/Base/app/config/i18n/en/2_app.rb +0 -0
  1017. /data/{mkbrut/templates → templates}/Base/app/public/static/manifest.json.erb +0 -0
  1018. /data/{mkbrut/templates → templates}/Base/app/src/app.rb.erb +0 -0
  1019. /data/{mkbrut/templates → templates}/Base/app/src/back_end/data_models/app_data_model.rb +0 -0
  1020. /data/{mkbrut/templates → templates}/Base/app/src/back_end/data_models/db.rb +0 -0
  1021. /data/{mkbrut/templates → templates}/Base/app/src/back_end/data_models/migrations/20240101130000_citext.rb +0 -0
  1022. /data/{mkbrut/templates → templates}/Base/app/src/back_end/data_models/seed/seed_data.rb +0 -0
  1023. /data/{mkbrut/templates → templates}/Base/app/src/front_end/components/app_component.rb +0 -0
  1024. /data/{mkbrut/templates → templates}/Base/app/src/front_end/components/custom_element_registration.rb.erb +0 -0
  1025. /data/{mkbrut/templates → templates}/Base/app/src/front_end/css/index.css +0 -0
  1026. /data/{mkbrut/templates → templates}/Base/app/src/front_end/css/svgs.css +0 -0
  1027. /data/{mkbrut/templates → templates}/Base/app/src/front_end/forms/app_form.rb +0 -0
  1028. /data/{mkbrut/templates → templates}/Base/app/src/front_end/handlers/app_handler.rb +0 -0
  1029. /data/{brutrb.com → templates/Base/app/src/front_end}/images/LogoPylon.png +0 -0
  1030. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/LogoTransit.png +0 -0
  1031. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/apple-touch-icon-120x120.png +0 -0
  1032. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/apple-touch-icon-152x152.png +0 -0
  1033. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/apple-touch-icon-167x167.png +0 -0
  1034. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/apple-touch-icon-180x180.png +0 -0
  1035. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/favicon.ico +0 -0
  1036. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/icon.png +0 -0
  1037. /data/{mkbrut/templates → templates}/Base/app/src/front_end/images/mkicons.sh +0 -0
  1038. /data/{mkbrut/templates → templates}/Base/app/src/front_end/js/index.js +0 -0
  1039. /data/{mkbrut/templates → templates}/Base/app/src/front_end/layouts/blank_layout.rb +0 -0
  1040. /data/{mkbrut/templates → templates}/Base/app/src/front_end/layouts/default_layout.rb.erb +0 -0
  1041. /data/{mkbrut/templates → templates}/Base/app/src/front_end/pages/app_page.rb +0 -0
  1042. /data/{mkbrut/templates → templates}/Base/app/src/front_end/pages/home_page.rb +0 -0
  1043. /data/{mkbrut/templates → templates}/Base/app/src/front_end/support/app_session.rb +0 -0
  1044. /data/{mkbrut/templates → templates}/Base/app/src/front_end/svgs/README.md +0 -0
  1045. /data/{mkbrut/templates → templates}/Base/app/src/front_end/svgs/comment-button.svg +0 -0
  1046. /data/{mkbrut/templates → templates}/Base/bin/README.md.erb +0 -0
  1047. /data/{mkbrut/templates → templates}/Base/bin/console +0 -0
  1048. /data/{mkbrut/templates → templates}/Base/bin/dbconsole +0 -0
  1049. /data/{mkbrut/templates → templates}/Base/bin/dev +0 -0
  1050. /data/{mkbrut/templates → templates}/Base/bin/run +0 -0
  1051. /data/{mkbrut/templates → templates}/Base/bin/run.run +0 -0
  1052. /data/{mkbrut/templates → templates}/Base/bin/startup-message +0 -0
  1053. /data/{mkbrut/templates → templates}/Base/config.ru +0 -0
  1054. /data/{mkbrut/templates → templates}/Base/docker-compose.dx.yml +0 -0
  1055. /data/{mkbrut/templates → templates}/Base/dx/README.md +0 -0
  1056. /data/{mkbrut/templates → templates}/Base/dx/bash_customizations +0 -0
  1057. /data/{mkbrut/templates → templates}/Base/dx/bash_customizations.local +0 -0
  1058. /data/{mkbrut/templates → templates}/Base/dx/build +0 -0
  1059. /data/{mkbrut/templates → templates}/Base/dx/dx.sh.lib +0 -0
  1060. /data/{mkbrut/templates → templates}/Base/dx/exec +0 -0
  1061. /data/{dx → templates/Base/dx}/prune +0 -0
  1062. /data/{mkbrut/templates → templates}/Base/dx/show-help-in-app-container-then-wait.sh +0 -0
  1063. /data/{dx → templates/Base/dx}/start +0 -0
  1064. /data/{dx → templates/Base/dx}/stop +0 -0
  1065. /data/{mkbrut/templates → templates}/Base/package.json.erb +0 -0
  1066. /data/{mkbrut/templates → templates}/Base/puma.config.rb +0 -0
  1067. /data/{mkbrut/templates → templates}/Base/specs/e2e/home_page.spec.rb.erb +0 -0
  1068. /data/{mkbrut/templates → templates}/Base/specs/front_end/js/SpecHelper.js +0 -0
  1069. /data/{mkbrut/templates → templates}/Base/specs/front_end/pages/home_page.spec.rb +0 -0
  1070. /data/{mkbrut/templates → templates}/Base/specs/lint_factories.spec.rb +0 -0
  1071. /data/{mkbrut/templates → templates}/Base/specs/spec_helper.rb +0 -0
  1072. /data/{mkbrut/templates → templates}/Base/specs/support.rb +0 -0
  1073. /data/{mkbrut/templates → templates}/segments/BareBones/app/src/front_end/handlers/trigger_exception_handler.rb +0 -0
  1074. /data/{mkbrut/templates → templates}/segments/BareBones/app/src/front_end/js/Example.js.erb +0 -0
  1075. /data/{mkbrut/templates → templates}/segments/BareBones/specs/front_end/handlers/trigger_exception_handler.spec.rb +0 -0
  1076. /data/{mkbrut/templates → templates}/segments/BareBones/specs/front_end/js/Example.spec.js.erb +0 -0
  1077. /data/{mkbrut/templates → templates}/segments/Demo/app/src/back_end/data_models/db/guestbook_message.rb +0 -0
  1078. /data/{mkbrut/templates → templates}/segments/Demo/app/src/back_end/data_models/migrations/20250628194124_guestbook.rb +0 -0
  1079. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/components/flash_component.rb +0 -0
  1080. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/css/constraint-violations.css +0 -0
  1081. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/fonts/monaspace-xenon.ttf +0 -0
  1082. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/forms/guestbook_message_form.rb +0 -0
  1083. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/handlers/guestbook_message_handler.rb +0 -0
  1084. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/pages/guestbook_page/message_component.rb +0 -0
  1085. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/pages/guestbook_page.rb +0 -0
  1086. /data/{mkbrut/templates → templates}/segments/Demo/app/src/front_end/pages/new_guestbook_message_page.rb +0 -0
  1087. /data/{mkbrut/templates → templates}/segments/Demo/specs/back_end/data_models/db/guestbook_message.spec.rb +0 -0
  1088. /data/{mkbrut/templates → templates}/segments/Demo/specs/e2e/guest_message.spec.rb +0 -0
  1089. /data/{mkbrut/templates → templates}/segments/Demo/specs/factories/db/guestbook_message.factory.rb +0 -0
  1090. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/components/flash_component.spec.rb +0 -0
  1091. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/handlers/guestbook_message_handler.spec.rb +0 -0
  1092. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/pages/guestbook_page/message_component.spec.rb +0 -0
  1093. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/pages/guestbook_page.spec.rb +0 -0
  1094. /data/{mkbrut/templates → templates}/segments/Demo/specs/front_end/pages/new_guestbook_message_page.spec.rb +0 -0
  1095. /data/{mkbrut/templates → templates}/segments/Heroku/bin/deploy +0 -0
  1096. /data/{mkbrut/templates → templates}/segments/Heroku/deploy/docker-entrypoint +0 -0
  1097. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/boot_sidekiq.rb +0 -0
  1098. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/config/sidekiq.yml +0 -0
  1099. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/src/back_end/jobs/app_job.rb +0 -0
  1100. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/src/back_end/jobs/example_job.rb +0 -0
  1101. /data/{mkbrut/templates → templates}/segments/Sidekiq/app/src/back_end/segments/sidekiq_segment.rb +0 -0
  1102. /data/{mkbrut/templates → templates}/segments/Sidekiq/bin/run.sidekiq +0 -0
  1103. /data/{mkbrut/templates → templates}/segments/Sidekiq/specs/back_end/jobs/example_job.spec.rb +0 -0
  1104. /data/{mkbrut/templates → templates}/segments/Sidekiq/specs/integration/sidekiq_works.spec.rb +0 -0
@@ -1,1654 +0,0 @@
1
- # Build a Blog in 15 Minutes
2
-
3
- This will start from nothing and show you the main features of Brut by building a very basic blog.
4
- You'll learn how to make a new Brut app, how to build pages, submit forms, validate data, and access
5
- data in a database. You'll also learn how to test it all.
6
-
7
- ## Set Up
8
-
9
- The only two pieces of software you need are Docker and a code editor:
10
-
11
- 1. [Install Docker](https://docker.com)
12
-
13
- > [!TIP]
14
- > If you are on Windows, we *highly* recommend you use the
15
- > Windows Subystem for Linux (WSL2), as this makes Brut, web developement,
16
- > and, honestly, your entire life as you know it, far easier than trying to
17
- > get things working natively in Windows.
18
- 2. If you are new to programming or new to Ruby and don't know what editor to get, use VSCode. If you are a vim or emacs person, those will be far better, but if you are used to an IDE, VSCode will be the easiest to get set up and learn to use.
19
-
20
- To check that docker is installed, open up a terminal and run:
21
-
22
- ```bash
23
- docker info
24
- ```
25
-
26
- This should produce a ton of output:
27
-
28
- ```
29
- # OUTPUT
30
- Client:
31
- Version: 28.2.2
32
- «LOTS OF OUTPUT»
33
- ```
34
-
35
- To be extra sure, **right after you ran `docker info`**, check `$?`, the exit code, to make sure it's a 0, which means the command ran successfully:
36
-
37
- ```bash
38
- echo $?
39
- ```
40
-
41
- ```
42
- # OUTPUT
43
- 0
44
- ```
45
-
46
- Now, let's create the app by first initializing it.
47
-
48
-
49
- ## Initialize Your App
50
-
51
- `mkbrut` is a command line app that will initialize your new app. It's available as a RubyGem or a Docker image. We'll use the Docker image since that doesn't require installing anything.
52
-
53
- We'll call the blog simply "blog". `mkbrut` will insert some demo features in new apps to show you have to use Brut. Since you're following this tutorial, you don't need that, so we'll use the `--no-demo` flag.
54
-
55
- `cd` to a folder where you'd like to work. `mkbrut` will create a folder called `blog` in there and in *that* folder, your app will be initialized.
56
-
57
- The command to do this is pretty long, because it downloads `mkbrut` and then runs it inside a Docker container, meaning you don't have to install anything new. Here it is:
58
-
59
- ```
60
- docker run \
61
- --pull always \
62
- -v "$PWD":"$PWD" \
63
- -w "$PWD" \
64
- -u $(id -u):$(id -g) \
65
- -it \
66
- thirdtank/mkbrut \
67
- mkbrut --no-demo blog
68
- ```
69
-
70
- You should see this output:
71
-
72
- ```
73
- # OUTPUT
74
- [ mkbrut ] Creating app with these options:
75
- [ mkbrut ] App name: blog
76
- [ mkbrut ] App ID: blog
77
- [ mkbrut ] Prefix: bl
78
- [ mkbrut ] Organization: blog
79
- [ mkbrut ] Include demo? false
80
- [ mkbrut ] Creating Base app
81
- [ mkbrut ] Creating segment: Bare bones framing
82
- [ mkbrut ] blog was created
83
-
84
- [ mkbrut ] Time to get building:
85
- [ mkbrut ] 1. cd blog
86
- [ mkbrut ] 2. dx/build
87
- [ mkbrut ] 3. dx/start
88
- [ mkbrut ] 4. [ in another terminal ] dx/exec bash
89
- [ mkbrut ] 5. [ inside the Docker container ] bin/setup
90
- [ mkbrut ] 6. [ inside the Docker container ] bin/dev
91
- [ mkbrut ] 7. Visit http://localhost:6502 in your browser
92
- [ mkbrut ] 8. [ inside the Docker container ] bin/setup help # to see more commands
93
- ```
94
-
95
- Before we follow the instructions in the output, `cd` to `blog` and check it out.
96
-
97
- ```bash
98
- cd blog
99
- ls
100
- ```
101
-
102
- ```
103
- #OUTPUT
104
- app Dockerfile.dx Procfile.development specs
105
- bin dx Procfile.test
106
- config.ru Gemfile puma.config.rb
107
- docker-compose.dx.yml package.json README.md
108
- ```
109
-
110
- * `app` is where all the code your app will be
111
- * `bin` has command line tools to manage your app
112
- * `dx` has command line tools to manage your development environment
113
- * `specs` is where your tests will go
114
-
115
- OK, let's start up the dev environment:
116
-
117
- ```bash
118
- dx/build
119
- ```
120
-
121
- ```
122
- # OUTPUT
123
- [ dx/build ] Could not find Gemfile.lock, which is needed to determine the playwright-ruby-client version
124
- [ dx/build ] Assuming your app is brand-new, this should be OK
125
- [+] Building 0.2s
126
- «LOTS OF OUTPUT»
127
- ```
128
-
129
- This may take a while, but it's building a Docker image where all your work will happen, although you'll be able to edit your code on your computer with the editor of your choice.
130
-
131
- When this is done, you should see a message like so:
132
-
133
- ```
134
- # OUTPUT
135
- [ dx/build ] 🌈 Your Docker image has been built tagged 'blog/blog:ruby-3.4'
136
- [ dx/build ] 🔄 You can now run dx/start to start it up, though you may need to stop it first with Ctrl-C
137
- ```
138
-
139
- Now, start up the environment:
140
-
141
- ```bash
142
- dx/start
143
- ```
144
-
145
- ```
146
- #OUTPUT
147
- [ dx/start ] 🚀 Starting docker-compose.dx.yml
148
- [+] Running 1/5
149
- ⠙ Container blog-postgres-1
150
- ⠙ Container blog-app-1
151
- ⠙ Container blog-otel-desktop-viewer-1
152
- «LOTS OF OUTPUT»
153
- app-1 | 2025-08-11T16:39:11.568390000-04:00
154
- app-1 | 2025-08-11T16:39:11.568978000-04:00
155
- app-1 | 2025-08-11T16:39:11.569430000-04:00
156
- app-1 | 2025-08-11T16:39:11.569825000-04:00 🎉 Dev Environment Initialized! 🎉
157
- app-1 | 2025-08-11T16:39:11.570214000-04:00
158
- app-1 | 2025-08-11T16:39:11.570599000-04:00 ℹ️ To use this environment, open a new terminal and run
159
- app-1 | 2025-08-11T16:39:11.570980000-04:00
160
- app-1 | 2025-08-11T16:39:11.571250000-04:00 dx/exec bash
161
- app-1 | 2025-08-11T16:39:11.571521000-04:00
162
- app-1 | 2025-08-11T16:39:11.571795000-04:00 🕹 Use `ctrl-c` to exit.
163
- app-1 | 2025-08-11T16:39:11.572064000-04:00
164
- app-1 | 2025-08-11T16:39:11.572327000-04:00
165
- app-1 | 2025-08-11T16:39:11.572596000-04:00
166
- ```
167
-
168
- `dx/start` will keep running. If you stop it, your dev environment will stop. It's running three containers:
169
-
170
- * `app`, which is where the app and its test will run
171
- * `postgres`, which is running PostgreSQL, a SQL database you'll use
172
- * `otel-desktop-viewer` which can view telemetry of your app. We'll see that later.
173
-
174
- Now, let's access the container we started.
175
-
176
- Open a new terminal window, `cd` to where `blog` is, and use `dx/exec` to run `bash`, effectively "logging in" to the container:
177
-
178
- ```bash
179
- dx/exec bash
180
- ```
181
-
182
- ```
183
- # OUTPUT
184
- [ dx/exec ] 🚂 Running 'ssh-agent bash' inside container with service name 'app'
185
- Now using node v22.18.0 (npm v10.9.3)
186
- docker-container - Projects/blog
187
- >
188
- ```
189
-
190
- At that prompt, you can now type commands. If you type `ls`, you'll see the same files you saw when we ran it above:
191
-
192
- ```bash
193
- ls
194
- ```
195
-
196
- ```
197
- #OUTPUT
198
- app Dockerfile.dx Procfile.development specs
199
- bin dx Procfile.test
200
- config.ru Gemfile puma.config.rb
201
- docker-compose.dx.yml package.json README.md
202
- ```
203
-
204
- This is because the folder on your computer is synced to the one inside the container. Changes in one are immediately reflected in the other.
205
-
206
- **From here on out, all command line invocations are run inside this container**, unless stated otherwise.
207
-
208
- ## Set Up the App Itself
209
-
210
- `mkbrut` created a lot of files for you, as well as command line apps to manage your app. We're going to perform app setup via `bin/setup`. This completely automates the following tasks:
211
-
212
- * Installing RubyGems
213
- * Installing Node Modules
214
- * Installing Shopfiy's Ruby LSP, and Microsoft's JS and CSS LSPs
215
- * Creating your dev and test databases
216
- * Setting up Chromium, which we'll use to run end-to-end tests
217
-
218
- Run it now (remember, this is inside the container, so you should've run `dx/exec bash` on your computer first)
219
-
220
- ```bash
221
- bin/setup
222
- ```
223
-
224
- ```
225
- # OUTPUT
226
- [ bin/setup ] Installing gems
227
- [ bin/setup ] Executing ["bundle check --no-color || bundle install --no-color --quiet"]
228
- «LOTS OF OUTPUT»
229
- [ bin/setup ] All set up.
230
-
231
- USEFUL COMMANDS
232
-
233
- bin/dev
234
- # run app locally, rebuilding and reloading as needed
235
-
236
- bin/ci
237
- # runs all tests and checks as CI would
238
-
239
- bin/console
240
- # get an IRB console with the app loaded
241
-
242
- bin/db
243
- # interact with the DB for migrations, information, etc
244
-
245
- bin/dbconsole
246
- # get a PSQL session to the database
247
-
248
- bin/scaffold
249
- # Create various structures in your app, like pages or forms
250
-
251
- bin/setup help
252
- # show this help
253
- ```
254
-
255
- When this is done, we can check that everything's working by running `bin/ci`. `bin/ci` runs all tests and quality checks. Even though you haven't written any code, there's just a bit included to ensure that what little is there is working properly. It's a good check before you start to make sure install and setup worked.
256
-
257
- ```bash
258
- bin/ci
259
- ```
260
-
261
- ```
262
- # OUTPUT
263
- [ bin/ci ] Building Assets
264
- «LOTS OF OUTPUT»
265
- [ bin/ci ] Running non E2E tests
266
- «LOTS OF OUTPUT»
267
- [ bin/ci ] Running JS tests
268
- «LOTS OF OUTPUT»
269
- [ bin/ci ] Running E2E tests
270
- «LOTS OF OUTPUT»
271
- [ bin/ci ] Analyzing Ruby gems for
272
- «LOTS OF OUTPUT»
273
- [ bin/ci ] security vulnerabilities
274
- «LOTS OF OUTPUT»
275
- [ bin/ci ] Checking to see that all classes have tests
276
- «LOTS OF OUTPUT»
277
- [ bin/ci ] Done
278
- ```
279
-
280
- Finally, we'll run the app itself via `bin/dev`
281
-
282
- ```bash
283
- bin/dev
284
- ```
285
-
286
- `bin/dev` won't exit, it'll sit there running your app until you hit `Ctrl-C`. Amongst the output you should see a message like:
287
-
288
- ```
289
- # OUTPUT
290
- « LOTS OF OUTPUT »
291
- 20:43:41 startup_message.1 | Your app is now running at
292
- 20:43:41 startup_message.1 |
293
- 20:43:41 startup_message.1 | http://localhost:6502
294
- 20:43:41 startup_message.1 |
295
- ```
296
-
297
- Go to http://localhost:6502 in your web browser. You should see a welcome screen like so:
298
-
299
- ![Screenshot of the Brut welcome screen](/images/tutorial/welcome-to-brut.png)
300
-
301
- ## The Blog We'll Build
302
-
303
- We're ready to write some code! Here's how the blog is going to work:
304
-
305
- * A blog post has a title and content, with each paragraph of the content separated with `\n\r`, which
306
- is what the browser inserts when you hit return.
307
- * The home page will show all the blog posts in reverse chronological order.
308
- * The home page will link to the edit blog post page where a blog post can be created.
309
- * Blog posts will be submitted to the backend to be saved, with the following constraints:
310
- - title and content are required
311
- - title must be at least three characters
312
- - content must be at least 5 words (i.e. space-separated tokens)
313
-
314
- We'll discuss tests later. To make it easier to follow Brut, we'll get things working first and then test them. You can absolutely do TDD with Brut, but we find it's hard to learn new things this way.
315
-
316
- Let's start not from the database, but from the user experience.
317
-
318
- ## Building and Styling Pages
319
-
320
- The home page of a Brut app is served, naturally, on `/` and is implemented by the class `HomePage`, located in `app/src/front_end/pages/home_page.rb`.
321
-
322
- A *page* in Brut is a Phlex component that is rendered inside a layout. A layout is common markup that all pages should have, such as the `<head>` section and perhaps a `<body>` or other tags. `mkbrut` provided a default layout that's good for now, so we just have to worry about the HTML that is specific to a page.
323
-
324
- Open up `app/src/front_end/pages/home_page.rb` in your editor. You should see something like this:
325
-
326
- ```ruby
327
- class HomePage < AppPage
328
- def page_template
329
- # The duplication and excessive class sizes here are to
330
- # make it easier for you to remove this when you start working
331
- # on your app. There are pros and cons to how this code
332
- # is written, so don't take this is as a directive on how to
333
- # build your app. You do you!
334
- img(src: "/static/images/LogoPylon.png",
335
- class: "dn db-ns pos-fixed top-0 left-0 h-100vh w-auto z-2")
336
-
337
- header(class: "flex flex-column items-center justify-center h-100vh") do
338
-
339
- # A lot more code
340
-
341
- end
342
- end
343
- end
344
- ```
345
-
346
- `page_template` is where you can call Phlex to generate HTML.
347
-
348
- > [!NOTE]
349
- > Phlex components use `view_template`, and that's what
350
- > components in Brut use, too. Pages, however, use
351
- > `page_template` so that the HTML can be placed inside
352
- > a layout. `page_template` is a Brut concept, not a Phlex one.
353
-
354
- ### Creating the HomePage
355
-
356
- Delete all the code in `page_template` and replace it with this:
357
-
358
- ```ruby
359
- def page_template
360
- header do
361
- h1 { "My Amazing Blog" }
362
- a(href: "") { "Write New Blog Post" }
363
- end
364
- main do
365
- p { "Posts go here" }
366
- end
367
- end
368
- ```
369
-
370
- If you've never used Phlex before, it's a Ruby API that defines one method for each HTML element (along with any custom elements you tell it about). You then call these methods to build up markup. As you can see, it's structurally identical to HTML, but it's Ruby.
371
-
372
- If your server is still running, refresh the page and you'll see this wonderful UI (otherwise, start your server with `bin/dev`):
373
-
374
- ![Screenshot of the page we built](/images/tutorial/initial-home-page.png)
375
-
376
- Let's make it a bit nicer.
377
-
378
- ### Using CSS
379
-
380
- Open up `app/src/front_end/css/index.css` in your editor. You should see this:
381
-
382
- ```css
383
- @import "brut-css/dist/brut.css";
384
- @import "svgs.css";
385
- ```
386
-
387
- Brut uses esbuild to bundle CSS. esbuild makes use of the standard `@import` directive. All `@imports` are relative to the current file or to `node_modules`. `brut-css/dist/brut.css` is the BrutCSS library that comes with Brut. We aren't going to use it, just to keep things focused. `svgs.css` is located in `app/src/front_end/css/svgs.css` and sets up a few classes for inline SVGs.
388
-
389
- We'll add some CSS for the home page right here. We'll use vanilla CSS to avoid going on a deep dive on CSS frameworks.
390
-
391
- Add this below `@import "svgs.css";`
392
-
393
- ```css
394
- body {
395
- width: 50%;
396
- margin-left: auto;
397
- margin-right: auto;
398
- }
399
-
400
- header {
401
- border-bottom: solid thin gray;
402
- display: flex;
403
- align-items: baseline;
404
- justify-content: space-between;
405
- width: 100%;
406
- gap: 0.5rem;
407
- }
408
- ```
409
-
410
- If you reload the home page in your browser, it now looks at least a little bit respectible:
411
-
412
- ![Screenshot of the styled home page](/images/tutorial/styled-home-page.png)
413
-
414
- Now, let's build the blog post editor.
415
-
416
- ## Creating Forms
417
-
418
- To create blog posts, we need three things:
419
-
420
- * A page where the creation happens, which will host an HTML `<form>`
421
- * A URL where that `<form>` will be submitted
422
- * Some code to handle the submissions
423
-
424
- ### Creating a New Page
425
-
426
- To make a new page in Brut, we'll need to declare a route, and Brut will choose the class name. We'll use `/blog_post_editor`, meaning Brut will expect `BlogPostEditorPage` to exist. We can do all this at once with `bin/scaffold page`. `bin/scaffold page` accepts the URL of the page we want to build. Brut will use that URL to figure out the page class' name and generate it, along with a failing test. It will also insert the route into `app.rb`. Run it now, like so:
427
-
428
- ```bash
429
- bin/scaffold page /blog_post_editor
430
- ```
431
-
432
- Your output should look like so:
433
-
434
- ```
435
- # OUTPUT
436
- [ bin/scaffold ] Inserted route into app/src/app.rb
437
- [ bin/scaffold ] Page source is in app/src/front_end/pages/blog_post_editor_page.rb
438
- [ bin/scaffold ] Page test is in specs/front_end/pages/blog_post_editor_page.spec.rb
439
- [ bin/scaffold ] Added title to app/config/i18n/en/2_app.rb
440
- [ bin/scaffold ] Added route to app/src/app.rb
441
- ```
442
-
443
- Restart your server (Brut currently cannot auto-reload new routes).
444
-
445
- If you manually navigate to `http://localhost:6502/blog_post_editor`, you can see a very basic page has been created. Before we build the actual page, let's change the home page so it links here.
446
-
447
- If you'll recall, we had a `a(href:"") { ... }` in our template. We now know the URL for that `href`. We *could* use the actual url, `/blog_post_editor`, but it's going to be easier to manage our app over time if we don't hard-code paths and instead use our new page class to generate the URL.
448
-
449
- Open up `app/src/front_end/pages/home_page.rb` and make this change:
450
-
451
- ```diff
452
- - a(href: "") { "Write New Blog Post" }
453
- + a(href: BlogPostEditorPage.routing) { "Write New Blog Post" }
454
- ```
455
-
456
- All page classes have a `.routing` method. By using this instead of building a URL ourselves, we get some advantages:
457
-
458
- * If we rename or remove `BlogPostEditorPage`, any page referencing it will generate a nice, easy-to-understand error.
459
- * `routing` can manage query strings and anchors in a safe way, plus it can check that if a page has
460
- required routing parameters (e.g. the `123` in `/posts/123`), that they are provided.
461
-
462
- With this change, you can now click the link and see the `BlogPostEditorPage`'s template we saw earlier.
463
-
464
- Most of the `BlogPostEditorPage`'s HTML will be a form to submit a new blog post. While we could write that using HTML, Brut makes life easier by allowing the use of a *form class* to do it, which also will describe the data to be submitted to the server. This data is handled by a *handler*.
465
-
466
- ### Create a Form and Handler
467
-
468
- A form gets submitted to a URL, and Brut routes that submission to a handler. To create both a form class and a handler, we'll use `bin/scaffold form`, giving it the URL to respond on.
469
-
470
- In this case, we'll use the URL `/new_blog_post`. Stop your server and run this command:
471
-
472
- ```bash
473
- bin/scaffold form /new_blog_post
474
- ```
475
-
476
- ```
477
- # OUTPUT
478
- [ bin/scaffold ] NewBlogPostForm in app/src/front_end/forms/new_blog_post_form.rb
479
- [ bin/scaffold ] NewBlogPostHandler in app/src/front_end/handlers/new_blog_post_handler.rb
480
- [ bin/scaffold ] Spec in specs/front_end/handlers/new_blog_post_handler.spec.rb
481
- [ bin/scaffold ] Inserted route into app/src/app.rb
482
- ```
483
-
484
- When creating a new form, the first thing we have to do is edit the form class (in this case,
485
- `NewBlogPostForm`, located in `app/src/front_end/forms/new_blog_post_form.rb`) to describe the values being accepted by the server.
486
-
487
- This can be done by calling static/class methods provided by `Brut::FrontEnd::Form`, the superclass of `AppForm`, which is the superclass of our app's forms.
488
-
489
- Open up `app/src/front_end/forms/new_blog_post_form.rb`. We'll call `input` twice, once for the title and once for the content. `input` takes keyword arguments that mirror those of the web platform's constraint validation system. Since our title must be at least 3 characters, that means we'll use `minlength` to specify this.
490
-
491
- Here's the code:
492
-
493
- ```ruby
494
- class NewBlogPostForm < AppForm
495
- input :title, minlength: 3
496
- input :content
497
- end
498
- ```
499
-
500
- Each field is required by default (you can set `required: false` on fields that aren't required).
501
-
502
- With these declarations, we can use an instance of this class to generate HTML.
503
-
504
- ### Generating an HTML Form
505
-
506
- The `BlogPostEditorPage` will contain the form used to write a blog post. This page must make sure two things happen:
507
-
508
- * When someone navigates to it, it should show the form with nothing in the fields.
509
- * When there is an error in what the blog post author has provided, it should show those errors, but
510
- also maintain the inputs the author provided.
511
-
512
- To do this, the `BlogPostEditorPage` will need an instance of `NewBlogPostForm`. We can create this in its constructor. Open up `app/src/front_end/pages/blog_post_editor_page.rb` and start it off like so:
513
-
514
- ```ruby
515
- class BlogPostEditorPage < AppPage
516
-
517
- def initialize
518
- @form = NewBlogPostForm.new
519
- end
520
-
521
- # ...
522
-
523
- end
524
- ```
525
-
526
- Next, we'll implement `page_template` and we'll use `@form` to create HTML for the form's inputs, including client-side constraints and, as we'll see later, pre-existing values from a previous submission.
527
-
528
- This will require four parts of Brut's API and use one optional one:
529
-
530
- * `brut_form`, a custom element (`<brut-form>`) that will progressively enhance the form to provide
531
- constraint violation visitor experience if JavaScript is enabled.
532
- * `FormTag`, a Phlex component provided by Brut that generates the correct `<form>` element, as well as
533
- CSRF protection.
534
- * `Inputs::` components, namely `Brut::FrontEnd::Components::Inputs::InputTag` and `Brut::FrontEnd::Components::Inputs::TextareaTag`, which generate `<input>` and `<textarea>`, respectively. These Phlex components (provided by Brut) will add the correct attributes for validation, and set the values if the form they are given has values set.
535
- * `Brut::FrontEnd::Components::ConstraintViolations`, a Phlex component provided by Brut that generates custom elements that, when JavaScript is enabled, allow for control over the visitor experience when there are constraint violations.
536
- * *(optional)* `t` provides access to localized strings, instead of hard-coding English.
537
-
538
- Create `page_template` to look like so:
539
-
540
- ```ruby
541
- def page_template
542
- h1 { t(:write_new_post) }
543
- brut_form do
544
- FormTag(for: @form) do
545
- label do
546
- Inputs::InputTag(form: @form,input_name: :title, autofocus: true)
547
- div { t([:form, :title]) }
548
- ConstraintViolations(form: @form, input_name: :title)
549
- end
550
- label do
551
- Inputs::TextareaTag(form: @form,input_name: :content, rows: 10)
552
- div { t([:form, :content] ) }
553
- ConstraintViolations(form: @form, input_name: :content)
554
- end
555
-
556
- button { t([:form, :post]) }
557
- end
558
- end
559
- end
560
- ```
561
-
562
- > [!TIP]
563
- > You'll notice that we mentioned classes like `Brut::FrontEnd::Components::Inputs::InputTag`, but the
564
- > code above is only using `Input::InputTag`. This is due to [*Phlex
565
- > Kits*](https://www.phlex.fun/components/kits.html), which allow you to use relative class names
566
- > in certain circumstances.
567
- >
568
- > Brut makes use of this so there is a clear and organized name for a component, but you almost never
569
- > have to type or read the entire thing.
570
-
571
- Make sure your server is running, then reload the blog post editor page. You should see an error like so:
572
-
573
- > `Translation missing. Options considered were: - en.pages.BlogPostEditorPage.write_new_post - en.write_new_post`
574
-
575
- Let's add those keys.
576
-
577
- ### Adding Translation Keys
578
-
579
- In Brut, translations aren't stored in YAML 🥳🎉, but in a Ruby hash. Brut provides standard translations in `app/config/i18n/en/1_defaults.rb`, but your app will set its own in `app/config/i18n/en/2_app.rb`:
580
-
581
- ```ruby
582
- # All app-specific translations, or overrides of Brut's defaults, go here.
583
- {
584
- # en: must be the first entry, thus indicating this is the EN translations
585
- en: {
586
- cv: {
587
- cs: {
588
- },
589
- ss: {
590
- email_taken: "This email has been taken",
591
- },
592
- },
593
- pages: { # Page-specific messages - this key is relied-upon by Brut to exist
594
- HomePage: {
595
- title: "Welcome!",
596
- },
597
- BlogPostEditorPage: {
598
- title: "BlogPostEditorPage"
599
- },
600
- },
601
- # ... more code
602
- ```
603
-
604
- When you use `t` on a page in Brut, it looks for `pages.«page name».«key»`, so Brut needs from our form:
605
-
606
- * `pages.BlogPostEditorPage.write_new_post`
607
- * `pages.BlogPostEditorPage.form.title`
608
- * `pages.BlogPostEditorPage.form.content`
609
- * `pages.BlogPostEditorPage.form.post`
610
-
611
- Give them values like so:
612
-
613
- ```ruby
614
- # All app-specific translations, or overrides of Brut's defaults, go here.
615
- {
616
- # en: must be the first entry, thus indicating this is the EN translations
617
- en: {
618
- cv: {
619
- cs: {
620
- },
621
- ss: {
622
- email_taken: "This email has been taken",
623
- },
624
- },
625
- pages: { # Page-specific messages - this key is relied-upon by Brut to exist
626
- HomePage: {
627
- title: "Welcome!",
628
- },
629
- BlogPostEditorPage: {
630
- title: "BlogPostEditorPage",
631
- write_new_post: "Write a new post!",
632
- form: {
633
- title: "Title",
634
- content: "Post Content",
635
- post: "Post It!",
636
- }
637
- },
638
- },
639
- # ... more code
640
- ```
641
-
642
- Now, when you reload the page, it should work:
643
-
644
- ![screenshot of the form working, but unstyled](/images/tutorial/basic-form.png)
645
-
646
- Without filling anything in, click the submit button. The form should show you some error messages that are unstyled:
647
-
648
- ![screenshot of the form working, but unstyled](/images/tutorial/basic-form-with-violations.png)
649
-
650
- Let's style them and learn about how the `<brut-cv>` tags created by `ConstraintViolations` work.
651
-
652
- ### Styling Constraint Violations
653
-
654
- If you view source, you should see HTML like so:
655
-
656
- ```html
657
- <brut-cv-messages input-name='title'>
658
- </brut-cv-messages>
659
- ```
660
-
661
- If you click submit and view source, you'll see something like this:
662
-
663
- ```html
664
- <brut-cv-messages input-name='title'>
665
- <brut-cv>This field is required</brut-cv>
666
- </brut-cv-messages>
667
- ```
668
-
669
- This was inserted by `<brut-form>` whenever an element of the form is invalid. This could happen before the visitor clicks "submit", however. To allow you to style these elements only when a submit has been attempted, `<brut-form>` will set the attribute `submitted-invalid` on itself when this happens.
670
-
671
- Open `app/src/front_end/css/index.css` in your editor. We want `<brut-cv>` messages to be red, bold, and in the body font size. We also want them hidden by default.
672
-
673
- ```css
674
- brut-cv {
675
- display: none;
676
- color: #A60053;
677
- font-weight: bold;
678
- font-size: 1rem;
679
- }
680
- ```
681
-
682
- When `submitted-invalid` is set on `brut-form`, *then* we show them. We *also* want to show them if they were generated from the server, which `ConstraintViolations` will do:
683
-
684
- ```css
685
- brut-form[submitted-invalid] brut-cv,
686
- brut-cv[server-side] {
687
- display: block;
688
- }
689
- ```
690
-
691
- Let's also do some styling for the form and its elements. Add this below the CSS you just wrote:
692
-
693
- ```css
694
- .BlogPostEditorPage {
695
- brut-form {
696
- display: block;
697
- padding: 1rem;
698
- border: solid thin gray;
699
- border-radius: 0.25rem;
700
- background-color: #eeeeee;
701
-
702
- form {
703
- display: flex;
704
- flex-direction: column;
705
- gap: 1rem;
706
- align-items: start;
707
- }
708
-
709
- input, textarea {
710
- width: 100%;
711
- padding: 0.5rem;
712
- font-size: 130%;
713
- }
714
- label {
715
- width: 100%;
716
- font-size: 120%;
717
- display: block;
718
- div {
719
- font-weight: bold;
720
- font-style: italic;
721
- }
722
- }
723
- button {
724
- padding-left: 2rem;
725
- padding-right: 2rem;
726
- padding-top: 1rem;
727
- padding-bottom: 1rem;
728
- background-color: #E5FFE5;
729
- border: solid thin #006300;
730
- color: #006300;
731
- border-radius: 1rem;
732
- font-size: 150%;
733
- align-self: end;
734
- cursor: pointer;
735
- &:hover {
736
- background-color: #ACFFAC;
737
- }
738
- }
739
- }
740
- }
741
- ```
742
-
743
- Two notes about this CSS:
744
-
745
- * It's using nesting, which is part of
746
- [Baseline](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility)
747
- * We've nested all the CSS inside the `.BlogPostEditorPage` class. The default layout Brut provides
748
- includes this:
749
-
750
- ```ruby
751
- body(class: @page_name) do
752
- yield
753
- end
754
- ```
755
-
756
- This means all pages have their page name set on the `<body>` tag, allowing nested CSS, if you like.
757
-
758
- *Now*, if you submit the form without providing any values, you'll see a decent-looking experience:
759
-
760
- ![screenshot of the styled form with constraint violations](/images/tutorial/styled-form-with-violations.png)
761
-
762
- If you fill out the fields correctly, you should see an error that you need to implement your handler. Let's do that next.
763
-
764
- ## Handling Form Submissions
765
-
766
- When you ran `bin/scaffold form` earlier, it created `NewBlogPostHandler`. It's located in `app/src/front_end/handlers/new_blog_post_handler.rb`, which should look like so:
767
-
768
- ```ruby
769
- class NewBlogPostHandler < AppHandler
770
- def initialize(form:)
771
- @form = form
772
- end
773
-
774
- def handle
775
- raise "You need to implement your handler"
776
- end
777
- end
778
- ```
779
-
780
- The `handle` method is expected to return a value that tells Brut how to respond to a form submission. In our case, we'll either want it to re-generate `BlogPostEditorPage`'s HTML with error messages and the visitor-supplied form fields pre-filled in, or save the blog post and redirect back to `HomePage`.
781
-
782
- To do that, we'll either return an instance of `BlogPostEditorPage`, or return a `URI` to `HomePage` (which we can do with the helper method `redirect_to`).
783
-
784
- Before `handle` is called, `NewBlogPostHandler` will be initialized and give an instance of `NewBlogPostForm` containing whatever data was submitted by the browser. `handle` can then use `@form` to determine what to do.
785
-
786
- First, we'll re-check client-side validations by calling `.valid?`. If that's true, we can perform server-side validations, calling `server_side_constraint_violation` for any violations we find. Then, we'll check `.valid?` again and return a `BlogPostEditorPage` or redirect.
787
-
788
- ```ruby
789
- class NewBlogPostHandler < AppHandler
790
- def initialize(form:)
791
- @form = form
792
- end
793
-
794
- def handle
795
- if @form.valid?
796
- if @form.content.split(/\s/).length < 5
797
- @form.server_side_constraint_violation(
798
- input_name: :content,
799
- key: :not_enough_words,
800
- context: { num_words: 5 }
801
- )
802
- end
803
- end
804
- if @form.valid?
805
- # TODO: Actually save the post
806
- redirect_to(HomePage)
807
- else
808
- BlogPostEditorPage.new(form: @form)
809
- end
810
- end
811
- end
812
- ```
813
-
814
- Of course, `BlogPostEditorPage` does not accept the form as a parameter. We'll need to change that. Since we are using the `@form` instance to help generate HTML, if we pass the instance from our handler to the `BlogPostEditorPage`, when *that* instance generates HTML, it will have errors indicated and show the visitor's provided values instead of defaults.
815
-
816
- Of course, we still need to create a blank form when the page is accessed for the first time, so we'll make `form:` default to `nil` and create it if we aren't given a value:
817
-
818
- ```ruby{2,3}
819
- class BlogPostEditorPage < AppPage
820
-
821
- def initialize(form: nil)
822
- @form = form || NewBlogPostForm.new
823
- end
824
- ```
825
-
826
- With this in place, create a new blog post but with only four words in the content. This will pass client-side checks, but fail server-side checks. When you submit, you'll see an error related to `cv.ss.not_enough_words`, which is the key Brut is trying to use to find the actual error message.
827
-
828
- > `Translation missing. Options considered were: -
829
- > en.components.Brut::FrontEnd::Components::ConstraintViolations.cv.ss.not_enough_words -
830
- > en.cv.ss.not_enough_words`
831
-
832
- Add it to `app/config/i18n/en/2_app.rb`, under `en`, `cv` (for constraint violations), `ss` (for server-side):
833
-
834
- ```ruby {10}
835
- # All app-specific translations, or overrides of Brut's defaults, go here.
836
- {
837
- # en: must be the first entry, thus indicating this is the EN translations
838
- en: {
839
- cv: {
840
- cs: {
841
- },
842
- ss: {
843
- email_taken: "This email has been taken",
844
- not_enough_words: "%{field} does not have enough words. Must have %{num_words}",
845
- },
846
- },
847
- ```
848
-
849
- *Now*, try again, and you'll see this message, rendered exactly like client-side errors:
850
-
851
- ![screenshot of the styled form with server-generated constraint violations](/images/tutorial/styled-form-with-server-side-violations.png)
852
-
853
- Now that we have the user experience in place, let's actually store the blog post in the database.
854
-
855
- ## Using a Database
856
-
857
- Brut uses Postgres, and includes and configures the [Sequel](https://sequel.jeremyevans.net/) library to access your data. Sequel has some similarity to Rails' Active Record, but it's not quite the same.
858
-
859
- The main way to access data is to create a *database model* class (which is similar to an Active Record). Sequel also provides a way to manage your database schema via *migrations*.
860
-
861
- The steps to take when creating a new table you want to access are:
862
-
863
- 1. Create a migration that creates the schema for the new table.
864
- 2. Create the database model class itself.
865
- 3. Create a FactoryBot factory that can create sample instances of rows in the table, useful for testing and development
866
- 4. Modify seed data to create sample data for development.
867
-
868
- Most of this can be done via `bin/scaffold db_model`.
869
-
870
- ### Creating a New Database Table
871
-
872
- Stop your server and run `bin/scaffold` like so:
873
-
874
- ```bash
875
- bin/scaffold db_model blog_post
876
- ```
877
-
878
- ```
879
- # OUTPUT
880
- [ bin/scaffold ] Executing ["bin/db new_migration create_blog_post"]
881
- [ bin/db ] Migration created:
882
- app/src/back_end/data_models/migrations/20250811213758_create_blog_post.rb
883
- [ bin/scaffold ] ["bin/db new_migration create_blog_post"] succeeded
884
- [ bin/scaffold ] Creating DB::BlogPost in app/src/back_end/data_models/db/blog_post.rb
885
- [ bin/scaffold ] Creating spec for DB::BlogPost in specs/back_end/data_models/db/blog_post.spec.rb
886
- [ bin/scaffold ] Creating factory for DB::BlogPost in specs/factories/db/blog_post.factory.rb
887
- ```
888
-
889
- Your migration file name will be different than ours, since it has a timestamp embedded into it.
890
-
891
- Open that file in your editor and use `create_table`, as provided by Sequel, to describe the table.
892
-
893
- Brut enhances Sequel's `create_table` in the following ways:
894
-
895
- * A numeric primary key called `id` is created.
896
- * `comment:` is required.
897
- * `external_id` can be given, and will create a managed unique key called `external_id` that is safe to externalize and is not used in foreign key or referential integrity.
898
- * A timestamped field, `created_at` is created and will be set when new rows are created from your app.
899
-
900
- Inside `create_table`, we'll call `column` to define columns. Brut defaults all columns to `NOT NULL`, so you don't need to specify `null: false`.
901
-
902
- All of this goes inside a block given to the method `up`, like so:
903
-
904
- ```ruby
905
- Sequel.migration do
906
- up do
907
- create_table :blog_posts,
908
- comment: "All the posts fit to post",
909
- external_id: true do
910
- column :title, :text
911
- column :content, :text
912
- end
913
- end
914
- end
915
- ```
916
-
917
- If you've used migrations before, you may know that `down` can be used to specify a way to undo the migration, or that a method like `change` can be used to automatically do both. Brut recommends only using forward migrations inside `up`. If you need to undo and redo your changes, you can use `bin/db rebuild` to rebuild your database from scratch.
918
-
919
- Save this file, then apply this migration to your development database:
920
-
921
- ```bash
922
- bin/db migrate
923
- ```
924
-
925
- ```
926
- # OUTPUT
927
- [ bin/db ] Migrations applied
928
- ```
929
-
930
- Now, apply it to your test database:
931
-
932
- ```bash
933
- bin/db migrate -e test
934
- ```
935
-
936
- ```
937
- # OUTPUT
938
- [ bin/db ] Migrations applied
939
- ```
940
-
941
- You can examine the table that was created by running `bin/dbconsole`:
942
-
943
- ```bash
944
- bin/dbconsole
945
- ```
946
-
947
- ```
948
- # OUTPUT
949
- psql (16.9 (Debian 16.9-1.pgdg120+1), server 16.4 (Debian 16.4-1.pgdg120+2))
950
- Type "help" for help.
951
-
952
- blog_development=#
953
- ```
954
-
955
- This will give you a new prompt where you can type commands to `psql`, the Postgres command-line client. Try describing the table:
956
-
957
- ```bash
958
- \d blog_posts
959
- ```
960
-
961
- ```
962
- Table "public.blog_posts"
963
- Column | Type | Collation | Nullable | Default
964
- -------------+--------------------------+-----------+----------+----------------------------------
965
- id | integer | | not null | generated by default as identity
966
- title | text | | not null |
967
- content | text | | not null |
968
- created_at | timestamp with time zone | | not null |
969
- external_id | citext | | not null |
970
- Indexes:
971
- "blog_posts_pkey" PRIMARY KEY, btree (id)
972
- "blog_posts_external_id_key" UNIQUE CONSTRAINT, btree (external_id)
973
- ```
974
-
975
- `bin/scaffold` created the database model for you in `app/src/back_end/data_models/db/blog_post.rb`:
976
-
977
- ```ruby
978
- class DB::BlogPost < AppDataModel
979
- has_external_id :bl
980
- end
981
- ```
982
-
983
- In Brut, database models are in the `DB::` namespace, so you do not conflate them with a *domain* model.
984
-
985
- > [!TIP]
986
- > Note `has_external_id`. This tells Brut and Sequel that the underlying table is expected
987
- > to have the field `external_id` and that it is expected to be unique. You can see this in
988
- > the output of `\d blog_posts`, where it says `UNIQUE CONSTRAINT, btree (external_id)`.
989
- >
990
- > `has_external_id` configures the database model to provide a value for this key when saving or
991
- > creating a row. To aid in understanding the values out of context, external ids are prefixed
992
- > with two values: one is an app-wide value, in our case `bl`. The other is a model-specific
993
- > value, also `bl`. Thus, external ids might look like `blbl_9783245789345789345789345`.
994
- >
995
- Before we use this database model, we want to be able to create instances in tests, as well as for local development. The way to do that in Brut is to create a factory.
996
-
997
- ### Creating Test and Development Data
998
-
999
- Brut uses [FactoryBot](https://github.com/thoughtbot/factory_bot) to create sample instance of your data. Open up `specs/factories/db/blog_post.factory.rb` in your editor.
1000
-
1001
- If you are new to FactoryBot, it is a lightweight (ish) DSL that allows creating test data. You'll call methods based on the column names in order to specify values. Brut also includes [Faker](https://github.com/faker-ruby/faker), which creates randomized but realistic test data.
1002
-
1003
- For the title, we'll use Faker's "hipster ipsum". For the content, we want several paragraphs delineated by `\n\r`, so we'll create between 2 and 6 paragraphs and join them.
1004
-
1005
- Make `specs/factories/db/blog_post.factory.rb` look like so:
1006
-
1007
- ```ruby
1008
- FactoryBot.define do
1009
- factory :blog_post, class: "DB::BlogPost" do
1010
- title { Faker::Hipster.sentence }
1011
- content {
1012
- (rand(4) + 2).times.map {
1013
- Faker::Hipster.paragraph_by_chars(characters: 200)
1014
- }.join("\n\r")
1015
- }
1016
- end
1017
- end
1018
- ```
1019
-
1020
- Brut includes a test to make sure your factories are valid and will work. It's in `specs/lint_factories.spec.rb`. Run it now to make sure this factory works:
1021
-
1022
- > [!TIP]
1023
- > If you stopped your entire dev environment (`dx/start`), when you restart it, you
1024
- > *must* re-run `bin/setup`, since the disk inside your dev environment is ephemeral.
1025
-
1026
- ```bash
1027
- bin/test run specs/lint_factories.spec.rb
1028
- ```
1029
-
1030
- ```
1031
- # OUTPUT
1032
- [ bin/test ] Executing ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" \"specs/lint_factories.spec.rb\""]
1033
- Run options: exclude {e2e: true}
1034
-
1035
- Randomized with seed 29315
1036
-
1037
- factories
1038
- should be possible to create them all
1039
-
1040
- Finished in 0.59465 seconds (files took 0.7718 seconds to load)
1041
- 1 example, 0 failures
1042
-
1043
- Randomized with seed 29315
1044
-
1045
- [ bin/test ] ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" \"specs/lint_factories.spec.rb\""] succeeded
1046
- ```
1047
-
1048
- We can use this factory for seed data to provide realistic data for development. Edit `app/src/back_end/data_models/seed/seed_data.rb`, and make it look like so, which will create 10 blog posts:
1049
-
1050
- ```ruby
1051
- require "brut/back_end/seed_data"
1052
- class SeedData < Brut::BackEnd::SeedData
1053
- include FactoryBot::Syntax::Methods
1054
- def seed!
1055
- 10.times do |i|
1056
- create(:blog_post, created_at: Date.today - i)
1057
- end
1058
- end
1059
- end
1060
- ```
1061
-
1062
- `create` is a method provided by Factory Bot that is brought in via `FactoryBot::Syntax::Methods`.
1063
-
1064
- Now, load the seed data into the development database with `bin/db seed`:
1065
-
1066
- ```bash
1067
- bin/db seed
1068
- ```
1069
-
1070
- We can now show this data on the home page.
1071
-
1072
- ## Accessing the Database
1073
-
1074
- On `HomePage`, we put in a `<p>` as a placeholder for blog posts. Let's replace that with code to fetch and display the blog posts.
1075
-
1076
- In Brut, since HTML is generated by Phlex and thus by Ruby code, we can structure our HTML generation however we like, including by accessing the database directly. This may not scale as our app gets large, but for now, it's the simplest thing to do.
1077
-
1078
- Sequel's database models are similar to Rails' Active Record's in that we can call class methods to access data. In this case, `DB::BlogPost` has a method `order` that will fetch all records sorted by the field we give it in the order we decide. The sort field and order is specified via `Sequel.desc` for descending or `Sequel.asc` for ascending. We want posts in reverse-chronological order, so `Sequel.desc(:created_at)` will achieve this.
1079
-
1080
- We can call `.each` on the result and iterate over each blog post. For the content, we'll split by `\n\r` to create paragraphs.
1081
-
1082
- Here's what `HomePage`'s `page_template` should look like now:
1083
-
1084
- ```ruby
1085
- def page_template
1086
- header do
1087
- h1 { "My Amazing Blog" }
1088
- a(href: BlogPostEditorPage.routing) { "Write New Blog Post" }
1089
- end
1090
- main do
1091
- DB::BlogPost.order(Sequel.desc(:created_at)).each do |blog_post|
1092
- article do
1093
- h2 { blog_post.title }
1094
- blog_post.content.split(/\n\r/).each do |paragraph|
1095
- p { paragraph }
1096
- end
1097
- end
1098
- hr
1099
- end
1100
- end
1101
- end
1102
- ```
1103
-
1104
- Start your server if you stopped it before. Go to the home page, and you should see our fake blog posts:
1105
-
1106
- ![Home page showing two posts from the seed data, formatted properly](/images/tutorial/styled-home-page-with-posts.png)
1107
-
1108
- If we modify our handler to save new posts to the database, they'll show up here.
1109
-
1110
- ## Saving to the Database
1111
-
1112
- To create rows in the database, the class method `create` can be called on `DB::BlogPost`. Let's change the handler to use that. Open up `app/src/front_end/handlers/new_blog_post_handler.rb` and make `handle` look like so (the changed lines are highlighted):
1113
-
1114
- ```ruby {12-15}
1115
- def handle
1116
- if !@form.constraint_violations?
1117
- if @form.content.split(/\s/).length < 5
1118
- @form.server_side_constraint_violation(
1119
- input_name: :content,
1120
- key: :not_enough_words,
1121
- context: { num_words: 5 }
1122
- )
1123
- end
1124
- end
1125
- if @form.valid?
1126
- DB::BlogPost.create(
1127
- title: @form.title,
1128
- content: @form.content
1129
- )
1130
- redirect_to(HomePage)
1131
- else
1132
- NewBlogPostPage.new(form: @form)
1133
- end
1134
- end
1135
- ```
1136
-
1137
- The form object provides access to the values of any field we've declared via a method call.
1138
-
1139
- Now, create a new blog post, provide valid data, and submit it.
1140
-
1141
- ![Screenshot of the blog post editor, with a new post filled in](/images/tutorial/new-post-editor.png)
1142
-
1143
- Once you submit it, you should see the home page with your new post at the top:
1144
-
1145
- ![Screenshot of the home page, showing the new blog post](/images/tutorial/new-post-home-page.png)
1146
-
1147
- Our work isn't quite done. We need tests.
1148
-
1149
- ## Testing Brut Apps
1150
-
1151
- We'll create the following tests:
1152
-
1153
- * Check that the logic in the handler is sound
1154
- * Check that blog posts show up on the home page
1155
- * Check that the entire workflow of create a blog post and seeing it show up on the home page works in a
1156
- real web browser
1157
-
1158
- Let's test our handler first, as that is where the main logic is.
1159
-
1160
- ### Testing Handlers
1161
-
1162
- Our handler will need three tests:
1163
-
1164
- * If the form was submitted without client-side validations happening, we should not create a new blog
1165
- post and re-generate the blog post editor page, showing the errors.
1166
- * If client-side validations pass, but the blog post isn't five words or more, we should not create a
1167
- new blog post and re-generate the blog post editor page, showing the errors.
1168
- * If everything looks good, we save the new blog post and redirect to the home page.
1169
-
1170
- Brut apps are tested with RSpec, and Brut provides several convenience methods and matchers to make testing as painless as possible.
1171
-
1172
- When testing a handler, the public method is `handle!`, not `handle`, so we want to call that (Brut implements `handle!` to call `handle`).
1173
-
1174
- First, we'll test client-side validations. Open up `specs/front_end/handlers/new_blog_post_handler.spec.rb` and replace the code there with this:
1175
-
1176
- ```ruby
1177
- require "spec_helper"
1178
-
1179
- RSpec.describe NewBlogPostHandler do
1180
- describe "#handle!" do
1181
- context "client-side violations got to the server" do
1182
- it "re-generates the HTML for the BlogPostEditorPage" do
1183
- form = NewBlogPostForm.new(params: {})
1184
-
1185
- result = described_class.new(form:).handle!
1186
-
1187
- expect(result).to have_generated(BlogPostEditorPage)
1188
- expect(form).to have_constraint_violation(:title, key: :valueMissing)
1189
- expect(form).to have_constraint_violation(:content, key: :valueMissing)
1190
- end
1191
- end
1192
- end
1193
- end
1194
- ```
1195
-
1196
- `have_generated` asserts that the value returned from `handle!` is an instance of the page given, `BlogPostEditorPage` in this case. You could just as easily write `expect(result.kind_of?(BlogPostEditorPage)).to eq(true)`, but `have_generated` expressed the intent of what's happening.
1197
-
1198
- `have_constraint_violation` checks that the form's `constraint_violations` contains one for the given field and the given key. In this case, we check both `:title` and `:content` for `:valueMissing`.
1199
-
1200
- > [!TIP]
1201
- > Client-side constraint violations use the same keys on the server as they do in the browser.
1202
- > In the case of a required field, the browser's
1203
- > [`ValidityState`](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) would set
1204
- > `valueMissing` to true. So, that's what Brut would do on the server-side, when reflecting
1205
- > client-side constraints.
1206
-
1207
- Next, we'll check that the server-side constraint violations are being checked. Add this just below the `context` you just added:
1208
-
1209
- ```ruby
1210
- context "post is not enough words" do
1211
- it "re-generates the HTML for the BlogPostEditorPage, with server-side errors indicated" do
1212
- form = NewBlogPostForm.new(params: {
1213
- title: "What a great post",
1214
- content: "Not enough words",
1215
- })
1216
-
1217
- confidence_check { expect(form.constraint_violations?).to eq(false) }
1218
-
1219
- result = described_class.new(form:).handle!
1220
-
1221
- expect(result).to have_generated(BlogPostEditorPage)
1222
- expect(form).to have_constraint_violation(:content, key: :not_enough_words)
1223
- end
1224
- end
1225
- ```
1226
-
1227
- This test introduces two new concepts:
1228
-
1229
- * To initialize a form with data, you must pass a `Hash` to the keyword argument `params:`. If the
1230
- `Hash` contains parameters that the form doesn't recognize, they are ignored and discarded.
1231
- * Although we aren't expecting the form to have client-side constraint violations, if there are any, the
1232
- test would fail in a confusing way. To manage this, Brut includes the [confidence-check](https://github.com/sustainable-rails/confidence-check) gem that allows you to make assertions that are not part of the test. If the confidence check fails, the test output will be obvious that the test could not run due to an assumption being violated.
1233
-
1234
-
1235
- Lastly, we'll check that everything worked when there aren't any constraint violations. Add this below the `context` you just added:
1236
-
1237
- ```ruby
1238
- context "post is good!" do
1239
- it "saves the post and redirects to the HomePage" do
1240
- form = NewBlogPostForm.new(params: {
1241
- title: "What a great post",
1242
- content: "This post is the best post that has been written in the history of posts",
1243
- })
1244
-
1245
- confidence_check { expect(form.constraint_violations?).to eq(false) }
1246
-
1247
- result = nil
1248
- expect {
1249
- result = described_class.new(form:).handle!
1250
- }.to change { DB::BlogPost.count }.by(1)
1251
-
1252
- expect(result).to have_redirected_to(HomePage)
1253
-
1254
- blog_post = DB::BlogPost.last
1255
- expect(blog_post.title).to eq("What a great post")
1256
- expect(blog_post.content).to eq("This post is the best post that has been written in the history of posts")
1257
-
1258
- end
1259
- end
1260
- ```
1261
-
1262
- This is using RSpec's `expect { ... }.to change { ... }.by(N)` to make sure that our handler created a row in the database. We then use the matcher `have_redirected_to` to assert that `result` is a URI to `HomePage`. We also check that the blog post we created in the database is correct.
1263
-
1264
- Let's run the test with `bin/test run`
1265
-
1266
- > [!TIP]
1267
- > If you stopped your entire dev environment (`dx/start`), when you restart it, you
1268
- > *must* re-run `bin/setup`, since the disk inside your dev environment is ephemeral.
1269
-
1270
- ```bash
1271
- bin/test run specs/front_end/handlers/new_blog_post_handler.spec.rb
1272
- ```
1273
-
1274
- ```
1275
- # OUTPUT
1276
- [ bin/test ] Executing ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" \"specs/front_end/handlers/new_blog_post_handler.spec.rb\""]
1277
- Run options: exclude {e2e: true}
1278
-
1279
- Randomized with seed 61034
1280
-
1281
- NewBlogPostHandler
1282
- post is not enough words
1283
- re-generates the HTML for the BlogPostEditorPage, with server-side errors indicated
1284
- post is good!
1285
- saves the post and redirects to the HomePage
1286
- #handle!
1287
- client-side violations got to the server
1288
- re-generates the HTML for the BlogPostEditorPage
1289
-
1290
- Finished in 0.0138 seconds (files took 0.73976 seconds to load)
1291
- 3 examples, 0 failures
1292
-
1293
- Randomized with seed 61034
1294
-
1295
- [ bin/test ] ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" \"specs/front_end/handlers/new_blog_post_handler.spec.rb\""] succeeded
1296
- ```
1297
-
1298
- It passes!
1299
-
1300
- Next, let's test `HomePage`.
1301
-
1302
- ### Testing Pages
1303
-
1304
- Unlike our handler, which accepts arguments and returns a result, pages generate HTML. We are better off testing pages by asking them to generate HTML and then examining the HTML directly.
1305
-
1306
- Brut provides the method `generate_and_parse` to generate a page's HTML, then use [Nokogiri](https://nokogiri.org/) to parse it. We can use CSS selectors on the result to assert things about the HTML.
1307
-
1308
- `mkbrut` created `specs/front_end/pages/home_page.spec.rb`, so let's open that up on your editor.
1309
-
1310
- The way we'll write this test is to generate four random blog posts using our factory, request the page, then assert that each blog post is on the page.
1311
-
1312
- Rather than assert that each blog post's text is just somewhere on the page, we'll make use of the `external_id` concept. We'll use it as the `id` attribute of the `<article>`.
1313
-
1314
- Brut intends for you to use Nokogiri's API to access information about the parsed document, however it provides a few convenience methods. In the test below, you'll see `e!`, which is added to Nokogiri nodes.
1315
-
1316
- `e!` asserts that exactly one node matches the given CSS selector and returns that node. This makes it more expedient to access something that should be there, but fail with a useful error message when it's not.
1317
-
1318
- Here's the test:
1319
-
1320
- ```ruby
1321
- require "spec_helper"
1322
-
1323
- RSpec.describe HomePage do
1324
- it "should show the blog posts" do
1325
-
1326
- blog_posts = 4.times.map { create(:blog_post) }
1327
-
1328
- result = generate_and_parse(described_class.new)
1329
-
1330
- expect(result.e!("h1").text).to eq("My Amazing Blog")
1331
-
1332
- blog_posts.each do |blog_post|
1333
- post_article = result.e!("article##{blog_post.external_id}")
1334
- expect(post_article.e!("h2").text).to eq(blog_post.title)
1335
- blog_post.content.split(/\n\r/).each do |paragraph|
1336
- expect(post_article.text).to include(paragraph)
1337
- end
1338
- end
1339
- end
1340
- end
1341
- ```
1342
-
1343
- Let's run the test, which should fail:
1344
-
1345
- > [!TIP]
1346
- > If you stopped your entire dev environment (`dx/start`), when you restart it, you
1347
- > *must* re-run `bin/setup`, since the disk inside your dev environment is ephemeral.
1348
-
1349
- ```bash
1350
- bin/test run specs/front_end/pages/home_page.spec.rb
1351
- ```
1352
-
1353
- ```
1354
- # OUTPUT
1355
- [ bin/test ] Executing ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" \"specs/front_end/pages/home_page.spec.rb\""]
1356
- Run options: exclude {e2e: true}
1357
-
1358
- Randomized with seed 44491
1359
-
1360
- HomePage
1361
- should show the blog posts (FAILED - 1)
1362
-
1363
- Failures:
1364
-
1365
- 1) HomePage should show the blog posts
1366
- Failure/Error: post_article = result.e!("article##{blog_post.external_id}")
1367
-
1368
- article#blbl_6f04feaefb9520d86b19c3ac4ad22c4f matched 0 elements, not exactly 1:
1369
-
1370
- «HUGE HTML DOCUMENT»
1371
-
1372
- # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/enhanced_node.rb:32:in 'Brut::SpecSupport::EnhancedNode#e!'
1373
- # ./specs/front_end/pages/home_page.spec.rb:13:in 'block (3 levels) in <top (required)>'
1374
- # ./specs/front_end/pages/home_page.spec.rb:12:in 'Array#each'
1375
- # ./specs/front_end/pages/home_page.spec.rb:12:in 'block (2 levels) in <top (required)>'
1376
- # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/rspec_setup.rb:158:in 'block (2 levels) in Brut::SpecSupport::RSpecSetup#setup!'
1377
- # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/database/transactions.rb:264:in 'Sequel::Database#_transaction'
1378
- # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/database/transactions.rb:239:in 'block in Sequel::Database#transaction'
1379
- # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/connection_pool/timed_queue.rb:90:in 'Sequel::TimedQueueConnectionPool#hold'
1380
- # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/database/connecting.rb:283:in 'Sequel::Database#synchronize'
1381
- # ./local-gems/gem-home/gems/sequel-5.95.1/lib/sequel/database/transactions.rb:197:in 'Sequel::Database#transaction'
1382
- # ./local-gems/gem-home/gems/brut-0.5.0/lib/brut/spec_support/rspec_setup.rb:156:in 'block in Brut::SpecSupport::RSpecSetup#setup!'
1383
-
1384
- Finished in 0.54876 seconds (files took 0.73025 seconds to load)
1385
- 1 example, 1 failure
1386
-
1387
- Failed examples:
1388
-
1389
- bin/test run ./specs/front_end/pages/home_page.spec.rb:4 # HomePage should show the blog posts
1390
-
1391
- Randomized with seed 44491
1392
-
1393
- [ bin/test ] error: ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" \"specs/front_end/pages/home_page.spec.rb\""] failed - exited 1
1394
- ```
1395
-
1396
- Brut obviously errs on the side of being verbose. But, you can see that the problem is that it cannot find an `<article>` with the `id=` of `blbl_6f04feaefb9520d86b19c3ac4ad22c4f`, the `external_id` of the first blog post.
1397
-
1398
- To make it pass, we'll need to add `id:` to each `<article>`. Make this one-line change in `HomePage`:
1399
-
1400
- ```diff
1401
- - article do
1402
- + article(id: blog_post.external_id) do
1403
- ```
1404
-
1405
- > [!TIP]
1406
- > This shows a useful feature of the `external_id`: Because it's not only unique
1407
- > to the database table, but also across *all* database tables, it makes a pretty
1408
- > good `ID` inside an HTML page, since it's highly unlikely any other part of the page
1409
- > would use that value for the `id=` of an element.
1410
-
1411
- Now, the test should pass:
1412
-
1413
- > [!TIP]
1414
- > If you stopped your entire dev environment (`dx/start`), when you restart it, you
1415
- > *must* re-run `bin/setup`, since the disk inside your dev environment is ephemeral.
1416
-
1417
- ```bash
1418
- bin/test run specs/front_end/pages/home_page.spec.rb
1419
- ```
1420
-
1421
- ```
1422
- # OUTPUT
1423
- [ bin/test ] Executing ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" \"specs/front_end/pages/home_page.spec.rb\""]
1424
- Run options: exclude {e2e: true}
1425
-
1426
- Randomized with seed 56951
1427
-
1428
- HomePage
1429
- should show the blog posts
1430
-
1431
- Finished in 0.53858 seconds (files took 0.69257 seconds to load)
1432
- 1 example, 0 failures
1433
-
1434
- Randomized with seed 56951
1435
-
1436
- [ bin/test ] ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" \"specs/front_end/pages/home_page.spec.rb\""] succeeded
1437
- ```
1438
-
1439
- For `BlogPostEditorPage`, there really isn't anything to test - it's static HTML at this point. Even still, it's good to record a decision about testing code or not, so it's clear we didn't just forget. Brut provides the method `implementation_is_covered_by_other_tests` to do just that. It accepts a string where we can describe where the coverage for this class is.
1440
-
1441
- In our case, it's going to be covered by an end-to-end test we'll write next.
1442
-
1443
- Open up `specs/front_end/pages/blog_post_editor_page.spec.rb` and make it look like so:
1444
-
1445
- ```ruby
1446
- require "spec_helper"
1447
-
1448
- RSpec.describe BlogPostEditorPage do
1449
- implementation_is_covered_by_other_tests "end-to-end test"
1450
- end
1451
- ```
1452
-
1453
- Now, all unit tests should pass, which we can check via `bin/test run`:
1454
-
1455
- > [!TIP]
1456
- > If you stopped your entire dev environment (`dx/start`), when you restart it, you
1457
- > *must* re-run `bin/setup`, since the disk inside your dev environment is ephemeral.
1458
-
1459
- ```bash
1460
- bin/test run
1461
- ```
1462
-
1463
- ```
1464
- # OUTPUT
1465
- [ bin/test ] Running all tests
1466
- [ bin/test ] Executing ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs/"]
1467
- Run options: exclude {e2e: true}
1468
-
1469
- Randomized with seed 63173
1470
- ...........
1471
-
1472
- Finished in 0.53248 seconds (files took 0.7012 seconds to load)
1473
- 11 examples, 0 failures
1474
-
1475
- Randomized with seed 63173
1476
-
1477
- [ bin/test ] ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag ~e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs/"] succeeded
1478
- ```
1479
-
1480
- As our last test, we'll write an end-to-end that uses a browser.
1481
-
1482
- ### Writing Browser End-to-End Tests
1483
-
1484
- Browser tests are expensive and slow, but it's good to test entire workflows that catch issues that a unit test might not. In this case, we want to go through the steps of writing a blog post:
1485
-
1486
- 1. Go to the post editor page and make sure client-side validations show us errors.
1487
- 2. Submit a post that's too short and make sure server-side errors show up.
1488
- 3. Submit a valid post and check that it makes it back to the home page.
1489
-
1490
- Brut uses [Playwright](https://playwright.dev/) to author end to end tests. Playwright is written in JavaScript, but there is a [Ruby wrapper library](https://playwright-ruby-client.vercel.app/) that alleviates us from having to worry about async/await style coding.
1491
-
1492
- Ideally, we'd use the same API here as we do in our page tests. Or, equally ideally, we'd be able to use the API of the web platform. Playwright went a third way. Such is life.
1493
-
1494
- The way this test will work is:
1495
-
1496
- 1. Use `HomePage.routing` to kick everything off
1497
- 2. Find a link to `BlogPostEditorPage.routing` on the page
1498
- 3. Use Playwright's `page.locator` to find elements on the page to interact with (which will naturally wait for the page to load before doing so).
1499
- 4. We'll use `fill` to fill in data for the form fields and `click` to submit the form by clicking the submit button.
1500
- 5. The matcher `have_text` will be used assert that text appears inside an element.
1501
-
1502
- Brut provides the matcher `be_page_for` to assert that we are viewing the page we think we are. Nothing is more frustrating than watching assertions fail because your test ended up on the wrong page.
1503
-
1504
- Open up `specs/e2e/home_page.spec.rb` and replace it with this:
1505
-
1506
- ```ruby
1507
- require "spec_helper"
1508
-
1509
- RSpec.describe "Posting blog posts" do
1510
- it "allows posting a post" do
1511
-
1512
- # 1. Go to the blog post editor page from the home page
1513
- page.goto(HomePage.routing)
1514
- new_post_link = page.locator("a[href='#{BlogPostEditorPage.routing}']")
1515
- new_post_link.click
1516
-
1517
- expect(page).to be_page_for(BlogPostEditorPage)
1518
-
1519
- # 2. Provide data that violates client-side constraints and check for error messages
1520
- title_field = page.locator("brut-form input[name='title']")
1521
- content_field = page.locator("brut-form textarea[name='content']")
1522
-
1523
- title_field.fill("XX")
1524
-
1525
- submit_button = page.locator("brut-form button")
1526
- submit_button.click
1527
-
1528
- expect(page).to be_page_for(BlogPostEditorPage)
1529
-
1530
- title_error_message = page.locator("brut-cv-messages[input-name='title'] brut-cv")
1531
- content_error_message = page.locator("brut-cv-messages[input-name='content'] brut-cv")
1532
-
1533
- expect(title_error_message).to have_text("This field is too short")
1534
- expect(content_error_message).to have_text("This field is required")
1535
-
1536
- # 3. Provide data that passes client-side constraints but violates server-side ones
1537
- title_field.fill("New blog post")
1538
- content_field.fill("Too short")
1539
-
1540
- submit_button.click
1541
-
1542
- expect(page).to be_page_for(BlogPostEditorPage)
1543
-
1544
- expect(content_error_message).to have_text("This field does not have enough words")
1545
-
1546
- # 4. Provide a valid blog post
1547
- content_field.fill("This is a longer post, so we should be OK")
1548
-
1549
- submit_button.click
1550
- expect(page).to be_page_for(HomePage)
1551
-
1552
- new_post = DB::BlogPost.order(Sequel.desc(:created_at)).first
1553
-
1554
- article = page.locator("article##{new_post.external_id}")
1555
-
1556
- expect(article).to have_text("New blog post")
1557
- expect(article).to have_text("This is a longer post, so we should be OK")
1558
-
1559
- end
1560
- end
1561
- ```
1562
-
1563
- Run it now with `bin/test e2e`:
1564
-
1565
- > [!TIP]
1566
- > If you stopped your entire dev environment (`dx/start`), when you restart it, you
1567
- > *must* re-run `bin/setup`, since the disk inside your dev environment is ephemeral.
1568
-
1569
- ```bash
1570
- bin/test e2e
1571
- ```
1572
-
1573
- It should pass:
1574
-
1575
- ```
1576
- # OUTPUT
1577
- [ bin/test ] Rebuilding test database schema
1578
- [ bin/test ] Executing ["bin/db rebuild --env=test"]
1579
- [ bin/db ] Database exists. Dropping...
1580
- [ bin/db ] blog_test does not exit. Creating...
1581
- [ bin/db ] Migrations applied
1582
- [ bin/test ] ["bin/db rebuild --env=test"] succeeded
1583
- [ bin/test ] Running all tests
1584
- [ bin/test ] Executing ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs/"]
1585
- Run options: include {e2e: true}
1586
-
1587
- Randomized with seed 27681
1588
- [ bin/test-server ] Building assets
1589
- «TONS OF OUTPUT»
1590
- [ bin/test-server ] Starting server
1591
- [ bin/run ] No pidfile-Starting up
1592
- [3352] Configuration:
1593
- «TONS OF OUTPUT»
1594
- [3352] Use Ctrl-C to stop
1595
- [3352] - Worker 0 (PID: 3361) booted in 0.0s, phase: 0
1596
- [3352] - Worker 1 (PID: 3364) booted in 0.0s, phase: 0
1597
- .[3352] === puma shutdown: 2025-08-11 22:18:16 +0000 ===
1598
- [3352] - Goodbye!
1599
- [3352] - Gracefully shutting down workers...
1600
-
1601
-
1602
- Finished in 3.45 seconds (files took 0.69401 seconds to load)
1603
- 1 example, 0 failures
1604
-
1605
- Randomized with seed 27681
1606
-
1607
- [ bin/test ] ["bin/rspec -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs -I /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/app/src -I lib/ --tag e2e -P \"**/*.spec.rb\" /Users/davec/Projects/ThirdTank/brutcasts/01-make-a-blog/blog/specs/"] succeeded
1608
- [ bin/test ] Re-Rebuilding test database schema
1609
- [ bin/test ] Executing ["bin/db rebuild --env=test"]
1610
- [ bin/db ] Database exists. Dropping...
1611
- [ bin/db ] blog_test does not exit. Creating...
1612
- [ bin/db ] Migrations applied
1613
- [ bin/test ] ["bin/db rebuild --env=test"] succeeded
1614
- ```
1615
-
1616
- With that test done, `bin/ci`, which we ran at the start, should run all tests, plus check for CVEs in our installed gems.
1617
-
1618
- ```bash
1619
- bin/ci
1620
- ```
1621
-
1622
- It should also pass:
1623
-
1624
- ```
1625
- # OUTPUT
1626
- «TONS OF OUTPUT»
1627
- [ bin/ci ] Analyzing Ruby gems for
1628
- [ bin/ci ] security vulnerabilities
1629
- Updating ruby-advisory-db ...
1630
- From https://github.com/rubysec/ruby-advisory-db
1631
- * branch master -> FETCH_HEAD
1632
- Already up to date.
1633
- Updated ruby-advisory-db
1634
- ruby-advisory-db:
1635
- advisories: 998 advisories
1636
- last updated: 2025-08-08 10:26:18 -0700
1637
- commit: 43149b540b701c9683e402fcd7fa4e5b6e5b60e9
1638
- No vulnerabilities found
1639
- [ bin/ci ] Checking to see that all classes have tests
1640
- [ bin/test ] All tests exists!
1641
- [ bin/ci ] Done
1642
- ```
1643
-
1644
- That's it!
1645
-
1646
- ## Areas for Self-Exploration
1647
-
1648
- Here are a few enhancement you can try to make:
1649
-
1650
- * Create a client-side constraint requiring the title to match a certain regexp.
1651
- * Add a server-side constraint requiring at least two paragraphs.
1652
- * Allow editing the blog post creation date
1653
- * Add an author field to allow entering the author's name
1654
- * Add pagination to the home page