brut 0.0.13 → 0.0.21

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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -6
  3. data/brut.gemspec +1 -3
  4. data/lib/brut/back_end/seed_data.rb +19 -2
  5. data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
  6. data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
  7. data/lib/brut/back_end/sidekiq.rb +2 -1
  8. data/lib/brut/back_end/validator.rb +5 -1
  9. data/lib/brut/back_end.rb +9 -0
  10. data/lib/brut/cli/apps/scaffold.rb +16 -24
  11. data/lib/brut/cli.rb +4 -3
  12. data/lib/brut/factory_bot.rb +0 -5
  13. data/lib/brut/framework/app.rb +70 -5
  14. data/lib/brut/framework/config.rb +9 -46
  15. data/lib/brut/framework/container.rb +3 -2
  16. data/lib/brut/framework/errors.rb +12 -4
  17. data/lib/brut/framework/mcp.rb +63 -2
  18. data/lib/brut/framework/project_environment.rb +6 -2
  19. data/lib/brut/framework.rb +1 -1
  20. data/lib/brut/front_end/asset_path_resolver.rb +15 -0
  21. data/lib/brut/front_end/component.rb +101 -246
  22. data/lib/brut/front_end/components/constraint_violations.rb +10 -10
  23. data/lib/brut/front_end/components/form_tag.rb +17 -29
  24. data/lib/brut/front_end/components/i18n_translations.rb +12 -13
  25. data/lib/brut/front_end/components/input.rb +0 -1
  26. data/lib/brut/front_end/components/inputs/csrf_token.rb +3 -3
  27. data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
  28. data/lib/brut/front_end/components/inputs/select.rb +13 -20
  29. data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
  30. data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
  31. data/lib/brut/front_end/components/locale_detection.rb +2 -2
  32. data/lib/brut/front_end/components/page_identifier.rb +3 -5
  33. data/lib/brut/front_end/components/{time.rb → time_tag.rb} +14 -11
  34. data/lib/brut/front_end/components/traceparent.rb +5 -6
  35. data/lib/brut/front_end/http_method.rb +4 -0
  36. data/lib/brut/front_end/inline_svg_locator.rb +21 -0
  37. data/lib/brut/front_end/layout.rb +19 -0
  38. data/lib/brut/front_end/page.rb +52 -40
  39. data/lib/brut/front_end/request_context.rb +13 -0
  40. data/lib/brut/front_end/routing.rb +8 -3
  41. data/lib/brut/front_end.rb +32 -0
  42. data/lib/brut/i18n/base_methods.rb +51 -11
  43. data/lib/brut/i18n/for_back_end.rb +8 -0
  44. data/lib/brut/i18n/for_cli.rb +5 -1
  45. data/lib/brut/i18n/for_html.rb +9 -1
  46. data/lib/brut/i18n/http_accept_language.rb +47 -0
  47. data/lib/brut/i18n.rb +1 -0
  48. data/lib/brut/instrumentation/open_telemetry.rb +25 -0
  49. data/lib/brut/instrumentation.rb +3 -5
  50. data/lib/brut/sinatra_helpers.rb +13 -7
  51. data/lib/brut/spec_support/component_support.rb +27 -13
  52. data/lib/brut/spec_support/e2e_support.rb +4 -0
  53. data/lib/brut/spec_support/general_support.rb +3 -0
  54. data/lib/brut/spec_support/handler_support.rb +6 -1
  55. data/lib/brut/spec_support/matcher.rb +1 -0
  56. data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
  57. data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
  58. data/lib/brut/spec_support/matchers/have_i18n_string.rb +3 -1
  59. data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
  60. data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
  61. data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
  62. data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
  63. data/lib/brut/spec_support/rspec_setup.rb +1 -0
  64. data/lib/brut/spec_support.rb +5 -4
  65. data/lib/brut/version.rb +1 -1
  66. data/lib/brut.rb +7 -50
  67. metadata +14 -49
  68. data/doc-src/architecture.md +0 -102
  69. data/doc-src/assets.md +0 -98
  70. data/doc-src/forms.md +0 -214
  71. data/doc-src/handlers.md +0 -83
  72. data/doc-src/javascript.md +0 -265
  73. data/doc-src/keyword-injection.md +0 -183
  74. data/doc-src/pages.md +0 -210
  75. data/doc-src/route-hooks.md +0 -59
  76. data/lib/brut/front_end/template.rb +0 -47
  77. data/lib/brut/front_end/templates/block_filter.rb +0 -61
  78. data/lib/brut/front_end/templates/erb_engine.rb +0 -26
  79. data/lib/brut/front_end/templates/erb_parser.rb +0 -84
  80. data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
  81. data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
  82. data/lib/brut/front_end/templates/locator.rb +0 -60
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e8b01889cc06839d9cca47e021c8b781e6f21417b08ba0658d55697285bb061
4
- data.tar.gz: 4d1bf20dac16e8e6322402aa364f886235140028e53f8aa313ddc6d3b2de81c6
3
+ metadata.gz: 13ef62dae2d2a40f20fce1aa2f5df7809fc41ae99b75b2a9aefd25423ef51fb3
4
+ data.tar.gz: 65b9e0b2b77a441210d0047da5c6c8f77d01d97a954d64626683d11fa7f2f1d1
5
5
  SHA512:
6
- metadata.gz: 77c38b80c31452da7db8ef80ce2532aa1d3193257193d70625135d23af01695cc5f0c3f46568988bcf0c02849af8b8f9f3f166f9065445efd90a160c1e3206c0
7
- data.tar.gz: e36fa4f081d48518cf941d0e0323cde04a6370082e0a594da73221f65c1f319d29df29c7cbd5663f30b2da2776fa8ec24295a16cc55b41b94c6f1096590282f7
6
+ metadata.gz: 4a524485b78b02dd2fee75c540b8cef83fc1c79e8ffa6ec8b214995000c42c6e41db5dbfe8ab65fe28b3e9f08eb9edb63f9156e4208618750f3165bf548995ee
7
+ data.tar.gz: af71268d9598c889731db3cbff39c47d14173ae4112b4eb9e723ff917bf036da93ee4efb3bdffc7f0fc1136abc71b089ab64fe0f8e503226f61e1b0511ffa9f1
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brut (0.0.13)
4
+ brut (0.0.21)
5
5
  concurrent-ruby
6
6
  i18n
7
7
  irb
@@ -9,15 +9,13 @@ PATH
9
9
  opentelemetry-exporter-otlp
10
10
  opentelemetry-sdk
11
11
  ostruct
12
+ phlex
12
13
  prism
13
14
  rack-protection
14
15
  rackup
15
- rexml
16
16
  semantic_logger
17
17
  sequel
18
18
  sinatra
19
- temple
20
- tilt
21
19
  tzinfo
22
20
  tzinfo-data
23
21
  zeitwerk
@@ -114,6 +112,8 @@ GEM
114
112
  opentelemetry-semantic_conventions (1.10.1)
115
113
  opentelemetry-api (~> 1.0)
116
114
  ostruct (0.6.1)
115
+ phlex (2.2.1)
116
+ zeitwerk (~> 2.7)
117
117
  pp (0.6.2)
118
118
  prettyprint
119
119
  prettyprint (0.2.0)
@@ -136,7 +136,6 @@ GEM
136
136
  psych (>= 4.0.0)
137
137
  reline (0.6.0)
138
138
  io-console (~> 0.5)
139
- rexml (3.3.9)
140
139
  rspec (3.13.0)
141
140
  rspec-core (~> 3.13.0)
142
141
  rspec-expectations (~> 3.13.0)
@@ -163,7 +162,6 @@ GEM
163
162
  rack-session (>= 2.0.0, < 3)
164
163
  tilt (~> 2.0)
165
164
  stringio (3.1.3)
166
- temple (0.10.3)
167
165
  tilt (2.4.0)
168
166
  tzinfo (2.0.6)
169
167
  concurrent-ruby (~> 1.0)
data/brut.gemspec CHANGED
@@ -39,15 +39,13 @@ Gem::Specification.new do |spec|
39
39
  spec.add_runtime_dependency "concurrent-ruby"
40
40
  spec.add_runtime_dependency "i18n"
41
41
  spec.add_runtime_dependency "nokogiri"
42
+ spec.add_runtime_dependency "phlex"
42
43
  spec.add_runtime_dependency "prism"
43
44
  spec.add_runtime_dependency "rack-protection"
44
45
  spec.add_runtime_dependency "rackup"
45
- spec.add_runtime_dependency "rexml"
46
46
  spec.add_runtime_dependency "semantic_logger"
47
47
  spec.add_runtime_dependency "sequel"
48
48
  spec.add_runtime_dependency "sinatra"
49
- spec.add_runtime_dependency "temple"
50
- spec.add_runtime_dependency "tilt"
51
49
  spec.add_runtime_dependency "tzinfo"
52
50
  spec.add_runtime_dependency "tzinfo-data"
53
51
  spec.add_runtime_dependency "zeitwerk"
@@ -1,10 +1,19 @@
1
1
  require_relative "../factory_bot"
2
2
  module Brut
3
3
  module BackEnd
4
- # Base class and manager of Seed Data for the app. Seed Data is data used for development. It is not for populating e.g.
5
- # reference data or other stuff in production.
4
+ # Base class and manager of Seed Data for the app. Seed Data is data used for development.
5
+ # It is not for populating e.g. reference data or other stuff in production, nor is it for
6
+ # managing test data.
6
7
  #
