brut 0.0.8 → 0.0.10
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/lib/brut/back_end/sidekiq/middlewares/server/flush_spans.rb +17 -0
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +3 -0
- data/lib/brut/back_end/sidekiq/middlewares.rb +3 -0
- data/lib/brut/back_end/sidekiq.rb +3 -0
- data/lib/brut/cli/app.rb +8 -1
- data/lib/brut/cli/apps/db.rb +7 -7
- data/lib/brut/cli/apps/test.rb +20 -12
- data/lib/brut/framework/config.rb +9 -0
- data/lib/brut/framework/errors/missing_configuration.rb +11 -0
- data/lib/brut/framework/errors.rb +1 -0
- data/lib/brut/framework/mcp.rb +71 -34
- data/lib/brut/front_end/handling_results.rb +1 -1
- data/lib/brut/front_end/request_context.rb +3 -1
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +2 -1
- data/lib/brut/front_end/routing.rb +44 -8
- data/lib/brut/spec_support/clock_support.rb +15 -2
- data/lib/brut/spec_support/component_support.rb +1 -1
- data/lib/brut/spec_support/general_support.rb +11 -1
- data/lib/brut/spec_support/matchers/be_routing_for.rb +2 -2
- data/lib/brut/spec_support/rspec_setup.rb +1 -0
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +1 -0
- metadata +7 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 2e624fb125bcb14a88b3b7adb148561c404befc86974a77423b6cd66293bba31
         | 
| 4 | 
            +
              data.tar.gz: e2a20fc23b6aa7d0708ea7adf9852333b10616aa4e633d972031c726b9d43316
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 59b7398fbf77fafd04c770e617b8fcb341887db4b14691828ab94775c4d36b44d8475889293a39a8cfc92c7b27ef935c487c60d8f7a7f25f96edbfa57db8b75b
         | 
| 7 | 
            +
              data.tar.gz: aa3ce63a03296837fb5c425ce430a8a0658d732edf32fbed4ac7d01d74fb39dfde6e0b91e0aeadfb7f26d970ac3f23fcf6aab3216354152884fe9f86f4772412
         | 
    
        data/Gemfile.lock
    CHANGED
    
    
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            # Designed to flush all OTel spans after each job is processed. You likely only
         | 
| 2 | 
            +
            # want this to be configured in development so you can see the results of individual
         | 
| 3 | 
            +
            # job executions.  Do not enable in production.
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            # When using, you want this to be inserted before OTel's sidekiq middleware:
         | 
| 6 | 
            +
            #
         | 
| 7 | 
            +
            #    config.server_middleware do |chain|
         | 
| 8 | 
            +
            #      chain.insert_before OpenTelemetry::Instrumentation::Sidekiq::Middlewares::Server::TracerMiddleware,
         | 
| 9 | 
            +
            #                          Brut::BackEnd::Sidekiq::Middlewares::Server::FlushSpans
         | 
| 10 | 
            +
            #    end
         | 
| 11 | 
            +
            class Brut::BackEnd::Sidekiq::Middlewares::Server::FlushSpans
         | 
| 12 | 
            +
              def call(worker, job, queue)
         | 
| 13 | 
            +
                yield
         | 
| 14 | 
            +
              ensure
         | 
| 15 | 
            +
                OpenTelemetry.tracer_provider.force_flush
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
    
        data/lib/brut/cli/app.rb
    CHANGED
    
    | @@ -208,7 +208,14 @@ class Brut::CLI::App | |
| 208 208 | 
             
                  return bootstrap_result
         | 
| 209 209 | 
             
                end
         | 
| 210 210 | 
             
                after_bootstrap
         | 
| 211 | 
            -
                 | 
| 211 | 
            +
                if self.class.configure_only?
         | 
| 212 | 
            +
                  as_execution_result(command.execute)
         | 
| 213 | 
            +
                else
         | 
| 214 | 
            +
                  result = Brut.container.instrumentation.span("CLI.#{$0}", class: self.class.name) do |span|
         | 
| 215 | 
            +
                    as_execution_result(command.execute)
         | 
| 216 | 
            +
                  end
         | 
| 217 | 
            +
                  result
         | 
| 218 | 
            +
                end
         | 
| 212 219 | 
             
              rescue Brut::CLI::Error => ex
         | 
| 213 220 | 
             
                abort_execution(ex.message)
         | 
| 214 221 | 
             
              end
         | 
    
        data/lib/brut/cli/apps/db.rb
    CHANGED
    
    | @@ -13,12 +13,10 @@ class Brut::CLI::Apps::DB < Brut::CLI::App | |
| 13 13 | 
             
                def handle_bootstrap_exception(ex)
         | 
| 14 14 | 
             
                  case ex
         | 
| 15 15 | 
             
                  when Sequel::DatabaseConnectionError
         | 
| 16 | 
            -
                     | 
| 17 | 
            -
                    stop_execution
         | 
| 16 | 
            +
                    abort_execution("Database needs to be created")
         | 
| 18 17 | 
             
                  when Sequel::DatabaseError
         | 
