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
@@ -83,25 +83,24 @@ Docker container for local observability via OpenTelemetry.
|
|
83
83
|
```
|
84
84
|
> dx/start
|
85
85
|
```
|
86
|
-
4. Now,
|
86
|
+
4. Now, install your aps gems and set it all up:
|
87
87
|
|
88
88
|
```
|
89
|
-
> dx/exec
|
90
|
-
```
|
91
|
-
|
92
|
-
5. Set everything up:
|
93
|
-
|
94
|
-
```
|
95
|
-
inside-container> bin/setup
|
89
|
+
> dx/exec bin/setup
|
96
90
|
```
|
97
91
|
|
98
92
|
Now, you're ready to go. See [Dev Environemnt](/dev-environment) for details on how
|
99
93
|
this all works.
|
100
94
|
|
95
|
+
> [!NOTE]
|
96
|
+
> Instead of running `dx/exec` in front of your commands, you
|
97
|
+
> can instead do `dx/exec bash` to "log in" to the running container.
|
98
|
+
> You'll have a normal prompt and can issue commands directly from there.
|
99
|
+
|
101
100
|
## Run the App
|
102
101
|
|
103
102
|
```
|
104
|
-
|
103
|
+
dx/exec bin/dev
|
105
104
|
```
|
106
105
|
|
107
106
|
You can now visit your app at `localhost:6502`
|
@@ -128,10 +127,10 @@ There are a few tests you can run, as well as some checks that you aren't using
|
|
128
127
|
RubyGems with security vulnerabilities. Run it all now with `bin/ci`:
|
129
128
|
|
130
129
|
```
|
131
|
-
|
130
|
+
dx/exec bin/ci
|
132
131
|
```
|
133
132
|
|
134
133
|
## Now Build The Rest of Your App 🦉
|
135
134
|
|
136
|
-
You can [follow the tutorial](/tutorial), check out the [conceptual overview](/overview), or dive straight into the API docs. You might also want to check out the docs for [LSP Support](/lsp).
|
135
|
+
You can [follow the tutorial](/tutorial), check out the [conceptual overview](/overview), or dive straight into the [API docs](/api/index.html). You might also want to check out the docs for [LSP Support](/lsp).
|
137
136
|
|
data/brutrb.com/handlers.md
CHANGED
@@ -6,137 +6,161 @@ Handlers process form submissions, and *actions* work similarly to process any a
|
|
6
6
|
|
7
7
|
Where a [page](/pages) renders a web page in HTML, a *handler* responds to all other HTTP requests. To respond to such HTTP requests, you'd first create a [route](/routes), using `form`, `action`, or `path`.
|
8
8
|
|
9
|
-
###
|
9
|
+
### Handler Structure
|
10
10
|
|
11
|
-
|
11
|
+
A handler's initializer is subject to [keyword injection](/keyword-injection), with
|
12
|
+
the addition of the `form:` keyword, which, if present, will be an instance of the
|
13
|
+
associated form class, populated with the data in the form submission (not available for `path` or `action` routes).
|
12
14
|
|
13
|
-
|
15
|
+
You must implement `handle` to process the form. **A handler's public API, as
|
16
|
+
called by Brut and your tests, is `handle!`**, however you implement `handle`, which
|
17
|
+
`handle!` calls.
|
14
18
|
|
15
|
-
|
16
|
-
# inside app/src/app.rb
|
17
|
-
path "/webhooks/stripe", method: :put
|
18
|
-
```
|
19
|
-
|
20
|
-
In all cases, the class to receive and process these requests is a handler, whose name is conventional based on the route. For example, the webhook above would be handled by `Webhooks::StripeHandler`.
|
21
|
-
|
22
|
-
### Implementing Handlers
|
19
|
+
`handle`'s return value dictates what will happen next:
|
23
20
|
|
24
|
-
|
21
|
+
| Return Value | Behavior |
|
22
|
+
|---|---|
|
23
|
+
|Instance of a page or component | That page or component's HTML is generated and returned. This is not a redirect, but more like `render :new` in a Rails controller |
|
24
|
+
|`Brut::FrontEnd::HttpStatus` | This HTTP status is returned with no body. Use `http_status` from `Brut::FrontEnd::HandlingResults` (included in all handlers) to create an instance from a number |
|
25
|
+
| `URI` | Redirect to the URI. Use `redirect_to` from `Brut::FrontEnd::HandlingResults` (included in all handlers) to generate a URI from a page class and parameters |
|
26
|
+
| Two element array with *element 0* being a `Brut::FrontEnd::HttpStatus`, and *element 1* being a page or component instance | Generates the page or component's HTML, but sets the given status instead of 200. Useful for Ajax responses where the HTTP status affects client-side behavior |
|
27
|
+
| `Brut::FrontEnd::Download` | Download a file |
|
28
|
+
| `Brut::FrontEnd::GenericResponse` | wrap any Rack response |
|
25
29
|
|
26
|
-
|
27
|
-
|
28
|
-
|
30
|
+
> [!IMPORTANT]
|
31
|
+
> The only way to render something other than HTML is to do so as a
|
32
|
+
> `GenericResponse`, which is basically the low-level Rack API. Brut
|
33
|
+
> encourages Ajax responses to be HTML and for you to use the browser's
|
34
|
+
> APIs to interact with that HTML. Brut may make it easier to work
|
35
|
+
> with other types of content in the future.
|
29
36
|
|
30
|
-
|
31
|
-
* A `Brut::FrontEnd::HttpStatus` which sends that status and an empty body back. This object can be created from
|
32
|
-
an integer using the `http_status` helper, available to all handlers.
|
33
|
-
* A `URI`, which will cause a redirect to that URI. You can create the `URI` yourself, or use the helper
|
34
|
-
`redirect_to`, which accepts a string.
|
35
|
-
* A two-element array with a page or component as the first element and an `Brut::FrontEnd::HttpStatus` as the
|
36
|
-
second. This will render that page or component's HTML, but use the given status instead of 200. This can be useful for Ajax requests where you want to use HTML your respond format, but also, say, a 422 status to indicate a constraint violation has occured.
|
37
|
-
* A `Brut::FrontEnd::Download`, which encapsulates a file to be downloaded.
|
38
|
-
* A `Brut::FrontEnd::GenericResponse`, which wraps any Rack response with a defined type.
|
37
|
+
### Handling a Form Submission
|
39
38
|
|
40
|
-
|
39
|
+
A common pattern when handling a form submisssion is to check for any constraint
|
40
|
+
violations. If there are some, re-generate the HTML for the page containing the
|
41
|
+
form, highlighting the violations. Otherwise, save the data and redirect to another
|
42
|
+
page.
|
41
43
|
|
42
|
-
|
44
|
+
Here's how that looks:
|
43
45
|
|
44
|
-
```ruby
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
@form = form
|
49
|
-
@session = session
|
46
|
+
```ruby
|
47
|
+
class NewWidgetHandler < AppHandler
|
48
|
+
def initialize(form:)
|
49
|
+
@form = form
|
50
50
|
end
|
51
51
|
|
52
52
|
def handle
|
53
|
+
# if no client-side violations were submitted
|
53
54
|
if !@form.constraint_violations?
|
54
|
-
|
55
|
-
|
56
|
-
password: form.password
|
57
|
-
)
|
58
|
-
if authorized_user.nil?
|
55
|
+
widget = DB::Widget.find(name: form.name)
|
56
|
+
if widget
|
59
57
|
@form.server_side_constraint_violation(
|
60
|
-
input_name: :
|
61
|
-
key: :
|
58
|
+
input_name: :name,
|
59
|
+
key: :name_is_taken
|
62
60
|
)
|
63
|
-
else
|
64
|
-
session.authorized_user = authorized_user
|
65
61
|
end
|
66
62
|
end
|
63
|
+
|
67
64
|
if @form.constraint_violations?
|
68
|
-
|
65
|
+
NewWidgetPage.new(form: @form)
|
69
66
|
else
|
70
|
-
|
67
|
+
DB::Widget.create(name: form.name,
|
68
|
+
quantity: form.quantity,
|
69
|
+
description: form.description)
|
70
|
+
redirect_to(WidgetsPage)
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|
74
74
|
```
|
75
75
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
> with other types of content in the future.
|
76
|
+
Unlike a Rails controller, the return value of `handle` controls the behavior. As
|
77
|
+
we saw in [Form Constraints](/form-constraints), the HTML generated by
|
78
|
+
`NewWidgetPage` will show constraint violations, so by returning
|
79
|
+
`NewWidgetPage.new(form: @form)`, the page has the same form instance, including all
|
80
|
+
the constraint violations, and the visitor will see the problems.
|
82
81
|
|
83
|
-
|
82
|
+
### Handling Other Requests
|
84
83
|
|
85
|
-
|
84
|
+
Non-form submissions work similarly, however there is no form available. If the
|
85
|
+
request could potentially involve a visitor-initiated error, you can use the
|
86
|
+
[flash](/flash-and-session) to communicate back. The flash is available for injection into the
|
87
|
+
initializer and, assuming your page uses it to show messages, can allow for
|
88
|
+
communication when something is wrong:
|
86
89
|
|
87
|
-
|
90
|
+
```ruby
|
91
|
+
class App
|
92
|
+
# ...
|
88
93
|
|
89
|
-
|
90
|
-
|
91
|
-
* `have_returned_http_status` will check that the handler returned an HTTP status
|
92
|
-
* `have_constraint_violation` will check if a form had a particular constraint violation set on it
|
94
|
+
routes do
|
95
|
+
action "/delete_widget/:widget_id"
|
93
96
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
context "when login is valid" do
|
114
|
-
it "forward to the DashboardPage" do
|
115
|
-
user = create(:user, # Assume this is set up via FactoryBot
|
116
|
-
email: "pat@example.com",
|
117
|
-
password: "1q2w3e4r5t6y7u8i9o")
|
118
|
-
|
119
|
-
form = LoginForm.new(params: {
|
120
|
-
email: "pat@example.com",
|
121
|
-
password: "1q2w3e4r5t6y7u8i9o",
|
122
|
-
})
|
123
|
-
session = empty_session
|
124
|
-
result = described_class.new(
|
125
|
-
form:,
|
126
|
-
session:,
|
127
|
-
)
|
128
|
-
expect(result).to have_redirected_to(DashboardPage.routing)
|
129
|
-
expect(session.authorized_user).not_to eq(nil)
|
130
|
-
# Session will be explained later
|
131
|
-
end
|
97
|
+
# ...
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class DeleteWidgetByIdHandler < AppHandler
|
102
|
+
def initialize(widget_id:, flash:)
|
103
|
+
@widget_id = widget_id
|
104
|
+
@flash = flash
|
105
|
+
end
|
106
|
+
|
107
|
+
def handle
|
108
|
+
widget = DB::Widget.find!(id: @widget_id)
|
109
|
+
if widget.can_delete?
|
110
|
+
widget.delete
|
111
|
+
@flash.notice = :widget_deleted
|
112
|
+
redirect_to(WidgetsPage)
|
113
|
+
else
|
114
|
+
@flash.alert = :widget_cannot_be_deleted
|
115
|
+
WidgetsPage.new
|
132
116
|
end
|
133
117
|
end
|
134
118
|
end
|
135
119
|
```
|
136
120
|
|
121
|
+
### Hooks
|
122
|
+
|
123
|
+
A handler's public API is `handle!`, because it first calls `before_handle`. This
|
124
|
+
operates as a before hook, and has the same return values as `handle`, with the
|
125
|
+
exception of also recognizing `nil`, which indicates processing should proceed to
|
126
|
+
`handle`.
|
127
|
+
|
128
|
+
Generally, you don't need to implement hooksd on a per-handler basis, but may find
|
129
|
+
it useful in a shared super class to implement cross-cutting behavior.
|
130
|
+
|
131
|
+
## Testing
|
132
|
+
|
133
|
+
See [Unit Testing](/unit-tests) for some basic assumptions and configuration available for all Brut unit tests.
|
134
|
+
|
135
|
+
Handler tests should be straightforward: you create your handler, call `handle!`
|
136
|
+
(remember, `handle!` is the public API, and will call your hooks, which you want in
|
137
|
+
a test), then examine the result returned and any ancillary behavior, such as
|
138
|
+
updated database records.
|
139
|
+
|
140
|
+
Some matchers are available to make assertions about `handle!`'s return value:
|
141
|
+
|
142
|
+
* `have_redirected_to` will check that the handler redirected to a give URI. See `Brut::SpecSupport::Matchers::HaveRedirectedTo`.
|
143
|
+
* `have_generated` will check that the handler generated a specific page or component's HTML. See `<D-f>Brut::SpecSupport::Matchers::HaveGenerated`.
|
144
|
+
* `have_returned_http_status` will check that the handler returned an HTTP status. See `Brut::SpecSupport::Matchers::HaveReturnedHttpStatus`.
|
145
|
+
* `have_constraint_violation` will check if a form had a particular constraint violation set on it. See `Brut::SpecSupport::Matchers::HaveConstraintViolation`.
|
146
|
+
|
137
147
|
## Recommended Practices
|
138
148
|
|
139
|
-
You
|
149
|
+
### You Don't Always Need Resourceful or RESTful Routes
|
150
|
+
|
151
|
+
For any code where the browser is performing a submission, use either `form` or
|
152
|
+
`action` to declare your route. These will both use an HTTP `POST` to your server and handler. In the example above, we had a `POST` to `/delete_widget/:widget_id`, and not, say, a `DELETE` to it.
|
153
|
+
|
154
|
+
The main reason is that a browser can only submit to a server using `GET` or `POST`, so there's little value in "tunneling" another verb of POST. It doesn't really matter. And, even though you may use Ajax to submit such data, having it degrade to normal browser-based HTTP is a good practice.
|
155
|
+
|
156
|
+
For API-like calls where a browser will never directly interact with the route, and
|
157
|
+
it would only be via a server-to-server call, RESTful routes makes sense. But they
|
158
|
+
don't need to be the default.
|
159
|
+
|
160
|
+
### Avoid Business Logic in Handlers
|
161
|
+
|
162
|
+
|
163
|
+
Since handlers bridge the gap between HTTP and your app, their API is naturally simplistic and String-based. The handler should defer to business logic (which can be done by either passing the form object directly, or extracting its data and passing that). Based on the response, the handler will then decide what HTTP response is approriate.
|
140
164
|
|
141
165
|
This means that your handlers will be relatively simple and their tests will as well. It does mean that their tests may require the use of mocks or stubs, but that's fine. Mocks and stubs exist for a reason.
|
142
166
|
|
data/brutrb.com/hooks.md
CHANGED
@@ -6,11 +6,10 @@ hooks can happen before a page or handler is called, or after.
|
|
6
6
|
## Overview
|
7
7
|
|
8
8
|
We've seen examples thusfar of using a route hook to place the authenticated user or account into the
|
9
|
-
request context for later injection into pages or handlers. Brut uses route hooks
|
10
|
-
for content security policies.
|
9
|
+
request context for later injection into pages or handlers. Brut uses route hooks for locale detection and for content security policies.
|
11
10
|
|
12
|
-
At its core, a before hook is a class that extends `Brut::FrontEnd::RouteHook` and implements `before`
|
13
|
-
and an after hook implements `after`. Both `before` and `after` can be [injected](/keyword-injection) with request-time information.
|
11
|
+
At its core, a *before* hook is a class that extends `Brut::FrontEnd::RouteHook` and implements `before`
|
12
|
+
and an *after* hook implements `after`. Both `before` and `after` can be [injected](/keyword-injection) with request-time information.
|
14
13
|
|
15
14
|
To register a hook, you'd call `before` or `after` in your `App`:
|
16
15
|
|
@@ -28,16 +27,11 @@ end
|
|
28
27
|
|
29
28
|
The value can be a string or symbol, but should not be the class itself, as this can mess with load order.
|
30
29
|
|
31
|
-
|
32
|
-
|
30
|
+
Let's implement a realistic hook that checks for authenticated users. Our hook will
|
31
|
+
detect if a user is logged in. If not, we'll redirect to a login page.
|
33
32
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
Let's suppose that the home page (`/`) and any page starting with `/auth/` are allowed for logged-out users (`/auth/` being pages related to logging in).
|
38
|
-
|
39
|
-
Brut also reserves some routes for its own, and we want those to be allowed to logged-out users as well. Brut sets
|
40
|
-
`"brut.owned_path"` in the Rack environment if the requested URL is one it is managing.
|
33
|
+
Of course, the login page will need to be accessible without logging in. We also
|
34
|
+
don't want Brut-owned paths to require login, either.
|
41
35
|
|
42
36
|
`before` will need access to the request context, session, Rack request, and Rack environment:
|
43
37
|
|
@@ -51,8 +45,7 @@ class RequireAuthBeforeHook < Brut::FrontEnd::RouteHook
|
|
51
45
|
end
|
52
46
|
```
|
53
47
|
|
54
|
-
We'll use the Rack request's `path_info` to check for allowed routes
|
55
|
-
check for a Brut-owned path:
|
48
|
+
We'll use the Rack request's `path_info` to check for allowed routes. Brut will set `"brut.owned_path"` in the Rack environment for any path that it owns. We can check that to allow access to those paths.
|
56
49
|
|
57
50
|
```ruby {4-6}
|
58
51
|
# app/src/front_end/route_hooks/require_auth_before_hook.rb
|
@@ -154,9 +147,7 @@ end
|
|
154
147
|
|
155
148
|
## Testing
|
156
149
|
|
157
|
-
Route hooks are normal classes, you could test them as you would a handler or other class. This may be
|
158
|
-
advisable for complex hooks, however it may be more realistic to test their behavior through end-to-end
|
159
|
-
tests as this will ensure they are configured correctly in the context of the app.
|
150
|
+
Route hooks are normal classes, you could test them as you would a handler or other class. This may be advisable for complex hooks, however it may be more realistic to test their behavior through end-to-end tests as this will ensure they are configured correctly in the context of the app.
|
160
151
|
|
161
152
|
## Recommended Practices
|
162
153
|
|
@@ -174,5 +165,12 @@ For page- or use-case-specific behavior, it may be better to put the logic in a
|
|
174
165
|
|
175
166
|
_Last Updated June 12, 2025_
|
176
167
|
|
177
|
-
Route hooks and Middlewares do not share implementations, however they are similar in concept. These
|
178
|
-
|
168
|
+
Route hooks and Middlewares do not share implementations, however they are similar in concept. These concepts may be unified in the future.
|
169
|
+
|
170
|
+
Hooks are applied in `Brut::Framework::MCP` usiung Sinatra's hooks mechanism. While
|
171
|
+
Brut may not always be based on Sinatra, it is now. You should not rely on it.
|
172
|
+
|
173
|
+
Lastly, there is some dissonance in how keyword injection works. Pages and Handlers
|
174
|
+
have initializer injection, while hooks use method injection. This may change -
|
175
|
+
Hooks may be re-designed to use initializer injection, and even changed so that
|
176
|
+
before and after hooks have different base classes.
|
data/brutrb.com/i18n.md
CHANGED
@@ -167,14 +167,16 @@ t("cv.be.required") # => This field is required
|
|
167
167
|
|
168
168
|
## Testing
|
169
169
|
|
170
|
-
In tests, you can call `t` and `l` to examine values as needed.
|
170
|
+
In tests, you can call `t` and `l` to examine values as needed. You may find the
|
171
|
+
`have_i18n_string` matcher usefult to check generated HTML for I18n values (see `Brut::SpecSupport::Matchers::HaveI18nString`).
|
171
172
|
|
172
173
|
> [!WARNING]
|
173
|
-
>
|
174
|
+
> Brut hardcodes English for tests, which you may not want. This will be addressed
|
175
|
+
> in the future.
|
174
176
|
|
175
177
|
## Recommended Practices
|
176
178
|
|
177
|
-
|
179
|
+
None at this time, however Brut's I18n has not been battle-tested.
|
178
180
|
|
179
181
|
|
180
182
|
## Technical Notes
|
@@ -185,4 +187,4 @@ Could use help here - The implementation feels like a bare minium
|
|
185
187
|
|
186
188
|
_Last Updated May 7, 2025_
|
187
189
|
|
188
|
-
|
190
|
+
None at this time.
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
data/brutrb.com/index.md
CHANGED
@@ -6,8 +6,8 @@ hero:
|
|
6
6
|
name: "Brut RB"
|
7
7
|
text: "Raw Ruby Web Apps"
|
8
8
|
tagline: Standards-based, No-nonsense, HTML-first, Low Ceremony
|
9
|
-
|
10
|
-
src: /images/
|
9
|
+
ximage:
|
10
|
+
src: /images/LogoTall.png
|
11
11
|
alt: "A Ruby gemstone embedded into a concrete, brutalist building"
|
12
12
|
actions:
|
13
13
|
- theme: brand
|
@@ -20,7 +20,7 @@ hero:
|
|
20
20
|
text: Conceptual Overview
|
21
21
|
link: /overview
|
22
22
|
|
23
|
-
|
23
|
+
xfeatures:
|
24
24
|
- title: Standards-Based
|
25
25
|
icon: 📄
|
26
26
|
details: "Brut leverages HTML, HTTP, SQL, and the Ruby standard library to let you write apps using standards you already know…or could quickly learn"
|
@@ -34,3 +34,4 @@ features:
|
|
34
34
|
icon: 🏘️
|
35
35
|
details: "Sequel, Phlex, I18n, RSpec. They do it best"
|
36
36
|
---
|
37
|
+

