brut 0.0.26 → 0.0.28
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/Gemfile.lock +1 -1
- data/brutrb.com/.vitepress/config.mjs +2 -0
- data/brutrb.com/ai.md +6 -1
- data/brutrb.com/components.md +1 -1
- data/brutrb.com/dev-environment.md +6 -5
- data/brutrb.com/doc-conventions.md +1 -1
- data/brutrb.com/forms.md +22 -26
- data/brutrb.com/getting-started.md +94 -23
- data/brutrb.com/instrumentation.md +12 -0
- data/brutrb.com/layouts.md +130 -0
- data/brutrb.com/lsp.md +23 -0
- data/docs/404.html +2 -2
- data/docs/ai.html +5 -5
- 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 +38 -13
- data/docs/api/Brut/CLI/AppRunner.html +1 -1
- data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +2 -2
- data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +2 -2
- data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +2 -2
- data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +2 -2
- data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
- data/docs/api/Brut/CLI/Apps/DB/Create.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Drop.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Migrate.html +9 -3
- data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +11 -11
- data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Seed.html +2 -2
- data/docs/api/Brut/CLI/Apps/DB/Status.html +12 -12
- data/docs/api/Brut/CLI/Apps/DB.html +1 -1
- data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +270 -0
- data/docs/api/Brut/CLI/Apps/DeployBase.html +257 -0
- data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +585 -0
- data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +196 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
- data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +2 -2
- data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
- data/docs/api/Brut/CLI/Apps/Test/Audit.html +12 -10
- data/docs/api/Brut/CLI/Apps/Test/E2e.html +8 -8
- data/docs/api/Brut/CLI/Apps/Test/JS.html +9 -9
- data/docs/api/Brut/CLI/Apps/Test/Run.html +18 -18
- data/docs/api/Brut/CLI/Apps/Test.html +1 -1
- data/docs/api/Brut/CLI/Apps.html +2 -2
- data/docs/api/Brut/CLI/Command.html +113 -28
- 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 +169 -38
- data/docs/api/Brut/CLI/InvalidOption.html +1 -1
- data/docs/api/Brut/CLI/Options.html +68 -19
- 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 +110 -29
- data/docs/api/Brut/Framework/Error.html +1 -1
- data/docs/api/Brut/Framework/Errors/AbstractMethod.html +89 -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 +31 -8
- 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 +36 -26
- data/docs/api/Brut/FrontEnd/Component.html +7 -7
- data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
- data/docs/api/Brut/FrontEnd/Components/FormTag.html +37 -29
- 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 +20 -117
- data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +25 -23
- data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +73 -380
- data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +22 -138
- 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 +23 -2
- 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 +1 -1
- data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +24 -68
- data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +4 -4
- data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +72 -22
- 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 +6 -2
- 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 +20 -20
- 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 +2 -2
- data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +142 -0
- data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +142 -0
- data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +155 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +55 -25
- data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +149 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +46 -19
- data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +149 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +149 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +165 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +158 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +156 -0
- data/docs/api/Brut/SpecSupport/Matchers.html +2 -2
- data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +24 -24
- data/docs/api/Brut/SpecSupport/RSpecSetup.html +55 -20
- 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 +5 -1
- data/docs/api/Sequel/Extensions/BrutMigrations.html +36 -28
- 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/SpecSupport/Matchers/BeABug.html +143 -0
- data/docs/api/_index.html +92 -1
- data/docs/api/class_list.html +1 -1
- data/docs/api/file.README.html +1 -1
- data/docs/api/index.html +1 -1
- data/docs/api/method_list.html +392 -336
- data/docs/api/top-level-namespace.html +1 -1
- data/docs/assets/{ai.md.tZrjP9im.js → ai.md._6HCDL6d.js} +1 -1
- data/docs/assets/ai.md._6HCDL6d.lean.js +1 -0
- data/docs/assets/{app.D_yaTITQ.js → app.BX81XO4N.js} +1 -1
- data/docs/assets/chunks/@localSearchIndexroot.CoYzciVi.js +1 -0
- data/docs/assets/chunks/{VPLocalSearchBox.B2-ZzyTY.js → VPLocalSearchBox.gABXcTWp.js} +1 -1
- data/docs/assets/chunks/{theme.CfGFVRvE.js → theme.DwUXXAL3.js} +2 -2
- data/docs/assets/{components.md.eCttGlN-.js → components.md.CRUMdRoN.js} +1 -1
- data/docs/assets/{configuration.md.BRriU0cL.js → configuration.md.BGHl8oRC.js} +1 -1
- data/docs/assets/{dev-environment.md.BNc8AYiK.js → dev-environment.md.GZv6xvi9.js} +1 -1
- data/docs/assets/doc-conventions.md.-kN3Xo5C.js +1 -0
- data/docs/assets/{doc-conventions.md.DCfRXXi-.lean.js → doc-conventions.md.-kN3Xo5C.lean.js} +1 -1
- data/docs/assets/{forms.md.CBTYQ_Cz.js → forms.md.B-koVgyw.js} +23 -23
- data/docs/assets/{forms.md.CBTYQ_Cz.lean.js → forms.md.B-koVgyw.lean.js} +1 -1
- data/docs/assets/getting-started.md.Ciz82L0m.js +25 -0
- data/docs/assets/getting-started.md.Ciz82L0m.lean.js +1 -0
- data/docs/assets/{instrumentation.md.CL6ax7nT.js → instrumentation.md.a9Pjps4P.js} +2 -2
- data/docs/assets/{instrumentation.md.CL6ax7nT.lean.js → instrumentation.md.a9Pjps4P.lean.js} +1 -1
- data/docs/assets/layouts.md.cPnh3NId.js +51 -0
- data/docs/assets/layouts.md.cPnh3NId.lean.js +1 -0
- data/docs/assets/lsp.md.Bsu-f6VU.js +1 -0
- data/docs/assets/lsp.md.Bsu-f6VU.lean.js +1 -0
- data/docs/assets/{overview.md.CDalkuxV.js → overview.md.C5wlBcR5.js} +3 -3
- data/docs/assets/recipes_authentication.md.CAsXf7hk.js +1 -0
- data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +1 -0
- data/docs/assets.html +4 -4
- 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 +4 -4
- data/docs/business-logic.html +4 -4
- data/docs/cli.html +4 -4
- data/docs/components.html +6 -6
- data/docs/configuration.html +5 -5
- data/docs/css.html +4 -4
- data/docs/custom-element-tests.html +4 -4
- data/docs/database-access.html +4 -4
- data/docs/database-schema.html +4 -4
- data/docs/deployment.html +4 -4
- data/docs/dev-environment.html +5 -5
- data/docs/doc-conventions.html +5 -5
- data/docs/end-to-end-tests.html +4 -4
- data/docs/flash-and-session.html +4 -4
- data/docs/forms.html +27 -27
- data/docs/getting-started.html +29 -6
- data/docs/handlers.html +4 -4
- data/docs/hashmap.json +1 -1
- data/docs/hooks.html +4 -4
- data/docs/i18n.html +4 -4
- data/docs/index.html +3 -3
- data/docs/instrumentation.html +5 -5
- data/docs/javascript.html +4 -4
- data/docs/jobs.html +4 -4
- data/docs/keyword-injection.html +4 -4
- data/docs/layouts.html +74 -0
- data/docs/lsp.html +24 -0
- data/docs/markdown-examples.html +4 -4
- data/docs/middleware.html +4 -4
- data/docs/not-released.html +4 -4
- data/docs/overview.html +8 -8
- data/docs/pages.html +5 -5
- data/docs/recipes/authentication.html +24 -0
- data/docs/routes.html +4 -4
- data/docs/security.html +4 -4
- data/docs/seed-data.html +4 -4
- data/docs/space-time-continuum.html +4 -4
- data/docs/tutorial.html +4 -4
- data/docs/unit-tests.html +4 -4
- data/lib/brut/cli/app.rb +7 -2
- data/lib/brut/cli/apps/deploy_base.rb +86 -0
- data/lib/brut/cli/apps/heroku_container_based_deploy.rb +194 -0
- data/lib/brut/cli/apps/test.rb +2 -1
- data/lib/brut/cli/command.rb +7 -13
- data/lib/brut/cli/executor.rb +31 -5
- data/lib/brut/cli/options.rb +4 -0
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/framework/container.rb +25 -7
- data/lib/brut/framework/errors/abstract_method.rb +7 -0
- data/lib/brut/framework/errors.rb +4 -2
- data/lib/brut/front_end/component.rb +33 -9
- data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +16 -74
- data/lib/brut/front_end/forms/constraint_violation.rb +2 -18
- data/lib/brut/front_end/forms/input.rb +8 -8
- data/lib/brut/front_end/forms/input_declarations.rb +3 -3
- data/lib/brut/front_end/forms/radio_button_group_input.rb +1 -1
- data/lib/brut/front_end/forms/select_input.rb +1 -1
- data/lib/brut/front_end/forms/validity_state.rb +23 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +2 -0
- data/lib/brut/spec_support/handler_support.rb +1 -1
- data/lib/brut/spec_support/matcher.rb +1 -1
- data/lib/brut/spec_support/matchers/be_a_bug.rb +11 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +10 -0
- data/lib/brut/spec_support/matchers/be_routing_for.rb +18 -0
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +10 -0
- data/lib/brut/spec_support/matchers/have_generated.rb +28 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +10 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +13 -0
- data/lib/brut/spec_support/matchers/have_link_to.rb +15 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +23 -0
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +20 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +22 -0
- data/lib/brut/spec_support/rspec_setup.rb +42 -2
- data/lib/brut/version.rb +1 -1
- data/lib/sequel/extensions/brut_instrumentation.rb +4 -0
- metadata +51 -24
- data/docs/assets/ai.md.tZrjP9im.lean.js +0 -1
- data/docs/assets/chunks/@localSearchIndexroot.BsN5i0Fi.js +0 -1
- data/docs/assets/doc-conventions.md.DCfRXXi-.js +0 -1
- data/docs/assets/getting-started.md.Bz2s1Vjb.js +0 -2
- data/docs/assets/getting-started.md.Bz2s1Vjb.lean.js +0 -1
- data/lib/brut/spec_support/matchers/have_rendered.rb +0 -14
- /data/docs/assets/{components.md.eCttGlN-.lean.js → components.md.CRUMdRoN.lean.js} +0 -0
- /data/docs/assets/{configuration.md.BRriU0cL.lean.js → configuration.md.BGHl8oRC.lean.js} +0 -0
- /data/docs/assets/{dev-environment.md.BNc8AYiK.lean.js → dev-environment.md.GZv6xvi9.lean.js} +0 -0
- /data/docs/assets/{overview.md.CDalkuxV.lean.js → overview.md.C5wlBcR5.lean.js} +0 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
require "brut/cli"
|
2
|
+
class Brut::CLI::Apps::HerokuContainerBasedDeploy < Brut::CLI::Apps::DeployBase
|
3
|
+
description "Deploy to Heroku using containers"
|
4
|
+
default_command :deploy
|
5
|
+
configure_only!
|
6
|
+
|
7
|
+
class Deploy < Brut::CLI::Command
|
8
|
+
description "Build images, push them to Heroku, and deploy them"
|
9
|
+
|
10
|
+
detailed_description %{
|
11
|
+
Manages a deploy process based on using Heroku's Container Registry. See
|
12
|
+
|
13
|
+
https://devcenter.heroku.com/articles/container-registry-and-runtime
|
14
|
+
|
15
|
+
for details. You are assumed to understand this. This command will make the process somewhat easier.
|
16
|
+
|
17
|
+
This will use deploy/Dockerfile as a template to create one Dockerfile for each process you want to run in Heroku. deploy/heroku_config.rb is where the processes and their commands are configured.
|
18
|
+
|
19
|
+
The release phase is included automatically, based on bin/release.
|
20
|
+
}
|
21
|
+
|
22
|
+
opts.on("--platform=PLATFORM","Override default platform. Can be any Docker platform.")
|
23
|
+
opts.on("--[no-]dry-run", "Print the commands that would be run and don't actually do anything. Implies --skip-checks")
|
24
|
+
opts.on("--[no-]skip-checks", "Skip checks for code having been committed and pushed")
|
25
|
+
opts.on("--[no-]deploy", "After images are pushed, actually deploy them")
|
26
|
+
opts.on("--[no-]push", "After images are created, push them to Heroku's registry. If false, implies --no-deploy")
|
27
|
+
|
28
|
+
def after_bootstrap(app:)
|
29
|
+
@app_id = app.id
|
30
|
+
@organization = app.organization
|
31
|
+
end
|
32
|
+
|
33
|
+
def execute
|
34
|
+
options.set_default(:deploy, true)
|
35
|
+
options.set_default(:push, true)
|
36
|
+
if !options.push?
|
37
|
+
options[:deploy] = false
|
38
|
+
end
|
39
|
+
local_repo_checks = Brut::CLI::Apps::DeployBase::GitChecks.new(
|
40
|
+
out: @out,
|
41
|
+
err: @err,
|
42
|
+
executor: @executor,
|
43
|
+
warn_only: options.skip_checks? || options.dry_run?
|
44
|
+
)
|
45
|
+
if options.dry_run?
|
46
|
+
def system!(*args)
|
47
|
+
out.puts "DRY RUN, NOT EXECUTING '#{args}'"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
version = begin
|
52
|
+
git_guess = %{git rev-parse HEAD}
|
53
|
+
stdout, stderr, status = Open3.capture3(git_guess)
|
54
|
+
if status.success?
|
55
|
+
stdout.strip
|
56
|
+
else
|
57
|
+
raise "Attempt to use git via command '#{git_guess}' to figure out the version failed: #{stdout}#{stderr}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
short_version = version[0..7]
|
61
|
+
|
62
|
+
platform = options.platform || "linux/amd64"
|
63
|
+
heroku_app_name = @app_id
|
64
|
+
|
65
|
+
out.puts "Reading HerokuConfig:"
|
66
|
+
require_relative Brut.container.project_root / "deploy" / "heroku_config"
|
67
|
+
|
68
|
+
additional_images = HerokuConfig.additional_images.map { |name,config|
|
69
|
+
cmd = config.fetch(:cmd)
|
70
|
+
out.puts " - #{name} will run #{cmd} in production"
|
71
|
+
image_name = %{#{@organization}/#{@app_id}:#{short_version}-#{name}}
|
72
|
+
[
|
73
|
+
name,
|
74
|
+
{
|
75
|
+
cmd:,
|
76
|
+
image_name:,
|
77
|
+
dockerfile: "deploy/Dockerfile.#{name}",
|
78
|
+
heroku_image_name: "registry.heroku.com/#{heroku_app_name}/#{name}",
|
79
|
+
}
|
80
|
+
]
|
81
|
+
}.to_h
|
82
|
+
|
83
|
+
images = {
|
84
|
+
"web" => {
|
85
|
+
cmd: "bin/run",
|
86
|
+
image_name: %{#{@organization}/#{@app_id}:#{short_version}-web},
|
87
|
+
dockerfile: "deploy/Dockerfile.web",
|
88
|
+
heroku_image_name: "registry.heroku.com/#{heroku_app_name}/web",
|
89
|
+
},
|
90
|
+
"release" => {
|
91
|
+
cmd: "bin/release",
|
92
|
+
image_name: %{#{@organization}/#{@app_id}:#{short_version}-release},
|
93
|
+
dockerfile: "deploy/Dockerfile.release",
|
94
|
+
heroku_image_name: "registry.heroku.com/#{heroku_app_name}/release",
|
95
|
+
},
|
96
|
+
}.merge(additional_images)
|
97
|
+
|
98
|
+
out.puts " - release will run bin/release in production"
|
99
|
+
|
100
|
+
local_repo_checks.check!
|
101
|
+
require_heroku_login!(options)
|
102
|
+
|
103
|
+
FileUtils.chdir Brut.container.project_root do
|
104
|
+
|
105
|
+
out.puts "Generating Dockerfiles"
|
106
|
+
images.each do |name,metadata|
|
107
|
+
cmd = metadata.fetch(:cmd)
|
108
|
+
dockerfile = metadata.fetch(:dockerfile)
|
109
|
+
|
110
|
+
out.puts "Creating '#{dockerfile}' for '#{name}' that will use command '#{cmd}'"
|
111
|
+
|
112
|
+
File.open(dockerfile,"w") do |file|
|
113
|
+
file.puts "# DO NOT EDIT - THIS IS GENERATED"
|
114
|
+
file.puts "# To make changes, modifiy deploy/Dockerfile and run #{$0}"
|
115
|
+
file.puts File.read("deploy/Dockerfile")
|
116
|
+
file.puts
|
117
|
+
file.puts "# Added by #{$0}"
|
118
|
+
file.puts %{CMD [ "bundle", "exec", "#{cmd}" ]}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
out.puts "Building images"
|
123
|
+
docker_quiet_option = if global_options.log_level != "debug"
|
124
|
+
"--quiet"
|
125
|
+
else
|
126
|
+
""
|
127
|
+
end
|
128
|
+
images.each do |name,metadata|
|
129
|
+
image_name = metadata.fetch(:image_name)
|
130
|
+
dockerfile = metadata.fetch(:dockerfile)
|
131
|
+
|
132
|
+
|
133
|
+
out.puts "Creating docker image with name '#{image_name}' and platform '#{platform}'"
|
134
|
+
command = %{docker build #{docker_quiet_option} --build-arg app_git_sha1=#{version} --file #{Brut.container.project_root}/#{dockerfile} --platform #{platform} --tag #{image_name} .}
|
135
|
+
system!(command)
|
136
|
+
end
|
137
|
+
|
138
|
+
out.puts "Taggging images for Heroku"
|
139
|
+
images.each do |name,metadata|
|
140
|
+
image_name = metadata.fetch(:image_name)
|
141
|
+
heroku_image_name = metadata.fetch(:heroku_image_name)
|
142
|
+
|
143
|
+
out.puts "Tagging '#{image_name}' with '#{heroku_image_name}' for Heroku"
|
144
|
+
command = %{docker tag #{image_name} #{heroku_image_name}}
|
145
|
+
system!(command)
|
146
|
+
end
|
147
|
+
|
148
|
+
if options.push?
|
149
|
+
out.puts "Pushing to Heroku Registry"
|
150
|
+
images.each do |name,metadata|
|
151
|
+
heroku_image_name = metadata.fetch(:heroku_image_name)
|
152
|
+
|
153
|
+
out.puts "Pushing '#{heroku_image_name}'"
|
154
|
+
|
155
|
+
command = %{docker push #{docker_quiet_option} #{heroku_image_name}}
|
156
|
+
system!(command)
|
157
|
+
end
|
158
|
+
else
|
159
|
+
out.puts "Not pushing images"
|
160
|
+
end
|
161
|
+
|
162
|
+
names = images.map(&:first).join(" ")
|
163
|
+
deploy_command = "heroku container:release #{names}"
|
164
|
+
if options.deploy?
|
165
|
+
out.puts "Deploying images to Heroku"
|
166
|
+
system!(deploy_command)
|
167
|
+
else
|
168
|
+
out.puts "Not deploying. To deploy the images just pushed:"
|
169
|
+
out.puts ""
|
170
|
+
out.puts " #{deploy_command}"
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
private
|
175
|
+
def require_heroku_login!(options)
|
176
|
+
if system("heroku whoami")
|
177
|
+
out.puts "You are logged in to Heroku"
|
178
|
+
else
|
179
|
+
out.puts "You are not logged into Heroku."
|
180
|
+
out.puts "Please run the following:"
|
181
|
+
out.puts ""
|
182
|
+
out.puts "heroku auth:login"
|
183
|
+
out.puts "heroku container:login"
|
184
|
+
out.puts ""
|
185
|
+
out.puts "Then, re-run this"
|
186
|
+
if options.dry_run?
|
187
|
+
out.puts "Dry run - ignoring"
|
188
|
+
return
|
189
|
+
end
|
190
|
+
exit 1
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
data/lib/brut/cli/apps/test.rb
CHANGED
@@ -133,7 +133,8 @@ class Brut::CLI::Apps::Test < Brut::CLI::App
|
|
133
133
|
test_expected: true,
|
134
134
|
}
|
135
135
|
if pathname.fnmatch?( (Brut.container.components_src_dir / "**").to_s )
|
136
|
-
if pathname.basename.to_s == "app_component.rb"
|
136
|
+
if pathname.basename.to_s == "app_component.rb" ||
|
137
|
+
pathname.basename.to_s == "custom_element_registration.rb"
|
137
138
|
hash[:type] = :infrastructure
|
138
139
|
hash[:test_expected] = false
|
139
140
|
else
|
data/lib/brut/cli/command.rb
CHANGED
@@ -198,6 +198,13 @@ class Brut::CLI::Command
|
|
198
198
|
def before_execute
|
199
199
|
end
|
200
200
|
|
201
|
+
# Called after all setup has been executed. Brut will have been started/loaded.
|
202
|
+
# This will *not* be called if anything caused execution to be aborted.
|
203
|
+
#
|
204
|
+
# @param [Brut::Framework::App] app Your Brut app.
|
205
|
+
def after_bootstrap(app:)
|
206
|
+
end
|
207
|
+
|
201
208
|
# @!visibility private
|
202
209
|
def set_env_if_needed
|
203
210
|
if self.class.requires_project_env?
|
@@ -219,19 +226,6 @@ class Brut::CLI::Command
|
|
219
226
|
raise ex
|
220
227
|
end
|
221
228
|
|
222
|
-
# @!visibility private
|
223
|
-
def bootstrap!(project_root:, configure_only:)
|
224
|
-
require "bundler"
|
225
|
-
Bundler.require(:default, ENV["RACK_ENV"].to_sym)
|
226
|
-
if configure_only
|
227
|
-
require "#{project_root}/app/pre_boot"
|
228
|
-
Brut::Framework.new(app: ::App.new)
|
229
|
-
else
|
230
|
-
require "#{project_root}/app/boot"
|
231
|
-
end
|
232
|
-
continue_execution
|
233
|
-
end
|
234
|
-
|
235
229
|
private
|
236
230
|
|
237
231
|
# @!visibility public
|
data/lib/brut/cli/executor.rb
CHANGED
@@ -16,14 +16,40 @@ class Brut::CLI::Executor
|
|
16
16
|
@err = err
|
17
17
|
end
|
18
18
|
|
19
|
-
# Execute a command, logging it to the standard output and outputing the
|
20
|
-
#
|
19
|
+
# Execute a command, logging it to the standard output and outputing the
|
20
|
+
# commands output and error to the standard output and error, respectively. If
|
21
|
+
# the command exits nonzero, the exit status is returned.
|
22
|
+
#
|
23
|
+
# Generally, you want to use {#system!} for commands that must succeed
|
24
|
+
# for the caller to continue. Only use this method if you need
|
25
|
+
# to do special error-handling when the underlying command fails.
|
26
|
+
#
|
27
|
+
# @see https://docs.ruby-lang.org/en/3.3/Open3.html#method-c-popen3
|
28
|
+
# @see #system!
|
29
|
+
#
|
30
|
+
# @param [String|Array] args Whatever you would give to `Kernel#system` or `Open3.popen3`.
|
31
|
+
# @return [int] 0 if the command completed normally, otherwise the nonzero exit status. **DO NOT TREAT THIS AS A BOOLEAN VALUE**
|
32
|
+
def system(*args)
|
33
|
+
self.system!(*args)
|
34
|
+
0
|
35
|
+
rescue Brut::CLI::SystemExecError => e
|
36
|
+
e.exit_status
|
37
|
+
end
|
38
|
+
|
39
|
+
# Execute a command, logging it to the standard output and outputing the
|
40
|
+
# commands output and error to the standard output and error, respectively. If
|
41
|
+
# the command exits nonzero, an exception is raised and your CLI app will
|
42
|
+
# also exit nonzero.
|
43
|
+
#
|
44
|
+
# If you need to handle the command exiting nonzero, use {#system}
|
45
|
+
# instead, as it will not raise an exception.
|
21
46
|
#
|
22
47
|
# @see https://docs.ruby-lang.org/en/3.3/Open3.html#method-c-popen3
|
48
|
+
# @see #system
|
23
49
|
#
|
24
50
|
# @param [String|Array] args Whatever you would give to `Kernel#system` or `Open3.popen3`.
|
25
|
-
# @raise Brut::CLI::Error::SystemExecError if the spawed command exits
|
26
|
-
# @return [
|
51
|
+
# @raise Brut::CLI::Error::SystemExecError if the spawed command exits nonzero
|
52
|
+
# @return [int] Always returns 0
|
27
53
|
def system!(*args)
|
28
54
|
@out.puts "Executing #{args}"
|
29
55
|
wait_thread = Open3.popen3(*args) do |_stdin,stdout,stderr,wait_thread|
|
@@ -52,6 +78,6 @@ class Brut::CLI::Executor
|
|
52
78
|
else
|
53
79
|
raise Brut::CLI::SystemExecError.new(args,wait_thread.value.exitstatus)
|
54
80
|
end
|
55
|
-
|
81
|
+
0
|
56
82
|
end
|
57
83
|
end
|
data/lib/brut/cli/options.rb
CHANGED
@@ -12,6 +12,10 @@ class Brut::CLI::Options
|
|
12
12
|
# Access an options value directly.
|
13
13
|
# @param [String] key the key to use. This must be the exact name you used when calling `opts.on` or when creating the `OptionParser` for your app or command. Generally, use the {#method_missing}-provided version instead of this.
|
14
14
|
def [](key) = @parsed_options[key]
|
15
|
+
|
16
|
+
def []=(key,value)
|
17
|
+
@parsed_options[key] = value
|
18
|
+
end
|
15
19
|
# Check if `key` was provided on the command line.
|
16
20
|
# @param [String] key the key to use. This must be the exact name you used when calling `opts.on` or when creating the `OptionParser` for your app or command.
|
17
21
|
def key?(key) = @parsed_options.key?(key)
|
data/lib/brut/cli.rb
CHANGED
@@ -42,9 +42,10 @@ module Brut
|
|
42
42
|
# Holds Brut-provided CLI apps that are set up in your project.
|
43
43
|
module Apps
|
44
44
|
autoload(:DB,"brut/cli/apps/db")
|
45
|
-
autoload(:
|
46
|
-
autoload(:
|
47
|
-
autoload(:
|
45
|
+
autoload(:Test,"brut/cli/apps/test")
|
46
|
+
autoload(:BuildAssets,"brut/cli/apps/build_assets")
|
47
|
+
autoload(:Scaffold,"brut/cli/apps/scaffold")
|
48
|
+
autoload(:DeployBase,"brut/cli/apps/deploy_base")
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
@@ -78,7 +78,7 @@ class Brut::Framework::Container
|
|
78
78
|
self.validate_name!(name:,type:,allow_app_override:)
|
79
79
|
if value == :use_block
|
80
80
|
derive_with = block
|
81
|
-
@container[name] = {
|
81
|
+
@container[name] = { derive_with: derive_with }
|
82
82
|
else
|
83
83
|
if type == :boolean
|
84
84
|
value = !!value
|
@@ -111,9 +111,11 @@ class Brut::Framework::Container
|
|
111
111
|
raise ArgumentError,"#{name} does not allow the app to override it"
|
112
112
|
end
|
113
113
|
if value == :use_block
|
114
|
-
@container[name] =
|
114
|
+
@container[name][:derive_with] = block
|
115
|
+
@container[name].delete(:value)
|
115
116
|
else
|
116
|
-
@container[name] =
|
117
|
+
@container[name][:value] = value
|
118
|
+
@container[name].delete(:derive_with)
|
117
119
|
end
|
118
120
|
end
|
119
121
|
|
@@ -191,16 +193,16 @@ class Brut::Framework::Container
|
|
191
193
|
# TODO: Provide a cleanr impl and better error checking if things go wrong
|
192
194
|
x = @container.fetch(name)
|
193
195
|
|
194
|
-
|
196
|
+
has_value = x.key?(:value)
|
195
197
|
|
196
|
-
if
|
198
|
+
if has_value
|
197
199
|
handle_path_values(name,x)
|
198
|
-
return value
|
200
|
+
return x[:value]
|
199
201
|
end
|
200
202
|
|
201
203
|
deriver = x[:derive_with]
|
202
204
|
if deriver.nil?
|
203
|
-
raise "Something is seriously wrong. '#{name}' was stored in container without a derive_with
|
205
|
+
raise "Something is seriously wrong. '#{name}' was stored in container without a static value, but also without a derive_with key"
|
204
206
|
end
|
205
207
|
|
206
208
|
parameters = deriver.parameters(lambda: true)
|
@@ -218,6 +220,22 @@ class Brut::Framework::Container
|
|
218
220
|
handle_path_values(name,x)
|
219
221
|
x[:value]
|
220
222
|
end
|
223
|
+
|
224
|
+
def reload
|
225
|
+
@container.each do |name, contained_value|
|
226
|
+
if contained_value.key?(:value) && contained_value[:type].to_s == "Class"
|
227
|
+
if contained_value.key?(:derive_with)
|
228
|
+
contained_value.delete(:value)
|
229
|
+
else
|
230
|
+
klass = contained_value[:value]
|
231
|
+
new_klass = klass.name.split(/::/).reduce(Module) { |mod,part|
|
232
|
+
mod.const_get(part)
|
233
|
+
}
|
234
|
+
contained_value[:value] = new_klass
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
221
239
|
private
|
222
240
|
|
223
241
|
def handle_path_values(name,contained_value)
|
@@ -2,4 +2,11 @@
|
|
2
2
|
# which methods a subclass is expected to override and for which no default behavior
|
3
3
|
# makes sense.
|
4
4
|
class Brut::Framework::Errors::AbstractMethod < Brut::Framework::Error
|
5
|
+
def initialize(method_name=nil)
|
6
|
+
if method_name
|
7
|
+
super("The method `#{method_name}` must be implemented")
|
8
|
+
else
|
9
|
+
super("An abstract method must be implemented")
|
10
|
+
end
|
11
|
+
end
|
5
12
|
end
|
@@ -27,9 +27,11 @@ module Brut
|
|
27
27
|
# a method that a subclass must implement, but for which there is no useful default
|
28
28
|
# implementation.
|
29
29
|
#
|
30
|
+
# @param [String|Symbol] method_name name of the method that must be implemented. If omitted, the value is guessed from `caller_locations`
|
30
31
|
# @raise [Brut::Framework::Errors::AbstractMethod]
|
31
|
-
def abstract_method!
|
32
|
-
|
32
|
+
def abstract_method!(method_name=nil)
|
33
|
+
method_name ||= caller_locations(1,1)[0].label
|
34
|
+
raise Brut::Framework::Errors::AbstractMethod.new(method_name)
|
33
35
|
end
|
34
36
|
end
|
35
37
|
# Base class for errors thrown by Brut classes. Generally, Brut will not create its own version
|
@@ -1,6 +1,22 @@
|
|
1
1
|
require "phlex"
|
2
2
|
|
3
|
-
#
|
3
|
+
# Namespace for Brut-provided components that are of general use to any web app.
|
4
|
+
# Also extends [`Phlex:::Kit`](https://www.phlex.fun/components/kits.html), meaning
|
5
|
+
# you can include this module in your pages and components to be able to
|
6
|
+
# create Brut's components without `.new` or without the full classname:
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# class AppPage < Brut::FrontEnd::Page
|
10
|
+
# include Brut::FrontEnd::Components
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# class HomePage < AppPage
|
14
|
+
# def page_template
|
15
|
+
# h1 do
|
16
|
+
# span { "It's }
|
17
|
+
# TimeTag(timestamp: Time.now)
|
18
|
+
# end
|
19
|
+
# end
|
4
20
|
module Brut::FrontEnd::Components
|
5
21
|
autoload(:FormTag,"brut/front_end/components/form_tag")
|
6
22
|
autoload(:Input,"brut/front_end/components/input")
|
@@ -72,16 +88,24 @@ class Brut::FrontEnd::Component < Phlex::HTML
|
|
72
88
|
}
|
73
89
|
end
|
74
90
|
|
75
|
-
#
|
76
|
-
# This
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
91
|
+
# Render (in Phlex parlance) a component that you would like Brut to
|
92
|
+
# instantiate. This is useful when you want to use a component that
|
93
|
+
# only requires values from the {Brut::FrontEnd::RequestContext}. By
|
94
|
+
# using this method, *this* component does not have to receive
|
95
|
+
# data from the {Brut::FrontEnd::RequestContext} that only serves to pass
|
96
|
+
# to the component you use here.
|
97
|
+
#
|
98
|
+
# For example, you may have a component that renders the flash message. To avoid requiring *this* component/page to be passed the flash, a global component can be injected with it from Brut.
|
99
|
+
#
|
100
|
+
# This component *will* be rendered into the Phlex context. Do not call
|
101
|
+
# `render` on the result, nor rely on the return value.
|
80
102
|
#
|
81
|
-
# @
|
82
|
-
#
|
103
|
+
# @param [Class] component_klass the component class to use in the view.
|
104
|
+
# This class's
|
105
|
+
# initializer must only require information available from the
|
106
|
+
# {Brut::FrontEnd::RequestContext}.
|
83
107
|
def global_component(component_klass)
|
84
|
-
Brut::FrontEnd::RequestContext.inject(component_klass)
|
108
|
+
render Brut::FrontEnd::RequestContext.inject(component_klass)
|
85
109
|
end
|
86
110
|
end
|
87
111
|
include Helpers
|
@@ -27,14 +27,14 @@ class Brut::FrontEnd::Components::Inputs::SelectTagWithOptions < Brut::FrontEnd:
|
|
27
27
|
# to be used as the `value` attribute and option text content, respectively.
|
28
28
|
#
|
29
29
|
# @return [Brut::FrontEnd::Components::Inputs::SelectTagWithOptions] the select input ready to be placed into a view.
|
30
|
-
def
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
30
|
+
def initialize(form:,
|
31
|
+
input_name:,
|
32
|
+
options:,
|
33
|
+
include_blank: false,
|
34
|
+
value_attribute:,
|
35
|
+
option_text_attribute:,
|
36
|
+
index: nil,
|
37
|
+
html_attributes: {})
|
38
38
|
html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
|
39
39
|
default_html_attributes = {}
|
40
40
|
index ||= 0
|
@@ -54,84 +54,26 @@ class Brut::FrontEnd::Components::Inputs::SelectTagWithOptions < Brut::FrontEnd:
|
|
54
54
|
input.name
|
55
55
|
end
|
56
56
|
|
57
|
-
|
58
|
-
name: name,
|
59
|
-
options:,
|
60
|
-
selected_value: input.value,
|
61
|
-
value_attribute:,
|
62
|
-
option_text_attribute:,
|
63
|
-
include_blank:,
|
64
|
-
html_attributes: default_html_attributes.merge(html_attributes)
|
65
|
-
)
|
66
|
-
end
|
57
|
+
input_value = input.value
|
67
58
|
|
68
|
-
# Create the element.
|
69
|
-
#
|
70
|
-
# @param [String] name the name of the input
|
71
|
-
# @param [Array<Object>] options An array of objects represented what is being selected.
|
72
|
-
# These can be any object and are ideally whatever domain object or
|
73
|
-
# data type you want on the backend to represent this selection.
|
74
|
-
# @param [Symbol|String] value_attribute the name of an attribute to determine an option's value.
|
75
|
-
# This will be called on each element of `options` to get the value used for the `<option>`'s
|
76
|
-
# `value` attribute. The value returned by `value_attribute` should be unique amongst the
|
77
|
-
# `options` provided *and* be distinct from whatever `value` is used for `include_blank`.
|
78
|
-
# @param [String] selected_value the value of the selected option. Note that this is the *value*
|
79
|
-
# of the selected option, not the selected option itself. To set the selected value
|
80
|
-
# based on a selected option, omit this and use `selected_option`
|
81
|
-
# @param [String] selected_option the selected option. Note that `value_attribute` will be called
|
82
|
-
# on this to determine the selected value to use when generating HTML. Also note that
|
83
|
-
# this object must be in `options` or an exeception is raised.
|
84
|
-
# @param [Symbol|String] option_text_attribute the name of an attribute to determine the text for an option.
|
85
|
-
# This will be called on each element of `options` to get the value used for the `<option>`'s
|
86
|
-
# text content. The value returned by `option_text_attribute` need not be unique, though if it
|
87
|
-
# is not unique, it will certainly be confusing.
|
88
|
-
# @param [Hash] html_attributes any additional HTML attributes to include on the `<select>` element.
|
89
|
-
# @param [false|true|Hash] include_blank configure how and if to include a blank element in the select.
|
90
|
-
# If this is false, there will be no blank element. If it's `true`, there will be one with
|
91
|
-
# no value or text. If this is a `Hash` it must contain a `value:` key and a `text_content:` key
|
92
|
-
# to be used as the `value` attribute and option text content, respectively.
|
93
|
-
#
|
94
|
-
# @raise ArgumentError if `selected_option` is present, but not in `options` or if `selected_value` is
|
95
|
-
# present, but no option's value for `value_attribute` is that value.
|
96
|
-
#
|
97
|
-
# XXX: Why does this not ask the form for the selected_value?
|
98
|
-
# XXX: This doesn't do well when values are strings
|
99
|
-
def initialize(name:,
|
100
|
-
options:,
|
101
|
-
value_attribute:,
|
102
|
-
selected_value: nil,
|
103
|
-
selected_option: nil,
|
104
|
-
option_text_attribute:,
|
105
|
-
include_blank: false,
|
106
|
-
html_attributes:)
|
107
59
|
@options = options
|
108
60
|
@include_blank = IncludeBlank.from_param(include_blank)
|
109
61
|
@value_attribute = value_attribute
|
110
62
|
@option_text_attribute = option_text_attribute
|
111
|
-
@html_attributes = html_attributes
|
63
|
+
@html_attributes = default_html_attributes.merge(html_attributes)
|
112
64
|
@html_attributes[:name] = name
|
113
65
|
|
114
|
-
if
|
115
|
-
|
116
|
-
@selected_value = nil # explicitly nothing is selected
|
117
|
-
else
|
118
|
-
option = options.detect { |option|
|
119
|
-
option.send(@value_attribute) == selected_option.send(@value_attribute)
|
120
|
-
}
|
121
|
-
if option.nil?
|
122
|
-
raise ArgumentError, "selected_option #{selected_option} (with #{value_attribute} '#{selected_option.send(value_attribute)}') was not found in options"
|
123
|
-
end
|
124
|
-
@selected_value = option.send(@value_attribute)
|
125
|
-
end
|
66
|
+
if input_value.nil?
|
67
|
+
@selected_value = nil # explicitly nothing is selected
|
126
68
|
else
|
127
|
-
if
|
128
|
-
raise "WTF: #{name}"
|
69
|
+
if input_value.kind_of?(Array)
|
70
|
+
raise "WTF: #{name}" # XXX?
|
129
71
|
end
|
130
72
|
option = options.detect { |option|
|
131
|
-
|
73
|
+
input_value == option.send(@value_attribute)
|
132
74
|
}
|
133
75
|
if option.nil?
|
134
|
-
raise ArgumentError, "selected_value #{
|
76
|
+
raise ArgumentError, "selected_value #{input_value} was not the value for #{value_attribute} on any of the options: #{options.map { |option| option.send(value_attribute) }.join(', ')}"
|
135
77
|
end
|
136
78
|
@selected_value = option.send(@value_attribute)
|
137
79
|
end
|
@@ -2,22 +2,6 @@
|
|
2
2
|
# form-related classes.
|
3
3
|
class Brut::FrontEnd::Forms::ConstraintViolation
|
4
4
|
|
5
|
-
# These are underscorized versions of the attributes of the browser's `ValidityState`'s properties.
|
6
|
-
#
|
7
|
-
# @see https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
|
8
|
-
CLIENT_SIDE_KEYS = [
|
9
|
-
"bad_input",
|
10
|
-
"custom_error",
|
11
|
-
"pattern_mismatch",
|
12
|
-
"range_overflow",
|
13
|
-
"range_underflow",
|
14
|
-
"step_mismatch",
|
15
|
-
"too_long",
|
16
|
-
"too_short",
|
17
|
-
"type_mismatch",
|
18
|
-
"value_missing",
|
19
|
-
]
|
20
|
-
|
21
5
|
# @return [String] the key fragment representing the violation
|
22
6
|
attr_reader :key
|
23
7
|
# @return [Hash] interpolated values useful in rendering the actual message
|
@@ -27,11 +11,11 @@ class Brut::FrontEnd::Forms::ConstraintViolation
|
|
27
11
|
#
|
28
12
|
# @param [String|Symbol] key I18n key fragment representing this violation.
|
29
13
|
# @param [Hash|nil] context interpolated values useful in rendering the message
|
30
|
-
# @param [true|:based_on_key] server_side If `:based_on_key`, {#client_side?} will return true if `key` is in {.
|
14
|
+
# @param [true|:based_on_key] server_side If `:based_on_key`, {#client_side?} will return true if `key` is in {ValidityState.KEYS}.
|
31
15
|
# If `true`, {#client_side?} will return false no matter what.
|
32
16
|
def initialize(key:,context:, server_side: :based_on_key)
|
33
17
|
@key = key.to_s
|
34
|
-
@client_side =
|
18
|
+
@client_side = Brut::FrontEnd::Forms::ValidityState::KEYS.include?(@key) && server_side != true
|
35
19
|
@context = context || {}
|
36
20
|
if !@context.kind_of?(Hash)
|
37
21
|
raise "#{self.class} created for key #{key} with an invalid context: '#{context}/#{context.class}'. Context must be nil or a hash"
|
@@ -82,14 +82,14 @@ class Brut::FrontEnd::Forms::Input
|
|
82
82
|
step_mismatch = false
|
83
83
|
|
84
84
|
@validity_state = Brut::FrontEnd::Forms::ValidityState.new(
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
85
|
+
valueMissing: missing,
|
86
|
+
tooShort: too_short,
|
87
|
+
tooLong: too_short,
|
88
|
+
rangeOverflow: range_overflow,
|
89
|
+
rangeUnderflow: range_underflow,
|
90
|
+
patternMismatch: pattern_mismatch,
|
91
|
+
stepMismatch: step_mismatch,
|
92
|
+
typeMismatch: type_mismatch,
|
93
93
|
)
|
94
94
|
@value = new_value
|
95
95
|
end
|
@@ -12,8 +12,8 @@ module Brut::FrontEnd::Forms::InputDeclarations
|
|
12
12
|
end
|
13
13
|
|
14
14
|
# Declares a select for this form, to be modeled via an HTML `<SELECT>` tag. Note that this will not define the values that appear
|
15
|
-
# in the select. That is done when the select is rendered, which you might do with
|
16
|
-
# {Brut::FrontEnd::Components::Inputs::Select
|
15
|
+
# in the select. That is done when the select is rendered, which you might do with a
|
16
|
+
# {Brut::FrontEnd::Components::Inputs::Select}
|
17
17
|
#
|
18
18
|
# @param [String] name The name of the input (used in the `name` attribute)
|
19
19
|
# @param [Hash] attributes Attributes to be used on the tag that represent its contraints. See {Brut::FrontEnd::Forms::SelectInputDefinition}
|
@@ -28,7 +28,7 @@ module Brut::FrontEnd::Forms::InputDeclarations
|
|
28
28
|
# input tags.
|
29
29
|
#
|
30
30
|
# Note that this is not where you would define the possible values for the group. That is done in
|
31
|
-
# {Brut::FrontEnd::Components::Inputs::RadioButton
|
31
|
+
# {Brut::FrontEnd::Components::Inputs::RadioButton}.
|
32
32
|
#
|
33
33
|
# @param [String] name The name of the group (used in the `name` attribute)
|
34
34
|
# @param [Hash] attributes Attributes to be used on the tag that represent its contraints. See {Brut::FrontEnd::Forms::RadioButtonGroupInputDefinition}
|