| 19 18 | 
             
                    if ex.cause.kind_of?(PG::UndefinedTable)
         | 
| 20 | 
            -
                       | 
| 21 | 
            -
                      stop_execution
         | 
| 19 | 
            +
                      abort_execution("Migrations need to be run")
         | 
| 22 20 | 
             
                    else
         | 
| 23 21 | 
             
                      super
         | 
| 24 22 | 
             
                    end
         | 
| @@ -87,6 +85,7 @@ class Brut::CLI::Apps::DB < Brut::CLI::App | |
| 87 85 | 
             
                    stop_execution
         | 
| 88 86 | 
             
                  when Sequel::DatabaseError
         | 
| 89 87 | 
             
                    if ex.cause.kind_of?(PG::UndefinedTable)
         | 
| 88 | 
            +
                      out.puts ex.message
         | 
| 90 89 | 
             
                      out.puts "Migrations need to be run"
         | 
| 91 90 | 
             
                      continue_execution
         | 
| 92 91 | 
             
                    else
         | 
| @@ -151,8 +150,7 @@ class Brut::CLI::Apps::DB < Brut::CLI::App | |
| 151 150 | 
             
                def handle_bootstrap_exception(ex)
         | 
| 152 151 | 
             
                  case ex
         | 
| 153 152 | 
             
                  when Sequel::DatabaseConnectionError
         | 
| 154 | 
            -
                     | 
| 155 | 
            -
                    stop_execution
         | 
| 153 | 
            +
                    abort_execution("Database does not exist. Create it first")
         | 
| 156 154 | 
             
                  when Sequel::DatabaseError
         | 
| 157 155 | 
             
                    if ex.cause.kind_of?(PG::UndefinedTable)
         | 
| 158 156 | 
             
                      # ignoring - we are running migrations which will address this
         | 
| @@ -189,7 +187,9 @@ class Brut::CLI::Apps::DB < Brut::CLI::App | |
| 189 187 | 
             
                    formatted
         | 
| 190 188 | 
             
                  }
         | 
| 191 189 | 
             
                  Brut.container.sequel_db_handle.logger = logger
         | 
| 192 | 
            -
                   | 
| 190 | 
            +
                  Brut.container.instrumentation.span("migrations.run") do
         | 
| 191 | 
            +
                    Sequel::Migrator.run(Brut.container.sequel_db_handle,migrations_dir)
         | 
| 192 | 
            +
                  end
         | 
| 193 193 | 
             
                  out.puts "Migrations applied"
         | 
| 194 194 | 
             
                end
         | 
| 195 195 | 
             
              end
         | 
    
        data/lib/brut/cli/apps/test.rb
    CHANGED
    
    | @@ -40,21 +40,29 @@ class Brut::CLI::Apps::Test < Brut::CLI::App | |
| 40 40 | 
             
                def execute
         | 
| 41 41 | 
             
                  Brut.container.sequel_db_handle.disconnect
         | 
| 42 42 | 
             
                  if options.rebuild?(default: rebuild_by_default?)
         | 
| 43 | 
            -
                     | 
| 44 | 
            -
             | 
| 43 | 
            +
                    Brut.container.instrumentation.span("schema.rebuild.before") do
         | 
| 44 | 
            +
                      out.puts "Rebuilding test database schema"
         | 
| 45 | 
            +
                      system! "bin/db rebuild --env=test"
         | 
| 46 | 
            +
                    end
         | 
| 45 47 | 
             
                  end
         | 
| 46 | 
            -
                   | 
| 47 | 
            -
                     | 
| 48 | 
            -
             | 
| 49 | 
            -
             | 
| 50 | 
            -
             | 
| 51 | 
            -
             | 
| 52 | 
            -
             | 
| 53 | 
            -
             | 
| 48 | 
            +
                  Brut.container.instrumentation.span("tests.run") do |span|
         | 
| 49 | 
            +
                    if args.empty?
         | 
| 50 | 
            +
                      span.add_attributes(tests: :all)
         | 
| 51 | 
            +
                      out.puts "Running all tests"
         | 
| 52 | 
            +
                      system! "#{rspec_command} #{Brut.container.app_specs_dir}/"
         | 
| 53 | 
            +
                    else
         | 
| 54 | 
            +
                      span.add_attributes(tests: args.length)
         | 
| 55 | 
            +
                      test_args = args.map { |_|
         | 
| 56 | 
            +
                        '"' + Shellwords.escape(_) + '"'
         | 
| 57 | 
            +
                      }.join(" ")
         | 
| 58 | 
            +
                      system! "#{rspec_command} #{test_args}"
         | 
| 59 | 
            +
                    end
         | 
| 54 60 | 
             
                  end
         | 
| 55 61 | 
             
                  if options.rebuild_after?(default: rebuild_after_by_default?)
         | 
| 56 | 
            -
                     | 
| 57 | 
            -
             | 
| 62 | 
            +
                    Brut.container.instrumentation.span("schema.rebuild.after") do
         | 
| 63 | 
            +
                      out.puts "Re-Rebuilding test database schema"
         | 
