brut 0.0.28 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/.projections.json +10 -0
- data/.rspec +3 -0
- data/Dockerfile.dx +32 -14
- data/Gemfile.lock +1 -1
- data/README.md +23 -2
- data/assets/Logo-Square.pxd +0 -0
- data/assets/LogoPylon.pxd +0 -0
- data/assets/LogoStop.pxd +0 -0
- data/assets/LogoTall.pxd +0 -0
- data/assets/MetroLogo.graffle +0 -0
- data/assets/SocialImage.png +0 -0
- data/assets/SocialImage.pxd +0 -0
- data/bin/docs +24 -2
- data/bin/rspec +27 -0
- data/bin/setup +3 -3
- data/brutrb.com/.vitepress/config.mjs +45 -8
- data/brutrb.com/.vitepress/theme/custom.css +7 -0
- data/brutrb.com/.vitepress/theme/style.css +29 -17
- data/brutrb.com/ai.md +10 -15
- data/brutrb.com/assets.md +2 -9
- data/brutrb.com/brut-js.md +12 -2
- data/brutrb.com/cli.md +9 -13
- data/brutrb.com/components.md +118 -96
- data/brutrb.com/configuration.md +3 -4
- data/brutrb.com/css.md +2 -2
- data/brutrb.com/custom-element-tests.md +3 -4
- data/brutrb.com/database-access.md +1 -1
- data/brutrb.com/database-schema.md +29 -41
- data/brutrb.com/deployment.md +123 -45
- data/brutrb.com/dev-environment.md +7 -7
- data/brutrb.com/dir-structure.md +120 -0
- data/brutrb.com/doc-conventions.md +18 -15
- data/brutrb.com/dx +1 -0
- data/brutrb.com/end-to-end-tests.md +12 -10
- data/brutrb.com/features.md +373 -0
- data/brutrb.com/flash-and-session.md +115 -131
- data/brutrb.com/form-constraints.md +266 -0
- data/brutrb.com/forms.md +140 -765
- data/brutrb.com/getting-started.md +10 -11
- data/brutrb.com/handlers.md +119 -95
- data/brutrb.com/hooks.md +18 -20
- data/brutrb.com/i18n.md +6 -4
- data/brutrb.com/images/LogoPylon.png +0 -0
- data/brutrb.com/images/LogoSquare.png +0 -0
- data/brutrb.com/images/LogoStop.png +0 -0
- data/brutrb.com/images/LogoTall.png +0 -0
- data/brutrb.com/images/OverviewMetro.graffle +0 -0
- data/brutrb.com/images/OverviewMetro.png +0 -0
- data/brutrb.com/index.md +4 -3
- data/brutrb.com/instrumentation.md +7 -10
- data/brutrb.com/javascript.md +14 -14
- data/brutrb.com/keyword-injection.md +72 -114
- data/brutrb.com/layouts.md +20 -52
- data/brutrb.com/lsp.md +1 -1
- data/brutrb.com/overview.md +35 -377
- data/brutrb.com/pages.md +119 -207
- data/brutrb.com/public/SocialImage.png +0 -0
- data/brutrb.com/public/favicon.ico +0 -0
- data/brutrb.com/recipes/alternate-layouts.md +32 -0
- data/brutrb.com/recipes/authentication.md +315 -6
- data/brutrb.com/recipes/blank-layouts.md +22 -0
- data/brutrb.com/recipes/custom-flash.md +51 -0
- data/brutrb.com/recipes/indexed-forms.md +149 -0
- data/brutrb.com/recipes/text-field-component.md +182 -0
- data/brutrb.com/routes.md +56 -82
- data/brutrb.com/security.md +0 -3
- data/brutrb.com/space-time-continuum.md +8 -12
- data/brutrb.com/tutorial.md +1 -1
- data/brutrb.com/why.md +19 -0
- data/docker-compose.dx.yml +5 -2
- data/docs/404.html +8 -3
- data/docs/SocialImage.png +0 -0
- data/docs/ai.html +11 -6
- data/docs/api/Brut/BackEnd/SeedData.html +1 -1
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
- data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
- data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
- data/docs/api/Brut/BackEnd/Validators.html +1 -1
- data/docs/api/Brut/BackEnd.html +1 -1
- data/docs/api/Brut/CLI/App.html +1 -1
- data/docs/api/Brut/CLI/AppRunner.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB.html +1 -1
- data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
- data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
- data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
- data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test.html +1 -1
- data/docs/api/Brut/CLI/Apps.html +1 -1
- data/docs/api/Brut/CLI/Command.html +1 -1
- data/docs/api/Brut/CLI/Error.html +1 -1
- data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
- data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
- data/docs/api/Brut/CLI/Executor.html +1 -1
- data/docs/api/Brut/CLI/InvalidOption.html +1 -1
- data/docs/api/Brut/CLI/Options.html +1 -1
- data/docs/api/Brut/CLI/Output.html +1 -1
- data/docs/api/Brut/CLI/SystemExecError.html +1 -1
- data/docs/api/Brut/CLI.html +1 -1
- data/docs/api/Brut/FactoryBot.html +1 -1
- data/docs/api/Brut/Framework/App.html +1 -1
- data/docs/api/Brut/Framework/Config.html +1 -1
- data/docs/api/Brut/Framework/Container.html +1 -1
- data/docs/api/Brut/Framework/Error.html +1 -1
- data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
- data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
- data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
- data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
- data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
- data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
- data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
- data/docs/api/Brut/Framework/Errors.html +1 -1
- data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
- data/docs/api/Brut/Framework/MCP.html +1 -1
- data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
- data/docs/api/Brut/Framework.html +1 -1
- data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
- data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
- data/docs/api/Brut/FrontEnd/Component.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
- data/docs/api/Brut/FrontEnd/Components.html +1 -1
- data/docs/api/Brut/FrontEnd/Download.html +1 -1
- data/docs/api/Brut/FrontEnd/Flash.html +1 -1
- data/docs/api/Brut/FrontEnd/Form.html +9 -11
- data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +201 -0
- data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +535 -0
- data/docs/api/Brut/FrontEnd/Forms/Input.html +983 -35
- data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +29 -19
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +141 -20
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +141 -20
- data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms.html +1 -1
- data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
- data/docs/api/Brut/FrontEnd/Handler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
- data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
- data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
- data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
- data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
- data/docs/api/Brut/FrontEnd/Layout.html +1 -1
- data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
- data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
- data/docs/api/Brut/FrontEnd/Page.html +1 -1
- data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
- data/docs/api/Brut/FrontEnd/Pages.html +1 -1
- data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
- data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
- data/docs/api/Brut/FrontEnd/Routing.html +1 -1
- data/docs/api/Brut/FrontEnd/Session.html +1 -1
- data/docs/api/Brut/FrontEnd.html +1 -1
- data/docs/api/Brut/I18n/BaseMethods.html +1 -1
- data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
- data/docs/api/Brut/I18n/ForCLI.html +1 -1
- data/docs/api/Brut/I18n/ForHTML.html +1 -1
- data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
- data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
- data/docs/api/Brut/I18n.html +1 -1
- data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
- data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
- data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
- data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
- data/docs/api/Brut/Instrumentation.html +1 -1
- data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
- data/docs/api/Brut/SinatraHelpers.html +1 -1
- data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
- data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
- data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
- data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
- data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
- data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
- data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
- data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
- data/docs/api/Brut/SpecSupport.html +1 -1
- data/docs/api/Brut.html +1 -1
- data/docs/api/Clock.html +1 -1
- data/docs/api/RichString.html +1 -1
- data/docs/api/SemanticLogger/Appender/Async.html +1 -1
- data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
- data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
- data/docs/api/Sequel/Extensions.html +1 -1
- data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
- data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
- data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
- data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
- data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
- data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
- data/docs/api/Sequel/Plugins/FindBang.html +1 -1
- data/docs/api/Sequel/Plugins.html +1 -1
- data/docs/api/Sequel.html +1 -1
- data/docs/api/_index.html +15 -1
- data/docs/api/class_list.html +1 -1
- data/docs/api/css/full_list.css +2 -1
- data/docs/api/css/style.css +14 -13
- data/docs/api/file.README.html +22 -3
- data/docs/api/index.html +22 -3
- data/docs/api/method_list.html +435 -275
- data/docs/api/top-level-namespace.html +1 -1
- data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
- data/docs/assets/OverviewMetro.DUS-5fUZ.png +0 -0
- data/docs/assets/{ai.md._6HCDL6d.js → ai.md.Cy9GWnER.js} +1 -1
- data/docs/assets/ai.md.Cy9GWnER.lean.js +1 -0
- data/docs/assets/{app.BX81XO4N.js → app.ClaS47Ru.js} +1 -1
- data/docs/assets/{assets.md.D3wunzLx.js → assets.md.7C3HWkga.js} +3 -3
- data/docs/assets/{assets.md.D3wunzLx.lean.js → assets.md.7C3HWkga.lean.js} +1 -1
- data/docs/assets/{brut-js.md.o2DAO2s2.js → brut-js.md.B4GYxQVw.js} +1 -1
- data/docs/assets/{brut-js.md.o2DAO2s2.lean.js → brut-js.md.B4GYxQVw.lean.js} +1 -1
- data/docs/assets/chunks/@localSearchIndexroot.Biqy1A4t.js +1 -0
- data/docs/assets/chunks/{VPLocalSearchBox.gABXcTWp.js → VPLocalSearchBox.DtgDfde2.js} +1 -1
- data/docs/assets/chunks/{theme.DwUXXAL3.js → theme.B45bvibT.js} +2 -2
- data/docs/assets/{cli.md.RmeA2b0i.js → cli.md.CjsktgFz.js} +15 -20
- data/docs/assets/components.md.DatoNgFo.js +96 -0
- data/docs/assets/{components.md.CRUMdRoN.lean.js → components.md.DatoNgFo.lean.js} +1 -1
- data/docs/assets/{configuration.md.BGHl8oRC.js → configuration.md.DeyhpqEx.js} +3 -3
- data/docs/assets/{css.md.DJgj2clw.js → css.md.CltvJqAa.js} +3 -3
- data/docs/assets/{custom-element-tests.md.BrYJQEl3.js → custom-element-tests.md.B_rbta32.js} +3 -3
- data/docs/assets/{database-access.md.C7l-Vuvb.js → database-access.md.gnluu54N.js} +1 -1
- data/docs/assets/{database-schema.md.BUjR0VS1.js → database-schema.md.CSYk6E6v.js} +6 -6
- data/docs/assets/{database-schema.md.BUjR0VS1.lean.js → database-schema.md.CSYk6E6v.lean.js} +1 -1
- data/docs/assets/deployment.md.BLseERGV.js +48 -0
- data/docs/assets/deployment.md.BLseERGV.lean.js +1 -0
- data/docs/assets/dev-environment.md.BroAOLhF.js +11 -0
- data/docs/assets/dir-structure.md.CWir1pic.js +46 -0
- data/docs/assets/dir-structure.md.CWir1pic.lean.js +1 -0
- data/docs/assets/doc-conventions.md.BzmSrTEW.js +1 -0
- data/docs/assets/doc-conventions.md.BzmSrTEW.lean.js +1 -0
- data/docs/assets/{end-to-end-tests.md.yfQHC0b5.js → end-to-end-tests.md.DzqRpZ43.js} +5 -3
- data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +1 -0
- data/docs/assets/features.md.DPFXsy0z.js +154 -0
- data/docs/assets/features.md.DPFXsy0z.lean.js +1 -0
- data/docs/assets/flash-and-session.md.nPvUpnUx.js +79 -0
- data/docs/assets/{flash-and-session.md.BXY8RvT0.lean.js → flash-and-session.md.nPvUpnUx.lean.js} +1 -1
- data/docs/assets/form-constraints.md.x5tNpTTI.js +90 -0
- data/docs/assets/form-constraints.md.x5tNpTTI.lean.js +1 -0
- data/docs/assets/forms.md.C2Dizvzq.js +64 -0
- data/docs/assets/forms.md.C2Dizvzq.lean.js +1 -0
- data/docs/assets/{getting-started.md.Ciz82L0m.js → getting-started.md.C93e0odB.js} +5 -5
- data/docs/assets/{getting-started.md.Ciz82L0m.lean.js → getting-started.md.C93e0odB.lean.js} +1 -1
- data/docs/assets/handlers.md.Chyri6KA.js +54 -0
- data/docs/assets/handlers.md.Chyri6KA.lean.js +1 -0
- data/docs/assets/{hooks.md.C4-moMny.js → hooks.md.Jmb5VOLA.js} +4 -4
- data/docs/assets/{hooks.md.C4-moMny.lean.js → hooks.md.Jmb5VOLA.lean.js} +1 -1
- data/docs/assets/{i18n.md.Do9i1qWl.js → i18n.md.xQhiGo1G.js} +2 -2
- data/docs/assets/{i18n.md.Do9i1qWl.lean.js → i18n.md.xQhiGo1G.lean.js} +1 -1
- data/docs/assets/index.md.CAMqGBJE.js +1 -0
- data/docs/assets/index.md.CAMqGBJE.lean.js +1 -0
- data/docs/assets/{instrumentation.md.a9Pjps4P.js → instrumentation.md.BgcaGVYH.js} +2 -2
- data/docs/assets/{instrumentation.md.a9Pjps4P.lean.js → instrumentation.md.BgcaGVYH.lean.js} +1 -1
- data/docs/assets/{javascript.md.GWbhRS51.js → javascript.md.DzrMxUmI.js} +7 -7
- data/docs/assets/{javascript.md.GWbhRS51.lean.js → javascript.md.DzrMxUmI.lean.js} +1 -1
- data/docs/assets/keyword-injection.md.95Zgh2eN.js +21 -0
- data/docs/assets/{keyword-injection.md.Dt2tKREs.lean.js → keyword-injection.md.95Zgh2eN.lean.js} +1 -1
- data/docs/assets/{layouts.md.cPnh3NId.js → layouts.md.CJGDFY-m.js} +2 -15
- data/docs/assets/layouts.md.CJGDFY-m.lean.js +1 -0
- data/docs/assets/{lsp.md.Bsu-f6VU.js → lsp.md.Dn1rIiW0.js} +1 -1
- data/docs/assets/{lsp.md.Bsu-f6VU.lean.js → lsp.md.Dn1rIiW0.lean.js} +1 -1
- data/docs/assets/overview.md.Bdq4qt3L.js +1 -0
- data/docs/assets/overview.md.Bdq4qt3L.lean.js +1 -0
- data/docs/assets/pages.md.B7Hc-i6H.js +45 -0
- data/docs/assets/pages.md.B7Hc-i6H.lean.js +1 -0
- data/docs/assets/recipes_alternate-layouts.md.BwEytl59.js +22 -0
- data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +1 -0
- data/docs/assets/recipes_authentication.md.Dzvi_g69.js +156 -0
- data/docs/assets/recipes_authentication.md.Dzvi_g69.lean.js +1 -0
- data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +15 -0
- data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +1 -0
- data/docs/assets/recipes_custom-flash.md.CrQbI5eH.js +26 -0
- data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +1 -0
- data/docs/assets/recipes_indexed-forms.md.CstYyOSo.js +74 -0
- data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +1 -0
- data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.js +101 -0
- data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +1 -0
- data/docs/assets/routes.md.B8kfUPHU.js +21 -0
- data/docs/assets/{routes.md.BMM7peut.lean.js → routes.md.B8kfUPHU.lean.js} +1 -1
- data/docs/assets/{security.md.C668yXCi.js → security.md.C0G_AZR-.js} +1 -1
- data/docs/assets/{security.md.C668yXCi.lean.js → security.md.C0G_AZR-.lean.js} +1 -1
- data/docs/assets/space-time-continuum.md.xl44xDos.js +1 -0
- data/docs/assets/{space-time-continuum.md.KPUIKysQ.lean.js → space-time-continuum.md.xl44xDos.lean.js} +1 -1
- data/docs/assets/{style.D73IYGCX.css → style.prAgp4yQ.css} +1 -1
- data/docs/assets/tutorial.md.a4a0eVOy.js +1 -0
- data/docs/assets/tutorial.md.a4a0eVOy.lean.js +1 -0
- data/docs/assets/why.md.C-hk5xgJ.js +1 -0
- data/docs/assets/why.md.C-hk5xgJ.lean.js +1 -0
- data/docs/assets.html +12 -7
- data/docs/brut-js/api/AjaxSubmit.html +1 -1
- data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
- data/docs/brut-js/api/Autosubmit.html +1 -1
- data/docs/brut-js/api/Autosubmit.js.html +1 -1
- data/docs/brut-js/api/BaseCustomElement.html +1 -1
- data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
- data/docs/brut-js/api/BrutCustomElements.html +1 -1
- data/docs/brut-js/api/BufferedLogger.html +1 -1
- data/docs/brut-js/api/ConfirmSubmit.html +1 -1
- data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
- data/docs/brut-js/api/ConfirmationDialog.html +1 -1
- data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
- data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
- data/docs/brut-js/api/CopyToClipboard.html +1 -1
- data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
- data/docs/brut-js/api/Form.html +1 -1
- data/docs/brut-js/api/Form.js.html +1 -1
- data/docs/brut-js/api/I18nTranslation.html +1 -1
- data/docs/brut-js/api/I18nTranslation.js.html +1 -1
- data/docs/brut-js/api/LocaleDetection.html +1 -1
- data/docs/brut-js/api/LocaleDetection.js.html +1 -1
- data/docs/brut-js/api/Logger.html +1 -1
- data/docs/brut-js/api/Logger.js.html +1 -1
- data/docs/brut-js/api/Message.html +1 -1
- data/docs/brut-js/api/Message.js.html +1 -1
- data/docs/brut-js/api/PrefixedLogger.html +1 -1
- data/docs/brut-js/api/RichString.html +1 -1
- data/docs/brut-js/api/RichString.js.html +1 -1
- data/docs/brut-js/api/Tabs.html +1 -1
- data/docs/brut-js/api/Tabs.js.html +1 -1
- data/docs/brut-js/api/Tracing.html +1 -1
- data/docs/brut-js/api/Tracing.js.html +1 -1
- data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
- data/docs/brut-js/api/external-Performance.html +1 -1
- data/docs/brut-js/api/external-Promise.html +1 -1
- data/docs/brut-js/api/external-ValidityState.html +1 -1
- data/docs/brut-js/api/external-Window.html +1 -1
- data/docs/brut-js/api/external-fetch.html +1 -1
- data/docs/brut-js/api/global.html +1 -1
- data/docs/brut-js/api/index.html +1 -1
- data/docs/brut-js/api/index.js.html +1 -1
- data/docs/brut-js/api/module-testing.html +1 -1
- data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
- data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
- data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
- data/docs/brut-js/api/testing.DOMCreator.html +1 -1
- data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
- data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
- data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
- data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
- data/docs/brut-js/api/testing_index.js.html +1 -1
- data/docs/brut-js.html +12 -7
- data/docs/business-logic.html +10 -5
- data/docs/cli.html +26 -26
- data/docs/components.html +61 -64
- data/docs/configuration.html +13 -8
- data/docs/css.html +14 -9
- data/docs/custom-element-tests.html +14 -9
- data/docs/database-access.html +12 -7
- data/docs/database-schema.html +15 -10
- data/docs/deployment.html +58 -6
- data/docs/dev-environment.html +12 -7
- data/docs/dir-structure.html +74 -0
- data/docs/doc-conventions.html +11 -6
- data/docs/end-to-end-tests.html +15 -8
- data/docs/favicon.ico +0 -0
- data/docs/features.html +182 -0
- data/docs/flash-and-session.html +73 -82
- data/docs/form-constraints.html +118 -0
- data/docs/forms.html +57 -367
- data/docs/getting-started.html +15 -10
- data/docs/handlers.html +51 -61
- data/docs/hashmap.json +1 -1
- data/docs/hooks.html +14 -9
- data/docs/i18n.html +12 -7
- data/docs/index.html +11 -6
- data/docs/instrumentation.html +12 -7
- data/docs/javascript.html +17 -12
- data/docs/jobs.html +10 -5
- data/docs/keyword-injection.html +22 -21
- data/docs/layouts.html +12 -20
- data/docs/lsp.html +11 -6
- data/docs/markdown-examples.html +10 -5
- data/docs/middleware.html +10 -5
- data/docs/not-released.html +10 -5
- data/docs/overview.html +11 -138
- data/docs/pages.html +49 -121
- data/docs/recipes/alternate-layouts.html +50 -0
- data/docs/recipes/authentication.html +166 -6
- data/docs/recipes/blank-layouts.html +43 -0
- data/docs/recipes/custom-flash.html +54 -0
- data/docs/recipes/indexed-forms.html +102 -0
- data/docs/recipes/text-field-component.html +129 -0
- data/docs/routes.html +16 -19
- data/docs/security.html +11 -6
- data/docs/seed-data.html +10 -5
- data/docs/space-time-continuum.html +11 -6
- data/docs/tutorial.html +11 -6
- data/docs/unit-tests.html +10 -5
- data/docs/why.html +29 -0
- data/dx/bash_customizations +7 -0
- data/dx/build +13 -2
- data/dx/docker-compose.env +1 -1
- data/dx/exec +25 -8
- data/lib/brut/front_end/form.rb +8 -8
- data/lib/brut/front_end/forms/input.rb +253 -20
- data/lib/brut/front_end/forms/input_definition.rb +15 -12
- data/lib/brut/front_end/forms/radio_button_group_input.rb +8 -1
- data/lib/brut/front_end/forms/select_input.rb +8 -1
- data/lib/brut/front_end.rb +1 -0
- data/lib/brut/version.rb +1 -1
- data/specs/brut/front_end/forms/input.spec.rb +978 -0
- data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +54 -0
- data/specs/brut/front_end/forms/select_input.spec.rb +54 -0
- data/specs/spec_helper.rb +27 -0
- data/specs/support/matchers/have_constraint_violation.rb +23 -0
- data/specs/support/matchers.rb +5 -0
- data/specs/support.rb +3 -0
- metadata +141 -77
- data/brutrb.com/public/images/logo-300.png +0 -0
- data/brutrb.com/public/images/logo.png +0 -0
- data/docs/assets/ai.md._6HCDL6d.lean.js +0 -1
- data/docs/assets/chunks/@localSearchIndexroot.CoYzciVi.js +0 -1
- data/docs/assets/components.md.CRUMdRoN.js +0 -104
- data/docs/assets/deployment.md.Dbka4OTr.js +0 -1
- data/docs/assets/deployment.md.Dbka4OTr.lean.js +0 -1
- data/docs/assets/dev-environment.md.GZv6xvi9.js +0 -11
- data/docs/assets/doc-conventions.md.-kN3Xo5C.js +0 -1
- data/docs/assets/doc-conventions.md.-kN3Xo5C.lean.js +0 -1
- data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +0 -1
- data/docs/assets/flash-and-session.md.BXY8RvT0.js +0 -93
- data/docs/assets/forms.md.B-koVgyw.js +0 -379
- data/docs/assets/forms.md.B-koVgyw.lean.js +0 -1
- data/docs/assets/handlers.md.089DVD3v.js +0 -69
- data/docs/assets/handlers.md.089DVD3v.lean.js +0 -1
- data/docs/assets/index.md.B28EwVpq.js +0 -1
- data/docs/assets/index.md.B28EwVpq.lean.js +0 -1
- data/docs/assets/keyword-injection.md.Dt2tKREs.js +0 -25
- data/docs/assets/layouts.md.cPnh3NId.lean.js +0 -1
- data/docs/assets/overview.Da81cB9R.png +0 -0
- data/docs/assets/overview.md.C5wlBcR5.js +0 -133
- data/docs/assets/overview.md.C5wlBcR5.lean.js +0 -1
- data/docs/assets/pages.md.BE3kfOc5.js +0 -122
- data/docs/assets/pages.md.BE3kfOc5.lean.js +0 -1
- data/docs/assets/recipes_authentication.md.CAsXf7hk.js +0 -1
- data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +0 -1
- data/docs/assets/routes.md.BMM7peut.js +0 -29
- data/docs/assets/space-time-continuum.md.KPUIKysQ.js +0 -1
- data/docs/assets/tutorial.md.BnoGjrdK.js +0 -1
- data/docs/assets/tutorial.md.BnoGjrdK.lean.js +0 -1
- data/docs/images/logo-300.png +0 -0
- data/docs/images/logo.png +0 -0
- /data/docs/assets/{cli.md.RmeA2b0i.lean.js → cli.md.CjsktgFz.lean.js} +0 -0
- /data/docs/assets/{configuration.md.BGHl8oRC.lean.js → configuration.md.DeyhpqEx.lean.js} +0 -0
- /data/docs/assets/{css.md.DJgj2clw.lean.js → css.md.CltvJqAa.lean.js} +0 -0
- /data/docs/assets/{custom-element-tests.md.BrYJQEl3.lean.js → custom-element-tests.md.B_rbta32.lean.js} +0 -0
- /data/docs/assets/{database-access.md.C7l-Vuvb.lean.js → database-access.md.gnluu54N.lean.js} +0 -0
- /data/docs/assets/{dev-environment.md.GZv6xvi9.lean.js → dev-environment.md.BroAOLhF.lean.js} +0 -0
@@ -0,0 +1,373 @@
|
|
1
|
+
# Quick Tour of Brut's Features
|
2
|
+
|
3
|
+
## Pages
|
4
|
+
|
5
|
+
A [*Page*](/pages) models, well, a web page. It's a class that holds all the data
|
6
|
+
necessary to generate its HTML as well as a method called `page_template`, which
|
7
|
+
generates the HTML via Phlex.
|
8
|
+
|
9
|
+
A page's routing is convention-based and starts with a URL:
|
10
|
+
|
11
|
+
```ruby{6}
|
12
|
+
class App < Brut::Framework::App
|
13
|
+
def id = "my-app"
|
14
|
+
def organization = "my-org"
|
15
|
+
|
16
|
+
routes do
|
17
|
+
page "/dashboard"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
This URL means our page class is expected in `DashboardPage`.
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
class DashboardPage < AppPage
|
26
|
+
def initialize
|
27
|
+
@now = Time.now
|
28
|
+
end
|
29
|
+
|
30
|
+
def page_template
|
31
|
+
main do
|
32
|
+
h1 { "Hello!" }
|
33
|
+
h2 do
|
34
|
+
plain("It's ")
|
35
|
+
time(datetime: l(@now, format: iso_8601)) do
|
36
|
+
l(@now, format: date)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
This would all produce HTML like so, depending on the value of
|
45
|
+
|
46
|
+
```html [/dashboard]
|
47
|
+
<main>
|
48
|
+
<h1>Hello!</h1>
|
49
|
+
<h2>It's
|
50
|
+
<time datetime="2025-02-17">
|
51
|
+
Monday, Feb 17
|
52
|
+
</time>
|
53
|
+
</h2>
|
54
|
+
</main>
|
55
|
+
```
|
56
|
+
|
57
|
+
Note that the actual HTML delivered would include the code for a layout.
|
58
|
+
|
59
|
+
## Layouts
|
60
|
+
|
61
|
+
Brut includes the concept of [layouts](/layouts), and they work similar to Rails.
|
62
|
+
Layouts are classes, however, and implement the Phlex-standard `view_template`
|
63
|
+
method:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class DefaultLayout < Brut::FrontEnd::Layout
|
67
|
+
def initialize(page_name:)
|
68
|
+
@page_name = page_name
|
69
|
+
end
|
70
|
+
|
71
|
+
def view_template
|
72
|
+
doctype
|
73
|
+
html(lang: "en") do
|
74
|
+
head do
|
75
|
+
meta(charset: "utf-8")
|
76
|
+
link(rel: "preload", as: "style", href: asset_path("/css/styles.css"))
|
77
|
+
link(rel: "stylesheet", href: asset_path("/css/styles.css"))
|
78
|
+
script(defer: true, src: asset_path("/js/app.js"))
|
79
|
+
title { app_name }
|
80
|
+
end
|
81
|
+
body do
|
82
|
+
yield
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
```
|
88
|
+
|
89
|
+
This produces this HTML:
|
90
|
+
|
91
|
+
```html
|
92
|
+
<!DOCTYPE html>
|
93
|
+
<html>
|
94
|
+
<head>
|
95
|
+
<meta charset="utf-8">
|
96
|
+
<link rel="preload" as="style" href="/css/styles-«HASH».css">
|
97
|
+
<link rel="stylesheet" href="/css/styles-«HASH».css">
|
98
|
+
<script defer src="/js/app-«HASH».js">
|
99
|
+
<title>My Awesome App</title>
|
100
|
+
</head>
|
101
|
+
<body>
|
102
|
+
<-- page HTML here -->
|
103
|
+
</body>
|
104
|
+
</html>
|
105
|
+
```
|
106
|
+
|
107
|
+
|
108
|
+
## Components
|
109
|
+
|
110
|
+
*Components* are a way to manage the complexity of HTML generation. The are Phlex components, meaning they are a class that implements `view_template`.
|
111
|
+
|
112
|
+
Here's an example of a flash message component:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
# components/flash_component.rb
|
116
|
+
class FlashComponent < AppComponent
|
117
|
+
def initialize(flash:)
|
118
|
+
if flash.notice?
|
119
|
+
@message_key = flash.notice
|
120
|
+
@role = :info
|
121
|
+
elsif flash.alert?
|
122
|
+
@message_key = flash.alert
|
123
|
+
@role = :alert
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def any_message? = !@message_key.nil?
|
128
|
+
|
129
|
+
def view_template
|
130
|
+
if any_message?
|
131
|
+
div(role: @role) do
|
132
|
+
t([ :flash, @message_key ])
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
```
|
138
|
+
|
139
|
+
You can then use this in any other view using `render`, provided by Phlex.
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
def page_template
|
143
|
+
header do
|
144
|
+
render FlashComponent.new(flash:)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
```
|
148
|
+
|
149
|
+
## Forms
|
150
|
+
|
151
|
+
[*Forms*](/forms) are a major concept like pages, since they are the way a browser
|
152
|
+
submits data to the server.
|
153
|
+
|
154
|
+
In Brut, a form does three things:
|
155
|
+
|
156
|
+
* Describes the data in the `<form>` tag
|
157
|
+
* Implies a route where its data is submitted via HTTP POST
|
158
|
+
* Provides access to the submitted data (via an object with methods, not a Hash of Whatever)
|
159
|
+
|
160
|
+
Like `page`, `form` declares a form's route:
|
161
|
+
|
162
|
+
```ruby{2}
|
163
|
+
routes do
|
164
|
+
form "/login"
|
165
|
+
end
|
166
|
+
```
|
167
|
+
|
168
|
+
Brut is convention-based, so it will expect a class named `LoginForm` to exist. It
|
169
|
+
will also expect `LoginHandler` to exist, which is a class that will receive the
|
170
|
+
form submission and process it. More on handlers below.
|
171
|
+
|
172
|
+
`LoginForm` uses class methods to declare its inputs. These class methods mirror
|
173
|
+
the various form element tags in HTML (`input`, `select`, etc.), and the methods
|
174
|
+
attributes allow you to declare names and client-side constraints:
|
175
|
+
|
176
|
+
```ruby [forms/login_form.rb]
|
177
|
+
class LoginForm < AppForm
|
178
|
+
input :email
|
179
|
+
input :password, minlength: 8
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
An instance of this class can be used to create HTML:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
def view_template
|
187
|
+
FormTag(for: @form) do
|
188
|
+
Inputs::InputTag(form: @form, input_name: :email)
|
189
|
+
Inputs::InputTag(form: @form, input_name: :password)
|
190
|
+
button { "Login" }
|
191
|
+
end
|
192
|
+
end
|
193
|
+
```
|
194
|
+
|
195
|
+
> [!NOTE]
|
196
|
+
> We'll explain what `FormTag` and `Inputs::InputTag` are in the [forms
|
197
|
+
> section](/forms)
|
198
|
+
|
199
|
+
This generates this HTML:
|
200
|
+
|
201
|
+
```html
|
202
|
+
<form action="/login" method="POST">
|
203
|
+
<input type="email" name="email" required>
|
204
|
+
<input type="password" name="password" required minlength="8">
|
205
|
+
<button>Login</button>
|
206
|
+
</form>
|
207
|
+
```
|
208
|
+
|
209
|
+
When the form is submitted, an instance of `LoginForm` is created and made available
|
210
|
+
to `LoginHandler`
|
211
|
+
|
212
|
+
## Handlers
|
213
|
+
|
214
|
+
A handler is like a controller in Rails, except it only has one method: `handle`.
|
215
|
+
Unlike a Rails controller, a handler class is given its arguments explicitly, and
|
216
|
+
`handle`'s return value dictates what will happen next.
|
217
|
+
|
218
|
+
```ruby
|
219
|
+
class LoginHandler < AppHandler
|
220
|
+
def initialize(form:)
|
221
|
+
@form = form
|
222
|
+
end
|
223
|
+
def handle
|
224
|
+
if @form.email == "secret@example.com" &&
|
225
|
+
@form.password = "sup3rs3cret!"
|
226
|
+
redirect_to(DashboardPage)
|
227
|
+
else
|
228
|
+
form.server_side_constraint_violation(
|
229
|
+
input_name: :email,
|
230
|
+
key: :no_such_user
|
231
|
+
)
|
232
|
+
LoginPage.new(form:)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
```
|
237
|
+
|
238
|
+
Note that we access the form's values as methods, not by digging into a Hash of
|
239
|
+
Whatever. Also note that returning an instance of a page will generate that page's
|
240
|
+
HTML, much like Rails' `render :edit` might. Lastly, `redirect_to` is a
|
241
|
+
convenience to generate a URL to `DashboardPage`, and ultimately causes `handle` to
|
242
|
+
return a `URI`, which Brut interprets as a redirect.
|
243
|
+
|
244
|
+
|
245
|
+
## JavaScript
|
246
|
+
|
247
|
+
Brut doesn't include a front-end framework, however you can certainly use one. All
|
248
|
+
JavaScript is bundled into a single bundle by [esbuild](https://esbuild.github.io/).
|
249
|
+
|
250
|
+
Brut includes [BrutJS](/brut-js), which is a collection of autonomous custom
|
251
|
+
elements AKA Web Components that provide convenient features like autosubmit, form
|
252
|
+
submission confirmation and more:
|
253
|
+
|
254
|
+
```ruby{5,7}
|
255
|
+
def view_template
|
256
|
+
FormTag(for: @form) do
|
257
|
+
Inputs::InputTag(form: @form, input_name: :email)
|
258
|
+
Inputs::InputTag(form: @form, input_name: :password)
|
259
|
+
brut_confirm_submit(message: "Really login? In this economy?!") do
|
260
|
+
button { "Login" }
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
```
|
265
|
+
|
266
|
+
When "Login" is pressed, `window.confirm` will ask if the visitor wants to proceed.
|
267
|
+
This custom element can also use a `<dialog>` that you style, and works even better
|
268
|
+
if that `<dialog>` makes use of `<brut-confirmation-dialog>`.
|
269
|
+
|
270
|
+
## CSS
|
271
|
+
|
272
|
+
Brut includes [BrutCSS](/css#using-brut-css), which is a lightweight utility-based
|
273
|
+
CSS library to let you get started quickly. It's *not* TailwindCSS, nor will it ever
|
274
|
+
be.
|
275
|
+
|
276
|
+
You can replace it with whatever you like easily enough.
|
277
|
+
|
278
|
+
|
279
|
+
## Database Schema
|
280
|
+
|
281
|
+
Brut provides access to an SQL database via Sequel. Brut uses Sequel's database schema management, however it is enhanced to
|
282
|
+
encourage good practices by default.
|
283
|
+
|
284
|
+
> [!NOTE]
|
285
|
+
> Brut currently *only supports* PostgreSQL. It may support all RDBMSes that Sequel supports, but as of now,
|
286
|
+
> it's just Postgres.
|
287
|
+
|
288
|
+
Consider a `households` table that relates to an `accounts` table.
|
289
|
+
|
290
|
+
```ruby
|
291
|
+
create_table :households,
|
292
|
+
comment: "Family unit managing the data" do
|
293
|
+
column :timezone, :text
|
294
|
+
column :dinner_time_of_day, :text
|
295
|
+
constraint(
|
296
|
+
:time_must_be_time,
|
297
|
+
%{
|
298
|
+
(dinner_time_of_day ~ '^[01][0-9]:[0-5][0-9]$') OR
|
299
|
+
(dinner_time_of_day ~ '^2[0-3]:[0-5][0-9]$')
|
300
|
+
}
|
301
|
+
)
|
302
|
+
end
|
303
|
+
|
304
|
+
create_table :accounts,
|
305
|
+
comment: "People or systems who can access this system",
|
306
|
+
external_id: true do
|
307
|
+
|
308
|
+
column :email, :email_address, unique: true
|
309
|
+
column :deactivated_at, :timestamptz, null: true
|
310
|
+
foreign_key :household_id, :households
|
311
|
+
end
|
312
|
+
```
|
313
|
+
|
314
|
+
This is mostly using [Sequel's built-in migrations API](https://sequel.jeremyevans.net/rdoc/files/doc/schema_modification_rdoc.html). But, a few additional behaviors are happening:
|
315
|
+
|
316
|
+
* Columns default to `NOT NULL`
|
317
|
+
* Tables require comments
|
318
|
+
* Foreign keys default to having constraints and indexes
|
319
|
+
|
320
|
+
There are other quality-of-life features of Brut's migration system, all designed to default to a good practice, with a way to do it
|
321
|
+
however you want when needed.
|
322
|
+
|
323
|
+
## Database Access
|
324
|
+
|
325
|
+
Brut uses `Sequel::Model` to access data in your database. To discourage the conflation of "models of database tables" with "models
|
326
|
+
of your application's domain", these classes are in the `DB` namespace. Thus, the class `DB::Household` would be able to access the
|
327
|
+
`households` table defined above. This frees you up to create a `Household` class to model your domain's logic without being coupled
|
328
|
+
to how you store some data in a database.
|
329
|
+
|
330
|
+
```ruby
|
331
|
+
class DB::Account < AppDataModel
|
332
|
+
has_external_id :ac
|
333
|
+
many_to_one :household
|
334
|
+
end
|
335
|
+
|
336
|
+
class DB::Household < AppDataModel
|
337
|
+
one_to_many :accounts
|
338
|
+
end
|
339
|
+
```
|
340
|
+
|
341
|
+
## Domain and Business Logic
|
342
|
+
|
343
|
+
Brut uses Zeitwerk for code loading, so any directories you create will be auto-loaded and refreshed during development. This means that you can create a class named `Household` in `app/src/back_end/domain/household.rb` and it would be loaded. Or, you could create `HouseholdService` in `app/src/back_end/services/household_service.rb` if you like.
|
344
|
+
|
345
|
+
> [!TIP]
|
346
|
+
> Providing a generally-useful abstraction for business or domain logic is not usually feasible.
|
347
|
+
> Thus, Brut doesn't provide much beyond Zeitwerk's auto-loading feature. It may provide more
|
348
|
+
> assistance in the future, but for now, Brut's approach is to free you from any prescription
|
349
|
+
> or moral imperative. Manage your domain and business logic how you see fit. You know your domain
|
350
|
+
> and team better than we do.
|
351
|
+
|
352
|
+
## Testing
|
353
|
+
|
354
|
+
Brut provides support for three types of tests:
|
355
|
+
|
356
|
+
* Unit Tests, using RSpec
|
357
|
+
* End-to-end tests, using RSpec and Playwright
|
358
|
+
* Custom Element tests, written in JavaScript, using Mocha
|
359
|
+
|
360
|
+
Since Brut is based on classes, objects, and methods, your unit tests will usually
|
361
|
+
be straightforward, however Brut provides helpers to test your Page and Component
|
362
|
+
HTML using Nokogiri. FactoryBot is included and configured to manage test data.
|
363
|
+
|
364
|
+
## Tasks
|
365
|
+
|
366
|
+
Brut doesn't use Rake tasks. It uses CLI apps powered by Ruby's `OptionParser`. Brut provides bootstrapping classes to make your own CLIs, as well as some light abstractions to make `OptionParser` a little more ergonomic. Brut's dev and production management CLIs are built using this support.
|
367
|
+
|
368
|
+
## Observability
|
369
|
+
|
370
|
+
Brut has built-in support for [OpenTelemetry](https://opentelemetry.io/). Brut includes configuration for the [otel-desktop-viewer](https://github.com/CtrlSpice/otel-desktop-viewer) or a text-based viewer suitable for development. For production, most observability vendors provide OpenTelemetry ingestion any many have free tiers.
|
371
|
+
|
372
|
+
Brut does support logging, however you are encouraged to use OpenTelemetry instead.
|
373
|
+
|
@@ -1,205 +1,189 @@
|
|
1
1
|
# Flash and Session
|
2
2
|
|
3
|
-
Brut sessions are stored in cookies, encrypted to prevent tampering. The *flash*, which is a way to temporarily
|
4
|
-
store small bits of information between page loads, is encoded in the session.
|
3
|
+
Brut sessions are stored in cookies, encrypted to prevent tampering. The *flash*, which is a way to temporarily store small bits of information between page loads, is encoded in the session.
|
5
4
|
|
6
5
|
## Overview
|
7
6
|
|
8
|
-
Unlike Rails, the session and flash are presented to you as objects, not Hashes. By declaring the `session:`
|
9
|
-
parameter on an initializer, you'll be given the current session for the request as an `AppSession`, which
|
10
|
-
inherits from `Brut::FrontEnd::Session`. Similarly, declaring `flash:`, you'll get a `Brut::FrontEnd::Flash`.
|
7
|
+
Unlike Rails, the session and flash are presented to you as objects, not Hashes of Whatever. By declaring the `session:`
|
8
|
+
parameter on an initializer, you'll be given the current session for the request as an `AppSession`, which inherits from `Brut::FrontEnd::Session`. Similarly, declaring `flash:`, you'll get a `Brut::FrontEnd::Flash`.
|
11
9
|
|
12
10
|
The idea is to use Ruby's type system to describe what data is in the session and flash.
|
13
11
|
|
14
12
|
### Session
|
15
13
|
|
16
|
-
Brut's session is somewhat richer than you might get from other frameworks. In particular, the session can
|
17
|
-
provide you:
|
14
|
+
Brut's session is somewhat richer than you might get from other frameworks. In particular, the session can provide you:
|
18
15
|
|
19
16
|
* The current `Brut::I18n::HTTPAcceptLanguage`, which is the visitor's locale. See [I18n](/i18n) for how this
|
20
17
|
works and how to use this value.
|
21
18
|
* The timezone as provided by the browser.
|
22
19
|
* An explicitly-set timezone that may or may not be what the browser provided. See [Space-Time Continuum](/space-time-continuum) for more details.
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
storing.
|
21
|
+
To access the session, declare it as a keyword argument to your page, handler, or
|
22
|
+
global component's intitializer:
|
27
23
|
|
28
|
-
|
24
|
+
```ruby
|
25
|
+
class HomePage < AppPage
|
26
|
+
def initialize(session:)
|
27
|
+
@session = session
|
28
|
+
end
|
29
|
+
end
|
30
|
+
```
|
31
|
+
|
32
|
+
When you create your Brut app, your `AppSession` won't have anything in it, although
|
33
|
+
it's a `Brut::FrontEnd::Session`, so you can certainly use `[]` and `[]=` on it.
|
34
|
+
However, you are encouraged to declare methods that describe precisely what is in
|
35
|
+
the session.
|
29
36
|
|
30
|
-
|
31
|
-
|
32
|
-
> makes an assumption about how the session is managed. It's for demonstration only.
|
33
|
-
> The [route hooks](/hooks) section has a more
|
34
|
-
> appropriate example.
|
37
|
+
Let's say the currently logged-in visitor is available in the session. Your
|
38
|
+
`HomePage` could look like so:
|
35
39
|
|
36
40
|
```ruby
|
37
|
-
class
|
38
|
-
def
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
41
|
+
class HomePage < AppPage
|
42
|
+
def initialize(session:)
|
43
|
+
@session = session
|
44
|
+
end
|
45
|
+
|
46
|
+
def view_template
|
47
|
+
h1 do
|
48
|
+
if @session.current_visitor
|
49
|
+
"Hello #{@session.current_visitor.name}"
|
50
|
+
else
|
51
|
+
"Hi!"
|
43
52
|
end
|
44
53
|
end
|
45
54
|
end
|
46
55
|
end
|
47
56
|
```
|
48
57
|
|
49
|
-
|
50
|
-
that class might look like:
|
58
|
+
Let's suppose a `LoginHandler` exists, that can set a value for `current_visitor`:
|
51
59
|
|
52
60
|
```ruby
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
end
|
58
|
-
|
59
|
-
def logout!
|
60
|
-
self[:current_user_id] = nil
|
61
|
+
class LoginHandler < AppHandler
|
62
|
+
def initialize(form:, session:)
|
63
|
+
@form = form
|
64
|
+
@session = session
|
61
65
|
end
|
62
66
|
|
63
|
-
def
|
64
|
-
|
67
|
+
def handle
|
68
|
+
visitor = Login.from_form(form:) # assume this exists
|
69
|
+
if visitor
|
70
|
+
@sesion.login!(visitor:)
|
71
|
+
else
|
72
|
+
# ...
|
73
|
+
end
|
65
74
|
end
|
66
|
-
|
67
|
-
def current_user_id = self[:current_user_id]
|
68
75
|
end
|
69
76
|
```
|
70
77
|
|
71
|
-
|
72
|
-
the lookup in the database:
|
78
|
+
`AppSession` would need to look like so:
|
73
79
|
|
74
|
-
```ruby
|
75
|
-
# app/src/front_end/support/app_session.rb
|
80
|
+
```ruby
|
76
81
|
class AppSession < Brut::FrontEnd::Session
|
77
|
-
def login!(
|
78
|
-
self[:
|
79
|
-
end
|
80
|
-
def logout!
|
81
|
-
self[:current_user_id] = nil
|
82
|
+
def login!(visitor:)
|
83
|
+
self[:current_visitor_id] = visitor.id
|
82
84
|
end
|
83
85
|
|
84
|
-
def
|
85
|
-
|
86
|
-
end
|
87
|
-
|
88
|
-
def current_user
|
89
|
-
DB::User.find(id: self[:current_user_id])
|
86
|
+
def current_visitor
|
87
|
+
DB::Visitor.find(id: self[:current_visitor_id])
|
90
88
|
end
|
91
89
|
end
|
92
90
|
```
|
93
91
|
|
94
|
-
|
92
|
+
Brut encourages your session to be a rich object. You can declare any methods you
|
93
|
+
like:
|
95
94
|
|
96
|
-
```ruby
|
97
|
-
class
|
98
|
-
def
|
99
|
-
if session.logged_in?
|
100
|
-
request_context[:current_user] = session.current_user
|
101
|
-
end
|
102
|
-
end
|
95
|
+
```ruby
|
96
|
+
class AppSession < Brut::FrontEnd::Session
|
97
|
+
def logged_in? = !!self.current_visitor
|
103
98
|
end
|
104
99
|
```
|
105
100
|
|
106
|
-
|
107
|
-
|
108
|
-
|
101
|
+
> [!NOTE]
|
102
|
+
> When dealing with auth, you can leverage
|
103
|
+
> keyword injection beyond injecting the session. This is
|
104
|
+
> discussed in [the auth recipe](/recipes/authentication.md)
|
109
105
|
|
110
|
-
|
111
|
-
# app/src/front_end/handlers/login_handler.rb
|
112
|
-
class LoginHandler < AppHandler
|
113
|
-
def initialize(form:, session:)
|
114
|
-
@form = form
|
115
|
-
@session = session
|
116
|
-
end
|
106
|
+
### Flash
|
117
107
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
input_name: :email,
|
127
|
-
key: :login_not_found
|
128
|
-
)
|
129
|
-
else
|
130
|
-
session.login!(current_user: authorized_user.user)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
if @form.constraint_violations?
|
134
|
-
LoginPage.new(form: @form)
|
135
|
-
else
|
136
|
-
redirect_to(DashboardPage.routing)
|
137
|
-
end
|
108
|
+
To access the flash, declare it as a keyword argument to your page, handler, or
|
109
|
+
global component's intitializer:
|
110
|
+
|
111
|
+
```ruby {2,4}
|
112
|
+
class DeleteWidgetByIdHandler < AppHandler
|
113
|
+
def initialize(widget_id:, flash:)
|
114
|
+
@widget_id = widget_id
|
115
|
+
@flash = flash
|
138
116
|
end
|
139
117
|
end
|
140
118
|
```
|
141
119
|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
By default, your app will use Brut's flash class, `Brut::FrontEnd::Flash`. This is because you typically don't
|
148
|
-
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 `[]`.
|
120
|
+
By default, the flash will be a `Brut::FrontEnd::Flash`. While you can set your own
|
121
|
+
class, this is less commonly needed, so Brut doesn't provide one by default. Like
|
122
|
+
the session, you can use `[]`, but are discouraged from this to avoid Hashes of
|
123
|
+
Whatever littering your code.
|
149
124
|
|
150
|
-
The
|
151
|
-
request, but not after that.
|
125
|
+
The default flash provides a `notice` attribute and an `alert` attribute. Their
|
126
|
+
values only survive one request, so anything you set will be available in that session's next request, but not after that.
|
152
127
|
|
153
|
-
|
154
|
-
is encouraged. If you pass an array into `alert=` or `notice=`, the elements will be joined to form an I18n key.
|
128
|
+
The values are expected to be I18n keys:
|
155
129
|
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
130
|
+
```ruby {5,8}
|
131
|
+
def handle
|
132
|
+
widget = DB::Widget.find!(id: @widget_id)
|
133
|
+
if widget.can_delete?
|
134
|
+
widget.delete
|
135
|
+
@flash.notice = :widget_deleted
|
136
|
+
redirect_to(WidgetsPage)
|
137
|
+
else
|
138
|
+
@flash.alert = :widget_cannot_be_deleted
|
139
|
+
WidgetsPage.new
|
140
|
+
end
|
166
141
|
end
|
167
|
-
def debug = self[:debug]
|
168
|
-
def debug? = !!self.debug
|
169
142
|
end
|
170
143
|
```
|
171
144
|
|
172
|
-
|
173
|
-
flash:
|
174
|
-
|
175
|
-
```ruby
|
176
|
-
class
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
145
|
+
This is only enforced by convention, but you should stick to one convention since
|
146
|
+
you will likely create a [global component](/components) for the flash:
|
147
|
+
|
148
|
+
```ruby {17}
|
149
|
+
class FlashComponent < AppComponent
|
150
|
+
def initialize(flash:)
|
151
|
+
if flash.notice?
|
152
|
+
@message_key = flash.notice
|
153
|
+
@role = :info
|
154
|
+
elsif flash.alert?
|
155
|
+
@message_key = flash.alert
|
156
|
+
@role = :alert
|
157
|
+
end
|
183
158
|
end
|
184
159
|
|
185
|
-
|
160
|
+
def any_message? = !@message_key.nil?
|
161
|
+
|
162
|
+
def view_template
|
163
|
+
if any_message?
|
164
|
+
div(role: @role) do
|
165
|
+
t([ :flash, @message_key ])
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
186
169
|
end
|
187
170
|
```
|
188
171
|
|
189
|
-
|
172
|
+
See [using your own Flash class](/recipes/custom-flash) to see how to enhance Brut's
|
173
|
+
flash with your own logic.
|
190
174
|
|
191
175
|
## Testing
|
192
176
|
|
193
177
|
Testing your session or flash classes may not be super valuable, however they are normal Ruby objects so you can
|
194
|
-
test them in a conventional way.
|
195
|
-
|
178
|
+
test them in a conventional way. Although you are discouraged from using `[]` and
|
179
|
+
`[]=` as the public API of your session or flash, they can be useful for assertions
|
180
|
+
or test setup.
|
196
181
|
|
197
182
|
## Recommended Practices
|
198
183
|
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
184
|
+
Do not treat the session or flash as a Hash of Whatever. Isolate all magic keys to
|
185
|
+
the class and provide a rich API. It doesn't take that much effort and will make
|
186
|
+
your app way easier to manage.
|
203
187
|
|
204
188
|
## Technical Notes
|
205
189
|
|