|
@@ -28,7 +28,7 @@ Brut automatically sets up OpenTelemetry (OTel) tracing. The primary interface
|
|
28
28
|
`Brut::Instrumentation::OpenTelemetry`, which is available via `Brut.container.instrumentation`. We'll
|
29
29
|
discuss that in a moment.
|
30
30
|
|
31
|
-
To configure the specifics of where the traces
|
31
|
+
To configure the specifics of where the traces will go, the OTel gem uses environment variables:
|
32
32
|
|
33
33
|
| Variable | Value | Purpose |
|
34
34
|
|--------------------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
@@ -44,7 +44,7 @@ When you created your Brut app, your `.env.development` and `.env.test` should h
|
|
44
44
|
environment variables that will send instrumentation to the otel-desktop-viewer that was also configured.
|
45
45
|
|
46
46
|
If you run your app using `bin/dev` and use the app for a bit, then go to `http://localhost:8000`, you
|
47
|
-
will see the otel-desktop-viewer UI and can
|
47
|
+
will see the otel-desktop-viewer UI and can browse the spans and traces sent by Brut.
|
48
48
|
|
49
49
|
|
50
50
|
### What is Instrumented By Default
|
@@ -54,7 +54,7 @@ data. Brut will attempt to conform to standard semantics for HTTP requests and
|
|
54
54
|
|
55
55
|
Here is a non-exhaustive list of what Brut automatically instruments:
|
56
56
|
|
57
|
-
* How long each page or handler request takes
|
57
|
+
* How long each page or handler request takes, broken down by components.
|
58
58
|
* CLI execution time
|
59
59
|
* Time to rebuild the schema for tests
|
60
60
|
* Time to run tests
|
@@ -75,7 +75,7 @@ Here is a non-exhaustive list of what Brut automatically instruments:
|
|
75
75
|
> the actual values inserted or used in `WHERE` clauses.
|
76
76
|
> While you should not be putting sensitive data into your database,
|
77
77
|
> be warned that this is happening. There are plans to improve this
|
78
|
-
> to be more flexible and reduce the
|
78
|
+
> to be more flexible and reduce the chance of sensitive data
|
79
79
|
> being sent in traces.
|
80
80
|
|
81
81
|
### Adding Your Own Instrumentation
|
@@ -155,16 +155,14 @@ The class `Brut::FrontEnd::Handlers::InstrumentationHandler` is set up to receiv
|
|
155
155
|
client-side to provide insights about client-side behavior as part of a server-side request. Brut
|
156
156
|
attempts to join up any client-side instrumentation to the request that served it.
|
157
157
|
|
158
|
-
It does this `Brut::FrontEnd::Components::Traceparent` component, which is included in your default layout
|
159
|
-
when you created your Brut app. This creates a `<meta>` tag containing standardized information used to
|
158
|
+
It does this via the `Brut::FrontEnd::Components::Traceparent` component, which is included in your default layout when you created your Brut app. This creates a `<meta>` tag containing standardized information used to
|
160
159
|
connect the client-side behavior to the server-side request.
|
161
160
|
|
162
161
|
The Brut custom element `<brut-tracing>` uses this information, along with statistics from the browser, to
|
163
162
|
send a custom payload back to Brut at the route `/__brut/instrumentation`, which is handled by the
|
164
163
|
aforementioned `InstrumentationHandler`.
|
165
164
|
|
166
|
-
You should then see client-side tracing information as a sub-span of your HTTP request. The information
|
167
|
-
available depends on the browser, and some browsers don't send much.
|
165
|
+
You should then see client-side tracing information as a sub-span of your HTTP request. The information available depends on the browser, and some browsers don't send much. Also keep in mind that clock drift is real and while client-side timings are accurate, the timestamps will not be.
|
168
166
|
|
169
167
|
## Testing
|
170
168
|
|
@@ -187,8 +185,7 @@ specific issues.
|
|
187
185
|
_Last Updated June 12, 2025_
|
188
186
|
|
189
187
|
|
190
|
-
Brut does not have plans to support non-OTel instrumentation, nor does it have plans to provide hooks to
|
191
|
-
use proprietary formats. This could change of course.
|
188
|
+
Brut does not have plans to support non-OTel instrumentation, nor does it have plans to provide hooks to use proprietary formats.
|
192
189
|
|
193
190
|
The client-side portion of this is highly customized. The Otel open source code for the client side is
|
194
191
|
massive and hugely complex, so Brut decided to try to produce something simple and straightforward as a
|
data/brutrb.com/javascript.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# JavaScript
|
2
2
|
|
3
|
-
Brut provides basic bundling using [esbuild](https://esbuild.github.io/).
|
4
|
-
use
|
5
|
-
|
6
|
-
enhancement without the need for a framework.
|
3
|
+
Brut provides basic bundling using [esbuild](https://esbuild.github.io/). You can
|
4
|
+
use any front-end framework with Brut, but you don't have to use one.
|
5
|
+
|
6
|
+
Brut provides [BrutJS](/brut-js), which is a lightweight library of HTML custom elements and utility code. These elements can provide a fair bit of front-end functionality using progressive enhancement without the need for a framework.
|
7
7
|
|
8
8
|
## Overview
|
9
9
|
|
@@ -19,7 +19,7 @@ For example, if you have a `Widget` class that uses a `Status` class, and you al
|
|
19
19
|
|
20
20
|
First, `package.json` (in your app's root) would include `"foobar"` (and it must set `"type"` to `"module"`):
|
21
21
|
|
22
|
-
```json {
|
22
|
+
```json {3,6}
|
23
23
|
{
|
24
24
|
"name": "your-app",
|
25
25
|
"type": "module",
|
@@ -50,7 +50,7 @@ Notice that "foobar", since it's brought in as a third party dependency, is impo
|
|
50
50
|
here! Every third party library has a different syntax for how to import whatever it is or does. Consult the
|
51
51
|
documentation of each third party library you wish to import.
|
52
52
|
|
53
|
-
The second `import` uses a `./` because it's importing a file in `app/src/front_end/js
|
53
|
+
The second `import` uses a `./` because it's importing a file in `app/src/front_end/js`, namely `Widget.js`. Be
|
54
54
|
careful here, too, as you must be sure to `export` the right thing. Here's what `app/src/front_end/js/Widget.js`
|
55
55
|
might look like:
|
56
56
|
|
@@ -96,16 +96,13 @@ details on this can be found in [assets](/assets).
|
|
96
96
|
## Testing
|
97
97
|
|
98
98
|
Client-side behavior is best tested with end-to-end tests, however you can simplify your end-to-end tests by
|
99
|
-
creating unit tests of your custom elements. BrutJS provides support for this.
|
99
|
+
creating unit tests of your custom elements. [BrutJS provides limited support for this](/brut-js/api/module-testing.html)
|
100
100
|
|
101
101
|
## Recommended Practices
|
102
102
|
|
103
|
-
Brut encourages you to use HTML custom elements as progressive enhancements over server-generated views. This
|
104
|
-
sort of client-side code will age well. The toolchain and dependencies are minimal, so you will not have to
|
105
|
-
worry too much about code written this way.
|
103
|
+
Brut encourages you to use HTML custom elements as progressive enhancements over server-generated views. This sort of client-side code will age well. The toolchain and dependencies are minimal, so you will not have to worry too much about code written this way.
|
106
104
|
|
107
|
-
It *will* be lower level and more verbose than existing frameworks. We would argue that it is not significantly
|
108
|
-
more difficult and the sustainability is worth it.
|
105
|
+
It *will* be lower level and more verbose than existing frameworks. We would argue that it is not significantly more difficult and the sustainability is worth it.
|
109
106
|
|
110
107
|
## Technical Notes
|
111
108
|
|
@@ -118,5 +115,8 @@ _Last Updated May 7, 2025_
|
|
118
115
|
Currently, Brut only supports a single entry point and bundle. This could be easily made more flexible if there
|
119
116
|
is a desire to finely tweak the JavaScript loaded on specific pages.
|
120
117
|
|
121
|
-
Brut also does not expose any esbuild configuration. This could be provided in the future, but for now, it is
|
122
|
-
|
118
|
+
Brut also does not expose any esbuild configuration. This could be provided in the future, but for now, it is hard-coded.
|
119
|
+
|
120
|
+
Brut may provide more direct support for import maps, but as of now, import maps are
|
121
|
+
not widely used outside of Rails, and tend to cause a lot of problems, especially if
|
122
|
+
you aren't able to field an HTTP/2 web server (or even know what that is).
|