7
8
  # Seed Data uses FactoryBot.
9
+ #
10
+ # To create your own seed data:
11
+ #
12
+ # 1. Inherit from this class. Doing so will register your class with an internal data structure
13
+ # Brut will use to create all seed data.
14
+ # 2. Provide a no-arg initializer (although you are unlikely to need any initializer at all).
15
+ # 3. Implement {#seed!} to use Factory Bot to create all the seed data. This method should be self-contained
16
+ # and not rely on other seed data classes. It need not be idempotent.
8
17
  class SeedData
9
18
  def self.inherited(seed_data_klass)
10
19
  @classes ||= []
@@ -12,10 +21,13 @@ module Brut
12
21
  end
13
22
  def self.classes = @classes || []
14
23
 
24
+ # Sets up anything needed before seed data can be created. Do not override this method.
15
25
  def setup!
16
26
  Brut::FactoryBot.new.setup!
17
27
  end
18
28
 
29
+ # Loads all seed data registered with this class. Seed data is registered when a class
30
+ # extends this one. Do not override this method.
19
31
  def load_seeds!
20
32
  DB.transaction do
21
33
  self.class.classes.each do |klass|
@@ -23,6 +35,11 @@ module Brut
23
35
  end
24
36
  end
25
37
  end
38
+
39
+ # Implement this to create your seed data.
40
+ def seed!
41
+ raise Brut::Framework::Errors::AbstractMethod
42
+ end
26
43
  end
27
44
  end
28
45
  end
@@ -1,3 +1,4 @@
1
- class Brut::BackEnd::Sidekiq::Middlewares::Server
1
+ # Useful server middlewares for Sidekiq
2
+ module Brut::BackEnd::Sidekiq::Middlewares::Server
2
3
  autoload(:FlushSpans, "brut/back_end/sidekiq/middlewares/server/flush_spans")
3
4
  end
@@ -1,3 +1,4 @@
1
- class Brut::BackEnd::Sidekiq::Middlewares
1
+ # Useful middlewares for Sidekiq jobs
2
+ module Brut::BackEnd::Sidekiq::Middlewares
2
3
  autoload(:Server, "brut/back_end/sidekiq/middlewares/server")
3
4
  end
@@ -1,3 +1,4 @@
1
- class Brut::BackEnd::Sidekiq
1
+ # Namespace for Sidekiq-related support.
2
+ module Brut::BackEnd::Sidekiq
2
3
  autoload(:Middlewares, "brut/back_end/sidekiq/middlewares")
3
4
  end
@@ -1,3 +1,7 @@
1
- class Brut::BackEnd::Validators
1
+ # Namespace for back-end validation support. Note that in Brut, validators
2
+ # are not a mechanism for ensuring data integrity. Validators are for helping
3
+ # a website visitor or app user to understand data entry mistakes. To ensure
4
+ # data integrity, use your databases constraints and other features.
5
+ module Brut::BackEnd::Validators
2
6
  autoload(:FormValidator, "brut/back_end/validators/form_validator")
3
7
  end
@@ -0,0 +1,9 @@
1
+ # The _back end_ of a Brut app is where your app's business logic and
2
+ # database are managed. While the bulk of your Brut app's code
3
+ # will be in the back end, Brut is far less prescriptive about how to manage
4
+ # that than it is the front end.
5
+ module Brut::BackEnd
6
+ autoload(:Validators, "brut/back_end/validator")
7
+ autoload(:Sidekiq, "brut/back_end/sidekiq")
8
+ # Do not put SeedData here - it must be loaded only when needed
9
+ end
@@ -184,7 +184,7 @@ end}
184
184
  end
185
185
 
186
186
  class Component < Brut::CLI::Command
187
- description "Create a new component, template, and associated test"
187
+ description "Create a new component and associated test"
188
188
  detailed_description "New components go in the `components/` folder of your app, however using --page will create a 'page private' component. To do that, the component name must be an inner class of an existing page, for example HomePage::Welcome. This component goes in a sub-folder inside the `pages/` area of your app"
189
189
  opts.on("--page","If set, this component is for a specific page and won't go with the other components")
190
190
  args "ComponentName"
@@ -217,12 +217,10 @@ end}
217
217
  end
218
218
 
219
219
  source_path = Pathname( (components_src_dir / relative_path).to_s + ".rb" )
220
- html_source_path = Pathname( (components_src_dir / relative_path).to_s + ".html.erb" )
221
220
  spec_path = Pathname( (components_specs_dir / relative_path).to_s + ".spec.rb" )
222
221
 
223
222
  exists = [
224
223
  source_path,
225
- html_source_path,
226
224
  spec_path,
227
225
  ].select(&:exist?)
