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.
- checksums.yaml +4 -4
- data/Gemfile.lock +4 -6
- data/brut.gemspec +1 -3
- data/lib/brut/back_end/seed_data.rb +19 -2
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
- data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
- data/lib/brut/back_end/sidekiq.rb +2 -1
- data/lib/brut/back_end/validator.rb +5 -1
- data/lib/brut/back_end.rb +9 -0
- data/lib/brut/cli/apps/scaffold.rb +16 -24
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/factory_bot.rb +0 -5
- data/lib/brut/framework/app.rb +70 -5
- data/lib/brut/framework/config.rb +9 -46
- data/lib/brut/framework/container.rb +3 -2
- data/lib/brut/framework/errors.rb +12 -4
- data/lib/brut/framework/mcp.rb +63 -2
- data/lib/brut/framework/project_environment.rb +6 -2
- data/lib/brut/framework.rb +1 -1
- data/lib/brut/front_end/asset_path_resolver.rb +15 -0
- data/lib/brut/front_end/component.rb +101 -246
- data/lib/brut/front_end/components/constraint_violations.rb +10 -10
- data/lib/brut/front_end/components/form_tag.rb +17 -29
- data/lib/brut/front_end/components/i18n_translations.rb +12 -13
- data/lib/brut/front_end/components/input.rb +0 -1
- data/lib/brut/front_end/components/inputs/csrf_token.rb +3 -3
- data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
- data/lib/brut/front_end/components/inputs/select.rb +13 -20
- data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
- data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
- data/lib/brut/front_end/components/locale_detection.rb +2 -2
- data/lib/brut/front_end/components/page_identifier.rb +3 -5
- data/lib/brut/front_end/components/{time.rb → time_tag.rb} +14 -11
- data/lib/brut/front_end/components/traceparent.rb +5 -6
- data/lib/brut/front_end/http_method.rb +4 -0
- data/lib/brut/front_end/inline_svg_locator.rb +21 -0
- data/lib/brut/front_end/layout.rb +19 -0
- data/lib/brut/front_end/page.rb +52 -40
- data/lib/brut/front_end/request_context.rb +13 -0
- data/lib/brut/front_end/routing.rb +8 -3
- data/lib/brut/front_end.rb +32 -0
- data/lib/brut/i18n/base_methods.rb +51 -11
- data/lib/brut/i18n/for_back_end.rb +8 -0
- data/lib/brut/i18n/for_cli.rb +5 -1
- data/lib/brut/i18n/for_html.rb +9 -1
- data/lib/brut/i18n/http_accept_language.rb +47 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/open_telemetry.rb +25 -0
- data/lib/brut/instrumentation.rb +3 -5
- data/lib/brut/sinatra_helpers.rb +13 -7
- data/lib/brut/spec_support/component_support.rb +27 -13
- data/lib/brut/spec_support/e2e_support.rb +4 -0
- data/lib/brut/spec_support/general_support.rb +3 -0
- data/lib/brut/spec_support/handler_support.rb +6 -1
- data/lib/brut/spec_support/matcher.rb +1 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +3 -1
- data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
- data/lib/brut/spec_support/rspec_setup.rb +1 -0
- data/lib/brut/spec_support.rb +5 -4
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +7 -50
- metadata +14 -49
- data/doc-src/architecture.md +0 -102
- data/doc-src/assets.md +0 -98
- data/doc-src/forms.md +0 -214
- data/doc-src/handlers.md +0 -83
- data/doc-src/javascript.md +0 -265
- data/doc-src/keyword-injection.md +0 -183
- data/doc-src/pages.md +0 -210
- data/doc-src/route-hooks.md +0 -59
- data/lib/brut/front_end/template.rb +0 -47
- data/lib/brut/front_end/templates/block_filter.rb +0 -61
- data/lib/brut/front_end/templates/erb_engine.rb +0 -26
- data/lib/brut/front_end/templates/erb_parser.rb +0 -84
- data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
- data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13ef62dae2d2a40f20fce1aa2f5df7809fc41ae99b75b2a9aefd25423ef51fb3
|
4
|
+
data.tar.gz: 65b9e0b2b77a441210d0047da5c6c8f77d01d97a954d64626683d11fa7f2f1d1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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.
|
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,7 @@
|
|
1
|
-
|
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
|
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
|
266
|
-
out.puts "Component
|
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
|
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
|
397
|
-
out.puts "Page
|
398
|
-
out.puts "
|
399
|
-
out.puts "Added
|
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
|
10
|
-
#
|
11
|
-
#
|
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
|
data/lib/brut/factory_bot.rb
CHANGED
@@ -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
|
data/lib/brut/framework/app.rb
CHANGED
@@ -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.
|
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,
|
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.
|
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
|
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
|
5
|
-
#
|
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
|
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::
|
252
|
+
"Brut::FrontEnd::InlineSvgLocator",
|
289
253
|
"Object to use to locate SVGs"
|
290
254
|
) do |svgs_src_dir|
|
291
|
-
Brut::FrontEnd::
|
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::
|
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::
|
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
|
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
|
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
|
-
#
|
4
|
-
#
|
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
|
data/lib/brut/framework/mcp.rb
CHANGED
@@ -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
|
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] =
|
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
|