brut 0.0.20 → 0.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +24 -3
- data/.nvim.lua +1 -0
- data/Dockerfile.dx +12 -3
- data/Gemfile.lock +9 -7
- data/README.md +0 -7
- data/Rakefile +6 -4
- data/bin/dev +20 -0
- data/bin/docs +27 -0
- data/bin/setup +47 -1
- data/brut-css/.nvim.lua +1 -0
- data/brut-css/README.md +28 -0
- data/brut-css/bin/build +31 -0
- data/brut-css/bin/dev +1 -0
- data/brut-css/bin/docs +15 -0
- data/brut-css/bin/setup +5 -0
- data/brut-css/config/media-queries-all.css +15 -0
- data/brut-css/config/media-queries-minimal.css +5 -0
- data/brut-css/config/postcss.config.cjs +7 -0
- data/brut-css/config/pseudo-classes-all.css +9 -0
- data/brut-css/dx +1 -0
- data/brut-css/package-lock.json +3217 -0
- data/brut-css/package.json +36 -0
- data/brut-css/src/css/appearance.css +145 -0
- data/brut-css/src/css/border.css +522 -0
- data/brut-css/src/css/colors.css +3502 -0
- data/brut-css/src/css/dimensions.css +548 -0
- data/brut-css/src/css/flex.css +179 -0
- data/brut-css/src/css/index.css +13 -0
- data/brut-css/src/css/layout.css +120 -0
- data/brut-css/src/css/list.css +41 -0
- data/brut-css/src/css/positioning.css +354 -0
- data/brut-css/src/css/properties/colors.css +455 -0
- data/brut-css/src/css/properties/index.css +3 -0
- data/brut-css/src/css/properties/spacing.css +140 -0
- data/brut-css/src/css/properties/typography.css +224 -0
- data/brut-css/src/css/reset.css +107 -0
- data/brut-css/src/css/spacing.css +585 -0
- data/brut-css/src/css/typography.css +519 -0
- data/brut-css/src/css/utils.css +104 -0
- data/brut-css/src/docs/1_getting-started/1_overview.md +46 -0
- data/brut-css/src/docs/1_getting-started/2_installation.md +25 -0
- data/brut-css/src/docs/1_getting-started/3_core-concepts.md +75 -0
- data/brut-css/src/docs/1_getting-started/4_simple-example.md +132 -0
- data/brut-css/src/docs/1_getting-started/page.html.ejs +10 -0
- data/brut-css/src/docs/2_properties/page.html.ejs +71 -0
- data/brut-css/src/docs/3_classes/color-demo.html.ejs +31 -0
- data/brut-css/src/docs/3_classes/page.html.ejs +87 -0
- data/brut-css/src/docs/4_customization/1_design-system.md +36 -0
- data/brut-css/src/docs/4_customization/2_breakpoints.md +75 -0
- data/brut-css/src/docs/4_customization/3_pseudo-classes.md +74 -0
- data/brut-css/src/docs/4_customization/4_advanced-configuration.md +40 -0
- data/brut-css/src/docs/4_customization/page.html.ejs +10 -0
- data/brut-css/src/docs/docs.css +98 -0
- data/brut-css/src/docs/includes/body-and-header.html.ejs +30 -0
- data/brut-css/src/docs/includes/footer-and-rest.html.ejs +9 -0
- data/brut-css/src/docs/includes/head.html.ejs +5 -0
- data/brut-css/src/docs/includes/nav.html.ejs +10 -0
- data/brut-css/src/docs/index.html.ejs +32 -0
- data/brut-css/src/docs/prism-twilight.min.css +1 -0
- data/brut-css/src/js/Logger.js +71 -0
- data/brut-css/src/js/build.js +111 -0
- data/brut-css/src/js/cli/CLIArgError.js +7 -0
- data/brut-css/src/js/cli/Debug.js +27 -0
- data/brut-css/src/js/cli/DocsDir.js +16 -0
- data/brut-css/src/js/cli/DocsTemplateSourceDir.js +16 -0
- data/brut-css/src/js/cli/InputFile.js +31 -0
- data/brut-css/src/js/cli/MediaQueryConfigFile.js +10 -0
- data/brut-css/src/js/cli/OutputFile.js +22 -0
- data/brut-css/src/js/cli/ParsedArg.js +17 -0
- data/brut-css/src/js/cli/PathToBrutCSSRoot.js +19 -0
- data/brut-css/src/js/cli/PseudoClassConfigFile.js +11 -0
- data/brut-css/src/js/cli.js +108 -0
- data/brut-css/src/js/docGenerator.js +467 -0
- data/brut-css/src/js/mediaQueryConfigParser.js +98 -0
- data/brut-css/src/js/post-css-plugins/addMediaQueriesPlugin.js +49 -0
- data/brut-css/src/js/post-css-plugins/addPseudoClassesPlugin.js +42 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Category.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/DocState.js +185 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Documentable.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Group.js +7 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/ParsedComment.js +73 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Property.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyCategory.js +4 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyGroup.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Rule.js +12 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleCategory.js +4 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleGroup.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeRef.js +5 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeURL.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin.js +49 -0
- data/brut-css/src/js/post-css-plugins/generateRootCustomPropertiesPlugin.js +45 -0
- data/brut-css/src/js/pseudoClassConfigParser.js +145 -0
- data/brut-js/.projections.json +10 -0
- data/brut-js/README.md +118 -0
- data/brut-js/bin/build +10 -0
- data/brut-js/bin/ci +5 -0
- data/brut-js/bin/setup +5 -0
- data/brut-js/docs/README.md +8 -0
- data/brut-js/docs/jsdoc-plugins/customElementTag.js +8 -0
- data/brut-js/docs/jsdoc-theme/publish.js +692 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/linenumber.js +25 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/Apache-License-2.0.txt +202 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/lang-css.js +2 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/prettify.js +28 -0
- data/brut-js/docs/jsdoc-theme/static/styles/jsdoc-default.css +327 -0
- data/brut-js/docs/jsdoc-theme/static/styles/prettify-jsdoc.css +111 -0
- data/brut-js/docs/jsdoc-theme/static/styles/prettify-tomorrow.css +132 -0
- data/brut-js/docs/jsdoc-theme/tmpl/augments.tmpl +10 -0
- data/brut-js/docs/jsdoc-theme/tmpl/container.tmpl +199 -0
- data/brut-js/docs/jsdoc-theme/tmpl/details.tmpl +143 -0
- data/brut-js/docs/jsdoc-theme/tmpl/example.tmpl +2 -0
- data/brut-js/docs/jsdoc-theme/tmpl/examples.tmpl +13 -0
- data/brut-js/docs/jsdoc-theme/tmpl/exceptions.tmpl +32 -0
- data/brut-js/docs/jsdoc-theme/tmpl/layout.tmpl +38 -0
- data/brut-js/docs/jsdoc-theme/tmpl/mainpage.tmpl +14 -0
- data/brut-js/docs/jsdoc-theme/tmpl/members.tmpl +38 -0
- data/brut-js/docs/jsdoc-theme/tmpl/method.tmpl +131 -0
- data/brut-js/docs/jsdoc-theme/tmpl/modifies.tmpl +14 -0
- data/brut-js/docs/jsdoc-theme/tmpl/params.tmpl +131 -0
- data/brut-js/docs/jsdoc-theme/tmpl/properties.tmpl +108 -0
- data/brut-js/docs/jsdoc-theme/tmpl/returns.tmpl +19 -0
- data/brut-js/docs/jsdoc-theme/tmpl/source.tmpl +8 -0
- data/brut-js/docs/jsdoc-theme/tmpl/tutorial.tmpl +19 -0
- data/brut-js/docs/jsdoc-theme/tmpl/type.tmpl +7 -0
- data/brut-js/docs/jsdoc.config.json +23 -0
- data/brut-js/docs/package-lock.json +343 -0
- data/brut-js/docs/package.json +7 -0
- data/brut-js/package-lock.json +2171 -0
- data/brut-js/package.json +32 -0
- data/brut-js/specs/AjaxSubmit.spec.js +256 -0
- data/brut-js/specs/Autosubmit.spec.js +127 -0
- data/brut-js/specs/ConfirmSubmit.spec.js +193 -0
- data/brut-js/specs/ConstraintViolationMessage.spec.js +33 -0
- data/brut-js/specs/ConstraintViolationMessages.spec.js +29 -0
- data/brut-js/specs/CopyToClipboard.spec.js +35 -0
- data/brut-js/specs/Form.spec.js +181 -0
- data/brut-js/specs/I18nTranslation.spec.js +19 -0
- data/brut-js/specs/LocaleDetection.spec.js +22 -0
- data/brut-js/specs/Message.spec.js +15 -0
- data/brut-js/specs/SpecHelper.js +23 -0
- data/brut-js/specs/Tabs.spec.js +41 -0
- data/brut-js/specs/config/asset_metadata.json +7 -0
- data/brut-js/src/AjaxSubmit.js +384 -0
- data/brut-js/src/Autosubmit.js +63 -0
- data/brut-js/src/BaseCustomElement.js +261 -0
- data/brut-js/src/ConfirmSubmit.js +116 -0
- data/brut-js/src/ConfirmationDialog.js +143 -0
- data/brut-js/src/ConstraintViolationMessage.js +125 -0
- data/brut-js/src/ConstraintViolationMessages.js +98 -0
- data/brut-js/src/CopyToClipboard.js +96 -0
- data/brut-js/src/Form.js +151 -0
- data/brut-js/src/I18nTranslation.js +61 -0
- data/brut-js/src/LocaleDetection.js +117 -0
- data/brut-js/src/Logger.js +90 -0
- data/brut-js/src/Message.js +56 -0
- data/brut-js/src/RichString.js +113 -0
- data/brut-js/src/Tabs.js +168 -0
- data/brut-js/src/Tracing.js +247 -0
- data/brut-js/src/appForTestingOnly.js +15 -0
- data/brut-js/src/index.js +130 -0
- data/brut-js/src/testing/AssetMetadata.js +35 -0
- data/brut-js/src/testing/AssetMetadataLoader.js +25 -0
- data/brut-js/src/testing/CustomElementTest.js +235 -0
- data/brut-js/src/testing/DOMCreator.js +45 -0
- data/brut-js/src/testing/index.js +48 -0
- data/brutrb.com/.vitepress/config.mjs +106 -0
- data/brutrb.com/.vitepress/plugins/jsdocLinker.js +34 -0
- data/brutrb.com/.vitepress/plugins/rdocLinker.js +18 -0
- data/brutrb.com/.vitepress/theme/custom.css +7 -0
- data/brutrb.com/.vitepress/theme/index.js +18 -0
- data/brutrb.com/.vitepress/theme/style.css +149 -0
- data/brutrb.com/ai.md +68 -0
- data/brutrb.com/assets.md +138 -0
- data/brutrb.com/bin/build +5 -0
- data/brutrb.com/bin/deploy +7 -0
- data/brutrb.com/bin/dev +5 -0
- data/brutrb.com/bin/setup +5 -0
- data/brutrb.com/brut-js.md +117 -0
- data/brutrb.com/business-logic.md +55 -0
- data/brutrb.com/cli.md +278 -0
- data/brutrb.com/components.md +243 -0
- data/brutrb.com/configuration.md +257 -0
- data/brutrb.com/css.md +103 -0
- data/brutrb.com/custom-element-tests.md +149 -0
- data/brutrb.com/database-access.md +201 -0
- data/brutrb.com/database-schema.md +312 -0
- data/brutrb.com/deployment.md +66 -0
- data/brutrb.com/dev-environment.md +179 -0
- data/brutrb.com/doc-conventions.md +39 -0
- data/brutrb.com/end-to-end-tests.md +174 -0
- data/brutrb.com/flash-and-session.md +224 -0
- data/brutrb.com/forms.md +866 -0
- data/brutrb.com/getting-started.md +66 -0
- data/brutrb.com/handlers.md +153 -0
- data/brutrb.com/hooks.md +178 -0
- data/brutrb.com/i18n.md +188 -0
- data/brutrb.com/images/Makefile +10 -0
- data/brutrb.com/images/dev-env-overview.dot +54 -0
- data/brutrb.com/images/dev-env-overview.png +0 -0
- data/brutrb.com/images/dev-env-protocol.dot +37 -0
- data/brutrb.com/images/dev-env-protocol.png +0 -0
- data/brutrb.com/images/logo-300.png +0 -0
- data/brutrb.com/images/logo.png +0 -0
- data/brutrb.com/images/overview.graffle +0 -0
- data/brutrb.com/images/overview.png +0 -0
- data/brutrb.com/images/spa.dot +19 -0
- data/brutrb.com/images/spa.png +0 -0
- data/brutrb.com/images/workspace-protocol.dot +44 -0
- data/brutrb.com/images/workspace-protocol.png +0 -0
- data/brutrb.com/index.md +36 -0
- data/brutrb.com/instrumentation.md +183 -0
- data/brutrb.com/javascript.md +122 -0
- data/brutrb.com/jobs.md +14 -0
- data/{doc-src → brutrb.com}/keyword-injection.md +122 -68
- data/brutrb.com/markdown-examples.md +85 -0
- data/brutrb.com/middleware.md +80 -0
- data/brutrb.com/not-released.md +5 -0
- data/brutrb.com/overview.md +404 -0
- data/brutrb.com/package-lock.json +2404 -0
- data/brutrb.com/package.json +11 -0
- data/brutrb.com/pages.md +378 -0
- data/brutrb.com/public/images/logo-300.png +0 -0
- data/brutrb.com/public/images/logo.png +0 -0
- data/brutrb.com/routes.md +215 -0
- data/brutrb.com/security.md +105 -0
- data/brutrb.com/seed-data.md +63 -0
- data/brutrb.com/space-time-continuum.md +85 -0
- data/brutrb.com/tutorial.md +3 -0
- data/brutrb.com/unit-tests.md +148 -0
- data/docker-compose.dx.yml +6 -3
- data/docs/404.html +21 -0
- data/docs/CNAME +1 -0
- data/docs/ai.html +24 -0
- data/docs/api/Brut/BackEnd/SeedData.html +493 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +214 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +125 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +125 -0
- data/docs/api/Brut/BackEnd/Sidekiq.html +125 -0
- data/docs/api/Brut/BackEnd/Validators/FormValidator.html +414 -0
- data/docs/api/Brut/BackEnd/Validators.html +128 -0
- data/docs/api/Brut/BackEnd.html +132 -0
- data/docs/api/Brut/CLI/App.html +1576 -0
- data/docs/api/Brut/CLI/AppRunner.html +491 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +264 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +306 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +262 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +314 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets.html +183 -0
- data/docs/api/Brut/CLI/Apps/DB/Create.html +365 -0
- data/docs/api/Brut/CLI/Apps/DB/Drop.html +357 -0
- data/docs/api/Brut/CLI/Apps/DB/Migrate.html +383 -0
- data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +335 -0
- data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +329 -0
- data/docs/api/Brut/CLI/Apps/DB/Seed.html +347 -0
- data/docs/api/Brut/CLI/Apps/DB/Status.html +383 -0
- data/docs/api/Brut/CLI/Apps/DB.html +183 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +303 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +512 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +398 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +374 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +410 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +262 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +303 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +480 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +450 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +380 -0
- data/docs/api/Brut/CLI/Apps/Scaffold.html +253 -0
- data/docs/api/Brut/CLI/Apps/Test/Audit.html +464 -0
- data/docs/api/Brut/CLI/Apps/Test/E2e.html +407 -0
- data/docs/api/Brut/CLI/Apps/Test/JS.html +262 -0
- data/docs/api/Brut/CLI/Apps/Test/Run.html +578 -0
- data/docs/api/Brut/CLI/Apps/Test.html +253 -0
- data/docs/api/Brut/CLI/Apps.html +125 -0
- data/docs/api/Brut/CLI/Command.html +2342 -0
- data/docs/api/Brut/CLI/Error.html +139 -0
- data/docs/api/Brut/CLI/ExecutionResults/Result.html +664 -0
- data/docs/api/Brut/CLI/ExecutionResults.html +675 -0
- data/docs/api/Brut/CLI/Executor.html +430 -0
- data/docs/api/Brut/CLI/InvalidOption.html +245 -0
- data/docs/api/Brut/CLI/Options.html +753 -0
- data/docs/api/Brut/CLI/Output.html +699 -0
- data/docs/api/Brut/CLI/SystemExecError.html +451 -0
- data/docs/api/Brut/CLI.html +263 -0
- data/docs/api/Brut/FactoryBot.html +225 -0
- data/docs/api/Brut/Framework/App.html +1097 -0
- data/docs/api/Brut/Framework/Config.html +1045 -0
- data/docs/api/Brut/Framework/Container.html +1379 -0
- data/docs/api/Brut/Framework/Error.html +140 -0
- data/docs/api/Brut/Framework/Errors/AbstractMethod.html +144 -0
- data/docs/api/Brut/Framework/Errors/Bug.html +234 -0
- data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +257 -0
- data/docs/api/Brut/Framework/Errors/MissingParameter.html +273 -0
- data/docs/api/Brut/Framework/Errors/NoClassForPath.html +471 -0
- data/docs/api/Brut/Framework/Errors/NotFound.html +308 -0
- data/docs/api/Brut/Framework/Errors/NotImplemented.html +234 -0
- data/docs/api/Brut/Framework/Errors.html +328 -0
- data/docs/api/Brut/Framework/FussyTypeEnforcement.html +392 -0
- data/docs/api/Brut/Framework/MCP.html +861 -0
- data/docs/api/Brut/Framework/ProjectEnvironment.html +648 -0
- data/docs/api/Brut/Framework.html +129 -0
- data/docs/api/Brut/FrontEnd/AssetPathResolver.html +317 -0
- data/docs/api/Brut/FrontEnd/Component/Helpers.html +326 -0
- data/docs/api/Brut/FrontEnd/Component.html +365 -0
- data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +470 -0
- data/docs/api/Brut/FrontEnd/Components/FormTag.html +518 -0
- data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +317 -0
- data/docs/api/Brut/FrontEnd/Components/Input.html +195 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +339 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +660 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +417 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +918 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +651 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs.html +125 -0
- data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +367 -0
- data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +336 -0
- data/docs/api/Brut/FrontEnd/Components/TimeTag.html +655 -0
- data/docs/api/Brut/FrontEnd/Components/Traceparent.html +352 -0
- data/docs/api/Brut/FrontEnd/Components.html +135 -0
- data/docs/api/Brut/FrontEnd/Download.html +467 -0
- data/docs/api/Brut/FrontEnd/Flash.html +1150 -0
- data/docs/api/Brut/FrontEnd/Form.html +1157 -0
- data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +634 -0
- data/docs/api/Brut/FrontEnd/Forms/Input.html +615 -0
- data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +547 -0
- data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1318 -0
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +609 -0
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +587 -0
- data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +613 -0
- data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +582 -0
- data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +609 -0
- data/docs/api/Brut/FrontEnd/Forms.html +127 -0
- data/docs/api/Brut/FrontEnd/GenericResponse.html +377 -0
- data/docs/api/Brut/FrontEnd/Handler.html +442 -0
- data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +318 -0
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +336 -0
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +399 -0
- data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +354 -0
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +151 -0
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +315 -0
- data/docs/api/Brut/FrontEnd/Handlers.html +125 -0
- data/docs/api/Brut/FrontEnd/HandlingResults.html +339 -0
- data/docs/api/Brut/FrontEnd/HttpMethod.html +661 -0
- data/docs/api/Brut/FrontEnd/HttpStatus.html +496 -0
- data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +284 -0
- data/docs/api/Brut/FrontEnd/Layout.html +318 -0
- data/docs/api/Brut/FrontEnd/Middleware.html +135 -0
- data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +288 -0
- data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +292 -0
- data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +324 -0
- data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +372 -0
- data/docs/api/Brut/FrontEnd/Middlewares.html +125 -0
- data/docs/api/Brut/FrontEnd/Page.html +773 -0
- data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +797 -0
- data/docs/api/Brut/FrontEnd/Pages.html +125 -0
- data/docs/api/Brut/FrontEnd/RequestContext.html +1312 -0
- data/docs/api/Brut/FrontEnd/RouteHook.html +424 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +242 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +249 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +264 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +261 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +284 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +252 -0
- data/docs/api/Brut/FrontEnd/RouteHooks.html +115 -0
- data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +227 -0
- data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +305 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +324 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +319 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +315 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +315 -0
- data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +327 -0
- data/docs/api/Brut/FrontEnd/Routing/Route.html +761 -0
- data/docs/api/Brut/FrontEnd/Routing.html +927 -0
- data/docs/api/Brut/FrontEnd/Session.html +1195 -0
- data/docs/api/Brut/FrontEnd.html +134 -0
- data/docs/api/Brut/I18n/BaseMethods.html +931 -0
- data/docs/api/Brut/I18n/ForBackEnd.html +302 -0
- data/docs/api/Brut/I18n/ForCLI.html +302 -0
- data/docs/api/Brut/I18n/ForHTML.html +296 -0
- data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +316 -0
- data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +930 -0
- data/docs/api/Brut/I18n.html +127 -0
- data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +435 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +286 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +302 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry.html +864 -0
- data/docs/api/Brut/Instrumentation.html +126 -0
- data/docs/api/Brut/SinatraHelpers/ClassMethods.html +532 -0
- data/docs/api/Brut/SinatraHelpers.html +281 -0
- data/docs/api/Brut/SpecSupport/ClockSupport.html +383 -0
- data/docs/api/Brut/SpecSupport/ComponentSupport.html +502 -0
- data/docs/api/Brut/SpecSupport/E2ETestServer.html +503 -0
- data/docs/api/Brut/SpecSupport/E2eSupport.html +142 -0
- data/docs/api/Brut/SpecSupport/EnhancedNode.html +403 -0
- data/docs/api/Brut/SpecSupport/FlashSupport.html +278 -0
- data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +401 -0
- data/docs/api/Brut/SpecSupport/GeneralSupport.html +195 -0
- data/docs/api/Brut/SpecSupport/HandlerSupport.html +160 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +553 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +439 -0
- data/docs/api/Brut/SpecSupport/Matchers.html +125 -0
- data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +335 -0
- data/docs/api/Brut/SpecSupport/RSpecSetup.html +602 -0
- data/docs/api/Brut/SpecSupport/SessionSupport.html +196 -0
- data/docs/api/Brut/SpecSupport.html +129 -0
- data/docs/api/Brut.html +225 -0
- data/docs/api/Clock.html +603 -0
- data/docs/api/RichString.html +968 -0
- data/docs/api/SemanticLogger/Appender/Async.html +219 -0
- data/docs/api/Sequel/Extensions/BrutInstrumentation.html +115 -0
- data/docs/api/Sequel/Extensions/BrutMigrations.html +533 -0
- data/docs/api/Sequel/Extensions.html +117 -0
- data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +105 -0
- data/docs/api/Sequel/Plugins/CreatedAt.html +125 -0
- data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +207 -0
- data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +186 -0
- data/docs/api/Sequel/Plugins/ExternalId.html +218 -0
- data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +202 -0
- data/docs/api/Sequel/Plugins/FindBang.html +125 -0
- data/docs/api/Sequel/Plugins.html +117 -0
- data/docs/api/Sequel.html +117 -0
- data/docs/api/_index.html +1553 -0
- data/docs/api/class_list.html +54 -0
- data/docs/api/css/common.css +1 -0
- data/docs/api/css/full_list.css +58 -0
- data/docs/api/css/style.css +503 -0
- data/docs/api/file.README.html +127 -0
- data/docs/api/file_list.html +59 -0
- data/docs/api/frames.html +22 -0
- data/docs/api/index.html +127 -0
- data/docs/api/js/app.js +344 -0
- data/docs/api/js/full_list.js +242 -0
- data/docs/api/js/jquery.js +4 -0
- data/docs/api/method_list.html +3998 -0
- data/docs/api/top-level-namespace.html +112 -0
- data/docs/assets/ai.md.tZrjP9im.js +1 -0
- data/docs/assets/ai.md.tZrjP9im.lean.js +1 -0
- data/docs/assets/app.D_yaTITQ.js +1 -0
- data/docs/assets/assets.md.D3wunzLx.js +19 -0
- data/docs/assets/assets.md.D3wunzLx.lean.js +1 -0
- data/docs/assets/brut-js.md.o2DAO2s2.js +12 -0
- data/docs/assets/brut-js.md.o2DAO2s2.lean.js +1 -0
- data/docs/assets/business-logic.md.BY4hGy0m.js +1 -0
- data/docs/assets/business-logic.md.BY4hGy0m.lean.js +1 -0
- data/docs/assets/chunks/@localSearchIndexroot.BsN5i0Fi.js +1 -0
- data/docs/assets/chunks/VPLocalSearchBox.B2-ZzyTY.js +8 -0
- data/docs/assets/chunks/framework.1L-BeKqY.js +18 -0
- data/docs/assets/chunks/theme.CfGFVRvE.js +2 -0
- data/docs/assets/cli.md.RmeA2b0i.js +127 -0
- data/docs/assets/cli.md.RmeA2b0i.lean.js +1 -0
- data/docs/assets/components.md.eCttGlN-.js +104 -0
- data/docs/assets/components.md.eCttGlN-.lean.js +1 -0
- data/docs/assets/configuration.md.BRriU0cL.js +78 -0
- data/docs/assets/configuration.md.BRriU0cL.lean.js +1 -0
- data/docs/assets/css.md.DJgj2clw.js +21 -0
- data/docs/assets/css.md.DJgj2clw.lean.js +1 -0
- data/docs/assets/custom-element-tests.md.BrYJQEl3.js +69 -0
- data/docs/assets/custom-element-tests.md.BrYJQEl3.lean.js +1 -0
- data/docs/assets/database-access.md.C7l-Vuvb.js +63 -0
- data/docs/assets/database-access.md.C7l-Vuvb.lean.js +1 -0
- data/docs/assets/database-schema.md.BUjR0VS1.js +63 -0
- data/docs/assets/database-schema.md.BUjR0VS1.lean.js +1 -0
- data/docs/assets/deployment.md.Dbka4OTr.js +1 -0
- data/docs/assets/deployment.md.Dbka4OTr.lean.js +1 -0
- data/docs/assets/dev-env-overview.Gj7NWM8-.png +0 -0
- data/docs/assets/dev-env-protocol.DysDAtnz.png +0 -0
- data/docs/assets/dev-environment.md.BNc8AYiK.js +11 -0
- data/docs/assets/dev-environment.md.BNc8AYiK.lean.js +1 -0
- data/docs/assets/doc-conventions.md.DCfRXXi-.js +1 -0
- data/docs/assets/doc-conventions.md.DCfRXXi-.lean.js +1 -0
- data/docs/assets/end-to-end-tests.md.yfQHC0b5.js +26 -0
- data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +1 -0
- data/docs/assets/flash-and-session.md.BXY8RvT0.js +93 -0
- data/docs/assets/flash-and-session.md.BXY8RvT0.lean.js +1 -0
- data/docs/assets/forms.md.CBTYQ_Cz.js +379 -0
- data/docs/assets/forms.md.CBTYQ_Cz.lean.js +1 -0
- data/docs/assets/getting-started.md.Bz2s1Vjb.js +2 -0
- data/docs/assets/getting-started.md.Bz2s1Vjb.lean.js +1 -0
- data/docs/assets/handlers.md.089DVD3v.js +69 -0
- data/docs/assets/handlers.md.089DVD3v.lean.js +1 -0
- data/docs/assets/hooks.md.C4-moMny.js +80 -0
- data/docs/assets/hooks.md.C4-moMny.lean.js +1 -0
- data/docs/assets/i18n.md.Do9i1qWl.js +23 -0
- data/docs/assets/i18n.md.Do9i1qWl.lean.js +1 -0
- data/docs/assets/index.md.B28EwVpq.js +1 -0
- data/docs/assets/index.md.B28EwVpq.lean.js +1 -0
- data/docs/assets/instrumentation.md.CL6ax7nT.js +35 -0
- data/docs/assets/instrumentation.md.CL6ax7nT.lean.js +1 -0
- data/docs/assets/javascript.md.GWbhRS51.js +31 -0
- data/docs/assets/javascript.md.GWbhRS51.lean.js +1 -0
- data/docs/assets/jobs.md.S-2amAYp.js +1 -0
- data/docs/assets/jobs.md.S-2amAYp.lean.js +1 -0
- data/docs/assets/keyword-injection.md.Dt2tKREs.js +25 -0
- data/docs/assets/keyword-injection.md.Dt2tKREs.lean.js +1 -0
- data/docs/assets/markdown-examples.md.CCFEQO44.js +33 -0
- data/docs/assets/markdown-examples.md.CCFEQO44.lean.js +1 -0
- data/docs/assets/middleware.md.Czz_UlJN.js +20 -0
- data/docs/assets/middleware.md.Czz_UlJN.lean.js +1 -0
- data/docs/assets/not-released.md.BBy28McC.js +1 -0
- data/docs/assets/not-released.md.BBy28McC.lean.js +1 -0
- data/docs/assets/overview.Da81cB9R.png +0 -0
- data/docs/assets/overview.md.CDalkuxV.js +133 -0
- data/docs/assets/overview.md.CDalkuxV.lean.js +1 -0
- data/docs/assets/pages.md.BE3kfOc5.js +122 -0
- data/docs/assets/pages.md.BE3kfOc5.lean.js +1 -0
- data/docs/assets/routes.md.BMM7peut.js +29 -0
- data/docs/assets/routes.md.BMM7peut.lean.js +1 -0
- data/docs/assets/security.md.C668yXCi.js +1 -0
- data/docs/assets/security.md.C668yXCi.lean.js +1 -0
- data/docs/assets/seed-data.md.BvFZlqIk.js +14 -0
- data/docs/assets/seed-data.md.BvFZlqIk.lean.js +1 -0
- data/docs/assets/spa.qejUdp-5.png +0 -0
- data/docs/assets/space-time-continuum.md.KPUIKysQ.js +1 -0
- data/docs/assets/space-time-continuum.md.KPUIKysQ.lean.js +1 -0
- data/docs/assets/style.D73IYGCX.css +1 -0
- data/docs/assets/tutorial.md.BnoGjrdK.js +1 -0
- data/docs/assets/tutorial.md.BnoGjrdK.lean.js +1 -0
- data/docs/assets/unit-tests.md.DUGrnLj5.js +13 -0
- data/docs/assets/unit-tests.md.DUGrnLj5.lean.js +1 -0
- data/docs/assets/workspace-protocol.C0gXsoDb.png +0 -0
- data/docs/assets.html +42 -0
- data/docs/brut-css/brut.css +1 -0
- data/docs/brut-css/brut.max.css +22372 -0
- data/docs/brut-css/classes/appearances.html +783 -0
- data/docs/brut-css/classes/background-colors.html +3529 -0
- data/docs/brut-css/classes/border-colors.html +3529 -0
- data/docs/brut-css/classes/borders.html +2293 -0
- data/docs/brut-css/classes/dimensions.html +2581 -0
- data/docs/brut-css/classes/flex.html +917 -0
- data/docs/brut-css/classes/foreground-colors.html +3261 -0
- data/docs/brut-css/classes/junk-drawer.html +431 -0
- data/docs/brut-css/classes/layout.html +668 -0
- data/docs/brut-css/classes/lists.html +331 -0
- data/docs/brut-css/classes/positioning.html +1751 -0
- data/docs/brut-css/classes/spacings.html +2633 -0
- data/docs/brut-css/classes/typography.html +2206 -0
- data/docs/brut-css/customization/advanced-configuration.html +204 -0
- data/docs/brut-css/customization/breakpoints.html +227 -0
- data/docs/brut-css/customization/design-system.html +197 -0
- data/docs/brut-css/customization/pseudo-classes.html +228 -0
- data/docs/brut-css/docs.css +98 -0
- data/docs/brut-css/getting-started/core-concepts.html +234 -0
- data/docs/brut-css/getting-started/installation.html +190 -0
- data/docs/brut-css/getting-started/overview.html +210 -0
- data/docs/brut-css/getting-started/simple-example.html +285 -0
- data/docs/brut-css/index.html +193 -0
- data/docs/brut-css/prism-twilight.min.css +1 -0
- data/docs/brut-css/properties/colors.html +1548 -0
- data/docs/brut-css/properties/spacings.html +614 -0
- data/docs/brut-css/properties/typography.html +777 -0
- data/docs/brut-js/api/AjaxSubmit.html +374 -0
- data/docs/brut-js/api/AjaxSubmit.js.html +435 -0
- data/docs/brut-js/api/Autosubmit.html +192 -0
- data/docs/brut-js/api/Autosubmit.js.html +114 -0
- data/docs/brut-js/api/BaseCustomElement.html +1091 -0
- data/docs/brut-js/api/BaseCustomElement.js.html +312 -0
- data/docs/brut-js/api/BrutCustomElements.html +172 -0
- data/docs/brut-js/api/BufferedLogger.html +173 -0
- data/docs/brut-js/api/ConfirmSubmit.html +278 -0
- data/docs/brut-js/api/ConfirmSubmit.js.html +167 -0
- data/docs/brut-js/api/ConfirmationDialog.html +425 -0
- data/docs/brut-js/api/ConfirmationDialog.js.html +194 -0
- data/docs/brut-js/api/ConstraintViolationMessage.html +448 -0
- data/docs/brut-js/api/ConstraintViolationMessage.js.html +176 -0
- data/docs/brut-js/api/ConstraintViolationMessages.html +590 -0
- data/docs/brut-js/api/ConstraintViolationMessages.js.html +149 -0
- data/docs/brut-js/api/CopyToClipboard.html +345 -0
- data/docs/brut-js/api/CopyToClipboard.js.html +147 -0
- data/docs/brut-js/api/Form.html +294 -0
- data/docs/brut-js/api/Form.js.html +202 -0
- data/docs/brut-js/api/I18nTranslation.html +409 -0
- data/docs/brut-js/api/I18nTranslation.js.html +112 -0
- data/docs/brut-js/api/LocaleDetection.html +312 -0
- data/docs/brut-js/api/LocaleDetection.js.html +168 -0
- data/docs/brut-js/api/Logger.html +702 -0
- data/docs/brut-js/api/Logger.js.html +141 -0
- data/docs/brut-js/api/Message.html +238 -0
- data/docs/brut-js/api/Message.js.html +107 -0
- data/docs/brut-js/api/PrefixedLogger.html +369 -0
- data/docs/brut-js/api/RichString.html +1049 -0
- data/docs/brut-js/api/RichString.js.html +164 -0
- data/docs/brut-js/api/Tabs.html +295 -0
- data/docs/brut-js/api/Tabs.js.html +219 -0
- data/docs/brut-js/api/Tracing.html +277 -0
- data/docs/brut-js/api/Tracing.js.html +298 -0
- data/docs/brut-js/api/external-CustomElementRegistry.html +140 -0
- data/docs/brut-js/api/external-Performance.html +138 -0
- data/docs/brut-js/api/external-Promise.html +138 -0
- data/docs/brut-js/api/external-ValidityState.html +138 -0
- data/docs/brut-js/api/external-Window.html +233 -0
- data/docs/brut-js/api/external-fetch.html +138 -0
- data/docs/brut-js/api/global.html +400 -0
- data/docs/brut-js/api/index.html +168 -0
- data/docs/brut-js/api/index.js.html +181 -0
- data/docs/brut-js/api/module-testing.html +383 -0
- data/docs/brut-js/api/scripts/linenumber.js +25 -0
- data/docs/brut-js/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- data/docs/brut-js/api/scripts/prettify/lang-css.js +2 -0
- data/docs/brut-js/api/scripts/prettify/prettify.js +28 -0
- data/docs/brut-js/api/styles/jsdoc-default.css +327 -0
- data/docs/brut-js/api/styles/prettify-jsdoc.css +111 -0
- data/docs/brut-js/api/styles/prettify-tomorrow.css +132 -0
- data/docs/brut-js/api/testing.AssetMetadata.html +172 -0
- data/docs/brut-js/api/testing.AssetMetadataLoader.html +171 -0
- data/docs/brut-js/api/testing.CustomElementTest.html +679 -0
- data/docs/brut-js/api/testing.DOMCreator.html +171 -0
- data/docs/brut-js/api/testing_AssetMetadata.js.html +86 -0
- data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +76 -0
- data/docs/brut-js/api/testing_CustomElementTest.js.html +286 -0
- data/docs/brut-js/api/testing_DOMCreator.js.html +96 -0
- data/docs/brut-js/api/testing_index.js.html +99 -0
- data/docs/brut-js.html +35 -0
- data/docs/business-logic.html +24 -0
- data/docs/cli.html +150 -0
- data/docs/components.html +127 -0
- data/docs/configuration.html +101 -0
- data/docs/css.html +44 -0
- data/docs/custom-element-tests.html +92 -0
- data/docs/database-access.html +86 -0
- data/docs/database-schema.html +86 -0
- data/docs/deployment.html +24 -0
- data/docs/dev-environment.html +34 -0
- data/docs/doc-conventions.html +24 -0
- data/docs/end-to-end-tests.html +49 -0
- data/docs/flash-and-session.html +116 -0
- data/docs/forms.html +402 -0
- data/docs/getting-started.html +25 -0
- data/docs/handlers.html +92 -0
- data/docs/hashmap.json +1 -0
- data/docs/hooks.html +103 -0
- data/docs/i18n.html +46 -0
- data/docs/images/logo-300.png +0 -0
- data/docs/images/logo.png +0 -0
- data/docs/index.html +24 -0
- data/docs/instrumentation.html +58 -0
- data/docs/javascript.html +54 -0
- data/docs/jobs.html +24 -0
- data/docs/keyword-injection.html +48 -0
- data/docs/markdown-examples.html +56 -0
- data/docs/middleware.html +43 -0
- data/docs/not-released.html +24 -0
- data/docs/overview.html +156 -0
- data/docs/pages.html +145 -0
- data/docs/routes.html +52 -0
- data/docs/security.html +24 -0
- data/docs/seed-data.html +37 -0
- data/docs/space-time-continuum.html +24 -0
- data/docs/tutorial.html +24 -0
- data/docs/unit-tests.html +36 -0
- data/docs/vp-icons.css +1 -0
- data/lib/brut/back_end/seed_data.rb +19 -2
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
- data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
- data/lib/brut/back_end/sidekiq.rb +2 -1
- data/lib/brut/back_end/validator.rb +5 -1
- data/lib/brut/back_end.rb +4 -2
- data/lib/brut/cli/app_runner.rb +1 -1
- data/lib/brut/cli/apps/test.rb +5 -0
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/factory_bot.rb +0 -5
- data/lib/brut/framework/app.rb +70 -5
- data/lib/brut/framework/config.rb +5 -3
- data/lib/brut/framework/container.rb +3 -2
- data/lib/brut/framework/errors.rb +12 -4
- data/lib/brut/framework/mcp.rb +58 -1
- data/lib/brut/framework/project_environment.rb +6 -2
- data/lib/brut/framework.rb +1 -1
- data/lib/brut/front_end/component.rb +69 -71
- data/lib/brut/front_end/components/constraint_violations.rb +1 -4
- data/lib/brut/front_end/components/form_tag.rb +1 -1
- data/lib/brut/front_end/components/input.rb +3 -3
- data/lib/brut/front_end/components/inputs/csrf_token.rb +1 -1
- data/lib/brut/front_end/components/inputs/{text_field.rb → input_tag.rb} +7 -9
- data/lib/brut/front_end/components/inputs/radio_button.rb +1 -1
- data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +187 -0
- data/lib/brut/front_end/components/inputs/{textarea.rb → textarea_tag.rb} +2 -2
- data/lib/brut/front_end/components/time_tag.rb +2 -1
- data/lib/brut/front_end/form.rb +4 -4
- data/lib/brut/front_end/forms/input.rb +2 -1
- data/lib/brut/front_end/forms/input_definition.rb +5 -2
- data/lib/brut/front_end/forms/radio_button_group_input.rb +2 -1
- data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +2 -2
- data/lib/brut/front_end/forms/select_input.rb +2 -4
- data/lib/brut/front_end/forms/select_input_definition.rb +2 -2
- data/lib/brut/front_end/handler.rb +28 -26
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +5 -2
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +8 -4
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -5
- data/lib/brut/front_end/handlers/missing_handler.rb +5 -2
- data/lib/brut/front_end/layout.rb +16 -0
- data/lib/brut/front_end/page.rb +52 -29
- data/lib/brut/front_end/request_context.rb +3 -2
- data/lib/brut/front_end/routing.rb +5 -1
- data/lib/brut/front_end.rb +4 -13
- data/lib/brut/i18n/base_methods.rb +167 -79
- data/lib/brut/i18n/for_back_end.rb +4 -0
- data/lib/brut/i18n/for_cli.rb +4 -0
- data/lib/brut/i18n/for_html.rb +32 -4
- data/lib/brut/i18n/http_accept_language.rb +47 -0
- data/lib/brut/instrumentation/open_telemetry.rb +36 -1
- data/lib/brut/instrumentation.rb +3 -5
- data/lib/brut/sinatra_helpers.rb +11 -3
- data/lib/brut/spec_support/component_support.rb +30 -16
- data/lib/brut/spec_support/e2e_support.rb +1 -1
- data/lib/brut/spec_support/e2e_test_server.rb +3 -0
- data/lib/brut/spec_support/general_support.rb +3 -0
- data/lib/brut/spec_support/handler_support.rb +6 -1
- data/lib/brut/spec_support/matcher.rb +1 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +2 -5
- data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
- data/lib/brut/spec_support.rb +1 -1
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +5 -4
- data/lib/sequel/extensions/brut_migrations.rb +1 -1
- metadata +648 -13
- data/doc-src/architecture.md +0 -102
- data/doc-src/assets.md +0 -98
- data/doc-src/forms.md +0 -214
- data/doc-src/handlers.md +0 -83
- data/doc-src/javascript.md +0 -265
- data/doc-src/pages.md +0 -210
- data/doc-src/route-hooks.md +0 -59
- data/lib/brut/front_end/components/inputs/select.rb +0 -117
@@ -0,0 +1,63 @@
|
|
1
|
+
import{_ as a,c as s,o as i,ag as t}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Database Schema / Migrations","description":"","frontmatter":{},"headers":[],"relativePath":"database-schema.md","filePath":"database-schema.md"}'),n={name:"database-schema.md"};function l(o,e,r,h,d,p){return i(),s("div",null,e[0]||(e[0]=[t(`<h1 id="database-schema-migrations" tabindex="-1">Database Schema / Migrations <a class="header-anchor" href="#database-schema-migrations" aria-label="Permalink to "Database Schema / Migrations""></a></h1><p>Brut provides access to the database via the <a href="https://sequel.jeremyevans.net/" target="_blank" rel="noreferrer">Sequel library</a>. Sequel is fully featured and provides a lot of ways of interacting with and managing your database. Brut includes several plugins and extensions to provide opinionated default behavior or additional features.</p><p>One thing to keep in mind is that Brut refers to your database layer as <em>database models</em> (notably not the un-qualified "models"). Brut treats this layer as a <em>model</em> of your database, not a model of your <em>domain</em> (though you are free to conflate the two).</p><p>This section details how to manage your database schema.</p><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>Brut currently only supports Postgres. Sequel supports many database systems, however Brut's extensions are currently geared toward Postgres only.</p></div><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to "Overview""></a></h2><p>Brut uses <em>migrations</em> to control and manage the schema of your database. Migrations are changes to the schema that depend on the changes before them. In a running production database, you will not be able to create the database schema from scratch—you will have to modify the existing schema to produce the schema you want.</p><p>For example, if you have a table <code>widgets</code> that has a <code>name</code> and <code>description</code>, to add a <code>status</code> field, you cannot <code>drop table widgets</code> and then <code>create table widgets(...)</code> with the fields. You must instead <code>alter table widgets(...)</code> to add the new column.</p><p>Thus, each migration file is a change to the schema produced by all previous migration files.</p><p>Brut's provides this via Sequels. See <a href="https://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html" target="_blank" rel="noreferrer">both</a> <a href="https://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html" target="_blank" rel="noreferrer">docs</a> for details on the API. Any schema modification method Sequel documents is available, however some default behavior has changed.</p><p>Schema files are located in <code>app/src/back_end/data_models/migrations</code> and are named using a timestamp-based scheme. This means that when you create a new migration, its name will be based on the time and date you created it, and any migrations that have not been applied will be applied in timestamp order.</p><h3 id="creating-migrations" tabindex="-1">Creating Migrations <a class="header-anchor" href="#creating-migrations" aria-label="Permalink to "Creating Migrations""></a></h3><p>To create a migration, use <code>bin/db new-migration</code>. It accepts any number of arguments that will be joined together to form the filename:</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 new-migration user accounts</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/20250508132646_user-accounts.rb</span></span></code></pre></div><p>The file is created mostly blank:</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:#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>
|
4
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> up </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
|
5
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
6
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Sequels' migration system is similar to Active Record's/Rails' in design and spirit, but the API is different. Please consult the documentation and don't assume Active Record's DSL will work. It will not.</p><p>One thing to note is that Brut encourages the creation of only "up" migrations. That is, migrations that change a database. "Down" migrations, which revert a change, are discouraged. See <em>Recommended Practices</em> for a detailed explanation.</p><p>This is also why Sequel's <code>change</code> method is not included in the scaffolded code. <code>change</code>, like Active Record's method of the same name, automagically creates both "up" and "down" migrations, but <em>only</em> if you use the DSL. If you use raw SQL, <code>change</code> doesn't work. But that doesn't matter for Brut (again, see <em>Recommended Practices</em>).</p><p>Let's create a user accounts table that has an email field, a <code>deactivated_at</code> timestamp, and a <code>created_at</code> timestamp:</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:#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>
|
7
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> up </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
|
8
|
+
<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>
|
9
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "People or systems who can access this system"</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
|
10
|
+
<span class="line"></span>
|
11
|
+
<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>
|
12
|
+
<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>
|
13
|
+
<span class="line"></span>
|
14
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
15
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
16
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>A few notes that aren't obvious without knowing about Brut's extensions:</p><ul><li><code>comment:</code> is required. You must provide documentation about what table is for</li><li>The table has a primary key named <code>id</code> of type <code>int</code> that is a serial.</li><li><code>created_at</code> is created by default, with time <code>timestamptz</code> (AKA <code>timestamp with time zone</code>, see <a href="/space-time-continuum.html">Space/Time Continuum</a>).</li><li><code>email</code> is not null by default. <code>deactivated_at</code> <em>is</em> null because it's specified as such.</li></ul><p>To apply this migration use <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><p>If you create a new migration, it will use a timestamp that is alphanumerically greater than the one we just made and thus that migration will be applied after this one. Thus, you can rely on previous migrations having been applied when authoring new ones.</p><h3 id="managing-migrations" tabindex="-1">Managing Migrations <a class="header-anchor" href="#managing-migrations" aria-label="Permalink to "Managing Migrations""></a></h3><p>Sequel uses a special database table to understand which migrations have been run. This table will exist in production and prevent you from applying migrations twice or skipping a migration.</p><p>Note that managing a production database in this way requires knowledge of both your database system and the data itself. Brut can only provide so much to make this process manageable. You should consult <a href="https://github.com/ankane/strong_migrations?tab=readme-ov-file" target="_blank" rel="noreferrer">Strong Migrations' README</a> and learn it deeply. Although it's targeted at Rails developers, the information here applies to any database management system.</p><h3 id="brut-extensions-and-changes-in-sequel-s-behavior" tabindex="-1">Brut Extensions and Changes in Sequel's Behavior <a class="header-anchor" href="#brut-extensions-and-changes-in-sequel-s-behavior" aria-label="Permalink to "Brut Extensions and Changes in Sequel's Behavior""></a></h3><p>Brut includes the following standard plugins and extensions:</p><ul><li><a href="https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/pg_array_rb.html" target="_blank" rel="noreferrer"><code>pg_array</code></a></li><li><a href="https://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/pg_json_rb.html" target="_blank" rel="noreferrer"><code>pg_json</code></a></li><li><a href="https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/TableSelect.html" target="_blank" rel="noreferrer"><code>table_select</code></a>, which changes queries to prepend <code>*</code> with the table name, e.g. <code>select accounts.*</code> instead of <code>select *</code>.</li><li><a href="https://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/SkipSavingColumns.html" target="_blank" rel="noreferrer"><code>skip_saving_columns</code></a> which will skip saving columns that the database generates.</li></ul><p>Brut also provides the following plugins and behavior changes:</p><ul><li><code>Sequel::Extensions::BrutInstrumentation</code>, which adds OpenTelemetry instrumentation to Sequel (see <a href="/instrumentation.html">Instrumentation</a>).</li><li><code>Sequel::Plugins::FindBang</code>, which adds <code>find!</code> to all models. This wraps Sequel's <code>first!</code> method, but provides a more helpful error message when no records are found</li><li><code>Sequel::Plugins::CreatedAt</code>, which automatically sets <code>created_at</code> when a record is created.</li><li><code>Sequel::Plugins::ExternalId</code>, which adds support for external IDs (see below)</li><li><code>Sequel::Extensions::BrutMigrations</code>, which enhances the migrations API (see below)</li></ul><h4 id="external-ids" tabindex="-1">External IDs <a class="header-anchor" href="#external-ids" aria-label="Permalink to "External IDs""></a></h4><p>It's often useful to provide a unique identifier for a record that is not the database primary key. There are many advantages to doing so, but the core value Brut has regarding this is that database primary and foreign keys are considered private and internal and for developer use only. It is trivial to produce externalizable keys, as you'll see, so there's no reason to expose primary keys.</p><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>Take care to differentiate the terms <em>primary key</em> and <em>key</em>. In relational database literature, and thus in Brut, a <em>key</em> is any value that uniquely identifies a record. <code>email</code> in the <code>accounts</code> table above is a key. The <em>primary key</em> is single source of truth for identifying records internally in the database. Thus, it can be used as a <em>foreign key</em> to create relationships between tables. Brut further implements this as a <em>surrogate</em> or <em>synthetic</em> key, which means the value itself has no business or domain meaning.</p></div><p>In Brut, an external ID is automatically generated by the database when a record is created. By convention, it is prefixed with a short string representing your app and a short string representing the table, followed by a unique hash.</p><p>For example, if our app's prefix is, say, "my" (for "my app"), and the accounts table's prefix is "ac" (for "accounts"), an external ID might look like <code>myac_3457238947239487</code>. This double-prefixing is extremely useful when sharing these values with the outside world. You can immediately identify an ID from your app <em>and</em> know what sort of thing it refers to.</p><p>To use external IDs in Brut, you must do three things:</p><ol><li><p>You must set your external ID prefix in <code>app/src/app.rb</code>. This should have been done when you created your Brut app, but it looks like so:</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;"> App</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</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;">App</span></span>
|
17
|
+
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
|
18
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span></span>
|
19
|
+
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
|
20
|
+
<span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">container</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">override</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"external_id_prefix"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"my"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
21
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
22
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> </span></span>
|
23
|
+
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
|
24
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div></li><li><p>When creating the table in a migration, use <code>external_id: true</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:#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>
|
25
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> up </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
|
26
|
+
<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>
|
27
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "People or systems who can access this system"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
28
|
+
<span class="line highlighted"><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>
|
29
|
+
<span class="line"></span>
|
30
|
+
<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>
|
31
|
+
<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>
|
32
|
+
<span class="line"></span>
|
33
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
34
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
35
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div></li><li><p>In your <a href="/database-access.html">data model class</a>, use <code>has_external_id</code> to specify the prefix for this table:</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>class DB::Account < AppDataModel</span></span>
|
36
|
+
<span class="line"><span> has_external_id :ac</span></span>
|
37
|
+
<span class="line"><span></span></span>
|
38
|
+
<span class="line"><span> # ...</span></span>
|
39
|
+
<span class="line"><span>end</span></span></code></pre></div></li></ol><p>Brut creates the external ID using Ruby code as part of Sequel's lifecycle hooks. It's only set a) on creation, and b) if there is no value provided when creating the record.</p><p>This means that you can set values explicitly if you like, <em>and</em> you can change them later. This is useful if you shared the value with someone you didn't mean to. Because these external IDs aren't use for referential integrity/foreign keys, they can be changed at any time, as long as the value is unique (which will be enforced by the database).</p><h3 id="brut-migration-changes-and-enhancement" tabindex="-1">Brut Migration Changes and Enhancement <a class="header-anchor" href="#brut-migration-changes-and-enhancement" aria-label="Permalink to "Brut Migration Changes and Enhancement""></a></h3><p>Brut attempts to set default behavior for migrations to encourage a modicum of best practices. This lists out the changes and a brief explanation for the purpose of the change.</p><ul><li><p><strong>Automatic synthetic primary key named <code>id</code> of type <code>int</code>.</strong> You almost always want this. You can change the primary key configuration per table if you like, but if you do nothing, you get a primary key that works for 99% of your needs.</p></li><li><p><strong>Automatic <code>created_at</code> of type <code>timestamptz</code>.</strong> It's a good practice to store the date a record was created. This can help with debugging and provide a reliable sort key for data that otherwise has none. It uses <code>timestamp with time zone</code>, which you are encouraged to use always. See <a href="/space-time-continuum.html">Space/Time Continuum</a> for details.</p></li><li><p><strong>No automatic <code>updated_at</code>.</strong> While you are free to add <code>updated_at</code>, in practice this column creates more problems than it solves. If you need to know when data has changed, it is almost always better to do this with an audit table, event log, or special-purpose field.</p></li><li><p><strong><code>create_table</code> requires <code>comment:</code>.</strong> Just document your tables. It takes two seconds and can save a lot of time later.</p></li><li><p><strong>Support for external IDs via <code>external_id:</code>.</strong> As discussed above, this will create a unique <code>external_id</code> column on your table and ensure it has a value on creation.</p></li><li><p><strong>Columns are <code>NOT NULL</code> by default.</strong> Null is not a valid value. In many cases, your columns should not allow <code>NULL</code> (<code>nil</code>), so in Brut apps, you must opt into nullable columns. You can use <code>null: true</code> to make a column nullable.</p></li><li><p><strong>Foreign keys are <code>NOT NULL</code> and have an index created for them by default.</strong> Foreign keys should rarely be <code>NULL</code> and you almost always want an index on them, since you are likely to using them in queries, e.g. <code>account.widgets</code> would join on <code>accounts.widget_id</code>. You can opt out of either via <code>null: true</code> and <code>index: false</code>.</p></li><li><p><strong>The method <code>key</code> allows you to specify a non-primary key, AKA a unique index</strong>. Suppose our <code>accounts</code> table allowed duplicate email addresses, but only one per <code>organization_id</code>. You'd model this by creating a unique index on <code>(email,organization_id)</code>. In Brut:</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:#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>
|
40
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> up </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
|
41
|
+
<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>
|
42
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> comment:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "People or systems who can access this system"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
43
|
+
<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>
|
44
|
+
<span class="line"></span>
|
45
|
+
<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>
|
46
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> foreign_key </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:organization_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:organizations</span></span>
|
47
|
+
<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>
|
48
|
+
<span class="line"></span>
|
49
|
+
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> key [</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;">:organization_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]</span></span>
|
50
|
+
<span class="line"></span>
|
51
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
52
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
53
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This allows your migrations to be more expressive <em>and</em> make it easier to set up unique constraints that relate to your business logic or domain.</p></li></ul><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>Generally, you don't test database migrations, however you may want to test constraints or other logic you have set up. Techniques for doing this are in the <a href="/database-access.html#testing">database access</a> section.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><h3 id="ephemeral-dev-database" tabindex="-1">Ephemeral Dev Database <a class="header-anchor" href="#ephemeral-dev-database" aria-label="Permalink to "Ephemeral Dev Database""></a></h3><p>Brut intends for your develompent database to be ephemeral. Your entire workflow should be built around it being OK and normal to completely blow away your development database and recreate it. This is why down migrations (and the use of <code>change</code>) are discouraged. You really don't need them.</p><p>Assuming you have <a href="/seed-data.html">seed data</a> set up properly, you can reliably reset everything like so:</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 rebuild</span></span>
|
54
|
+
<span class="line"><span>> bin/db seed</span></span>
|
55
|
+
<span class="line"><span>> bin/db rebuild -e test</span></span></code></pre></div><p>As long as you don't change migrations that have been applied in production, you can safely run the above commands to iterate on a schema change.</p><p>This does imply that you should not run business logic or make <em>data</em> changes in your migration files. No migration should rely on specific data being in the database.</p><p>This workflow my be much different from what you are used to, but you will be quite happy when you adopt it. The days of downloading a carefully-curated database image and taking great care to never delete it are over.</p><h3 id="use-your-database-it-is-awesome" tabindex="-1">Use Your Database, It is Awesome <a class="header-anchor" href="#use-your-database-it-is-awesome" aria-label="Permalink to "Use Your Database, It is Awesome""></a></h3><p>Your database is the only part of the system that has any chance of ensuring data integrity. You can use constraints, types, foreign keys, etc. to ensure that the data in your database is correct, based on your current understandings. Code-based validation systems <strong>cannot achieve this on any level</strong>.</p><p>Thus, you are encouraged to learn about your database's features and use them!</p><p>For example, here's a way to add full text search to an existing table in Postgres:</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;">add_column </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:full_text_search</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
56
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :tsvector</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
57
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> generated_always_as:</span><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;">lit</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">%{</span></span>
|
58
|
+
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> (</span></span>
|
59
|
+
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> setweight(to_tsvector('english', name),'A') ||</span></span>
|
60
|
+
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> setweight(to_tsvector('english', coalesce(description,'')),'B')</span></span>
|
61
|
+
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> )</span></span>
|
62
|
+
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> }</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">),</span></span>
|
63
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> generated_type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :stored</span></span></code></pre></div><p>If you are using Postgtes, why <em>not</em> use its features? Unless your app is database-agnostic, you should be using the features of your database, even if they aren't explicitly exposed via Sequel's Ruby API (that's why <code>Sequel.lit</code> exists).</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></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's internals, the source code is always more correct.</p></div><p><em>Last Updated May 8, 2025</em></p><p>As mentioned, Brut uses Sequel under the covers. This is unlikely to change.</p><p>As also mentioned, Brut's extensions often rely on Postgres. While we can all dream of a world where every developer uses the same database server, we don't live in that world. Brut should, some day, support all the databases that Sequel supports. For now, however, it only supports Postgres.</p><p>This hard-coded support is due to:</p><ul><li><code>pg_array</code></li><li><code>pg_json</code></li><li>Reliance on <code>citext</code> and <code>comment</code></li><li>Reliance on <code>timestamptz</code></li></ul><p>Brut is likely to add more Postgres-specific features before adding support for other databases.</p>`,70)]))}const k=a(n,[["render",l]]);export{u as __pageData,k as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as a,c as s,o as i,ag as t}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Database Schema / Migrations","description":"","frontmatter":{},"headers":[],"relativePath":"database-schema.md","filePath":"database-schema.md"}'),n={name:"database-schema.md"};function l(o,e,r,h,d,p){return i(),s("div",null,e[0]||(e[0]=[t("",70)]))}const k=a(n,[["render",l]]);export{u as __pageData,k as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as a,ag as i}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Deployment","description":"","frontmatter":{},"headers":[],"relativePath":"deployment.md","filePath":"deployment.md"}'),n={name:"deployment.md"};function s(r,e,l,d,c,p){return a(),o("div",null,e[0]||(e[0]=[i('<h1 id="deployment" tabindex="-1">Deployment <a class="header-anchor" href="#deployment" aria-label="Permalink to "Deployment""></a></h1><p>Brut apps are Rack apps, so they can be deployed in conventional ways. Brut apps are 12-factor apps and the scripts used for development are inteded to work for production as well.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to "Overview""></a></h2><p>Everyone deploys apps in different ways. Brut can't provide a simple solution for all deployment setups, so this document will outline considerations when setting up deployment.</p><p>The most direct way to understand what needs to happen is to look at <code>deploy/Dockerfile</code>, which is the foundation of a <code>Dockerfile</code> you can use. In particular, it shows you the commands needed to setup and run the app in production:</p><p>Beyond installing system software to run any Ruby web app, as well as whatever is needed for NodeJS and Postgres, the Brut-specific parts look like so:</p><ol><li>Install Ruby Gems with <code>bundle install</code></li><li>Install Node modules with <code>npm clean-install</code></li><li>Build all assets with <code>bin/build-assets</code> (this will bundle all CSS and Javascript, plus copy over any other <a href="/assets.html">assets</a> to the locations from where the Brut app will serve them)</li><li>Run the app with <code>bin/run</code></li></ol><p>Your Brut app also includes <code>bin/release</code> which is a script intended to run in the production environment after the code has been deployed, but before the app starts up. By default, it applies any needed migrations to the database.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>If you are using Docker, you can create the <code>Dockerfile</code>s and run them locally to see how they work. You will need to have local versions of all infrastructure (database, Redis, etc.), but if these work locally, there is a high chance they work in production.</p><p>If you are not using Docker, you will need to apply various techniques that are beyond the scope of this documentation.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><p>Brut goes to great lengths to avoid environment-specific code. Much of Brut's behavior works the same in dev as it does in production. For example, assets are hashed in all environments.</p><p>Assuming your code does the same thing, there should be a minium of surprises. That all being said, here are some recommendations:</p><ul><li>Create a way to interact with external services in a testing capacity. For example, ensure you have a test user with a known email address and trigger an email to them. Or a company credit card you charge and refund.</li><li>Configure observability so you know what your app is doing at all times.</li><li>Configure a URL that, when accessed, produces an error. This allows you to check your error reporting system.</li><li>Create a page somewhere that shows the git SHA of your deployment, or some other unique, unambiguous version number. This will clarify what version of the code is actually running.</li></ul>',15)]))}const m=t(n,[["render",s]]);export{u as __pageData,m as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as a,ag as i}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Deployment","description":"","frontmatter":{},"headers":[],"relativePath":"deployment.md","filePath":"deployment.md"}'),n={name:"deployment.md"};function s(r,e,l,d,c,p){return a(),o("div",null,e[0]||(e[0]=[i("",15)]))}const m=t(n,[["render",s]]);export{u as __pageData,m as default};
|
Binary file
|
Binary file
|
@@ -0,0 +1,11 @@
|
|
1
|
+
import{_ as t,c as a,o as s,ag as i}from"./chunks/framework.1L-BeKqY.js";const o="/assets/dev-env-overview.Gj7NWM8-.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]=[i('<h1 id="dev-environment" tabindex="-1">Dev Environment <a class="header-anchor" href="#dev-environment" aria-label="Permalink to "Dev Environment""></a></h1><p>Brut provides sophisticatd tooling to manage your dev environment</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to "Overview""></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'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 "Conceptual Overview""></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="'+o+'" alt="Conceptual Overview"></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 "Foundational Core Command Line Apps""></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 terminoloy notes if you aren't familiar with Docker:</p><ul><li>A Docker <em>container</em> is akin to a virtual machine. On Linux this isn'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 conatiner when it'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 instrutions 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 "Workspace Command Line Apps""></a></h3><p>The workspace is where you'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, effecitvely 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'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'll run <code>bin/setup</code> to get everything set up for working.</li><li>You'll start your dev server with <code>bin/dev</code>.</li><li>You'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'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 "Extending and Enhancing""></a></h3><p>TBD</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>There aren't tests for this code, because you are using all day every day. Brut'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 "Recommended Practices""></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>You are encouraged to understand how it works, especially because the Docker skills you learn in doing so will help with production deployment and operations.</p><p>Beyond this, we recommend that you keep a few things in mind with respect to automation:</p><ul><li>The <em>Foundational Core</em> is bootstrapped in a degenerate environment without reliable tools beyond Bash. This is why it's almost entirely written in Bash, since it'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. Just keep in mind that <code>bin/setup</code> can only rely on programming language runtimes and not any particular Ruby gem having been installed.</li></ul><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></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'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>
|
2
|
+
<span class="line"></span>
|
3
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "bundler"</span></span>
|
4
|
+
<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>
|
5
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "pathname"</span></span>
|
6
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">require</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "brut/cli/apps/db"</span></span>
|
7
|
+
<span class="line"></span>
|
8
|
+
<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>
|
9
|
+
<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>
|
10
|
+
<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;"> ".."</span></span>
|
11
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span></code></pre></div><p>These files have a lot of 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};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as a,o as s,ag as i}from"./chunks/framework.1L-BeKqY.js";const o="/assets/dev-env-overview.Gj7NWM8-.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]=[i("",48)]))}const y=t(r,[["render",l]]);export{g as __pageData,y as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as s,ag as a}from"./chunks/framework.1L-BeKqY.js";const m=JSON.parse('{"title":"Documentation Conventions","description":"","frontmatter":{},"headers":[],"relativePath":"doc-conventions.md","filePath":"doc-conventions.md"}'),r={name:"doc-conventions.md"};function i(n,e,u,d,c,l){return s(),o("div",null,e[0]||(e[0]=[a('<h1 id="documentation-conventions" tabindex="-1">Documentation Conventions <a class="header-anchor" href="#documentation-conventions" aria-label="Permalink to "Documentation Conventions""></a></h1><h2 id="terminology" tabindex="-1">Terminology <a class="header-anchor" href="#terminology" aria-label="Permalink to "Terminology""></a></h2><p>Brut attempts to use existing terminology where possible, particularly where that technology applies to the web platform. For example, there is not a thing called "CSS variables", rather the term is "custom properties". HTML entities are <em>elements</em> or <em>tags</em> that have <em>attributes</em>. As another example, HTML doesn't have <em>validations</em>, rather it as <em>constraints</em>, which can be <em>violated</em>.</p><p>When speaking about Ruby, we prefer the term <em>initializer</em> over constructor, <em>parameters</em> over arguments, and <em>methods</em> over messages. We also prefer <em>tests</em> over specs, however test files <em>are</em> located in <code>specs/</code> and named <code>*.spec.rb</code> to be consistent with RSpec's nomenclature. We prefer <em>end-to-end</em> or <em>e2e</em> tests instead of browser tests or request specs.</p><p>Further, Brut doesn't render HTML, it <em>generates</em> it. The browser renders the HTML for the website's visitor.</p><p>Lastly, the documentation tries to talk about the person accessing a website as a "vistor" not a "user". Though the "user" nomenclature is near-ossified in software development, we feel "visitor" is more apt.</p><h2 id="structure-of-these-documents" tabindex="-1">Structure of These Documents <a class="header-anchor" href="#structure-of-these-documents" aria-label="Permalink to "Structure of These Documents""></a></h2><p>Each page here documents on aspect of Brut, called a <em>module</em>, and these pages are organized along four sections:</p><ul><li><strong>Overview</strong> - provides detailed information on how this part of Brut works, with minimal examples to orient you to the terms and design of that module. Links to reference documentation are provided inline as needed.</li><li><strong>Testing</strong> - information about how to write tests for the code in this module. For example, in <a href="/pages.html">Pages</a>, we detail how you are intended to test page classes.</li><li><strong>Recommended Practices</strong> - this section outlines what we believe is the best way to use the module, along with justifications for the recommended approach. While you are free to ignore this advice, it's often useful to understand the intention of the authors.</li><li><strong>Technical Notes</strong> - where appropriate, technical details about how or why the module works the way it does are provided. This section should be marked with a date to allow you to understand the recency of the information. It may not always be up to date, but this can help further clarify what is happening under the covers and why.</li></ul><h2 id="names-of-the-library-and-associated-modules" tabindex="-1">Names of the Library and Associated Modules <a class="header-anchor" href="#names-of-the-library-and-associated-modules" aria-label="Permalink to "Names of the Library and Associated Modules""></a></h2><p>This framework is called "Brut" though may be called "BrutRB". It lives at <code>brutrb.com</code>.</p><p>The JavaScript library is called "BrutJS", but is <code>brut-js</code> in code or the filesystem. "Brut-JS" is wrong, as is <code>brut_js</code>.</p><p>The CSS library is called "BrutCSS", but is <code>brut-css</code> in code or the filesystem. "Brut-CSS" is wrong, as is "brut-css".</p><h2 id="on-using-vitepress" tabindex="-1">On Using VitePress <a class="header-anchor" href="#on-using-vitepress" aria-label="Permalink to "On Using VitePress""></a></h2><p>This site is built using <a href="https://vitepress.dev" target="_blank" rel="noreferrer">VitePress</a>, which is a client-side heavy framework. It kinda goes against the ethos of Brut, but it is allowing me to write documentation that looks decent and is mostly navigable. I would like to use a more accessible, customized system for documenting Brut, but for now, it's more important to get the documentation out. A better documentation experience is planned.</p>',15)]))}const p=t(r,[["render",i]]);export{m as __pageData,p as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as s,ag as a}from"./chunks/framework.1L-BeKqY.js";const m=JSON.parse('{"title":"Documentation Conventions","description":"","frontmatter":{},"headers":[],"relativePath":"doc-conventions.md","filePath":"doc-conventions.md"}'),r={name:"doc-conventions.md"};function i(n,e,u,d,c,l){return s(),o("div",null,e[0]||(e[0]=[a("",15)]))}const p=t(r,[["render",i]]);export{m as __pageData,p as default};
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import{_ as s,c as t,o as a,ag as i}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"End to End Tests","description":"","frontmatter":{},"headers":[],"relativePath":"end-to-end-tests.md","filePath":"end-to-end-tests.md"}'),n={name:"end-to-end-tests.md"};function o(r,e,l,h,p,d){return a(),t("div",null,e[0]||(e[0]=[i(`<h1 id="end-to-end-tests" tabindex="-1">End to End Tests <a class="header-anchor" href="#end-to-end-tests" aria-label="Permalink to "End to End Tests""></a></h1><p>Is there a greater pain the world than an end-to-end test? Is there a more punishing API than trying to convince a browser to browse and use a website? Is there an answer for why the way we interact with browsers when writing code is 100% different than the we do when writing a test?</p><p>Brut cannot answer these things, but it does provide a way to write end-to-end tests with a browser, with a somewhat slightly reduced amount of pain.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to "Overview""></a></h2><p>Brut uses <a href="https://playwright.dev/" target="_blank" rel="noreferrer">Playwright</a> and the <a href="https://playwright-ruby-client.vercel.app/" target="_blank" rel="noreferrer">playwright-ruby-client</a> to allow you to write end-to-end tests that use a web browser. Brut sets up headless Chromium to do this.</p><p>You can run End-to-End (e2e) tests with <code>bin/test e2e</code>. You must use this to run individual tests as well, since this will ensure proper set up for the tests, which is more than is needed for a normal unit test.</p><h3 id="using-playwright" tabindex="-1">Using Playwright <a class="header-anchor" href="#using-playwright" aria-label="Permalink to "Using Playwright""></a></h3><p>At a high level, e2e tests look like normal RSpec tests:</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;"> "spec_helper"</span></span>
|
2
|
+
<span class="line"></span>
|
3
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RSpec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">describe</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "logging into the website"</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
|
4
|
+
<span class="line"></span>
|
5
|
+
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
|
6
|
+
<span class="line"></span>
|
7
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>The contents of your <code>it</code> blocks will use <code>playwright-ruby-client</code> to interact with the browser and make assertions. The value <code>page</code> is available to use this API.</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;"> "spec_helper"</span></span>
|
8
|
+
<span class="line"></span>
|
9
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RSpec</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">describe</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "logging into the website"</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
|
10
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> it </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"shows an error when login is invalid"</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
|
11
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">goto</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"/"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
12
|
+
<span class="line"></span>
|
13
|
+
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"form input[type='email']"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
14
|
+
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"form input[type='password']"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
15
|
+
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"form button"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
16
|
+
<span class="line"></span>
|
17
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> email.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"pat@example.com"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
18
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> password.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">fill</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"12345678"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
19
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">click</span></span>
|
20
|
+
<span class="line"></span>
|
21
|
+
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> flash</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"[role='alert']"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
22
|
+
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> expect</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(flash).</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">to</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> have_text</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"No email/password in our system"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</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></code></pre></div><p><code>playwright-ruby-client</code> provides excellent documentation on how it has adapter Playwright's API for use in Ruby.</p><h3 id="test-setup" tabindex="-1">Test Setup <a class="header-anchor" href="#test-setup" aria-label="Permalink to "Test Setup""></a></h3><p>Brut will run your app via <a href="/api/Brut/SpecSupport/E2ETestServer.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::E2ETestServer</code></a>. It will run it before the first e2e test, leave it running during the remainder of the test suite, then stop it. This means that database changes your test makes will persist across tests.</p><p>If you are using Sidekiq, Sidekiq will be set up like normal. Jobs queued will go to Redis, and those jobs will be processed by Sidekiq, just like in production. Thus, you should not assert things about specific jobs, but rather assert the effects those jobs will have. Redis is flushed between each test.</p><h3 id="test-helpers-and-configuration" tabindex="-1">Test Helpers and Configuration <a class="header-anchor" href="#test-helpers-and-configuration" aria-label="Permalink to "Test Helpers and Configuration""></a></h3><p>Inside your test, <code>t</code> is available to produce translations. You can also access all your page and handler classes, so you can (and should) use <code>.routing</code>, e.g. <code>DashboardPage.routing</code>, to generate or access routes for your app.</p><p>You can set <code>e2e_timeout</code> on any test to override the default amount of time Playwright will wait for a locator to locate an element. The default is 5 seconds. \`, to generate or access routes for your app. You can also configure behavior with environment variables:</p><table tabindex="0"><thead><tr><th>Variable</th><th>Default</th><th>Purpose</th></tr></thead><tbody><tr><td><code>E2E_TIMEOUT_MS</code></td><td>5000</td><td>Number of milliseconds Playwright will wait for a locator to appear</td></tr><tr><td><code>E2E_SLOW_MO</code></td><td>0</td><td>Number of milliseconds Playwright will pause between operations. Useful to detect race conditions</td></tr><tr><td><code>E2E_RECORD_VIDEOS</code></td><td>unset</td><td>If set, videos of each test run are saved in <code>tmp/e2e-videos</code></td></tr></tbody></table><h3 id="quirks-of-playwright" tabindex="-1">Quirks of Playwright <a class="header-anchor" href="#quirks-of-playwright" aria-label="Permalink to "Quirks of Playwright""></a></h3><p>The Playwright JavaScript API is heavily asycnhronous, requiring liberal use of <code>await</code>. The <code>playwright-ruby-client</code> wrapper abstracts that so you can write more straightfoward code.</p><p>The main thing to be aware of is that locators that fail only fail when you attempt to assert something.</p><p>For example</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:#E36209;--shiki-dark:#FFAB70;">button</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = page.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">locator</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"form button"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># suppose this button doesn't exist</span></span>
|
25
|
+
<span class="line"></span>
|
26
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">button.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">click</span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # This is where you'll see a failure</span></span></code></pre></div><p>This hidden asynchronous behavior also means that certain calls will wait a period of time for the element you are locating to appear. This is why the example test above works without having to explicitly wait for a page refresh. After <code>button.click</code>, presumably the back-end is contacted and the page is re-rendered with an error. As long as that happens within a second or so, the code will wait for an element matching <code>[role='alert']</code> to show up.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><p>E2e tests are slow. They can also be flaky if you aren't careful in how you write them and how you author your HTML.</p><h3 id="test-major-flows-not-exhaustive-branches" tabindex="-1">Test Major Flows, Not Exhaustive Branches <a class="header-anchor" href="#test-major-flows-not-exhaustive-branches" aria-label="Permalink to "Test Major Flows, Not Exhaustive Branches""></a></h3><p>E2e tests give the most value when they assert that a sequence of actions the visitor takes result in what you expect—a "major flow". Testing some error cases can be useful, but you should not use e2e tests to assert every single possible thing that could happen on a page.</p><p>In fact, your app might be better off leaving some behaviors better untested instead of tested by an e2e test. Use your judgement and be aware of the carrying cost of each e2e test.</p><h3 id="use-css-selectors" tabindex="-1">Use CSS Selectors <a class="header-anchor" href="#use-css-selectors" aria-label="Permalink to "Use CSS Selectors""></a></h3><p>The main Playwright documentation encourages you to locate elements by "accessible names" and other indirect ways of finding elements. In practice, this is error prone and tedious. Determining the accessible name of an element is not always easy.</p><p>We recommend you assess your app's accssibility in another way than trying to do it while performing end-to-end tests. Instead, locate elements with CSS selectors—this is what you'd use to debug your app so it makes sense as a testing technique.</p><p>Insulating your end-to-end tests from markup changes does not produce significant savinsg and can make tests more difficult to write.</p><h3 id="testing-must-inform-your-html" tabindex="-1">Testing Must Inform your HTML <a class="header-anchor" href="#testing-must-inform-your-html" aria-label="Permalink to "Testing Must Inform your HTML""></a></h3><p>To allow CSS selectors to survive minor changes to a page, your HTML should be authored with testing in mind. In the example above, we locate the flash by looking for <code>[role='alert']</code>, since this is the most semantically correct way to mark up a flash message that contains an error.</p><p>ARIA roles that should be applied for accessibility purposes can be leveraged as locators, as can custom elements. Remember that any custom element is valid, even if it has no associated JavaScript. Custom elements are an excellent way to "tag" markup for use in tests or progressively-enhanced behavior.</p><p>CSS classes, on the other hand, are not a good candidate for identifying markup in a test. CSS classes exist to afford visual styling of elements and are the most likely to change as the app evolves. A better fallback if there is no other way to locate an element is to use <code>data-testid</code>. It makes itself painfully clear why it's there. Use this sparingly, but it's there if you need it.</p><h3 id="asserting-the-lack-of-content-basically-doesn-t-work" tabindex="-1">Asserting the Lack of Content Basically Doesn't Work <a class="header-anchor" href="#asserting-the-lack-of-content-basically-doesn-t-work" aria-label="Permalink to "Asserting the Lack of Content Basically Doesn't Work""></a></h3><p>To assert that some content or an element <strong>is not</strong> on the page requires locating it and waiting the timeout for that locate to fail. This sucks. Don't do it.</p><p>If you need to assert that something did not happen, you may want to design your page or app such that markup appears that indicates whatever it is didn't happen. This is not ideal, but a web page is a living thing that never stops changing, so your test can't just assume it's all synchronous.</p><h3 id="try-to-use-the-defaults-for-timeouts" tabindex="-1">Try to Use the Defaults for Timeouts <a class="header-anchor" href="#try-to-use-the-defaults-for-timeouts" aria-label="Permalink to "Try to Use the Defaults for Timeouts""></a></h3><p>Your app should not take 5 seconds to do anythning, especially not inside a test. You may need to bump up the timeout to figure out what's going wrong, or set <code>E2E_SLOW_MO</code> to watch a video test, but once you've sorted out the issue, restore these to their defaults.</p><p>If you <em>must</em> set <code>e2e_timeout</code> as metadata on test, <strong>explain why</strong> and try removing it every so often to make sure it's still needed.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></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's internals, the source code is always more correct.</p></div><p><em>Last Updated June 13, 2025</em></p><p>The test server is run bin <code>bin/test-server</code>, which is why Sidekiq will be running when your app is running for an e2e test.</p>`,48)]))}const k=s(n,[["render",o]]);export{u as __pageData,k as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as s,c as t,o as a,ag as i}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"End to End Tests","description":"","frontmatter":{},"headers":[],"relativePath":"end-to-end-tests.md","filePath":"end-to-end-tests.md"}'),n={name:"end-to-end-tests.md"};function o(r,e,l,h,p,d){return a(),t("div",null,e[0]||(e[0]=[i("",48)]))}const k=s(n,[["render",o]]);export{u as __pageData,k as default};
|
@@ -0,0 +1,93 @@
|
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const g=JSON.parse('{"title":"Flash and Session","description":"","frontmatter":{},"headers":[],"relativePath":"flash-and-session.md","filePath":"flash-and-session.md"}'),h={name:"flash-and-session.md"};function t(l,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[e(`<h1 id="flash-and-session" tabindex="-1">Flash and Session <a class="header-anchor" href="#flash-and-session" aria-label="Permalink to "Flash and Session""></a></h1><p>Brut sessions are stored in cookies, encrypted to prevent tampering. The <em>flash</em>, which is a way to temporarily store small bits of information between page loads, is encoded in the session.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to "Overview""></a></h2><p>Unlike Rails, the session and flash are presented to you as objects, not Hashes. By declaring the <code>session:</code> parameter on an initializer, you'll be given the current session for the request as an <code>AppSession</code>, which inherits from <a href="/api/Brut/FrontEnd/Session.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Session</code></a>. Similarly, declaring <code>flash:</code>, you'll get a <a href="/api/Brut/FrontEnd/Flash.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Flash</code></a>.</p><p>The idea is to use Ruby's type system to describe what data is in the session and flash.</p><h3 id="session" tabindex="-1">Session <a class="header-anchor" href="#session" aria-label="Permalink to "Session""></a></h3><p>Brut's session is somewhat richer than you might get from other frameworks. In particular, the session can provide you:</p><ul><li>The current <a href="/api/Brut/I18n/HTTPAcceptLanguage.html" target="_self" rel="noopener" data-no-router><code>Brut::I18n::HTTPAcceptLanguage</code></a>, which is the visitor's locale. See <a href="/i18n.html">I18n</a> for how this works and how to use this value.</li><li>The timezone as provided by the browser.</li><li>An explicitly-set timezone that may or may not be what the browser provided. See <a href="/space-time-continuum.html">Space-Time Continuum</a> for more details.</li></ul><p>The session also handles serializing the flash to and from the browser's cookies and can store any arbitrary data you like via <code>[]</code>. You are encouraged to add methods to your app's <code>AppSession</code> to make it explicit what you are storing.</p><p>Let's see the <a href="/hooks.html">route hook</a> from the <a href="/pages.html">pages</a> section again.</p><div class="caution custom-block github-alert"><p class="custom-block-title">CAUTION</p><p>This hook is not production-ready. It lacks certain error-handling situations and makes an assumption about how the session is managed. It's for demonstration only. The <a href="/hooks.html">route hooks</a> section has a more appropriate example.</p></div><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;"> RequireAuthBeforeHook</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</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;">RouteHook</span></span>
|
2
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> before</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">request_context:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
3
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">current_user_id</span></span>
|
4
|
+
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> user</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;">User</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;">id:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">current_user_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
5
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> user</span></span>
|
6
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> request_context[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:current_user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> user</span></span>
|
7
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
8
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
9
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
10
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>When this hook executes, <code>session</code> will be an <code>AppSession</code>, serialized from the browser's cookies. Here's what that class might look like:</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/front_end/support/app_session.rb</span></span>
|
11
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppSession</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</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;">Session</span></span>
|
12
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> login!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">current_user:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
13
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:current_user_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> current_user.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">id</span></span>
|
14
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
15
|
+
<span class="line"></span>
|
16
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> logout!</span></span>
|
17
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:current_user_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span></span>
|
18
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
19
|
+
<span class="line"></span>
|
20
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> logged_in?</span></span>
|
21
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !!</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">current_user_id</span></span>
|
22
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
23
|
+
<span class="line"></span>
|
24
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> current_user_id</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> self[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:current_user_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]</span></span>
|
25
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>The session is a rich object and not just a thin wrapper over a Hash. You could even have the session perform the lookup in the database:</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/front_end/support/app_session.rb</span></span>
|
26
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppSession</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</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;">Session</span></span>
|
27
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> login!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">current_user:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
28
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:current_user_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> current_user.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">id</span></span>
|
29
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
30
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> logout!</span></span>
|
31
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:current_user_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span></span>
|
32
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
33
|
+
<span class="line"></span>
|
34
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> logged_in?</span></span>
|
35
|
+
<span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !!</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">current_user</span></span>
|
36
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
37
|
+
<span class="line"></span>
|
38
|
+
<span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> current_user</span></span>
|
39
|
+
<span class="line highlighted"><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;">User</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;">id:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:current_user_id</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">])</span></span>
|
40
|
+
<span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
41
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Now, the hook could call <code>current_user</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:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> RequireAuthBeforeHook</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</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;">RouteHook</span></span>
|
42
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> before</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">request_context:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
43
|
+
<span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">logged_in?</span></span>
|
44
|
+
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> request_context[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:current_user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">current_user</span></span>
|
45
|
+
<span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
46
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
47
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Let's see <code>LoginHandler</code> from the <a href="/handlers.html">handlers</a> section, to see how to save the current user. Given what we've learned, the declaration of the <code>session:</code> parameter to the initializer means the relevant instance of <code>AppSession</code> will be passed in.</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/front_end/handlers/login_handler.rb</span></span>
|
48
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
|
49
|
+
<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:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
50
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form</span></span>
|
51
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @session </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session</span></span>
|
52
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
53
|
+
<span class="line"></span>
|
54
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> handle</span></span>
|
55
|
+
<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>
|
56
|
+
<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> authorized_user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">AuthorizedUser</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">login</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
|
57
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
58
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> password:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">password</span></span>
|
59
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
|
60
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> if</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> authorized_user.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">nil?</span></span>
|
61
|
+
<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>
|
62
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
63
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> key:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :login_not_found</span></span>
|
64
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
|
65
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
|
66
|
+
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> session.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">login!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">current_user:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> authorized_user.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">user</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
67
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
68
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
69
|
+
<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;">constraint_violations?</span></span>
|
70
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form)</span></span>
|
71
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> else</span></span>
|
72
|
+
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> redirect_to</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DashboardPage</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</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></code></pre></div><p>Brut will handle saving the updated values in the response so when, in this case, the <code>DashboardPage</code> is rendered, it can see which user is logged in.</p><h3 id="flash" tabindex="-1">Flash <a class="header-anchor" href="#flash" aria-label="Permalink to "Flash""></a></h3><p>By default, your app will use Brut's flash class, <a href="/api/Brut/FrontEnd/Flash.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Flash</code></a>. This is because you typically don't need to enhance the flash. Brut's flash has an "alert" and "notice", and you can use them however you see fit. You can also set arbitrary messages in the flash via <code>[]</code>.</p><p>The contents of the flash only survive one request, so anything you set will be available in that session's next request, but not after that.</p><p>Note that the flash's alert and notice are intended to be I18n keys. You don't have to use them this way, but it is encouraged. If you pass an array into <code>alert=</code> or <code>notice=</code>, the elements will be joined to form an I18n key.</p><p>You can create your own subclass if you need a richer flash class than the one Brut provides.</p><p>First, create your class. It can be anywhere, but we recommend <code>app/src/front_end/support/app_flash.rb</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;"># app/src/front_end/support/app_flash.rb</span></span>
|
76
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppFlash</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</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;">Flash</span></span>
|
77
|
+
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # For example</span></span>
|
78
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> debug=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(debug)</span></span>
|
79
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> self</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> debug</span></span>
|
80
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
81
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> debug</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> self[</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:debug</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">]</span></span>
|
82
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> debug?</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> =</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> !!</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">self.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">debug</span></span>
|
83
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Then, in your <code>App</code>, located in <code>app/src/app.rb</code>, use <code>Brut.container.override</code> to change the class used for the flash:</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;"> App</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</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;">Framework</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">App</span></span>
|
84
|
+
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
|
85
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span></span>
|
86
|
+
<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:#6F42C1;--shiki-dark:#B392F0;">container</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">override</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
|
87
|
+
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "flash_class"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
88
|
+
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppFlash</span></span>
|
89
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> )</span></span>
|
90
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
91
|
+
<span class="line"></span>
|
92
|
+
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
|
93
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Brut's configuration system is discussed in more detail in <a href="/configuration.html">Configuration</a>.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>Testing your session or flash classes may not be super valuable, however they are normal Ruby objects so you can test them in a conventional way. Both classes treat their internals as a Hash, so you can implement and assert via the <code>[]</code> and <code>[]=</code> methods.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><p>While you can use both the flash and the session has a hash of whatever, your are encouraged to avoid this in your production code. Create well-defined attributes or methods to manipulate these objects using the language of your domain.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></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's internals, the source code is always more correct.</p></div><p><em>Last Updated May 7, 2025</em></p><p>The session is based on <a href="https://github.com/rack/rack-session" target="_blank" rel="noreferrer"><code>Rack::Session</code></a>, which is configured explicitly in your app's <code>config.ru</code>. (TBD: WHY?)</p><p>The session object itself is created on demand for any route hook that needs it. Since <a href="/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::RouteHooks::SetupRequestContext</code></a> requires the session, the <a href="/api/Brut/FrontEnd/RequestContext.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::RequestContext</code></a> is created here and given the session (and flash) that is used for subsequent hooks and HTML generation.</p><p>The flash is created largely on-demand and is a special hash serialized into the session. The hash contains the current age of the flash and then all the messages. This format could use improvement and may change. <a href="/api/Brut/FrontEnd/RouteHooks/AgeFlash.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::RouteHooks::AgeFlash</code></a> is a route hook that handles increasing the age of the flash, however the flash itself controls when to "age out" messages. None of this is currently configurable.</p>`,41)]))}const c=i(h,[["render",t]]);export{g as __pageData,c as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const g=JSON.parse('{"title":"Flash and Session","description":"","frontmatter":{},"headers":[],"relativePath":"flash-and-session.md","filePath":"flash-and-session.md"}'),h={name:"flash-and-session.md"};function t(l,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[e("",41)]))}const c=i(h,[["render",t]]);export{g as __pageData,c as default};
|