228
226
 
@@ -236,22 +234,21 @@ end}
236
234
 
237
235
  if global_options.dry_run?
238
236
  out.puts "FileUtils.mkdir_p #{source_path.dirname}"
239
- out.puts "FileUtils.mkdir_p #{html_source_path.dirname}"
240
237
  out.puts "FileUtils.mkdir_p #{spec_path.dirname}"
241
238
  else
242
239
  FileUtils.mkdir_p source_path.dirname
243
- FileUtils.mkdir_p html_source_path.dirname
244
240
  FileUtils.mkdir_p spec_path.dirname
245
241
 
246
242
  File.open(source_path,"w") do |file|
247
243
  file.puts %{class #{class_name} < AppComponent
248
244
  def initialize
249
245
  end
246
+
247
+ def view_template
248
+ h2 { "Welcome to your new template" }
249
+ end
250
250
  end}
251
251
  end
252
- File.open(html_source_path,"w") do |file|
253
- file.puts "<h1>#{class_name} is ready!</h1>"
254
- end
255
252
  File.open(spec_path,"w") do |file|
256
253
  file.puts %{require "spec_helper"
257
254
 
@@ -262,9 +259,8 @@ RSpec.describe #{class_name} do
262
259
  end}
263
260
  end
264
261
  end
265
- out.puts "Component source is in #{source_path.relative_path_from(Brut.container.project_root)}"
266
- out.puts "Component HTML template is in #{html_source_path.relative_path_from(Brut.container.project_root)}"
267
- out.puts "Component test is in #{spec_path.relative_path_from(Brut.container.project_root)}"
262
+ out.puts "Component source is in #{source_path.relative_path_from(Brut.container.project_root)}"
263
+ out.puts "Component test is in #{spec_path.relative_path_from(Brut.container.project_root)}"
268
264
  0
269
265
  end
270
266
  end
@@ -283,7 +279,7 @@ end}
283
279
  end
284
280
  end
285
281
  end
286
- description "Create a new page, template, and associated test"
282
+ description "Create a new page and associated test"
287
283
  args "page_route"
288
284
  def execute
289
285
  if args.length != 1
@@ -299,14 +295,12 @@ end}
299
295
  i18n_locales_dir = Brut.container.i18n_locales_dir
300
296
 
301
297
  page_source_path = Pathname( (pages_src_dir / page_relative_path).to_s + ".rb" )
302
- template_source_path = Pathname( (pages_src_dir / page_relative_path).to_s + ".html.erb" )
303
298
  page_spec_path = Pathname( (pages_specs_dir / page_relative_path).to_s + ".spec.rb" )
304
299
  app_path = Pathname( Brut.container.app_src_dir / "app.rb" )
305
300
  app_translations = Pathname( i18n_locales_dir / "en" / "2_app.rb")
306
301
 
307
302
  exists = [
308
303
  page_source_path,
309
- template_source_path,
310
304
  page_spec_path,
311
305
  ].select(&:exist?)
312
306
 
@@ -319,7 +313,6 @@ end}
319
313
  end
320
314
 
321
315
  FileUtils.mkdir_p page_source_path.dirname, noop: global_options.dry_run?
322
- FileUtils.mkdir_p template_source_path.dirname, noop: global_options.dry_run?
323
316
  FileUtils.mkdir_p page_spec_path.dirname, noop: global_options.dry_run?
324
317
 
325
318
  route_code = "page \"#{route.path_template}\""
@@ -334,8 +327,11 @@ end}
334
327
  page_class_code = %{class #{page_class_name} < AppPage
335
328
  def initialize#{initializer_params_code} # add needed arguments here
336
329
  end
330
+
331
+ def page_template
332
+ h1 { "#{page_class_name} is ready!" }
333
+ end
337
334
  end}
338
- template_code = %{<h1>#{page_class_name} is ready!</h1>}
339
335
  page_spec_code = %{require "spec_helper"
340
336
 
341
337
  RSpec.describe #{page_class_name} do
@@ -352,8 +348,6 @@ end}
352
348
  out.puts "will contain:\n\n#{route_code}\n\n"
353
349
  out.puts page_source_path.relative_path_from(Brut.container.project_root)
354
350
  out.puts "will contain:\n\n#{page_class_code}\n\n"
355
- out.puts template_source_path.relative_path_from(Brut.container.project_root)
356
- out.puts "will contain:\n\n#{template_code}\n\n"
357
351
  out.puts page_spec_path.relative_path_from(Brut.container.project_root)
358
352
  out.puts "will contain:\n\n#{page_spec_code}\n\n"
359
353
  out.puts app_translations.relative_path_from(Brut.container.project_root)
@@ -361,7 +355,6 @@ end}
361
355
  else
362
356
 
