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