| 64 | 
            +
                      system! "bin/db rebuild --env=test"
         | 
| 65 | 
            +
                    end
         | 
| 58 66 | 
             
                  end
         | 
| 59 67 | 
             
                  0
         | 
| 60 68 | 
             
                end
         | 
| @@ -424,6 +424,15 @@ class Brut::Framework::Config | |
| 424 424 | 
             
                    allow_nil: true,
         | 
| 425 425 | 
             
                  )
         | 
| 426 426 |  | 
| 427 | 
            +
                  Brut.container.store(
         | 
| 428 | 
            +
                    "fallback_host",
         | 
| 429 | 
            +
                    URI,
         | 
| 430 | 
            +
                    "Hostname to use in situations where the request is not available",
         | 
| 431 | 
            +
                    nil,
         | 
| 432 | 
            +
                    allow_app_override: true,
         | 
| 433 | 
            +
                    allow_nil: true
         | 
| 434 | 
            +
                  )
         | 
| 435 | 
            +
             | 
| 427 436 | 
             
                  c.store(
         | 
| 428 437 | 
             
                    "local_hostname",
         | 
| 429 438 | 
             
                    String,
         | 
| @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            # Raised when Brut configuration is missing an expected value.  This is mostly raised when values that must be set per app
         | 
| 2 | 
            +
            # have not been set.
         | 
| 3 | 
            +
            class Brut::Framework::Errors::MissingConfiguration < Brut::Framework::Error
         | 
| 4 | 
            +
              # Create the exception
         | 
| 5 | 
            +
              #
         | 
| 6 | 
            +
              # @param [String|Symbol] config_name the name of the missing configuration parameter
         | 
| 7 | 
            +
              # @param [String] context Any additional context to understand the error
         | 
| 8 | 
            +
              def initialize(config_name, context)
         | 
| 9 | 
            +
                super("Configuration parameter '#{config_name}' did not have a value, but was expected to. #{context}")
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
            end
         | 
| @@ -7,6 +7,7 @@ module Brut | |
| 7 7 | 
             
                  autoload(:NotImplemented,"brut/framework/errors/not_implemented")
         | 
| 8 8 | 
             
                  autoload(:NotFound,"brut/framework/errors/not_found")
         | 
| 9 9 | 
             
                  autoload(:MissingParameter,"brut/framework/errors/missing_parameter")
         | 
| 10 | 
            +
                  autoload(:MissingConfiguration,"brut/framework/errors/missing_configuration")
         | 
| 10 11 | 
             
                  autoload(:AbstractMethod,"brut/framework/errors/abstract_method")
         | 
| 11 12 | 
             
                  autoload(:NoClassForPath,"brut/framework/errors/no_class_for_path")
         | 
| 12 13 | 
             
                  # Raises {Brut::Framework::Errors::Bug}
         | 
    
        data/lib/brut/framework/mcp.rb
    CHANGED
    
    | @@ -17,6 +17,9 @@ require "opentelemetry/exporter/otlp" | |
| 17 17 | 
             
            # intended to use or interact with this class at all. End of line.
         | 
| 18 18 | 
             
            class Brut::Framework::MCP
         | 
| 19 19 |  | 
| 20 | 
            +
              @otel_shutdown = Concurrent::AtomicBoolean.new(false)
         | 
| 21 | 
            +
              def self.otel_shutdown = @otel_shutdown
         | 
| 22 | 
            +
             | 
| 20 23 | 
             
              # Create and configure the MCP.  The app will not work until {#boot!} has been called, however most of the core configuration
         | 
| 21 24 | 
             
              # will be available via `Brut.container`.
         | 
| 22 25 | 
             
              #
         | 
| @@ -33,7 +36,7 @@ class Brut::Framework::MCP | |
| 33 36 | 
             
              #
         | 
| 34 37 | 
             
              # * Create the instance and *do not* call `boot!`.  This is what you'd do if you can't or don't want to connect to external services
         | 
| 35 38 | 
             
              # like the database.  For example, when Brut builds assets, it does not call `boot!`.
         | 
| 36 | 
            -
              # * Create the  | 
| 39 | 
            +
              # * Create the instance and immediately call `boot!`.  This is what happens most of the time, in particular when the app is started
         | 
| 37 40 | 
             
              # up by Puma to start serving requests.
         | 
| 38 41 | 
             
              #
         | 
| 39 42 | 
             
              # What you should avoid doing is creating an instance of this class and performing logic before later calling `boot!`.
         | 
| @@ -66,49 +69,34 @@ class Brut::Framework::MCP | |
| 66 69 | 
             
                  begin
         | 
| 67 70 | 
             
                    Brut.container.sequel_db_handle.disconnect
         | 
| 68 71 | 
             
                  rescue Sequel::DatabaseConnectionError
         | 
| 69 | 
            -
                    SemanticLogger[ | 
| 72 | 
            +
                    SemanticLogger[self.class].info "Not connected to database, so not disconnecting"
         | 
| 73 | 
            +
                  end
         | 
| 74 | 
            +
                  otel_configured = OpenTelemetry.tracer_provider.is_a?(OpenTelemetry::SDK::Trace::TracerProvider)
         | 
| 75 | 
            +
                  if otel_configured
         | 
| 76 | 
            +
                    if $PROGRAM_NAME =~ /^sidekiq/
         | 
| 77 | 
            +
                      SemanticLogger[self.class].info "Assuming we are sidekiq, which will shutdown OpenTelemetry, so doing nothing", program: $PROGRAM_NAME
         | 
| 78 | 
            +
                    else
         | 
| 79 | 
            +
                      if self.class.otel_shutdown.make_true
         | 
| 80 | 
            +
                        SemanticLogger[self.class].info "Shutting down OpenTelemetry", program: $PROGRAM_NAME
         | 
| 81 | 
            +
                        OpenTelemetry.tracer_provider.shutdown
         | 
| 82 | 
            +
                      else
         | 
| 83 | 
            +
                        SemanticLogger[self.class].info "OpenTelemetry already shutdown", program: $PROGRAM_NAME
         | 
| 84 | 
            +
                      end
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  else
         | 
| 87 | 
            +
                    SemanticLogger[self.class].info "OpenTelemetry was not configured, so no shutdown needed", program: $PROGRAM_NAME
         | 
| 70 88 | 
             
                  end
         | 
| 71 89 | 
             
                end
         | 
| 72 | 
            -
                Sequel::Database.extension :pg_array
         | 
| 73 | 
            -
             | 
| 74 | 
            -
                sequel_db = Brut.container.sequel_db_handle
         | 
| 75 | 
            -
             | 
| 76 | 
            -
                Sequel::Model.db = sequel_db
         | 
| 77 90 |  | 
| 78 | 
            -
                 | 
| 79 | 
            -
                 | 
| 80 | 
            -
                Sequel::Model.plugin :table_select
         | 
| 81 | 
            -
                Sequel::Model.plugin :skip_saving_columns
         | 
| 91 | 
            +
                boot_postgres!
         | 
| 92 | 
            +
                boot_otel!
         | 
| 82 93 |  | 
| 83 | 
            -
                if !Brut.container.external_id_prefix.nil?
         | 
| 84 | 
            -
                  Sequel::Model.plugin :external_id, global_prefix: Brut.container.external_id_prefix
         | 
| 85 | 
            -
                end
         | 
| 86 94 | 
             
                if Brut.container.eager_load_classes?
         | 
| 87 95 | 
             
                  SemanticLogger["Brut"].info("Eagerly loading app's classes")
         | 
| 88 96 | 
             
                  @loader.eager_load
         | 
| 89 97 | 
             
                else
         | 
| 90 98 | 
             
                  SemanticLogger["Brut"].info("Lazily loading app's classes")
         | 
| 91 99 | 
             
                end
         | 
| 92 | 
            -
                OpenTelemetry::SDK.configure do |c|
         | 
| 93 | 
            -
                  c.service_name = @app.id
         | 
| 94 | 
            -
                  if ENV["OTEL_TRACES_EXPORTER"]
         | 
| 95 | 
            -
                    SemanticLogger[self.class].info "OTEL_TRACES_EXPORTER was set (to '#{ENV['OTEL_TRACES_EXPORTER']}'), so Brut's OTel logging is disabled"
         | 
| 96 | 
            -
                  else
         | 
| 97 | 
            -
                    c.add_span_processor(
         | 
| 98 | 
            -
                      OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
         | 
| 99 | 
            -
                        Brut::Instrumentation::LoggerSpanExporter.new
         | 
| 100 | 
            -
                      )
         | 
| 101 | 
            -
                    )
         | 
| 102 | 
            -
                  end
         | 
| 103 | 
            -
                end
         | 
| 104 | 
            -
             | 
| 105 | 
            -
                Brut.container.store(
         | 
| 106 | 
            -
                  "tracer",
         | 
| 107 | 
            -
                  OpenTelemetry::SDK::Trace::Tracer,
         | 
| 108 | 
            -
                  "Tracer for Open Telemetry",
         | 
| 109 | 
            -
                  OpenTelemetry.tracer_provider.tracer(@app.id)
         | 
| 110 | 
            -
                )
         | 
| 111 | 
            -
                Sequel::Database.extension :brut_instrumentation
         | 
| 112 100 |  | 
| 113 101 | 
             
                @app.boot!
         | 
| 114 102 |  | 
| @@ -295,4 +283,53 @@ private | |
| 295 283 |  | 
| 296 284 | 
             
                @loader.setup
         | 
| 297 285 | 
             
              end
         | 
| 286 | 
            +
             | 
| 287 | 
            +
              def boot_postgres!
         | 
| 288 | 
            +
                Sequel::Database.extension :pg_array
         | 
| 289 | 
            +
                Sequel::Database.extension :pg_json
         | 
| 290 | 
            +
             | 
| 291 | 
            +
                sequel_db = Brut.container.sequel_db_handle
         | 
| 292 | 
            +
             | 
| 293 | 
            +
                Sequel::Model.db = sequel_db
         | 
| 294 | 
            +
             | 
| 295 | 
            +
                Sequel::Model.plugin :find_bang
         | 
| 296 | 
            +
                Sequel::Model.plugin :created_at
         | 
| 297 | 
            +
                Sequel::Model.plugin :table_select
         | 
| 298 | 
            +
                Sequel::Model.plugin :skip_saving_columns
         | 
| 299 | 
            +
             | 
| 300 | 
            +
                if !Brut.container.external_id_prefix.nil?
         | 
| 301 | 
            +
                  Sequel::Model.plugin :external_id, global_prefix: Brut.container.external_id_prefix
         | 
| 302 | 
            +
                end
         | 
| 303 | 
            +
                Sequel::Database.extension :brut_instrumentation
         | 
| 304 | 
            +
              end
         | 
| 305 | 
            +
             | 
| 306 | 
            +
              def boot_otel!
         | 
| 307 | 
            +
                OpenTelemetry::SDK.configure do |c|
         | 
| 308 | 
            +
                  c.service_name = @app.id
         | 
| 309 | 
            +
                  if ENV["OTEL_TRACES_EXPORTER"]
         | 
| 310 | 
            +
                    SemanticLogger[self.class].info "OTEL_TRACES_EXPORTER was set (to '#{ENV['OTEL_TRACES_EXPORTER']}'), so Brut's OTel logging is disabled"
         | 
| 311 | 
            +
                  else
         | 
| 312 | 
            +
                    c.add_span_processor(
         | 
| 313 | 
            +
                      OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(
         | 
| 314 | 
            +
                        Brut::Instrumentation::LoggerSpanExporter.new
         | 
| 315 | 
            +
                      )
         | 
| 316 | 
            +
                    )
         | 
| 317 | 
            +
                  end
         | 
| 318 | 
            +
             | 
| 319 | 
            +
                  if defined?(OpenTelemetry::Instrumentation::Sidekiq)
         | 
| 320 | 
            +
                    c.use 'OpenTelemetry::Instrumentation::Sidekiq', {
         | 
| 321 | 
            +
                      span_naming: :job_class,
         | 
| 322 | 
            +
                    }
         | 
| 323 | 
            +
                  else
         | 
| 324 | 
            +
                    SemanticLogger[self.class].info "OpenTelemetry::Instrumentation::Sidekiq is not loaded, so Sidekiq traces will not be captured"
         | 
| 325 | 
            +
                  end
         | 
| 326 | 
            +
                end
         | 
| 327 | 
            +
             | 
| 328 | 
            +
                Brut.container.store(
         | 
| 329 | 
            +
                  "tracer",
         | 
| 330 | 
            +
                  OpenTelemetry::SDK::Trace::Tracer,
         | 
| 331 | 
            +
                  "Tracer for Open Telemetry",
         | 
| 332 | 
            +
                  OpenTelemetry.tracer_provider.tracer(@app.id)
         | 
| 333 | 
            +
                )
         | 
| 334 | 
            +
              end
         | 
| 298 335 | 
             
            end
         | 
| @@ -15,7 +15,7 @@ module Brut::FrontEnd::HandlingResults | |
| 15 15 | 
             
                if !klass.kind_of?(Class)
         | 
| 16 16 | 
             
                  raise ArgumentError,"redirect_to should be given a Class, not a #{klass.class}"
         | 
| 17 17 | 
             
                end
         | 
| 18 | 
            -
                Brut.container.routing. | 
| 18 | 
            +
                Brut.container.routing.path(klass,with_method: :get,**query_string_params)
         | 
| 19 19 | 
             
              end
         | 
| 20 20 |  | 
| 21 21 | 
             
              # Return this to return an HTTP status code from a number or string containing the code.
         | 
| @@ -15,13 +15,15 @@ class Brut::FrontEnd::RequestContext | |
| 15 15 | 
             
              # @param [Brut::FrontEnd::Flash] flash the current flash
         | 
| 16 16 | 
             
              # @param [true|false] xhr true if this is an XHR request.
         | 
| 17 17 | 
             
              # @param [Object] body the `request.body` as provided by Rack
         | 
| 18 | 
            -
               | 
| 18 | 
            +
              # @param [URI] host URI the `request.host` and `request.scheme`, and `request.port` as provided by Rack
         | 
| 19 | 
            +
              def initialize(env:,session:,flash:,xhr:,body:,host:)
         | 
| 19 20 | 
             
                @hash = {
         | 
| 20 21 | 
             
                  env:,
         | 
| 21 22 | 
             
                  session:,
         | 
| 22 23 | 
             
                  flash:,
         | 
| 23 24 | 
             
                  xhr:,
         | 
| 24 25 | 
             
                  body:,
         | 
| 26 | 
            +
                  host:,
         | 
| 25 27 | 
             
                  csrf_token: Rack::Protection::AuthenticityToken.token(env["rack.session"]),
         | 
| 26 28 | 
             
                  clock: Clock.new(session.timezone),
         | 
| 27 29 | 
             
                }
         | 
| @@ -5,9 +5,10 @@ class Brut::FrontEnd::RouteHooks::SetupRequestContext < Brut::FrontEnd::RouteHoo | |
| 5 5 | 
             
              def before(session:,request:,env:)
         | 
| 6 6 | 
             
                flash = session.flash
         | 
| 7 7 | 
             
                session[:_flash] ||= flash
         | 
| 8 | 
            +
                host_uri = URI.parse("#{request.scheme}://#{request.host}:#{request.port}")
         | 
| 8 9 | 
             
                Thread.current.thread_variable_set(
         | 
| 9 10 | 
             
                  :request_context,
         | 
| 10 | 
            -
                  Brut::FrontEnd::RequestContext.new(env:,session:session,flash:,xhr: request.xhr?,body: request.body)
         | 
| 11 | 
            +
                  Brut::FrontEnd::RequestContext.new(env:,session:session,flash:,xhr: request.xhr?,body: request.body, host: host_uri)
         | 
| 11 12 | 
             
                )
         | 
| 12 13 | 
             
                continue
         | 
| 13 14 | 
             
              end
         | 
| @@ -116,7 +116,25 @@ class Brut::FrontEnd::Routing | |
| 116 116 | 
             
                route
         | 
| 117 117 | 
             
              end
         | 
| 118 118 |  | 
| 119 | 
            -
              def  | 
| 119 | 
            +
              def path(handler_class, with_method: :any, **rest)
         | 
| 120 | 
            +
                route = self.route_for(handler_class, with_method:)
         | 
| 121 | 
            +
                route.path(**rest)
         | 
| 122 | 
            +
              end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
              def url(handler_class, with_method: :any, **rest)
         | 
| 125 | 
            +
                route = self.route_for(handler_class, with_method:)
         | 
| 126 | 
            +
                route.url(**rest)
         | 
| 127 | 
            +
              end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
              def inspect
         | 
| 130 | 
            +
                @routes.map { |route|
         | 
| 131 | 
            +
                  "#{route.http_method}:#{route.path_template} - #{route.handler_class.name}"
         | 
| 132 | 
            +
                }.join("\n")
         | 
| 133 | 
            +
              end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
            private
         | 
| 136 | 
            +
             | 
| 137 | 
            +
              def route_for(handler_class, with_method: :any)
         | 
| 120 138 | 
             
                route = self.route(handler_class)
         | 
| 121 139 | 
             
                route_allowed_for_method = if with_method == :any
         | 
| 122 140 | 
             
                                             true
         | 
| @@ -128,14 +146,9 @@ class Brut::FrontEnd::Routing | |
| 128 146 | 
             
                if !route_allowed_for_method
         | 
| 129 147 | 
             
                  raise ArgumentError,"The route for '#{handler_class}' (#{route.path}) is not supported by HTTP method '#{with_method}'"
         | 
| 130 148 | 
             
                end
         | 
| 131 | 
            -
                route | 
| 149 | 
            +
                route
         | 
| 132 150 | 
             
              end
         | 
| 133 151 |  | 
| 134 | 
            -
              def inspect
         | 
| 135 | 
            -
                @routes.map { |route|
         | 
| 136 | 
            -
                  "#{route.http_method}:#{route.path_template} - #{route.handler_class.name}"
         | 
| 137 | 
            -
                }.join("\n")
         | 
| 138 | 
            -
              end
         | 
| 139 152 |  | 
| 140 153 | 
             
              def add_routing_method(route)
         | 
| 141 154 | 
             
                handler_class = route.handler_class
         | 
| @@ -143,7 +156,10 @@ class Brut::FrontEnd::Routing | |
| 143 156 | 
             
                [ handler_class, form_class ].compact.each do |klass|
         | 
| 144 157 | 
             
                  klass.class_eval do
         | 
| 145 158 | 
             
                    def self.routing(**args)
         | 
| 146 | 
            -
                      Brut.container.routing. | 
| 159 | 
            +
                      Brut.container.routing.path(self,**args)
         | 
| 160 | 
            +
                    end
         | 
| 161 | 
            +
                    def self.full_routing(**args)
         | 
| 162 | 
            +
                      Brut.container.routing.url(self,**args)
         | 
| 147 163 | 
             
                    end
         | 
| 148 164 | 
             
                  end
         | 
| 149 165 | 
             
                end
         | 
| @@ -205,11 +221,31 @@ class Brut::FrontEnd::Routing | |
| 205 221 | 
             
                  uri
         | 
| 206 222 | 
             
                end
         | 
| 207 223 |  | 
| 224 | 
            +
                def url(**query_string_params)
         | 
| 225 | 
            +
                  request_context = Thread.current.thread_variable_get(:request_context)
         | 
| 226 | 
            +
                  path = self.path(**query_string_params)
         | 
| 227 | 
            +
                  host = if request_context
         | 
| 228 | 
            +
                           request_context[:host]
         | 
| 229 | 
            +
                         end
         | 
| 230 | 
            +
                  host ||= Brut.container.fallback_host
         | 
| 231 | 
            +
                  host.merge(path)
         | 
| 232 | 
            +
                rescue ArgumentError => ex
         | 
| 233 | 
            +
                  request_context_note = if request_context
         | 
| 234 | 
            +
                                           "the RequestContext did not contain request.host, which is unusual"
         | 
| 235 | 
            +
                                         else
         | 
| 236 | 
            +
                                           "the RequestContext was not available (likely due to calling `full_routing` outside an HTTP request)"
         | 
| 237 | 
            +
                                         end
         | 
| 238 | 
            +
                  raise Brut::Framework::Errors::MissingConfiguration(
         | 
| 239 | 
            +
                    :fallback_host,
         | 
| 240 | 
            +
                    "Attempting to get the full URL for route #{self.path_template}, #{request_context_note}, and Brut.container.fallback_host was not set.  You must set this value if you expect to generate complete URLs outside of an HTTP request")
         | 
| 241 | 
            +
                end
         | 
| 242 | 
            +
             | 
| 208 243 | 
             
                def ==(other)
         | 
| 209 244 | 
             
                  self.method == other.method && self.path == other.path
         | 
| 210 245 | 
             
                end
         | 
| 211 246 |  | 
| 212 247 | 
             
              private
         | 
| 248 | 
            +
             | 
| 213 249 | 
             
                def locate_handler_class(suffix,preposition, on_missing: :raise)
         | 
| 214 250 | 
             
                  if @path_template == "/"
         | 
| 215 251 | 
             
                    return Module.const_get("HomePage") # XXX Needs error handling
         | 
| @@ -2,10 +2,23 @@ | |
| 2 2 | 
             
            module Brut::SpecSupport::ClockSupport
         | 
| 3 3 | 
             
              # Return a real lock in UTC
         | 
| 4 4 | 
             
              def real_clock = Clock.new(TZInfo::Timezone.get("UTC"))
         | 
| 5 | 
            -
             | 
| 5 | 
            +
             | 
| 6 | 
            +
              # Return a clock whose value for now is `now`, in UTC
         | 
| 6 7 | 
             
              #
         | 
| 7 8 | 
             
              # @param [String] now a string containing the value you want for {Clock#now} to return.
         | 
| 8 9 | 
             
              def clock_at(now:)
         | 
| 9 | 
            -
                 | 
| 10 | 
            +
                self.clock_in_timezone_at(timezone_name: "UTC", now: now)
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              # Return a clock whose value for now is `now` in the given timezone
         | 
| 14 | 
            +
              #
         | 
| 15 | 
            +
              # @param [String] timezone_name a string that is the name of the timezone to use.
         | 
| 16 | 
            +
              # @param [String] now a string containing the value you want for {Clock#now} to return.
         | 
| 17 | 
            +
              def clock_in_timezone_at(timezone_name:, now:)
         | 
| 18 | 
            +
                time = Time.parse(now)
         | 
| 19 | 
            +
                timezone = TZInfo::Timezone.get(timezone_name)
         | 
| 20 | 
            +
                same_time_in_timezone = timezone.local_time(time.year, time.month, time.day, time.hour, time.min, time.sec)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                Clock.new(TZInfo::Timezone.get(timezone_name), now: same_time_in_timezone)
         | 
| 10 23 | 
             
              end
         | 
| 11 24 | 
             
            end
         | 
| @@ -78,7 +78,7 @@ module Brut::SpecSupport::ComponentSupport | |
| 78 78 |  | 
| 79 79 | 
             
              # @!visibility private
         | 
| 80 80 | 
             
              def routing_for(klass,**args)
         | 
| 81 | 
            -
                Brut.container.routing. | 
| 81 | 
            +
                Brut.container.routing.path(klass,**args)
         | 
| 82 82 | 
             
              end
         | 
| 83 83 |  | 
| 84 84 | 
             
              # Escape HTML using the same code Brut uses for rendering templates.
         | 
| @@ -5,6 +5,16 @@ module Brut::SpecSupport::GeneralSupport | |
| 5 5 | 
             
              end
         | 
| 6 6 |  | 
| 7 7 | 
             
              module ClassMethods
         | 
| 8 | 
            +
                def implementation_is_needed(check_again_at:)
         | 
| 9 | 
            +
                  check_again_at = if check_again_at.kind_of?(Time)
         | 
| 10 | 
            +
                                     check_again_at
         | 
| 11 | 
            +
                                   else
         | 
| 12 | 
            +
                                     check_again_at = Date.parse(check_again_at).to_time
         | 
| 13 | 
            +
                                   end
         | 
| 14 | 
            +
                  it "has no tests for now, but they are needed eventually" do
         | 
| 15 | 
            +
                    expect(Time.now < check_again_at).to eq(true),"It's after #{check_again_at}. Implementation is needed"
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 8 18 | 
             
                # To pass bin/test audit with a class whose implementation is trivial, call this inside the RSpec `describe` block. This is better
         | 
| 9 19 | 
             
                # than an empty test as it makes it more explicit that you believe the implementation is trivial enough to not require a test. You
         | 
| 10 20 | 
             
                # can also set an expiration for this thinking.
         | 
| @@ -23,7 +33,7 @@ module Brut::SpecSupport::GeneralSupport | |
| 23 33 | 
             
                    if check_again_at.nil?
         | 
| 24 34 | 
             
                      expect(true).to eq(true)
         | 
| 25 35 | 
             
                    else
         | 
| 26 | 
            -
                      expect(Time.now < check_again_at).to eq(true)," | 
| 36 | 
            +
                      expect(Time.now < check_again_at).to eq(true),"It's after #{check_again_at}. Check that the implementation of the class under test is still trivial. If it is, update or remove check_again_at:"
         | 
| 27 37 | 
             
                    end
         | 
| 28 38 | 
             
                  end
         | 
| 29 39 | 
             
                end
         | 
| @@ -1,10 +1,10 @@ | |
| 1 1 | 
             
            RSpec::Matchers.define :be_routing_for do |klass,**args|
         | 
| 2 2 | 
             
              match do |uri|
         | 
| 3 | 
            -
                uri == Brut.container.routing. | 
| 3 | 
            +
                uri == Brut.container.routing.path(klass,**args)
         | 
| 4 4 | 
             
              end
         | 
| 5 5 |  | 
| 6 6 | 
             
              failure_message do |uri|
         | 
| 7 | 
            -
                expected = Brut.container.routing. | 
| 7 | 
            +
                expected = Brut.container.routing.path(klass,**args)
         | 
| 8 8 | 
             
                "Expected route for #{klass}: #{expected}, but got #{uri}"
         | 
| 9 9 | 
             
              end
         | 
| 10 10 |  | 
| @@ -103,6 +103,7 @@ class Brut::SpecSupport::RSpecSetup | |
| 103 103 | 
             
                      flash: empty_flash,
         | 
| 104 104 | 
             
                      body: nil,
         | 
| 105 105 | 
             
                      xhr: false,
         | 
| 106 | 
            +
                      host: URI("https://example.com")
         | 
| 106 107 | 
             
                    )
         | 