363
357
  File.open(page_source_path,"w") { it.puts page_class_code }
364
- File.open(template_source_path,"w") { it.puts template_code }
365
358
  File.open(page_spec_path,"w") { it.puts page_spec_code }
366
359
 
367
360
  existing_translations = File.read(app_translations).split(/\n/)
@@ -393,11 +386,10 @@ end}
393
386
  out.puts "Please make sure everything is correct. Here is the defintion that was not inserted:\n\n#{route_code}"
394
387
  end
395
388
  end
396
- out.puts "Page source is in #{page_source_path.relative_path_from(Brut.container.project_root)}"
397
- out.puts "Page HTML template is in #{template_source_path.relative_path_from(Brut.container.project_root)}"
398
- out.puts "Page test is in #{page_spec_path.relative_path_from(Brut.container.project_root)}"
399
- out.puts "Added title to #{app_translations.relative_path_from(Brut.container.project_root)}"
400
- out.puts "Added route to #{app_path.relative_path_from(Brut.container.project_root)}"
389
+ out.puts "Page source is in #{page_source_path.relative_path_from(Brut.container.project_root)}"
390
+ out.puts "Page test is in #{page_spec_path.relative_path_from(Brut.container.project_root)}"
391
+ out.puts "Added title to #{app_translations.relative_path_from(Brut.container.project_root)}"
392
+ out.puts "Added route to #{app_path.relative_path_from(Brut.container.project_root)}"
401
393
  0
402
394
  end
403
395
  end
data/lib/brut/cli.rb CHANGED
@@ -6,9 +6,10 @@ module Brut
6
6
  # that your CLI will respond to. See {Brut::CLI::app}.
7
7
  module CLI
8
8
 
9
- # Execute your CLI based on its command line invocation. You would call this method inside the executable file placed in `bin/`
10
- # in your project. For example, if you have `YourApp::CLI::CleanOldFiles` and you wish to execute it via `bin/clean-files`, you'd
11
- # create `bin/clean-files` like so:
9
+ # Execute your CLI based on its command line invocation. You would call this method inside the
10
+ # executable file placed in `bin/` in your project.
11
+ # For example, if you have `YourApp::CLI::CleanOldFiles` and you wish to execute
12
+ # it via `bin/clean-files`, you'd create `bin/clean-files` like so:
12
13
  #
13
14
  # ```
14
15
  # #!/usr/bin/env ruby
@@ -1,7 +1,3 @@
1
- # Because FactoryBot 6.4.6 has a bug where it is not properly
2
- # requiring active support, active supporot must be required first,
3
- # then factory bot. When 6.4.7 is released, this can be removed. See Gemfile
4
- require "active_support"
5
1
  require "factory_bot"
6
2
  require "faker"
7
3
 
@@ -17,6 +13,5 @@ class Brut::FactoryBot
17
13
  to_create { |instance| instance.save }
18
14
  end
19
15
  FactoryBot.find_definitions
20
-
21
16
  end
22
17
  end
@@ -1,9 +1,10 @@
1
1
  # An "App" in Brut paralance is the collection of source code and configuration that is needed to operate
2
2
  # a website. This includes everything needed to serve HTTP requests, but also includes ancillary
3
- # tasks and any related files required for the app to exist and function. Your app will have an `App` class that subclasses this
4
- # class.
3
+ # tasks and any related files required for the app to exist and function.
4
+ # Your app will have an `App` class that subclasses this class.
5
5
  #
6
- # When your app is initialized, Brut will have been configured, but access to internal resources may not be available. It is here
6
+ # When your app is initialized, Brut will have been configured,
7
+ # but access to internal resources may not be available. It is here
7
8
  # that you can override configuration values or do any other setup before everything boots.
8
9
  class Brut::Framework::App
9
10
  include Brut::Framework::Errors
@@ -18,7 +19,8 @@ class Brut::Framework::App
18
19
  # actions where an app needs to exist inside some organizational context.
19
20
  def organization = id
20
21
 
21
- # Call this in your app's definition to define your app's routes. The contents of the block will be evaluated in the context of
22
+ # Call this in your app's definition to define your app's routes.
23
+ # The contents of the block will be evaluated in the context of
22
24
  # {Brut::SinatraHelpers::ClassMethods}, and the methods there are generally the ones you should be calling.
23
25
  #
24
26
  # You can call this multiple times and the routes will be concatenated together.
@@ -31,6 +33,69 @@ class Brut::Framework::App
31
33
  end
32
34
  end
33
35
 
