brut 0.3.0 → 0.4.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 (374) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/Gemfile.lock +1 -1
  4. data/brut-js/CHANGELOG.md +5 -0
  5. data/brut-js/package-lock.json +2 -2
  6. data/brut-js/package.json +1 -1
  7. data/brut-js/specs/ConstraintViolationMessage.spec.js +1 -1
  8. data/brut-js/specs/ConstraintViolationMessages.spec.js +2 -2
  9. data/brut-js/specs/Form.spec.js +5 -5
  10. data/brut-js/src/ConstraintViolationMessage.js +3 -3
  11. data/brut-js/src/ConstraintViolationMessages.js +2 -2
  12. data/brutrb.com/.vitepress/config.mjs +1 -0
  13. data/brutrb.com/database-schema.md +22 -2
  14. data/brutrb.com/dev-environment.md +1 -0
  15. data/brutrb.com/form-constraints.md +2 -2
  16. data/brutrb.com/handlers.md +1 -1
  17. data/brutrb.com/i18n.md +2 -2
  18. data/brutrb.com/layouts.md +1 -1
  19. data/brutrb.com/recipes/migrations.md +210 -0
  20. data/docs/404.html +2 -2
  21. data/docs/adrs.html +4 -4
  22. data/docs/ai.html +4 -4
  23. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  24. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  25. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  26. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  27. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  28. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  29. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  30. data/docs/api/Brut/BackEnd.html +1 -1
  31. data/docs/api/Brut/CLI/App.html +1 -1
  32. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  33. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +2 -2
  34. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +6 -6
  35. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  36. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +6 -6
  37. data/docs/api/Brut/CLI/Apps/BuildAssets.html +2 -2
  38. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  39. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  40. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  41. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +6 -2
  42. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  43. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  44. data/docs/api/Brut/CLI/Apps/DB/Status.html +9 -9
  45. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  46. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  47. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  48. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  49. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  50. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  51. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +8 -12
  52. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  53. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +5 -5
  54. data/docs/api/Brut/CLI/Apps/Scaffold/DbModel.html +384 -0
  55. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  56. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +5 -5
  57. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  58. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  59. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +36 -36
  60. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  61. data/docs/api/Brut/CLI/Apps/Scaffold.html +2 -2
  62. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  63. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  64. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  65. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  66. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  67. data/docs/api/Brut/CLI/Apps.html +1 -1
  68. data/docs/api/Brut/CLI/Command.html +29 -31
  69. data/docs/api/Brut/CLI/Error.html +1 -1
  70. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  71. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  72. data/docs/api/Brut/CLI/Executor.html +1 -1
  73. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  74. data/docs/api/Brut/CLI/Options.html +1 -1
  75. data/docs/api/Brut/CLI/Output.html +1 -1
  76. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  77. data/docs/api/Brut/CLI.html +1 -1
  78. data/docs/api/Brut/FactoryBot.html +1 -1
  79. data/docs/api/Brut/Framework/App.html +1 -1
  80. data/docs/api/Brut/Framework/Config.html +16 -2
  81. data/docs/api/Brut/Framework/Container.html +1 -1
  82. data/docs/api/Brut/Framework/Error.html +1 -1
  83. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  84. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  85. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  86. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  87. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  88. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  89. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  90. data/docs/api/Brut/Framework/Errors.html +1 -1
  91. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  92. data/docs/api/Brut/Framework/MCP.html +1 -1
  93. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  94. data/docs/api/Brut/Framework.html +1 -1
  95. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  96. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  97. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  98. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +3 -3
  99. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  100. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +10 -14
  101. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  102. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  103. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  104. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  105. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +3 -3
  106. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  107. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  108. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  109. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  110. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  111. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  112. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  113. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  114. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  115. data/docs/api/Brut/FrontEnd/Form.html +92 -20
  116. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  117. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
  118. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
  119. data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
  120. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  121. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
  122. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +1 -1
  123. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  124. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +1 -1
  125. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  126. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  127. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  128. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  129. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  130. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  131. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  132. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  133. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  134. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +2 -2
  135. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  136. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  137. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  138. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  139. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  140. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  141. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  143. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  145. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  146. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  147. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  151. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  152. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  153. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  154. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  155. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  156. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  157. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  158. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  159. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  162. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  164. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  167. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  168. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  170. data/docs/api/Brut/FrontEnd.html +1 -1
  171. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  172. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  173. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  174. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  175. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  176. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  177. data/docs/api/Brut/I18n.html +1 -1
  178. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  179. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  180. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  181. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  182. data/docs/api/Brut/Instrumentation.html +1 -1
  183. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  184. data/docs/api/Brut/SinatraHelpers.html +1 -1
  185. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  186. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  187. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  188. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  189. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  190. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  191. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  192. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  193. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  194. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  195. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  196. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  197. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  198. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  199. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  200. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  201. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  202. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  203. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  204. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  205. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  206. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  207. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  208. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  209. data/docs/api/Brut/SpecSupport.html +1 -1
  210. data/docs/api/Brut.html +1 -1
  211. data/docs/api/Clock.html +1 -1
  212. data/docs/api/ModuleName.html +595 -0
  213. data/docs/api/RichString.html +1 -1
  214. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  215. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  216. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  217. data/docs/api/Sequel/Extensions.html +1 -1
  218. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  219. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  220. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  221. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  222. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  223. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  224. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  225. data/docs/api/Sequel/Plugins.html +1 -1
  226. data/docs/api/Sequel.html +1 -1
  227. data/docs/api/_index.html +24 -12
  228. data/docs/api/class_list.html +1 -1
  229. data/docs/api/file.README.html +1 -1
  230. data/docs/api/index.html +1 -1
  231. data/docs/api/method_list.html +478 -406
  232. data/docs/api/top-level-namespace.html +2 -2
  233. data/docs/assets/{app._UukLsdv.js → app.AkS4fHzI.js} +1 -1
  234. data/docs/assets/chunks/@localSearchIndexroot.C9FqcQxD.js +1 -0
  235. data/docs/assets/chunks/{VPLocalSearchBox.DhYUVvsa.js → VPLocalSearchBox.By6un1FD.js} +1 -1
  236. data/docs/assets/chunks/{theme.DHNq8t6D.js → theme.DwiUPdci.js} +2 -2
  237. data/docs/assets/{components.md.DP8maNoF.js → components.md.BfeWD9sX.js} +3 -3
  238. data/docs/assets/{configuration.md.BDXwuZHU.js → configuration.md.DoSBNc0H.js} +1 -1
  239. data/docs/assets/{database-schema.md.CSYk6E6v.js → database-schema.md.C5gXexJi.js} +10 -3
  240. data/docs/assets/{database-schema.md.CSYk6E6v.lean.js → database-schema.md.C5gXexJi.lean.js} +1 -1
  241. data/docs/assets/{dev-environment.md.Dy6EldaM.js → dev-environment.md.DRH2D2-O.js} +3 -3
  242. data/docs/assets/dev-environment.md.DRH2D2-O.lean.js +1 -0
  243. data/docs/assets/{form-constraints.md.x5tNpTTI.js → form-constraints.md.DK5adCgM.js} +2 -2
  244. data/docs/assets/{forms.md.BwBNYrrL.js → forms.md.DFveP5g_.js} +1 -1
  245. data/docs/assets/{getting-started.md.B4LlHfav.js → getting-started.md.DZWjCVC0.js} +2 -2
  246. data/docs/assets/{handlers.md.Chyri6KA.js → handlers.md.h84MMB1R.js} +1 -1
  247. data/docs/assets/{i18n.md.xQhiGo1G.js → i18n.md.BAm9t9JJ.js} +1 -1
  248. data/docs/assets/{layouts.md.CJGDFY-m.js → layouts.md.CVGl9xIO.js} +1 -1
  249. data/docs/assets/recipes_migrations.md.DPN3gQE3.js +97 -0
  250. data/docs/assets/recipes_migrations.md.DPN3gQE3.lean.js +1 -0
  251. data/docs/assets.html +4 -4
  252. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  253. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  254. data/docs/brut-js/api/Autosubmit.html +1 -1
  255. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  256. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  257. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  258. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  259. data/docs/brut-js/api/BufferedLogger.html +1 -1
  260. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  261. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  262. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  263. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  264. data/docs/brut-js/api/ConstraintViolationMessage.html +2 -2
  265. data/docs/brut-js/api/ConstraintViolationMessage.js.html +4 -4
  266. data/docs/brut-js/api/ConstraintViolationMessages.html +3 -3
  267. data/docs/brut-js/api/ConstraintViolationMessages.js.html +3 -3
  268. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  269. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  270. data/docs/brut-js/api/Form.html +1 -1
  271. data/docs/brut-js/api/Form.js.html +1 -1
  272. data/docs/brut-js/api/I18nTranslation.html +1 -1
  273. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  274. data/docs/brut-js/api/LocaleDetection.html +1 -1
  275. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  276. data/docs/brut-js/api/Logger.html +1 -1
  277. data/docs/brut-js/api/Logger.js.html +1 -1
  278. data/docs/brut-js/api/Message.html +1 -1
  279. data/docs/brut-js/api/Message.js.html +1 -1
  280. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  281. data/docs/brut-js/api/RichString.html +1 -1
  282. data/docs/brut-js/api/RichString.js.html +1 -1
  283. data/docs/brut-js/api/Tabs.html +1 -1
  284. data/docs/brut-js/api/Tabs.js.html +1 -1
  285. data/docs/brut-js/api/Tracing.html +1 -1
  286. data/docs/brut-js/api/Tracing.js.html +1 -1
  287. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  288. data/docs/brut-js/api/external-Performance.html +1 -1
  289. data/docs/brut-js/api/external-Promise.html +1 -1
  290. data/docs/brut-js/api/external-ValidityState.html +1 -1
  291. data/docs/brut-js/api/external-Window.html +1 -1
  292. data/docs/brut-js/api/external-fetch.html +1 -1
  293. data/docs/brut-js/api/global.html +1 -1
  294. data/docs/brut-js/api/index.html +1 -1
  295. data/docs/brut-js/api/index.js.html +1 -1
  296. data/docs/brut-js/api/module-testing.html +1 -1
  297. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  298. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  299. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  300. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  301. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  302. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  303. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  304. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  305. data/docs/brut-js/api/testing_index.js.html +1 -1
  306. data/docs/brut-js.html +4 -4
  307. data/docs/business-logic.html +4 -4
  308. data/docs/cli.html +4 -4
  309. data/docs/components.html +8 -8
  310. data/docs/configuration.html +5 -5
  311. data/docs/css.html +4 -4
  312. data/docs/custom-element-tests.html +4 -4
  313. data/docs/database-access.html +4 -4
  314. data/docs/database-schema.html +13 -6
  315. data/docs/deployment.html +4 -4
  316. data/docs/dev-environment.html +6 -6
  317. data/docs/dir-structure.html +4 -4
  318. data/docs/doc-conventions.html +4 -4
  319. data/docs/end-to-end-tests.html +4 -4
  320. data/docs/features.html +4 -4
  321. data/docs/flash-and-session.html +4 -4
  322. data/docs/form-constraints.html +7 -7
  323. data/docs/forms.html +6 -6
  324. data/docs/getting-started.html +7 -7
  325. data/docs/handlers.html +6 -6
  326. data/docs/hashmap.json +1 -1
  327. data/docs/hooks.html +4 -4
  328. data/docs/i18n.html +6 -6
  329. data/docs/index.html +3 -3
  330. data/docs/instrumentation.html +4 -4
  331. data/docs/javascript.html +4 -4
  332. data/docs/jobs.html +4 -4
  333. data/docs/keyword-injection.html +4 -4
  334. data/docs/layouts.html +6 -6
  335. data/docs/lsp.html +4 -4
  336. data/docs/markdown-examples.html +4 -4
  337. data/docs/middleware.html +4 -4
  338. data/docs/overview.html +4 -4
  339. data/docs/pages.html +4 -4
  340. data/docs/recipes/alternate-layouts.html +4 -4
  341. data/docs/recipes/authentication.html +5 -5
  342. data/docs/recipes/blank-layouts.html +4 -4
  343. data/docs/recipes/custom-flash.html +4 -4
  344. data/docs/recipes/indexed-forms.html +4 -4
  345. data/docs/recipes/migrations.html +125 -0
  346. data/docs/recipes/text-field-component.html +4 -4
  347. data/docs/roadmap.html +4 -4
  348. data/docs/routes.html +4 -4
  349. data/docs/security.html +4 -4
  350. data/docs/seed-data.html +4 -4
  351. data/docs/space-time-continuum.html +4 -4
  352. data/docs/tutorial.html +4 -4
  353. data/docs/unit-tests.html +4 -4
  354. data/docs/why.html +4 -4
  355. data/lib/brut/cli/apps/db.rb +2 -0
  356. data/lib/brut/cli/apps/scaffold.rb +77 -0
  357. data/lib/brut/cli/command.rb +1 -2
  358. data/lib/brut/framework/config.rb +7 -0
  359. data/lib/brut/front_end/components/constraint_violations.rb +2 -2
  360. data/lib/brut/front_end/components/i18n_translations.rb +6 -10
  361. data/lib/brut/front_end/form.rb +5 -2
  362. data/lib/brut/junk_drawer.rb +53 -0
  363. data/lib/brut/version.rb +1 -1
  364. metadata +32 -25
  365. data/docs/assets/chunks/@localSearchIndexroot.DkpwyjLd.js +0 -1
  366. data/docs/assets/dev-environment.md.Dy6EldaM.lean.js +0 -1
  367. /data/docs/assets/{components.md.DP8maNoF.lean.js → components.md.BfeWD9sX.lean.js} +0 -0
  368. /data/docs/assets/{configuration.md.BDXwuZHU.lean.js → configuration.md.DoSBNc0H.lean.js} +0 -0
  369. /data/docs/assets/{form-constraints.md.x5tNpTTI.lean.js → form-constraints.md.DK5adCgM.lean.js} +0 -0
  370. /data/docs/assets/{forms.md.BwBNYrrL.lean.js → forms.md.DFveP5g_.lean.js} +0 -0
  371. /data/docs/assets/{getting-started.md.B4LlHfav.lean.js → getting-started.md.DZWjCVC0.lean.js} +0 -0
  372. /data/docs/assets/{handlers.md.Chyri6KA.lean.js → handlers.md.h84MMB1R.lean.js} +0 -0
  373. /data/docs/assets/{i18n.md.xQhiGo1G.lean.js → i18n.md.BAm9t9JJ.lean.js} +0 -0
  374. /data/docs/assets/{layouts.md.CJGDFY-m.lean.js → layouts.md.CVGl9xIO.lean.js} +0 -0