| 107 108 | 
             
                    Thread.current.thread_variable_set(:request_context, request_context)
         | 
| 108 109 | 
             
                    example.example_group.let(:request_context) { request_context }
         | 
    
        data/lib/brut/version.rb
    CHANGED
    
    
    
        data/lib/brut.rb
    CHANGED
    
    | @@ -50,6 +50,7 @@ module Brut | |
| 50 50 | 
             
              # will be in the back end, Brut is far less prescriptive about how to manage that than it is the front end.
         | 
| 51 51 | 
             
              module BackEnd
         | 
| 52 52 | 
             
                autoload(:Validators, "brut/back_end/validator")
         | 
| 53 | 
            +
                autoload(:Sidekiq, "brut/back_end/sidekiq")
         | 
| 53 54 | 
             
                # Do not put SeedData here - it must be loaded only when needed
         | 
| 54 55 | 
             
              end
         | 
| 55 56 | 
             
              # I18n is where internationalization and localization support lives.
         | 
    
        metadata
    CHANGED
    
    | @@ -1,13 +1,13 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: brut
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.0. | 
| 4 | 
            +
              version: 0.0.10
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - David Bryant Copeland
         | 
| 8 8 | 
             
            bindir: exe
         | 
| 9 9 | 
             
            cert_chain: []
         | 