36
+ # Call this to specify what happens when an unhandled exception occurs. You may call this mulitple times,
37
+ # however note that if an error is caught that matches more than one block's condition, the one that is called
38
+ # will be the first one declared.
39
+ #
40
+ # The only deviation from this rule is when you call this
41
+ # without any condition. Doing that establishes the behavior for a "catch all" handler, which is
42
+ # only called when no other configured block can handle the exception. You can declare
43
+ # this at any time. **Do note** the "catch all" handler is more of a best effort. Brut is currently
44
+ # based on Sinatra which provides no way to arbitrarily catch all exceptions. What Brut does here is to
45
+ # explicitly catch the range of http status codes from 400 to 999.
46
+ #
47
+ # Note that Brut will record the exception via OpenTelemetry so you should not do this in your handlers. It
48
+ # would be preferable to instead record an event if you want to have observability from your error handlers.
49
+ #
50
+ # @param [Class|Integer|Range<Integer>] condition if given this specifies the conditions under which the given
51
+ # block will handle the error. If omitted, this block will handle any error that doesn't have a more
52
+ # specific handler configured. Meaning of values:
53
+ # * A class - this is an exception class that, if caught, triggers the handler
54
+ # * An integer - this is an HTTP status code that, if returned, triggers the handler
55
+ # * A range of integers - this is a range of HTTP status codes that, if returned, triggers the handler
56
+ # @yield [Exception] the block is given two named parameters: `exception:` and `http_status_code:`. Your block
57
+ # can declare both, either, or none. Any that are declared will be given values. At least one
58
+ # will be non-`nil`, however are encouraged to code defensively inside this block.
59
+ # @yieldparam [Exception] exception: the exception that was raised. This will be `nil`
60
+ # if the error was caused by an HTTP status code.
61
+ # @yieldparam [Integer] http_status_code: the HTTP status code that was returned. If `exception:` is
62
+ # not `nil`, this value is highly likely to be 500.
63
+ # @yieldreturn The block should return a valid Rack response. For now.
64
+ def self.error(condition=:catch_all, &block)
65
+ @error_blocks ||= {}
66
+ if block.nil?
67
+ raise ArgumentError, "You must provide a block to error"
68
+ end
69
+ parameters = block.parameters.reject { |type,name|
70
+ type == :keyreq && [ :http_status_code, :exception ].include?(name)
71
+ }
72
+ if parameters.any?
73
+ messages = parameters.map { |type,name|
74
+ case type
75
+ when :keyreq
76
+ "required keyword parameter '#{name}:'"
77
+ when :key
78
+ "optional keyword parameter '#{name}:'"
79
+ when :rest
80
+ "rest parameter '#{name}'"
81
+ when :opt
82
+ "optional parameter '#{name}'"
83
+ when :req
84
+ "required parameter '#{name}'"
85
+ else
86
+ "unknown parameter '#{name}'"
87
+ end
88
+ }
89
+ raise ArgumentError, "Your error handler block may only accept exception: and http_status_code: as required keyword parameters. The following parameters were found:\n #{messages.join("\n ")}"
90
+ end
91
+ if @error_blocks[condition]
92
+ raise ArgumentError, "You have already configured error handling for condition '#{condition.to_s}'"
93
+ end
94
+ @error_blocks[condition] = block
95
+ end
96
+
97
+ def self.error_blocks = @error_blocks || {}
98
+
34
99
  # Add a Rack middleware to your app. Middlewares are configured in the order in which you call this method.
35
100
  #
36
101
  # @param [Class] middleware a class that implements [Rack Middleware](https://github.com/rack/rack/blob/main/SPEC.rdoc).
@@ -81,7 +146,7 @@ class Brut::Framework::App
81
146
  # code required *after* Brut has been set up and started. You can rely on the
82
147
  # database being available. Any attempts to override configuration values
83
148
  # may not succeed. This is called after the framework has booted, but before
84
- # your apps' routes are set up.
149
+ # your app's routes are set up.
85
150
  def boot!
86
151
  end
87
152
 
@@ -1,11 +1,13 @@
1
1
  require_relative "project_environment"
2
2
  require "pathname"
3
3
 
4
- # Holds configuration for the framework and your app. In general, you should not interact with this class, however it's source code
5
- # is a good reference for what is configured by default by Brut.
4
+ # Holds configuration for the framework and your app. In general, you
5
+ # should not interact with this class, however it's source code is a good
6
+ # reference for what is configured by default by Brut.
6
7
  class Brut::Framework::Config
7
8
 
8
- # Configures all defaults. In general, this attempts to be lazy in setting things up, so calling this should not attempt to make a
9
+ # Configures all defaults. In general, this attempts to be lazy in
10
+ # setting things up, so calling this should not attempt to make a
9
11
  # connection to your database.
10
12
  def configure!
11
13
  Brut.container do |c|
@@ -238,21 +240,6 @@ class Brut::Framework::Config
238
240
  config_dir / "asset_metadata.json"
239
241
  end
240
242
 