@@ -1,9 +1,9 @@
1
- import{_ as t,c as a,o as s,ag as o}from"./chunks/framework.1L-BeKqY.js";const i="/assets/DevEnvironment.DaFcVfwP.png",n="/assets/dev-env-protocol.DysDAtnz.png",d="/assets/workspace-protocol.C0gXsoDb.png",g=JSON.parse('{"title":"Dev Environment","description":"","frontmatter":{},"headers":[],"relativePath":"dev-environment.md","filePath":"dev-environment.md"}'),r={name:"dev-environment.md"};function l(c,e,p,h,u,m){return s(),a("div",null,e[0]||(e[0]=[o('<h1 id="dev-environment" tabindex="-1">Dev Environment <a class="header-anchor" href="#dev-environment" aria-label="Permalink to &quot;Dev Environment&quot;">​</a></h1><p>Brut provides sophisticated tooling to manage your dev environment</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>A development environments or <em>dev environment</em> is made up of two parts:</p><ul><li><em>Foundational Core</em> - the operating system and tools needed to run the app and <em>its</em> tools. This includes language runtimes, system libraries (like ImageMagick), and system tools like web browsers.</li><li><em>Workspace</em> - the tools and code bundled with the app that you use day-to-day to work on the app itself. This would include scripts to run the app in development, run tests, perform scaffolding, or manage the database.</li></ul><p>On many teams, the Foundational Core is different per developer, since some run Linux, some run MacOs. Some might use mise to manage their version of Ruby while others use rbenv. Some will set up Postgres via homebrew, while others might use Popstgres.app.</p><p>Brut takes a different approach. Everyone shares the same Foundational Core, and this is defined by a <code>Dockerfile</code>, a <code>docker-compose.yml</code> file, and some lightweight Bash scripts.</p><p>This means that everyone uses the same version of everything, and they are all managed the same way.</p><p>Brut also provides sophisticated tooling for the Workspace. Like Rails, Brut provides a command-line based flow that can be scripted into any editor. Unlike Rails, Brut&#39;s Workspace is comprised of separate command-line apps and not Rake tasks.</p><h3 id="conceptual-overview" tabindex="-1">Conceptual Overview <a class="header-anchor" href="#conceptual-overview" aria-label="Permalink to &quot;Conceptual Overview&quot;">​</a></h3><p>Your dev environment consists of a Docker container that has languages, an operating system, and other system components installed in it. It will have access to the files on your computer so that it can run your app. The app will be exposed so that a browser on your computer can access it. Postgres will be run as a separate Docker container available to the dev Docker container.</p><p>Your editor and version control system run on your computer.</p><p><img src="'+i+`" alt="Diagram showing the parts of the dev environment. The foundational core is also
1
+ import{_ as t,c as a,o,ag as s}from"./chunks/framework.1L-BeKqY.js";const i="/assets/DevEnvironment.DaFcVfwP.png",n="/assets/dev-env-protocol.DysDAtnz.png",d="/assets/workspace-protocol.C0gXsoDb.png",g=JSON.parse('{"title":"Dev Environment","description":"","frontmatter":{},"headers":[],"relativePath":"dev-environment.md","filePath":"dev-environment.md"}'),r={name:"dev-environment.md"};function l(c,e,p,h,u,m){return o(),a("div",null,e[0]||(e[0]=[s('<h1 id="dev-environment" tabindex="-1">Dev Environment <a class="header-anchor" href="#dev-environment" aria-label="Permalink to &quot;Dev Environment&quot;">​</a></h1><p>Brut provides sophisticated tooling to manage your dev environment</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>A development environments or <em>dev environment</em> is made up of two parts:</p><ul><li><em>Foundational Core</em> - the operating system and tools needed to run the app and <em>its</em> tools. This includes language runtimes, system libraries (like ImageMagick), and system tools like web browsers.</li><li><em>Workspace</em> - the tools and code bundled with the app that you use day-to-day to work on the app itself. This would include scripts to run the app in development, run tests, perform scaffolding, or manage the database.</li></ul><p>On many teams, the Foundational Core is different per developer, since some run Linux, some run MacOs. Some might use mise to manage their version of Ruby while others use rbenv. Some will set up Postgres via homebrew, while others might use Popstgres.app.</p><p>Brut takes a different approach. Everyone shares the same Foundational Core, and this is defined by a <code>Dockerfile</code>, a <code>docker-compose.yml</code> file, and some lightweight Bash scripts.</p><p>This means that everyone uses the same version of everything, and they are all managed the same way.</p><p>Brut also provides sophisticated tooling for the Workspace. Like Rails, Brut provides a command-line based flow that can be scripted into any editor. Unlike Rails, Brut&#39;s Workspace is comprised of separate command-line apps and not Rake tasks.</p><h3 id="conceptual-overview" tabindex="-1">Conceptual Overview <a class="header-anchor" href="#conceptual-overview" aria-label="Permalink to &quot;Conceptual Overview&quot;">​</a></h3><p>Your dev environment consists of a Docker container that has languages, an operating system, and other system components installed in it. It will have access to the files on your computer so that it can run your app. The app will be exposed so that a browser on your computer can access it. Postgres will be run as a separate Docker container available to the dev Docker container.</p><p>Your editor and version control system run on your computer.</p><p><img src="'+i+`" alt="Diagram showing the parts of the dev environment. The foundational core is also
2
2
  labeled &quot;Docker containers&quot; and it contains three boxes labeled &quot;Container&quot;. One
3
3
  box contains ValKey, another contains Postrgres. The third box contains &quot;Your Brut
4
4
  App&quot;, &quot;NodeJS&quot;, &quot;Ruby&quot;, and part of &quot;Source Code&quot;. The &quot;Source Code&quot; also also
5
5
  partially inside a box containing the foundational core labeled &quot;Your Computer&quot;.
6
- This box contains &quot;Browser&quot; and &quot;source code editor&quot;."></p><h3 id="foundational-core-command-line-apps" tabindex="-1">Foundational Core Command Line Apps <a class="header-anchor" href="#foundational-core-command-line-apps" aria-label="Permalink to &quot;Foundational Core Command Line Apps&quot;">​</a></h3><p>These are the commands you will use to manage the <em>foundational core</em>, which is the Docker containers and their contents.</p><p>A few brief terminology notes if you aren&#39;t familiar with Docker:</p><ul><li>A Docker <em>container</em> is akin to a virtual machine. On Linux this isn&#39;t strictly true, but conceptually, you can think of this like a virtual computer.</li><li>A Docker <em>image</em> is what you use to start a container. This is akin to a disk image you might use to create a new computer or virtual machine.</li><li>A Dockerfile (often named <code>Dockerfile</code>) is a set of instructions to create an image.</li></ul><p>A few verbs to provide additional help:</p><ul><li>One <em>builds</em> a Docker image from a Dockerfile.</li><li>One <em>starts</em> a Docker container from an image.</li><li>One <em>stops</em> a Docker container when it&#39;s no longer needed.</li></ul><table tabindex="0"><thead><tr><th>App</th><th>Purpose</th></tr></thead><tbody><tr><td><code>dx/build</code></td><td>Builds a Docker <em>image</em> from a <code>Dockerfile.dx</code></td></tr><tr><td><code>dx/start</code></td><td>Starts all Docker containers, including those for databases and caches</td></tr><tr><td><code>dx/stop</code></td><td>Stops all Docker containers</td></tr><tr><td><code>dx/exec</code></td><td>Execute a command inside a running Docker container</td></tr></tbody></table><p>The workflow for the foundational core is shown in this diagram.</p><p><img src="`+n+'" alt="Foundational Core Workflow"></p><p>In words:</p><ol><li>You build the images based on the latest instructions via <code>dx/build</code>.</li><li>You start up the environment with <code>dx/start</code>.</li><li>You then use <code>dx/exec</code> to execute commands from the Workspace (see below).</li><li>When you are done working for the day, <code>dx/stop</code> shuts everything down.</li></ol><h3 id="workspace-command-line-apps" tabindex="-1">Workspace Command Line Apps <a class="header-anchor" href="#workspace-command-line-apps" aria-label="Permalink to &quot;Workspace Command Line Apps&quot;">​</a></h3><p>The workspace is where you&#39;ll run your day-to-day commands, such as running tests, starting the dev server, managing the database schema, etc.</p><p>Several of the commands accept or require subcommands. Each CLI app responds to <code>--help</code> and will show you full documentation about what the command and subcommands do.</p><table tabindex="0"><thead><tr><th>App</th><th>Subcommand</th><th>Descriptions</th></tr></thead><tbody><tr><td><code style="white-space:nowrap;">bin/ci</code></td><td>None</td><td>Runs all tests and security checks</td></tr><tr><td><code style="white-space:nowrap;">bin/console</code></td><td>None</td><td>Starts up a local IRB session with your app loaded</td></tr><tr><td><code style="white-space:nowrap;">bin/db</code></td><td></td><td>Tools for managing the database</td></tr><tr><td></td><td><code>create</code></td><td>Create the database if it does not exist</td></tr><tr><td></td><td><code>drop</code></td><td>Drop the database if it exists</td></tr><tr><td></td><td><code>migrate</code></td><td>Apply any outstanding migrations to the database</td></tr><tr><td></td><td><code>new_migration</code></td><td>Create a new migration file</td></tr><tr><td></td><td><code>rebuild</code></td><td>Drop, re-create, and run migrations, effectively rebuilding the entire database</td></tr><tr><td></td><td><code>seed</code></td><td>Load seed data into the database</td></tr><tr><td></td><td><code>status</code></td><td>Check the status of the database and migrations</td></tr><tr><td><code style="white-space:nowrap;">bin/dbconsole</code></td><td>None</td><td>Starts up a <code>psql</code> session to your database</td></tr><tr><td><code style="white-space:nowrap;">bin/dev</code></td><td>None</td><td>Starts the app in dev mode, rebuilding assets and reload as needed</td></tr><tr><td><code style="white-space:nowrap;">bin/setup</code></td><td>None</td><td>Install and setup all third party libraries and other configuration needed to use the app</td></tr><tr><td><code style="white-space:nowrap;">bin/scaffold</code></td><td></td><td>Generate Brut classes or files like database migrations or page classes</td></tr><tr><td></td><td><code>action</code></td><td>Create a handler for an action</td></tr><tr><td></td><td><code>component</code></td><td>Create a new component and associated test</td></tr><tr><td></td><td><code>custom_element_test</code></td><td>Create a test for a custom element in your app</td></tr><tr><td></td><td><code>form</code></td><td>Create a form and handler</td></tr><tr><td></td><td><code>page</code></td><td>Create a new page and associated test</td></tr><tr><td></td><td><code>test</code></td><td>Create the shell of a unit test based on an existing source file</td></tr><tr><td></td><td><code>test:e2e</code></td><td>Create the shell of an end-to-end test</td></tr><tr><td><code style="white-space:nowrap;">bin/test</code></td><td></td><td>Run tests</td></tr><tr><td></td><td><code>audit</code></td><td>Audits all of the app&#39;s classes to see if test files exist</td></tr><tr><td></td><td><code>e2e</code></td><td>Run e2e tests</td></tr><tr><td></td><td><code>js</code></td><td>Run JavaScript unit tests</td></tr><tr><td></td><td><code>run</code></td><td>Run non-e2e tests (default)</td></tr></tbody></table><p>The workflow for your Workspace is shown in this diagram</p><p><img src="'+d+`" alt="Workspace Workflow"></p><p>In words:</p><ol><li>You&#39;ll run <code>bin/setup</code> to get everything set up for working.</li><li>You&#39;ll start your dev server with <code>bin/dev</code>.</li><li>You&#39;ll write code, using tools like <code>bin/db</code> and <code>bin/scaffold</code> to assist.</li><li>Using <code>bin/test</code>, you can test any code you&#39;ve written a test for.</li><li>When you are at a stopping point, use <code>bin/ci</code> to test the entire app.</li></ol><h3 id="extending-and-enhancing" tabindex="-1">Extending and Enhancing <a class="header-anchor" href="#extending-and-enhancing" aria-label="Permalink to &quot;Extending and Enhancing&quot;">​</a></h3><p>TBD</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>There aren&#39;t tests for this code, because you are using all day every day. Brut&#39;s test suite will ensure that the versions of these command line apps provided when you set up your app are working.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>While you are free to set up mise or rbenv or whatever to run all this on your computer, this way of working is currently not supported nor encouraged. For now, Brut will focus on the Docker-based approach.</p><p>The primary reason is that it&#39;s a tightly controlled environment that is almost entirely scriptable, but does not require devs to abandon their preferred editor. Environment manager-based approaches tend to be more fussy and require documentation to ensure they are set up.</p><p>Keep in mind a few things when adding your own automation:</p><ul><li>The <em>Foundational Core</em> is bootstrapped in a degenerate environment without reliable tools beyond Bash. This is why it&#39;s almost entirely written in Bash, since it&#39;s available everywhere and relatively stable.</li><li>The <em>Workspace</em> <strong>can and should</strong> rely on the languages and third party modules that are part of your app. The only exception is <code>bin/setup</code>, since it installs third party modules. As such, it should work entirely based on Ruby and its standard library.</li></ul><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated June 12, 2025</em></p><p>Everything in <code>bin/</code> is intended to be a short shim that calls into classes managed either by Brut or by your app. For example, here is <code>bin/db</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;">#!/usr/bin/env ruby</span></span>
6
+ This box contains &quot;Browser&quot; and &quot;source code editor&quot;."></p><h3 id="foundational-core-command-line-apps" tabindex="-1">Foundational Core Command Line Apps <a class="header-anchor" href="#foundational-core-command-line-apps" aria-label="Permalink to &quot;Foundational Core Command Line Apps&quot;">​</a></h3><p>These are the commands you will use to manage the <em>foundational core</em>, which is the Docker containers and their contents.</p><p>A few brief terminology notes if you aren&#39;t familiar with Docker:</p><ul><li>A Docker <em>container</em> is akin to a virtual machine. On Linux this isn&#39;t strictly true, but conceptually, you can think of this like a virtual computer.</li><li>A Docker <em>image</em> is what you use to start a container. This is akin to a disk image you might use to create a new computer or virtual machine.</li><li>A Dockerfile (often named <code>Dockerfile</code>) is a set of instructions to create an image.</li></ul><p>A few verbs to provide additional help:</p><ul><li>One <em>builds</em> a Docker image from a Dockerfile.</li><li>One <em>starts</em> a Docker container from an image.</li><li>One <em>stops</em> a Docker container when it&#39;s no longer needed.</li></ul><table tabindex="0"><thead><tr><th>App</th><th>Purpose</th></tr></thead><tbody><tr><td><code>dx/build</code></td><td>Builds a Docker <em>image</em> from a <code>Dockerfile.dx</code></td></tr><tr><td><code>dx/start</code></td><td>Starts all Docker containers, including those for databases and caches</td></tr><tr><td><code>dx/stop</code></td><td>Stops all Docker containers</td></tr><tr><td><code>dx/exec</code></td><td>Execute a command inside a running Docker container</td></tr></tbody></table><p>The workflow for the foundational core is shown in this diagram.</p><p><img src="`+n+'" alt="Foundational Core Workflow"></p><p>In words:</p><ol><li>You build the images based on the latest instructions via <code>dx/build</code>.</li><li>You start up the environment with <code>dx/start</code>.</li><li>You then use <code>dx/exec</code> to execute commands from the Workspace (see below).</li><li>When you are done working for the day, <code>dx/stop</code> shuts everything down.</li></ol><h3 id="workspace-command-line-apps" tabindex="-1">Workspace Command Line Apps <a class="header-anchor" href="#workspace-command-line-apps" aria-label="Permalink to &quot;Workspace Command Line Apps&quot;">​</a></h3><p>The workspace is where you&#39;ll run your day-to-day commands, such as running tests, starting the dev server, managing the database schema, etc.</p><p>Several of the commands accept or require subcommands. Each CLI app responds to <code>--help</code> and will show you full documentation about what the command and subcommands do.</p><table tabindex="0"><thead><tr><th>App</th><th>Subcommand</th><th>Descriptions</th></tr></thead><tbody><tr><td><code style="white-space:nowrap;">bin/ci</code></td><td>None</td><td>Runs all tests and security checks</td></tr><tr><td><code style="white-space:nowrap;">bin/console</code></td><td>None</td><td>Starts up a local IRB session with your app loaded</td></tr><tr><td><code style="white-space:nowrap;">bin/db</code></td><td></td><td>Tools for managing the database</td></tr><tr><td></td><td><code>create</code></td><td>Create the database if it does not exist</td></tr><tr><td></td><td><code>drop</code></td><td>Drop the database if it exists</td></tr><tr><td></td><td><code>migrate</code></td><td>Apply any outstanding migrations to the database</td></tr><tr><td></td><td><code>new_migration</code></td><td>Create a new migration file</td></tr><tr><td></td><td><code>rebuild</code></td><td>Drop, re-create, and run migrations, effectively rebuilding the entire database</td></tr><tr><td></td><td><code>seed</code></td><td>Load seed data into the database</td></tr><tr><td></td><td><code>status</code></td><td>Check the status of the database and migrations</td></tr><tr><td><code style="white-space:nowrap;">bin/dbconsole</code></td><td>None</td><td>Starts up a <code>psql</code> session to your database</td></tr><tr><td><code style="white-space:nowrap;">bin/dev</code></td><td>None</td><td>Starts the app in dev mode, rebuilding assets and reload as needed</td></tr><tr><td><code style="white-space:nowrap;">bin/setup</code></td><td>None</td><td>Install and setup all third party libraries and other configuration needed to use the app</td></tr><tr><td><code style="white-space:nowrap;">bin/scaffold</code></td><td></td><td>Generate Brut classes or files like database migrations or page classes</td></tr><tr><td></td><td><code>action</code></td><td>Create a handler for an action</td></tr><tr><td></td><td><code>component</code></td><td>Create a new component and associated test</td></tr><tr><td></td><td><code>custom_element_test</code></td><td>Create a test for a custom element in your app</td></tr><tr><td></td><td><code>form</code></td><td>Create a form and handler</td></tr><tr><td></td><td><code>page</code></td><td>Create a new page and associated test</td></tr><tr><td></td><td><code>db_model</code></td><td>Create one or more database models, specs, and factories, plus a migration to create the tables for those models</td></tr><tr><td></td><td><code>test</code></td><td>Create the shell of a unit test based on an existing source file</td></tr><tr><td></td><td><code>test:e2e</code></td><td>Create the shell of an end-to-end test</td></tr><tr><td><code style="white-space:nowrap;">bin/test</code></td><td></td><td>Run tests</td></tr><tr><td></td><td><code>audit</code></td><td>Audits all of the app&#39;s classes to see if test files exist</td></tr><tr><td></td><td><code>e2e</code></td><td>Run e2e tests</td></tr><tr><td></td><td><code>js</code></td><td>Run JavaScript unit tests</td></tr><tr><td></td><td><code>run</code></td><td>Run non-e2e tests (default)</td></tr></tbody></table><p>The workflow for your Workspace is shown in this diagram</p><p><img src="'+d+`" alt="Workspace Workflow"></p><p>In words:</p><ol><li>You&#39;ll run <code>bin/setup</code> to get everything set up for working.</li><li>You&#39;ll start your dev server with <code>bin/dev</code>.</li><li>You&#39;ll write code, using tools like <code>bin/db</code> and <code>bin/scaffold</code> to assist.</li><li>Using <code>bin/test</code>, you can test any code you&#39;ve written a test for.</li><li>When you are at a stopping point, use <code>bin/ci</code> to test the entire app.</li></ol><h3 id="extending-and-enhancing" tabindex="-1">Extending and Enhancing <a class="header-anchor" href="#extending-and-enhancing" aria-label="Permalink to &quot;Extending and Enhancing&quot;">​</a></h3><p>TBD</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>There aren&#39;t tests for this code, because you are using all day every day. Brut&#39;s test suite will ensure that the versions of these command line apps provided when you set up your app are working.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>While you are free to set up mise or rbenv or whatever to run all this on your computer, this way of working is currently not supported nor encouraged. For now, Brut will focus on the Docker-based approach.</p><p>The primary reason is that it&#39;s a tightly controlled environment that is almost entirely scriptable, but does not require devs to abandon their preferred editor. Environment manager-based approaches tend to be more fussy and require documentation to ensure they are set up.</p><p>Keep in mind a few things when adding your own automation:</p><ul><li>The <em>Foundational Core</em> is bootstrapped in a degenerate environment without reliable tools beyond Bash. This is why it&#39;s almost entirely written in Bash, since it&#39;s available everywhere and relatively stable.</li><li>The <em>Workspace</em> <strong>can and should</strong> rely on the languages and third party modules that are part of your app. The only exception is <code>bin/setup</code>, since it installs third party modules. As such, it should work entirely based on Ruby and its standard library.</li></ul><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated June 12, 2025</em></p><p>Everything in <code>bin/</code> is intended to be a short shim that calls into classes managed either by Brut or by your app. For example, here is <code>bin/db</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;">#!/usr/bin/env ruby</span></span>
7
7
  <span class="line"></span>
8
8
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;bundler&quot;</span></span>
9
9
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Bundler</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">require</span></span>
@@ -13,4 +13,4 @@ This box contains &quot;Browser&quot; and &quot;source code editor&quot;."></p><
13
13
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">exit</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">CLI</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">app</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
14
14
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">CLI</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Apps</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
15
15
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> project_root:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Pathname</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">($0).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">dirname</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> /</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;..&quot;</span></span>
16
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span></code></pre></div><p>These files have some duplication, but should be relatively stable.</p><p>This means that Brut-provided CLIs <em>will</em> be updated when you update Brut. Compare this to the files in <code>dx/</code> which are entire Bash scripts that will not be updated when Brut is updated.</p>`,48)]))}const y=t(r,[["render",l]]);export{g as __pageData,y as default};
16
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span></code></pre></div><p>These files have some duplication, but should be relatively stable.</p><p>This means that Brut-provided CLIs <em>will</em> be updated when you update Brut. Compare this to the files in <code>dx/</code> which are entire Bash scripts that will not be updated when Brut is updated.</p>`,48)]))}const b=t(r,[["render",l]]);export{g as __pageData,b as default};
@@ -0,0 +1 @@
1
+ import{_ as t,c as a,o,ag as s}from"./chunks/framework.1L-BeKqY.js";const i="/assets/DevEnvironment.DaFcVfwP.png",n="/assets/dev-env-protocol.DysDAtnz.png",d="/assets/workspace-protocol.C0gXsoDb.png",g=JSON.parse('{"title":"Dev Environment","description":"","frontmatter":{},"headers":[],"relativePath":"dev-environment.md","filePath":"dev-environment.md"}'),r={name:"dev-environment.md"};function l(c,e,p,h,u,m){return o(),a("div",null,e[0]||(e[0]=[s("",48)]))}const b=t(r,[["render",l]]);export{g as __pageData,b as default};
@@ -1,7 +1,7 @@
1
1
  import{_ as i,c as a,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const E=JSON.parse('{"title":"Form Constraint Validations","description":"","frontmatter":{},"headers":[],"relativePath":"form-constraints.md","filePath":"form-constraints.md"}'),e={name:"form-constraints.md"};function h(l,s,p,k,r,o){return t(),a("div",null,s[0]||(s[0]=[n(`<h1 id="form-constraint-validations" tabindex="-1">Form Constraint Validations <a class="header-anchor" href="#form-constraint-validations" aria-label="Permalink to &quot;Form Constraint Validations&quot;">​</a></h1><p>Aside from simply collecting data and submitting it to the server, form data has <em>constraints</em> that must be validated before data is accepted. Brut provides support for both client-side and server-side constraints.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>When validating form data against its constraints, Brut provides assistance in two ways:</p><ul><li>Specifying constraint violations that only the server can evaluate.</li><li>Unifying the user experience for both client-side and server-side constraint violations.</li></ul><h3 id="specifying-constraints" tabindex="-1">Specifying Constraints <a class="header-anchor" href="#specifying-constraints" aria-label="Permalink to &quot;Specifying Constraints&quot;">​</a></h3><p>For both client and server-side constraint violations, Brut uses the <a href="/api/Brut/FrontEnd/Forms/ConstraintViolation.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::ConstraintViolation</code></a> class to represent a specific error on a specific field. This class is a wrapper around an i18n key, context to generate that key&#39;s messaging, and a flag indicating if the violation is server or client side.</p><p>To specify a server-side constraint violation on a form, call <code>server_side_constraint_violation</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server_side_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
2
2
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
3
3
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name_is_taken</span></span>
4
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span></code></pre></div><p>The <code>input_name</code> is the same value you used when creating your form class, and <code>key</code> is an <a href="/i18n.html">I18n</a> key that will have <code>cv.be</code> prepended to it (for **c*onstratin <strong>v</strong>iolation, <strong>b</strong>ack <strong>e</strong>nd). Thus, the key in the above example is <code>&quot;cv.be.name_is_taken&quot;</code>.</p><p>Brut forms will automatically add client-side constraints based on the value assigned to the input. For example, since <code>name</code> must be 3 or more characters, this code would implicitly set <code>:rangeOverflow</code> as a client-side constraint violation:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">value</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;xx&quot;</span></span></code></pre></div><h3 id="accessing-constraints-when-generating-html" tabindex="-1">Accessing Constraints when Generating HTML <a class="header-anchor" href="#accessing-constraints-when-generating-html" aria-label="Permalink to &quot;Accessing Constraints when Generating HTML&quot;">​</a></h3><p><a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a> provides the method <code>constraint_violations</code> to access the constraints, however we recommend using the <a href="/api/Brut/FrontEnd/Components/ConstraintViolations.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::ConstraintViolations</code></a> component instead. This component generates particular markup useful for unifying the UX around constraint violations, which we&#39;ll discuss in a moment.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
4
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span></code></pre></div><p>The <code>input_name</code> is the same value you used when creating your form class, and <code>key</code> is an <a href="/i18n.html">I18n</a> key that will have <code>cv.ss</code> prepended to it (for **c*onstratin <strong>v</strong>iolation, <strong>s</strong>server <strong>s</strong>ide). Thus, the key in the above example is <code>&quot;cv.ss.name_is_taken&quot;</code>.</p><p>Brut forms will automatically add client-side constraints based on the value assigned to the input. For example, since <code>name</code> must be 3 or more characters, this code would implicitly set <code>:rangeOverflow</code> as a client-side constraint violation:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">value</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;xx&quot;</span></span></code></pre></div><h3 id="accessing-constraints-when-generating-html" tabindex="-1">Accessing Constraints when Generating HTML <a class="header-anchor" href="#accessing-constraints-when-generating-html" aria-label="Permalink to &quot;Accessing Constraints when Generating HTML&quot;">​</a></h3><p><a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a> provides the method <code>constraint_violations</code> to access the constraints, however we recommend using the <a href="/api/Brut/FrontEnd/Components/ConstraintViolations.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::ConstraintViolations</code></a> component instead. This component generates particular markup useful for unifying the UX around constraint violations, which we&#39;ll discuss in a moment.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
5
5
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span></span>
6
6
  <span class="line"></span>
7
7
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
@@ -39,7 +39,7 @@ import{_ as i,c as a,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const E
39
39
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p><a href="/brut-js/api/Form.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-form&gt;</code></a> listens for events from the <code>&lt;form&gt;</code> it contains. For an &quot;invalid&quot; events, it will locate the element relevant to the event, locate its <a href="/brut-js/api/ConstraintViolationMessages.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv-messages&gt;</code></a> tag, and insert one <a href="/brut-js/api/ConstraintViolationMessage.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv&gt;</code></a> tag for each error from the inputs <code>ValidityState</code>. That may look like so:</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;text&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;name&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> required</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> minlength</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;3&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
40
40
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> input-name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;name&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
41
41
  <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> input-name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;name&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> key</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;rangeUnderflow&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
42
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p>They <code>key</code> attribute is for an I18n key that is expected to be on the page inside a <a href="/brut-js/api/I18nTranslation.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-i18n-translation&gt;</code></a> element. These are typically included in the <a href="/layouts.html">layout</a>, and generate HTML like so:</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-i18n-translation</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> key</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.fe.rangeUnderflow&quot;</span></span>
42
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p>They <code>key</code> attribute is for an I18n key that is expected to be on the page inside a <a href="/brut-js/api/I18nTranslation.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-i18n-translation&gt;</code></a> element. These are typically included in the <a href="/layouts.html">layout</a>, and generate HTML like so:</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-i18n-translation</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> key</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.cs.rangeUnderflow&quot;</span></span>
43
43
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> value</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;%{field} is too short&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-i18n-translation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p><a href="/brut-js/api/ConstraintViolationMessage.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-cv&gt;</code></a> will, whenever its <code>key</code> attribute is set or changed, locate the corrsponding <a href="/brut-js/api/I18nTranslation.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-i18n-translation&gt;</code></a> element, and perform substitution, result in this HTML:</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;text&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;name&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> required</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> minlength</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;3&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
44
44
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv-messages</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> input-name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;name&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
45
45
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">brut-cv</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> input-name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;name&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> key</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;rangeUnderflow&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
@@ -31,7 +31,7 @@ import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.1L-BeKqY.js";const c
31
31
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;number&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;quantity&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> required</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> min</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;0&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> step</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;1&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
32
32
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">textarea</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;description&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
33
33
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">textarea</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
34
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p>Forms accept a single initializer parameter, <code>params</code> that is a <code>Hash</code>. <a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a> implements this initializer, and will pluck values from the hash to initialize the inputs:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-Yosjt" id="tab-s8VmLww" checked><label data-title="Form Class" for="tab-s8VmLww">Form Class</label><input type="radio" name="group-Yosjt" id="tab-dv33rvJ"><label data-title="HTML Generated" for="tab-dv33rvJ">HTML Generated</label></div><div class="blocks"><div class="language-ruby vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
34
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">form</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><p>Forms accept a single initializer parameter, <code>params</code> that is a <code>Hash</code>. <a href="/api/Brut/FrontEnd/Form.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Form</code></a> implements this initializer, and will pluck values from the hash to initialize the inputs:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-QhqsY" id="tab-2HDPScw" checked><label data-title="Form Class" for="tab-2HDPScw">Form Class</label><input type="radio" name="group-QhqsY" id="tab-qeqAk4J"><label data-title="HTML Generated" for="tab-qeqAk4J">HTML Generated</label></div><div class="blocks"><div class="language-ruby vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
35
35
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span></span>
36
36
  <span class="line"></span>
37
37
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
@@ -4,12 +4,12 @@ import{_ as s,c as e,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const u
4
4
  <span class="line"><span> -it \\</span></span>
5
5
  <span class="line"><span> thirdtank/mkbrut \\</span></span>
6
6
  <span class="line"><span> mkbrut my-new-app</span></span></code></pre></div><p>If you already have Ruby 3.4 installed, you can install <code>mkbrut</code> directly:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>&gt; gem install mkbrut</span></span>
7
- <span class="line"><span>&gt; mkbrut my-new-app</span></span></code></pre></div><h2 id="init-your-app" tabindex="-1">Init Your App <a class="header-anchor" href="#init-your-app" aria-label="Permalink to &quot;Init Your App&quot;">​</a></h2><p>A Brut app just needs a name, which will be used to derive a few more useful values. For now:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-tz7D0" id="tab-XogbJ8N" checked><label data-title="Docker-based" for="tab-XogbJ8N">Docker-based</label><input type="radio" name="group-tz7D0" id="tab-iA66ic9"><label data-title="RubyGems-based" for="tab-iA66ic9">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
7
+ <span class="line"><span>&gt; mkbrut my-new-app</span></span></code></pre></div><h2 id="init-your-app" tabindex="-1">Init Your App <a class="header-anchor" href="#init-your-app" aria-label="Permalink to &quot;Init Your App&quot;">​</a></h2><p>A Brut app just needs a name, which will be used to derive a few more useful values. For now:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-iE1W-" id="tab-HqNdUBb" checked><label data-title="Docker-based" for="tab-HqNdUBb">Docker-based</label><input type="radio" name="group-iE1W-" id="tab-79FT0-g"><label data-title="RubyGems-based" for="tab-79FT0-g">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
8
8
  <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
9
9
  <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
10
10
  <span class="line"><span> -it \\</span></span>
11
11
  <span class="line"><span> thirdtank/mkbrut \\</span></span>
12
- <span class="line"><span> mkbrut my-new-app</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>mkbrut my-new-app</span></span></code></pre></div></div></div><p>This will create your new app, along with some demo routes, components, handlers, and tests. If this is your first time using Brut, we recommend you examine these demo components.</p><p>To create your app without the demo components:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-a_NC1" id="tab-zeUjNZA" checked><label data-title="Docker-based" for="tab-zeUjNZA">Docker-based</label><input type="radio" name="group-a_NC1" id="tab-BOZm9eG"><label data-title="RubyGems-based" for="tab-BOZm9eG">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
12
+ <span class="line"><span> mkbrut my-new-app</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>mkbrut my-new-app</span></span></code></pre></div></div></div><p>This will create your new app, along with some demo routes, components, handlers, and tests. If this is your first time using Brut, we recommend you examine these demo components.</p><p>To create your app without the demo components:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-kPVrM" id="tab-OWsfiPR" checked><label data-title="Docker-based" for="tab-OWsfiPR">Docker-based</label><input type="radio" name="group-kPVrM" id="tab-snCH9OJ"><label data-title="RubyGems-based" for="tab-snCH9OJ">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
13
13
  <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
14
14
  <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
15
15
  <span class="line"><span> -it \\</span></span>
@@ -5,7 +5,7 @@ import{_ as a,c as i,o as e,ag as t}from"./chunks/framework.1L-BeKqY.js";const c
5
5
  <span class="line"></span>
6
6
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
7
7
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # if no client-side violations were submitted</span></span>
8
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">@form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">constraint_violations?</span></span>
8
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">valid?</span></span>
9
9
  <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Widget</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">find</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">name:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
10
10
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> widget</span></span>
11
11
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">server_side_constraint_violation</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
@@ -20,4 +20,4 @@ import{_ as e,c as a,o as i,ag as t}from"./chunks/framework.1L-BeKqY.js";const k
20
20
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
21
21
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
22
22
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
23
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>The result of <code>t</code> is safe HTML, so you must use <code>raw</code> to avoid escaping it.</p><p>In a CLI or back-end context, HTML escaping is not relevant and can actually create problems, so <code>ForCLI</code> and <code>ForBackend</code> no-op <code>safe</code> and <code>capture</code>.</p><p>When using <code>ForHTML</code>, all interpolated values are HTML-escaped. <code>ForCLI</code> and <code>ForBackend</code> are not.</p><h3 id="localizing-dates-and-times" tabindex="-1">Localizing Dates and Times <a class="header-anchor" href="#localizing-dates-and-times" aria-label="Permalink to &quot;Localizing Dates and Times&quot;">​</a></h3><p><code>l</code> can be called and this defers to the Ruby I18n library.</p><p>Date and time formats can be configured in the translation files. <code>l</code> does not accept a full key for the format. It is created dynamically by the library, so you must take care in which one you use. If you pass a <code>Date</code> into <code>l</code>, <code>date.formats.«format»</code> is used. If you pass a <code>Time</code> in, <code>time.formats.«format»</code> is used.</p><p>The values of the formats are strings suitable for <a href="https://www.man7.org/linux/man-pages/man3/strftime.3.html" target="_blank" rel="noreferrer"><code>strftime</code></a>. The site <a href="https://www.strfti.me/" target="_blank" rel="noreferrer">strif.me</a> can be helpful in conjuring the right value.</p><p>Brut includes translations for various formats that you can inspect in <code>app/config/i18n/«lang»/1_defaults.rb</code>.</p><h3 id="displaying-dates-and-times-in-html" tabindex="-1">Displaying Dates and Times in HTML <a class="header-anchor" href="#displaying-dates-and-times-in-html" aria-label="Permalink to &quot;Displaying Dates and Times in HTML&quot;">​</a></h3><p>While <code>l</code> will return a string you can use anywhere, you are most likely going to show dates and times in HTML. For that, you should use a <code>&lt;time&gt;</code> element. Brut provides <a href="/api/Brut/FrontEnd/Components/TimeTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::TimeTag</code></a> (remember that if you <code>include Brut::FrontEnd::Components</code>, it&#39;s a Phlex <em>kit</em> and thus you can use <code>TimeTag(...)</code> directly) to do this. It contains additional behavior to make friendly dates and times.</p><ul><li>You can give it a <code>timestamp:</code> or <code>date:</code> to control which formatting style is used.</li><li><code>skip_year_if_same</code>, if true, will omit the year from any format if the current year is the same as the year being displayed. This is true by default</li><li><code>skip_dow_if_not_this_week</code>, if true, will omit the day of week if the date or time is more than 7 days in the past. This is true by default.</li></ul><p>The way <code>skip_year_if_same</code> and <code>skip_dow_if_not_this_week</code> work is to append <code>no_year</code> and/or <code>no_dow</code> to existing format strings which are assumed to omit this elements.</p><p>If you wish to create your own formats, you can add them as well.</p><h3 id="constraint-violations-and-field-names" tabindex="-1">Constraint Violations and Field Names <a class="header-anchor" href="#constraint-violations-and-field-names" aria-label="Permalink to &quot;Constraint Violations and Field Names&quot;">​</a></h3><p>The interpolated value <code>{field}</code> is special. It is assumed to be the name of a field in a constraint violation message, e.g. <code>&quot;%{field} is required&quot;</code>. It is the only interpolated value that can be omitted without causing an error.</p><p>If included, it will work as normal:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">t</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.be.required&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">field:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Email&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># =&gt; Email is required</span></span></code></pre></div><p>If omitted, the value of <code>&quot;cv.this_field&quot;</code> is used. This is included in <code>1_default.rb</code>, but if it&#39;s missing, Brut will raise. Assuming the value is <code>&quot;This field&quot;</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">t</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.be.required&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># =&gt; This field is required</span></span></code></pre></div><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>In tests, you can call <code>t</code> and <code>l</code> to examine values as needed. You may find the <code>have_i18n_string</code> matcher usefult to check generated HTML for I18n values (see <a href="/api/Brut/SpecSupport/Matchers/HaveI18nString.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveI18nString</code></a>).</p><div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p><p>Brut hardcodes English for tests, which you may not want. This will be addressed in the future.</p></div><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>None at this time, however Brut&#39;s I18n has not been battle-tested.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 7, 2025</em></p><p>None at this time.</p>`,61)]))}const u=e(n,[["render",o]]);export{k as __pageData,u as default};
23
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>The result of <code>t</code> is safe HTML, so you must use <code>raw</code> to avoid escaping it.</p><p>In a CLI or back-end context, HTML escaping is not relevant and can actually create problems, so <code>ForCLI</code> and <code>ForBackend</code> no-op <code>safe</code> and <code>capture</code>.</p><p>When using <code>ForHTML</code>, all interpolated values are HTML-escaped. <code>ForCLI</code> and <code>ForBackend</code> are not.</p><h3 id="localizing-dates-and-times" tabindex="-1">Localizing Dates and Times <a class="header-anchor" href="#localizing-dates-and-times" aria-label="Permalink to &quot;Localizing Dates and Times&quot;">​</a></h3><p><code>l</code> can be called and this defers to the Ruby I18n library.</p><p>Date and time formats can be configured in the translation files. <code>l</code> does not accept a full key for the format. It is created dynamically by the library, so you must take care in which one you use. If you pass a <code>Date</code> into <code>l</code>, <code>date.formats.«format»</code> is used. If you pass a <code>Time</code> in, <code>time.formats.«format»</code> is used.</p><p>The values of the formats are strings suitable for <a href="https://www.man7.org/linux/man-pages/man3/strftime.3.html" target="_blank" rel="noreferrer"><code>strftime</code></a>. The site <a href="https://www.strfti.me/" target="_blank" rel="noreferrer">strif.me</a> can be helpful in conjuring the right value.</p><p>Brut includes translations for various formats that you can inspect in <code>app/config/i18n/«lang»/1_defaults.rb</code>.</p><h3 id="displaying-dates-and-times-in-html" tabindex="-1">Displaying Dates and Times in HTML <a class="header-anchor" href="#displaying-dates-and-times-in-html" aria-label="Permalink to &quot;Displaying Dates and Times in HTML&quot;">​</a></h3><p>While <code>l</code> will return a string you can use anywhere, you are most likely going to show dates and times in HTML. For that, you should use a <code>&lt;time&gt;</code> element. Brut provides <a href="/api/Brut/FrontEnd/Components/TimeTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::TimeTag</code></a> (remember that if you <code>include Brut::FrontEnd::Components</code>, it&#39;s a Phlex <em>kit</em> and thus you can use <code>TimeTag(...)</code> directly) to do this. It contains additional behavior to make friendly dates and times.</p><ul><li>You can give it a <code>timestamp:</code> or <code>date:</code> to control which formatting style is used.</li><li><code>skip_year_if_same</code>, if true, will omit the year from any format if the current year is the same as the year being displayed. This is true by default</li><li><code>skip_dow_if_not_this_week</code>, if true, will omit the day of week if the date or time is more than 7 days in the past. This is true by default.</li></ul><p>The way <code>skip_year_if_same</code> and <code>skip_dow_if_not_this_week</code> work is to append <code>no_year</code> and/or <code>no_dow</code> to existing format strings which are assumed to omit this elements.</p><p>If you wish to create your own formats, you can add them as well.</p><h3 id="constraint-violations-and-field-names" tabindex="-1">Constraint Violations and Field Names <a class="header-anchor" href="#constraint-violations-and-field-names" aria-label="Permalink to &quot;Constraint Violations and Field Names&quot;">​</a></h3><p>The interpolated value <code>{field}</code> is special. It is assumed to be the name of a field in a constraint violation message, e.g. <code>&quot;%{field} is required&quot;</code>. It is the only interpolated value that can be omitted without causing an error.</p><p>If included, it will work as normal:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">t</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.ss.required&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">field:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Email&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># =&gt; Email is required</span></span></code></pre></div><p>If omitted, the value of <code>&quot;cv.this_field&quot;</code> is used. This is included in <code>1_default.rb</code>, but if it&#39;s missing, Brut will raise. Assuming the value is <code>&quot;This field&quot;</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">t</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.ss.required&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># =&gt; This field is required</span></span></code></pre></div><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>In tests, you can call <code>t</code> and <code>l</code> to examine values as needed. You may find the <code>have_i18n_string</code> matcher usefult to check generated HTML for I18n values (see <a href="/api/Brut/SpecSupport/Matchers/HaveI18nString.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::Matchers::HaveI18nString</code></a>).</p><div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p><p>Brut hardcodes English for tests, which you may not want. This will be addressed in the future.</p></div><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>None at this time, however Brut&#39;s I18n has not been battle-tested.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 7, 2025</em></p><p>None at this time.</p>`,61)]))}const u=e(n,[["render",o]]);export{k as __pageData,u as default};
@@ -18,7 +18,7 @@ import{_ as i,c as a,o as t,ag as e}from"./chunks/framework.1L-BeKqY.js";const E
18
18
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> script</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">defer:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">src:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> asset_path</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/js/app.js&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">))</span></span>
19
19
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> title { app_name }</span></span>
20
20
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> PageIdentifier</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(@page_name)</span></span>
21
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> I18nTranslations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.fe&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
21
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> I18nTranslations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.cs&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
22
22
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> I18nTranslations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;cv.this_field&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
23
23
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Traceparent</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">()</span></span>
24
24
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
@@ -0,0 +1,97 @@
1
+ import{_ as a,c as i,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Migration Example","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/migrations.md","filePath":"recipes/migrations.md"}'),t={name:"recipes/migrations.md"};function l(p,s,h,k,d,o){return n(),i("div",null,s[0]||(s[0]=[e(`<h1 id="migration-example" tabindex="-1">Migration Example <a class="header-anchor" href="#migration-example" aria-label="Permalink to &quot;Migration Example&quot;">​</a></h1><p>If you&#39;ve not used <a href="https://sequel.jeremyevans.net/" target="_blank" rel="noreferrer">Sequel</a> before, this recipe will show you the basics of creating <a href="https://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html" target="_blank" rel="noreferrer">migrations</a>, which are how the database schema is managed in Brut.</p><h2 id="feature" tabindex="-1">Feature <a class="header-anchor" href="#feature" aria-label="Permalink to &quot;Feature&quot;">​</a></h2><ul><li>An accounts table will store an email and a deactivated date</li><li>A blog posts table will store a title and content, and be attributed to an account.</li></ul><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to &quot;Recipe&quot;">​</a></h2><p>We&#39;ll create the migration, create data models, create and lint factories, then create seed data.</p><h3 id="create-the-migration" tabindex="-1">Create the Migration <a class="header-anchor" href="#create-the-migration" aria-label="Permalink to &quot;Create the Migration&quot;">​</a></h3><p>Create the migration file with <code>bin/db new_migration</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>&gt; bin/db new_migration Accounts and Blog Posts</span></span>
2
+ <span class="line"><span>[ bin/db ] Migration created:</span></span>
3
+ <span class="line"><span> app/src/back_end/data_models/migrations/20250711215310_Accounts-and-Blog-Posts.rb</span></span></code></pre></div><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>Your filename will be different, since it embeds a timestamp for when <code>bin/db new_migration</code> was run.</p></div><p>Now, use Sequel&#39;s migrations API, keeping in mind <a href="/database-schema.html">Brut&#39;s augmentations</a>, to create our tables.</p><p>Our tables will use <a href="/database-schema.html#external-ids">an external ids</a>. Note that Brut will ensure both tables have primary keys and have <code>created_at</code> fields.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/back_end/data_models/migrations/20250711215310_Accounts-and-Blog-Posts.rb</span></span>
4
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Sequel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">migration</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
5
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> up </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
6
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> create_table </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:accounts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
7
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;People or systems who can access this system&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
8
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> external_id:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
9
+ <span class="line"></span>
10
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">unique:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span></span>
11
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:deactivated_at</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:timestamptz</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">null:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span></span>
12
+ <span class="line"></span>
13
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
14
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> </span></span>
15
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> create_table </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_posts</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
16
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Posts on our amazing blog&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
17
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> external_id:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
18
+ <span class="line"></span>
19
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:title</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:text</span></span>
20
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:content</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:text</span></span>
21
+ <span class="line"></span>
22
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> foreign_key </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:account_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:accounts</span></span>
23
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
24
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
25
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>You can apply this migration with <code>bin/db migrate</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/db migrate</span></span></code></pre></div><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>This only applied migrations to the dev database. <code>bin/ci</code> and <code>bin/test e2e</code> will apply them to the test database, but you may need to do it yourself via <code>bin/db migration -e test</code></p></div><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>There is no down migration. If you need to change and re-apply this before you have promoted it to production, rebuild your dev database with <code>bin/db rebuild</code>. It will apply all migrations from a fresh, empty database.</p></div><p>You can examine the tables with <code>psql</code>, via <code>bin/dbconsole</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>&gt; bin/dbconsole</span></span>
26
+ <span class="line"><span>development=# \\d accounts</span></span>
27
+ <span class="line"><span> Table &quot;public.accounts&quot;</span></span>
28
+ <span class="line"><span> Column | Type | Collation | Nullable | Default </span></span>
29
+ <span class="line"><span>----------------+--------------------------+-----------+----------+----------------------------------</span></span>
30
+ <span class="line"><span> id | integer | | not null | generated by default as identity</span></span>
31
+ <span class="line"><span> email | text | | not null | </span></span>
32
+ <span class="line"><span> deactivated_at | timestamp with time zone | | | </span></span>
33
+ <span class="line"><span> created_at | timestamp with time zone | | not null | </span></span>
34
+ <span class="line"><span> external_id | citext | | not null | </span></span>
35
+ <span class="line"><span>Indexes:</span></span>
36
+ <span class="line"><span> &quot;accounts_pkey&quot; PRIMARY KEY, btree (id)</span></span>
37
+ <span class="line"><span> &quot;accounts_email_key&quot; UNIQUE CONSTRAINT, btree (email)</span></span>
38
+ <span class="line"><span> &quot;accounts_external_id_key&quot; UNIQUE CONSTRAINT, btree (external_id)</span></span>
39
+ <span class="line"><span>Referenced by:</span></span>
40
+ <span class="line"><span> TABLE &quot;blog_posts&quot; CONSTRAINT &quot;blog_posts_account_id_fkey&quot; FOREIGN KEY (account_id) REFERENCES accounts(id)</span></span>
41
+ <span class="line"><span></span></span>
42
+ <span class="line"><span>development=# \\d blog_posts</span></span>
43
+ <span class="line"><span> Table &quot;public.blog_posts&quot;</span></span>
44
+ <span class="line"><span> Column | Type | Collation | Nullable | Default </span></span>
45
+ <span class="line"><span>-------------+--------------------------+-----------+----------+----------------------------------</span></span>
46
+ <span class="line"><span> id | integer | | not null | generated by default as identity</span></span>
47
+ <span class="line"><span> title | text | | not null | </span></span>
48
+ <span class="line"><span> content | text | | not null | </span></span>
49
+ <span class="line"><span> account_id | integer | | not null | </span></span>
50
+ <span class="line"><span> created_at | timestamp with time zone | | not null | </span></span>
51
+ <span class="line"><span> external_id | citext | | not null | </span></span>
52
+ <span class="line"><span>Indexes:</span></span>
53
+ <span class="line"><span> &quot;blog_posts_pkey&quot; PRIMARY KEY, btree (id)</span></span>
54
+ <span class="line"><span> &quot;blog_posts_account_id_index&quot; btree (account_id)</span></span>
55
+ <span class="line"><span> &quot;blog_posts_external_id_key&quot; UNIQUE CONSTRAINT, btree (external_id)</span></span>
56
+ <span class="line"><span>Foreign-key constraints:</span></span>
57
+ <span class="line"><span> &quot;blog_posts_account_id_fkey&quot; FOREIGN KEY (account_id) REFERENCES accounts(id)</span></span></code></pre></div><p>Note that all columns are <code>NOT NULL</code> except <code>deactivated_at</code>, which we explicitly set as nullable. Note that the foreign key on <code>account_id</code> has an index and is non-nullable. And note that both tables have <code>external_id</code> and <code>created_at</code> columns. Brut will manage their contents.</p><h3 id="create-data-models" tabindex="-1">Create Data Models <a class="header-anchor" href="#create-data-models" aria-label="Permalink to &quot;Create Data Models&quot;">​</a></h3><p>Brut doesn&#39;t create your data models for you, since it assumes you prefer writing code in your editor and not on the command line. Your data models will initially be pretty short.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/back_end/data_models/db/account.rb</span></span>
58
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Account</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppDataModel</span></span>
59
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> has_external_id </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:ac</span></span>
60
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> one_to_many </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_posts</span></span>
61
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span>
62
+ <span class="line"></span>
63
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/back_end/data_models/db/blog_post.rb</span></span>
64
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> DB</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BlogPost</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppDataModel</span></span>
65
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> many_to_one </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:account</span></span>
66
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>You can run <code>bin/console</code> to try to test these, but it&#39;s easier to create factories and use the <code>specs/lint_factories.spec.rb</code> to do that for us.</p><h3 id="create-factories" tabindex="-1">Create Factories <a class="header-anchor" href="#create-factories" aria-label="Permalink to &quot;Create Factories&quot;">​</a></h3><p>Factories go in <code>specs/factories/db</code> and have a <code>.factory.db</code> suffix:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># specs/factories/db/account.factory.rb</span></span>
67
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FactoryBot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">define</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
68
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> factory </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:account</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;DB::Account&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
69
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> email { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Faker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Internet</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">unique</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
70
+ <span class="line"></span>
71
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> trait </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:inactive</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
72
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> deactivated_at { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Time</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">now</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
73
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
74
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
75
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span>
76
+ <span class="line"></span>
77
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># specs/factories/db/blog_post.factory.rb</span></span>
78
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FactoryBot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">define</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
79
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> factory </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;DB::BlogPost&quot;</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
80
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> title { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Faker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Lorem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">sentence</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
81
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> content { </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Faker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Lorem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">paragraphs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">join</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">\\n\\n</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) }</span></span>
82
+ <span class="line"></span>
83
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> account</span></span>
84
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
85
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Note that the blog post factory creates an account. This is so that <code>create(:blog_post)</code> will always succeeed in creating valid data.</p><p>To prove it, we&#39;ll lint our factories:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/test run specs/lint_factories.spec.rb</span></span></code></pre></div><p>This will create every combination of every factory and fail if doing so raises an error.</p><h3 id="create-seed-data" tabindex="-1">Create Seed Data <a class="header-anchor" href="#create-seed-data" aria-label="Permalink to &quot;Create Seed Data&quot;">​</a></h3><p>Now, we&#39;ll set up seed data. <code>mkbrut</code> should&#39;ve created <code>app/src/back_end/data_models/seed/seed_data.rb</code>, so we&#39;ll use that.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;brut/back_end/seed_data&quot;</span></span>
86
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> SeedData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BackEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">SeedData</span></span>
87
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> FactoryBot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Syntax</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Methods</span></span>
88
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> seed!</span></span>
89
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> pat</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:account</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">email:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;pat@example.com&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
90
+ <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> chris</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:account</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:inactive</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">email:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;chris@example.com&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
91
+ <span class="line"></span>
92
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 5</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">times</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
93
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">account:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> pat)</span></span>
94
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">account:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> chris)</span></span>
95
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
96
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
97
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>We can apply this with <code>bin/db seed</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/db seed</span></span></code></pre></div><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p><code>bin/db rebuild</code> will <em>not</em> apply seed data, however <code>bin/setup</code> should. For now, if you want to totally reset your database, you will need to do <code>bin/db rebuild &amp;&amp; bin/db seed &amp;&amp; bin/db rebuild -e test</code></p></div>`,37)]))}const g=a(t,[["render",l]]);export{c as __pageData,g as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as i,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const c=JSON.parse('{"title":"Migration Example","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/migrations.md","filePath":"recipes/migrations.md"}'),t={name:"recipes/migrations.md"};function l(p,s,h,k,d,o){return n(),i("div",null,s[0]||(s[0]=[e("",37)]))}const g=a(t,[["render",l]]);export{c as __pageData,g as default};