| 10 | 
            -
            date: 2025- | 
| 10 | 
            +
            date: 2025-04-02 00:00:00.000000000 Z
         | 
| 11 11 | 
             
            dependencies:
         | 
| 12 12 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 13 13 | 
             
              name: irb
         | 
| @@ -455,6 +455,10 @@ files: | |
| 455 455 | 
             
            - dx/stop
         | 
| 456 456 | 
             
            - lib/brut.rb
         | 
| 457 457 | 
             
            - lib/brut/back_end/seed_data.rb
         | 
| 458 | 
            +
            - lib/brut/back_end/sidekiq.rb
         | 
| 459 | 
            +
            - lib/brut/back_end/sidekiq/middlewares.rb
         | 
| 460 | 
            +
            - lib/brut/back_end/sidekiq/middlewares/server.rb
         | 
| 461 | 
            +
            - lib/brut/back_end/sidekiq/middlewares/server/flush_spans.rb
         | 
| 458 462 | 
             
            - lib/brut/back_end/validator.rb
         | 
| 459 463 | 
             
            - lib/brut/back_end/validators/form_validator.rb
         | 
| 460 464 | 
             
            - lib/brut/cli.rb
         | 
| @@ -478,6 +482,7 @@ files: | |
| 478 482 | 
             
            - lib/brut/framework/errors.rb
         | 
| 479 483 | 
             
            - lib/brut/framework/errors/abstract_method.rb
         | 
| 480 484 | 
             
            - lib/brut/framework/errors/bug.rb
         | 
| 485 | 
            +
            - lib/brut/framework/errors/missing_configuration.rb
         | 
| 481 486 | 
             
            - lib/brut/framework/errors/missing_parameter.rb
         | 
| 482 487 | 
             
            - lib/brut/framework/errors/no_class_for_path.rb
         | 
| 483 488 | 
             
            - lib/brut/framework/errors/not_found.rb
         |