241
-
242
- c.store(
243
- "layout_locator",
244
- "Brut::FrontEnd::Templates::Locator",
245
- "Object to use to locate templates for layouts"
246
- ) do |layouts_src_dir,project_env,brut_internal_dir|
247
- paths = if project_env.development?
248
- [ layouts_src_dir, brut_internal_dir / "lib" / "brut" / "front_end" / "layouts" ]
249
- else
250
- layouts_src_dir
251
- end
252
- Brut::FrontEnd::Templates::Locator.new(paths: paths,
253
- extension: "html.erb")
254
- end
255
-
256
243
  c.store_required_path(
257
244
  "brut_internal_dir",
258
245
  "Location to where the Brut gem is installed."
@@ -260,44 +247,20 @@ class Brut::Framework::Config
260
247
  (Pathname(__FILE__).dirname / ".." / ".." / "..").expand_path
261
248
  end
262
249
 
263
- c.store(
264
- "page_locator",
265
- "Brut::FrontEnd::Templates::Locator",
266
- "Object to use to locate templates for pages"
267
- ) do |pages_src_dir,project_env,brut_internal_dir|
268
- paths = if project_env.development?
269
- [ pages_src_dir, brut_internal_dir / "lib" / "brut" / "front_end" / "pages" ]
270
- else
271
- pages_src_dir
272
- end
273
- Brut::FrontEnd::Templates::Locator.new(paths: paths,
274
- extension: "html.erb")
275
- end
276
-
277
- c.store(
278
- "component_locator",
279
- "Brut::FrontEnd::Templates::Locator",
280
- "Object to use to locate templates for components"
281
- ) do |components_src_dir, pages_src_dir|
282
- Brut::FrontEnd::Templates::Locator.new(paths: [ components_src_dir, pages_src_dir ],
283
- extension: "html.erb")
284
- end
285
-
286
250
  c.store(
287
251
  "svg_locator",
288
- "Brut::FrontEnd::Templates::Locator",
252
+ "Brut::FrontEnd::InlineSvgLocator",
289
253
  "Object to use to locate SVGs"
290
254
  ) do |svgs_src_dir|
291
- Brut::FrontEnd::Templates::Locator.new(paths: svgs_src_dir,
292
- extension: "svg")
255
+ Brut::FrontEnd::InlineSvgLocator.new(paths: svgs_src_dir)
293
256
  end
294
257
 
295
258
  c.store(
296
259
  "asset_path_resolver",
297
- "Brut::FrontEnd::Component::AssetPathResolver",
260
+ "Brut::FrontEnd::AssetPathResolver",
298
261
  "Object to use to resolve logical asset paths to actual asset paths"
299
262
  ) do |asset_metadata_file|
300
- Brut::FrontEnd::Component::AssetPathResolver.new(metadata_file: asset_metadata_file)
263
+ Brut::FrontEnd::AssetPathResolver.new(metadata_file: asset_metadata_file)
301
264
  end
302
265
 
303
266
  c.store(
@@ -21,7 +21,8 @@ end
21
21
  #
22
22
  # There is no namespacing/hierarchy.
23
23
  #
24
- # In general, you should not create instances of this class, but you may need to access it via {Brut.container} in order to obtain
24
+ # In general, you should not create instances of this class, but you may
25
+ # need to access it via {Brut.container} in order to obtain
25
26
  # configuration values or set your own.
26
27
  class Brut::Framework::Container
27
28
  def initialize
@@ -92,7 +93,7 @@ class Brut::Framework::Container
92
93
  end
93
94
 
94
95
  # Called by your app to override an existing value. The value must be overridable (see {#store}). Generally, you should call this
95
- # in the initializer of your {Brut::Framework::App} subclass. Calling this after the fact may not have the affect you want.
96
+ # in the initializer of your {Brut::Framework::App} subclass. Calling this after the fact may not have the effect you want.
96
97
  #
97
98
  # @param [String|Symbol] name name of the value to override. Will be coerced to a String. This name must have been previously
98
99
  # configured.
@@ -1,7 +1,7 @@
1
1
  module Brut
2
2
  module Framework
3
- # Include this in your class to access some helpful methods that throw commonly-needed
4
- # errors
3
+ # Namespace for Brut-specific error classes, and a holder of several error-related convienience
4
+ # methods. Include this module to gain access to those methods.
5
5
  module Errors
6
6
  autoload(:Bug,"brut/framework/errors/bug")
7
7
  autoload(:NotImplemented,"brut/framework/errors/not_implemented")
@@ -10,7 +10,12 @@ module Brut
10
10
  autoload(:MissingConfiguration,"brut/framework/errors/missing_configuration")
11
11
  autoload(:AbstractMethod,"brut/framework/errors/abstract_method")
12
12
  autoload(:NoClassForPath,"brut/framework/errors/no_class_for_path")
13
- # Raises {Brut::Framework::Errors::Bug}
13
+ # Raises {Brut::Framework::Errors::Bug}, used to indicate a codepath is a bug. "But, why write a
14
+ # bug in the first place?" you may be asking. Sometimes, a code path exists, but external factors
15
+ # mean that it should never be executed. Or, sometimes an API can be mis-used, but the current
16
+ # state of the system is such that it would never be misused.
17
+ #
18
+ # This method allows you to indicate such situations and provide a meaningful explanation.
14
19
  #
15
20
  # @param message Message to include in the error
16
21
  # @raise [Brut::Framework::Errors::Bug]
@@ -18,7 +23,10 @@ module Brut
18
23
  raise Brut::Framework::Errors::Bug,message
19
24
  end
20
25
 
21
- # Raises {Brut::Framework::Errors::AbstractMethod}
26
+ # Raises {Brut::Framework::Errors::AbstractMethod}, which is useful if you need to document
27
+ # a method that a subclass must implement, but for which there is no useful default
28
+ # implementation.
29
+ #
22
30
  # @raise [Brut::Framework::Errors::AbstractMethod]
23
31
  def abstract_method!
24
32
  raise Brut::Framework::Errors::AbstractMethod
@@ -116,8 +116,69 @@ class Brut::Framework::MCP
116
116
  @sinatra_app = Class.new(Sinatra::Base)
117
117
  @sinatra_app.include(Brut::SinatraHelpers)
118
118
 
119
+ safely_record_exception = ->(exception,http_status_code) {
120
+ begin
121
+ if exception.nil?
122
+ Brut.container.instrumentation.add_event("error triggered without exception", http_status_code:)
123
+ else
124
+ Brut.container.instrumentation.record_exception(exception, http_status_code:)
125
+ end
126
+ rescue => ex
127
+ begin
128
+ SemanticLogger[self.class].error(
129
+ "Error recording exception",
130
+ original_excerption: exception,
131
+ exception: ex)
132
+ rescue => ex2
133
+ $stderr.puts "While handling an error recording an exception, we get another error from SemanticLogger." + [
134
+ [ :original_exception, exception, ].join(": "),
135
+ [ :exception_from_recording_exception, ex, ].join(": "),
136
+ [ :exception_from_semantic_logger, ex2 ].join(": "),
137
+
138
+ ].join(", ")
139
+ end
140
+ end
141
+ }
142
+
143
+ @app.class.error_blocks.each do |condition,block|
144
+ puts "Setting up error handling for #{condition}"
145
+ if condition != :catch_all
146
+ @sinatra_app.error(condition) do
147
+ puts "Error block triggered"
148
+ exception = request.env["sinatra.error"]
149
+ safely_record_exception.(exception, response.status)
150
+ block_args = if exception
151
+ puts "Exception: #{exception.class}"
152
+ { exception: }
153
+ else
154
+ puts "HTTP: #{response.status}"
155
+ { http_status_code: response.status }
156
+ end
157
+ block.(**block_args)
158
+ end
159
+ end
160
+ end
161
+ if @app.class.error_blocks[:catch_all]
162
+ block = @app.class.error_blocks[:catch_all]
163
+ block_args = block.parameters.map { |(type,name)|
164
+ [ name, nil ]
165
+ }.to_h
166
+ @sinatra_app.error(400..999) do
167
+ exception = request.env["sinatra.error"]
168
+ safely_record_exception.(exception, response.status)
169
+ if block_args.key?(:exception)
170
+ block_args[:exception] = exception
171
+ end
172
+ if block_args.key?(:http_status_code)
173
+ block_args[:http_status_code] = response.status
174
+ end
175
+ block.(**block_args)
176
+ end
177
+ else
178
+ end
179
+
119
180
  message = if Brut.container.project_env.development?
120
- "Form submission did not include an authenticity token. All forms must include one. To add one, use the `form_tag` helper, or include <%= component(Brut::FrontEnd::Components::Inputs::CsrfToken) %> somewhere inside your <form> tag"
181
+ "Form submission did not include an authenticity token. All forms must include one. To add one, use the `form_tag` helper, or include Brut::FrontEnd::Components::Inputs::CsrfToken somewhere inside your <form> tag"
121
182
  else
122
183
  "Forbidden"
123
184
  end
@@ -186,7 +247,7 @@ class Brut::Framework::MCP
186
247
  end
187
248
 
188
249
  if name == :request_context
189
- args[name] = Thread.current.thread_variable_get(:request_context)
250
+ args[name] = Brut::FrontEnd::RequestContext.current
190
251
  elsif name == :session
191
252
  args[name] = Brut.container.session_class.new(rack_session: session)
192
253
  elsif name == :request