brut 0.0.1 → 0.0.2

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/Gemfile.lock +66 -1
  4. data/README.md +36 -0
  5. data/Rakefile +22 -0
  6. data/brut.gemspec +7 -0
  7. data/doc-src/architecture.md +102 -0
  8. data/doc-src/assets.md +98 -0
  9. data/doc-src/forms.md +214 -0
  10. data/doc-src/handlers.md +83 -0
  11. data/doc-src/javascript.md +265 -0
  12. data/doc-src/keyword-injection.md +183 -0
  13. data/doc-src/pages.md +210 -0
  14. data/doc-src/route-hooks.md +59 -0
  15. data/docs-todo.md +32 -0
  16. data/lib/brut/back_end/seed_data.rb +5 -1
  17. data/lib/brut/back_end/validator.rb +1 -1
  18. data/lib/brut/back_end/validators/form_validator.rb +31 -6
  19. data/lib/brut/cli/app.rb +100 -4
  20. data/lib/brut/cli/app_runner.rb +38 -5
  21. data/lib/brut/cli/apps/build_assets.rb +4 -6
  22. data/lib/brut/cli/apps/db.rb +2 -7
  23. data/lib/brut/cli/apps/scaffold.rb +413 -7
  24. data/lib/brut/cli/apps/test.rb +14 -1
  25. data/lib/brut/cli/command.rb +141 -6
  26. data/lib/brut/cli/error.rb +30 -3
  27. data/lib/brut/cli/execution_results.rb +44 -6
  28. data/lib/brut/cli/executor.rb +21 -1
  29. data/lib/brut/cli/options.rb +29 -2
  30. data/lib/brut/cli/output.rb +47 -0
  31. data/lib/brut/cli.rb +26 -0
  32. data/lib/brut/factory_bot.rb +2 -0
  33. data/lib/brut/framework/app.rb +38 -5
  34. data/lib/brut/framework/config.rb +97 -54
  35. data/lib/brut/framework/container.rb +97 -33
  36. data/lib/brut/framework/errors/abstract_method.rb +3 -7
  37. data/lib/brut/framework/errors/missing_parameter.rb +12 -0
  38. data/lib/brut/framework/errors/no_class_for_path.rb +25 -0
  39. data/lib/brut/framework/errors/not_found.rb +12 -2
  40. data/lib/brut/framework/errors/not_implemented.rb +14 -0
  41. data/lib/brut/framework/errors.rb +18 -0
  42. data/lib/brut/framework/fussy_type_enforcement.rb +17 -12
  43. data/lib/brut/framework/mcp.rb +106 -49
  44. data/lib/brut/framework/project_environment.rb +9 -0
  45. data/lib/brut/framework.rb +1 -0
  46. data/lib/brut/front_end/asset_metadata.rb +7 -1
  47. data/lib/brut/front_end/component.rb +129 -38
  48. data/lib/brut/front_end/components/constraint_violations.rb +57 -0
  49. data/lib/brut/front_end/components/form_tag.rb +23 -32
  50. data/lib/brut/front_end/components/i18n_translations.rb +34 -1
  51. data/lib/brut/front_end/components/input.rb +3 -0
  52. data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -0
  53. data/lib/brut/front_end/components/inputs/radio_button.rb +41 -0
  54. data/lib/brut/front_end/components/inputs/select.rb +26 -2
  55. data/lib/brut/front_end/components/inputs/text_field.rb +27 -5
  56. data/lib/brut/front_end/components/inputs/textarea.rb +24 -4
  57. data/lib/brut/front_end/components/locale_detection.rb +8 -1
  58. data/lib/brut/front_end/components/page_identifier.rb +2 -0
  59. data/lib/brut/front_end/components/time.rb +95 -0
  60. data/lib/brut/front_end/components/traceparent.rb +22 -0
  61. data/lib/brut/front_end/download.rb +11 -0
  62. data/lib/brut/front_end/flash.rb +32 -0
  63. data/lib/brut/front_end/form.rb +109 -106
  64. data/lib/brut/front_end/forms/constraint_violation.rb +16 -11
  65. data/lib/brut/front_end/forms/input.rb +30 -42
  66. data/lib/brut/front_end/forms/input_declarations.rb +90 -0
  67. data/lib/brut/front_end/forms/input_definition.rb +45 -30
  68. data/lib/brut/front_end/forms/radio_button_group_input.rb +46 -0
  69. data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +29 -0
  70. data/lib/brut/front_end/forms/select_input.rb +47 -0
  71. data/lib/brut/front_end/forms/select_input_definition.rb +27 -0
  72. data/lib/brut/front_end/forms/validity_state.rb +23 -9
  73. data/lib/brut/front_end/handler.rb +27 -8
  74. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +4 -2
  75. data/lib/brut/front_end/handlers/instrumentation_handler.rb +99 -0
  76. data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -4
  77. data/lib/brut/front_end/handlers/missing_handler.rb +9 -0
  78. data/lib/brut/front_end/handling_results.rb +14 -4
  79. data/lib/brut/front_end/http_method.rb +13 -1
  80. data/lib/brut/front_end/http_status.rb +10 -0
  81. data/lib/brut/front_end/layouts/_internal.html.erb +68 -0
  82. data/lib/brut/front_end/middleware.rb +5 -0
  83. data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +15 -0
  84. data/lib/brut/front_end/middlewares/favicon.rb +16 -0
  85. data/lib/brut/front_end/middlewares/open_telemetry_span.rb +12 -0
  86. data/lib/brut/front_end/middlewares/reload_app.rb +27 -9
  87. data/lib/brut/front_end/page.rb +50 -11
  88. data/lib/brut/front_end/pages/_missing_page.html.erb +17 -0
  89. data/lib/brut/front_end/pages/missing_page.rb +36 -0
  90. data/lib/brut/front_end/request_context.rb +117 -8
  91. data/lib/brut/front_end/route_hook.rb +43 -1
  92. data/lib/brut/front_end/route_hooks/age_flash.rb +3 -2
  93. data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +8 -0
  94. data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +18 -10
  95. data/lib/brut/front_end/route_hooks/locale_detection.rb +11 -5
  96. data/lib/brut/front_end/route_hooks/setup_request_context.rb +7 -4
  97. data/lib/brut/front_end/routing.rb +138 -31
  98. data/lib/brut/front_end/session.rb +86 -7
  99. data/lib/brut/front_end/template.rb +17 -2
  100. data/lib/brut/front_end/templates/block_filter.rb +4 -3
  101. data/lib/brut/front_end/templates/erb_parser.rb +1 -1
  102. data/lib/brut/front_end/templates/escapable_filter.rb +2 -0
  103. data/lib/brut/front_end/templates/html_safe_string.rb +30 -2
  104. data/lib/brut/front_end/templates/locator.rb +60 -0
  105. data/lib/brut/i18n/base_methods.rb +4 -0
  106. data/lib/brut/i18n.rb +1 -0
  107. data/lib/brut/instrumentation/logger_span_exporter.rb +75 -0
  108. data/lib/brut/instrumentation/open_telemetry.rb +107 -0
  109. data/lib/brut/instrumentation.rb +4 -6
  110. data/lib/brut/junk_drawer.rb +54 -4
  111. data/lib/brut/sinatra_helpers.rb +42 -38
  112. data/lib/brut/spec_support/clock_support.rb +6 -0
  113. data/lib/brut/spec_support/component_support.rb +53 -26
  114. data/lib/brut/spec_support/e2e_test_server.rb +82 -0
  115. data/lib/brut/spec_support/enhanced_node.rb +45 -0
  116. data/lib/brut/spec_support/general_support.rb +14 -3
  117. data/lib/brut/spec_support/handler_support.rb +2 -0
  118. data/lib/brut/spec_support/matcher.rb +6 -3
  119. data/lib/brut/spec_support/matchers/be_page_for.rb +3 -2
  120. data/lib/brut/spec_support/matchers/have_constraint_violation.rb +22 -9
  121. data/lib/brut/spec_support/matchers/have_html_attribute.rb +9 -2
  122. data/lib/brut/spec_support/matchers/have_i18n_string.rb +24 -0
  123. data/lib/brut/spec_support/matchers/have_link_to.rb +14 -0
  124. data/lib/brut/spec_support/matchers/have_redirected_to.rb +30 -0
  125. data/lib/brut/spec_support/matchers/have_rendered.rb +4 -11
  126. data/lib/brut/spec_support/matchers/have_returned_http_status.rb +16 -8
  127. data/lib/brut/spec_support/rspec_setup.rb +182 -0
  128. data/lib/brut/spec_support.rb +8 -3
  129. data/lib/brut/version.rb +2 -1
  130. data/lib/brut.rb +28 -5
  131. data/lib/sequel/extensions/brut_instrumentation.rb +5 -27
  132. data/lib/sequel/extensions/brut_migrations.rb +18 -8
  133. data/lib/sequel/plugins/created_at.rb +2 -0
  134. data/lib/sequel/plugins/external_id.rb +39 -1
  135. data/lib/sequel/plugins/find_bang.rb +4 -1
  136. metadata +140 -13
  137. data/lib/brut/back_end/action.rb +0 -3
  138. data/lib/brut/back_end/result.rb +0 -46
  139. data/lib/brut/front_end/components/timestamp.rb +0 -33
  140. data/lib/brut/instrumentation/basic.rb +0 -66
  141. data/lib/brut/instrumentation/event.rb +0 -19
  142. data/lib/brut/instrumentation/http_event.rb +0 -5
  143. data/lib/brut/instrumentation/subscriber.rb +0 -41
@@ -1,16 +1,27 @@
1
- # An "App" in Brut paralance is the collection of source code and configure that is needed to operate
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.
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.
5
+ #
6
+ # When your app is initialized, Brut will have been configured, but access to internal resources may not be available. It is here
7
+ # that you can override configuration values or do any other setup before everything boots.
4
8
  class Brut::Framework::App
9
+ include Brut::Framework::Errors
5
10
 
6
- # An identifier for this app that can be used as a hostname
7
- def id = raise "Subclass must implement"
11
+ # An identifier for this app that can be used as a hostname. Your app must provide this.
12
+ def id
13
+ abstract_method!
14
+ end
8
15
 
9
16
  # An identifier for the app's 'organization' that can be used as a hostname.
10
17
  # This isn't relevant in all contexts, but is useful for deploys or other
11
18
  # actions where an app needs to exist inside some organizational context.
12
19
  def organization = id
13
20
 
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
+ # {Brut::SinatraHelpers::ClassMethods}, and the methods there are generally the ones you should be calling.
23
+ #
24
+ # You can call this multiple times and the routes will be concatenated together.
14
25
  def self.routes(&block)
15
26
  @routes_blocks ||= []
16
27
  if block.nil?
@@ -19,6 +30,14 @@ class Brut::Framework::App
19
30
  @routes_blocks << block
20
31
  end
21
32
  end
33
+
34
+ # Add a Rack middleware to your app. Middlewares are configured in the order in which you call this method.
35
+ #
36
+ # @param [Class] middleware a class that implements [Rack Middleware](https://github.com/rack/rack/blob/main/SPEC.rdoc).
37
+ # @param [Array] args arguments to be given to the `middleware` class' initializer.
38
+ # @param [block] block a block that is given to the initializer of the `middleware` class.
39
+ #
40
+ # @return [Array] if no parameters are given, returns all the currently-configured middleware.
22
41
  def self.middleware(middleware=nil,*args,&block)
23
42
  @middlewares ||= []
24
43
  if middleware.nil? && args.empty? && block.nil?
@@ -27,6 +46,13 @@ class Brut::Framework::App
27
46
  @middlewares << [ middleware, args, block ]
28
47
  end
29
48
  end
49
+
50
+ # Configure a {Brut::FrontEnd::RouteHook} to be called before each request.
51
+ #
52
+ # @param [String] klass_name The name of the class that extends {Brut::FrontEnd::RouteHook} and implements #before. This uses the
53
+ # name (not the class itself) to avoid loading issues.
54
+ #
55
+ # @return [Array] If no parameters given, returns all configured before hooks.
30
56
  def self.before(klass_name=nil)
31
57
  @before ||= []
32
58
  if klass_name.nil?
@@ -35,6 +61,13 @@ class Brut::Framework::App
35
61
  @before << klass_name
36
62
  end
37
63
  end
64
+
65
+ # Configure a {Brut::FrontEnd::RouteHook} to be called after each request.
66
+ #
67
+ # @param [String] klass_name The name of the class that extends {Brut::FrontEnd::RouteHook} and implements #after. This uses the
68
+ # name (not the class itself) to avoid loading issues.
69
+ #
70
+ # @return [Array] If no parameters given, returns all configured after hooks.
38
71
  def self.after(klass_name=nil)
39
72
  @after ||= []
40
73
  if klass_name.nil?
@@ -48,7 +81,7 @@ class Brut::Framework::App
48
81
  # code required *after* Brut has been set up and started. You can rely on the
49
82
  # database being available. Any attempts to override configuration values
50
83
  # may not succeed. This is called after the framework has booted, but before
51
- # your apps routes are set up.
84
+ # your apps' routes are set up.
52
85
  def boot!
53
86
  end
54
87
 
@@ -1,32 +1,12 @@
1
1
  require_relative "project_environment"
2
2
  require "pathname"
3
3
 
4
- # Exists to hold configuration for the Brut framework.
5
- # This is a wrapper around a series of calls to Brut.container.store
6
- # but is a class and thus invokable, so that the configuration can
7
- # be controlled.
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.
8
6
  class Brut::Framework::Config
9
7
 
10
- class DockerPathComponent
11
- PATH_REGEXP = /\A[a-z0-9]+(-|_)?[a-z0-9]+\z/
12
- def initialize(string)
13
- if string.to_s.match?(PATH_REGEXP)
14
- @string = string
15
- else
16
- raise ArgumentError.new("Value must be only lower case letters, digits, and may have at most one underscore: '#{string}'")
17
- end
18
- end
19
-
20
- def to_str = @string
21
- def to_s = self.to_str
22
- end
23
-
24
- class AppId < DockerPathComponent
25
- end
26
-
27
- class AppOrganizationName < DockerPathComponent
28
- end
29
-
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
+ # connection to your database.
30
10
  def configure!
31
11
  Brut.container do |c|
32
12
  # Brut Stuff that should not be changed
@@ -110,6 +90,13 @@ class Brut::Framework::Config
110
90
  project_root / "specs"
111
91
  end
112
92
 
93
+ c.store_ensured_path(
94
+ "e2e_specs_dir",
95
+ "Path to the root of all end-to-end tests"
96
+ ) do |app_specs_dir|
97
+ app_specs_dir / "e2e"
98
+ end
99
+
113
100
  c.store_ensured_path(
114
101
  "js_specs_dir",
115
102
  "Path to root of where all JS-based specs/tests are",
@@ -117,49 +104,56 @@ class Brut::Framework::Config
117
104
  app_specs_dir / "front_end" / "js"
118
105
  end
119
106
 
120
- c.store_required_path(
107
+ c.store_ensured_path(
121
108
  "front_end_src_dir",
122
109
  "Path to the root of the front end layer for the app"
123
110
  ) do |app_src_dir|
124
111
  app_src_dir / "front_end"
125
112
  end
126
113
 
127
- c.store_required_path(
114
+ c.store_ensured_path(
128
115
  "components_src_dir",
129
116
  "Path to where components classes and templates are stored"
130
117
  ) do |front_end_src_dir|
131
118
  front_end_src_dir / "components"
132
119
  end
133
120
 
134
- c.store_required_path(
121
+ c.store_ensured_path(
135
122
  "components_specs_dir",
136
123
  "Path to where tests of components classes are stored",
137
124
  ) do |app_specs_dir|
138
125
  app_specs_dir / "front_end" / "components"
139
126
  end
140
127
 
141
- c.store_required_path(
128
+ c.store_ensured_path(
142
129
  "forms_src_dir",
143
130
  "Path to where form classes are stored"
144
131
  ) do |front_end_src_dir|
145
132
  front_end_src_dir / "forms"
146
133
  end
147
134
 
148
- c.store_required_path(
135
+ c.store_ensured_path(
149
136
  "handlers_src_dir",
150
137
  "Path to where handlers are stored"
151
138
  ) do |front_end_src_dir|
152
139
  front_end_src_dir / "handlers"
153
140
  end
154
141
 
155
- c.store_required_path(
142
+ c.store_ensured_path(
143
+ "handlers_specs_dir",
144
+ "Path to where tests of handler classes are stored",
145
+ ) do |app_specs_dir|
146
+ app_specs_dir / "front_end" / "handlers"
147
+ end
148
+
149
+ c.store_ensured_path(
156
150
  "svgs_src_dir",
157
151
  "Path to where svgs are stored"
158
152
  ) do |front_end_src_dir|
159
153
  front_end_src_dir / "svgs"
160
154
  end
161
155
 
162
- c.store_required_path(
156
+ c.store_ensured_path(
163
157
  "images_src_dir",
164
158
  "Path to where images are stored"
165
159
  ) do |front_end_src_dir|
@@ -194,7 +188,7 @@ class Brut::Framework::Config
194
188
  front_end_src_dir / "js"
195
189
  end
196
190
 
197
- c.store_required_path(
191
+ c.store_ensured_path(
198
192
  "back_end_src_dir",
199
193
  "Path to the root of the back end layer for the app"
200
194
  ) do |app_src_dir|
@@ -229,6 +223,13 @@ class Brut::Framework::Config
229
223
  project_root / "app" / "config"
230
224
  end
231
225
 
226
+ c.store_ensured_path(
227
+ "i18n_locales_dir",
228
+ "Path to where I18N locale files are stored"
229
+ ) do |config_dir|
230
+ config_dir / "i18n"
231
+ end
232
+
232
233
  c.store(
233
234
  "asset_metadata_file",
234
235
  Pathname,
@@ -240,38 +241,55 @@ class Brut::Framework::Config
240
241
 
241
242
  c.store(
242
243
  "layout_locator",
243
- "Brut::FrontEnd::Component::TemplateLocator",
244
+ "Brut::FrontEnd::Templates::Locator",
244
245
  "Object to use to locate templates for layouts"
245
- ) do |layouts_src_dir|
246
- Brut::FrontEnd::Component::TemplateLocator.new(paths: layouts_src_dir,
247
- extension: "html.erb")
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
+ c.store_required_path(
257
+ "brut_internal_dir",
258
+ "Location to where the Brut gem is installed."
259
+ ) do
260
+ (Pathname(__FILE__).dirname / ".." / ".." / "..").expand_path
248
261
  end
249
262
 
250
263
  c.store(
251
264
  "page_locator",
252
- "Brut::FrontEnd::Component::TemplateLocator",
265
+ "Brut::FrontEnd::Templates::Locator",
253
266
  "Object to use to locate templates for pages"
254
- ) do |pages_src_dir|
255
- Brut::FrontEnd::Component::TemplateLocator.new(paths: pages_src_dir,
256
- extension: "html.erb")
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")
257
275
  end
258
276
 
259
277
  c.store(
260
278
  "component_locator",
261
- "Brut::FrontEnd::Component::TemplateLocator",
279
+ "Brut::FrontEnd::Templates::Locator",
262
280
  "Object to use to locate templates for components"
263
281
  ) do |components_src_dir, pages_src_dir|
264
- Brut::FrontEnd::Component::TemplateLocator.new(paths: [ components_src_dir, pages_src_dir ],
265
- extension: "html.erb")
282
+ Brut::FrontEnd::Templates::Locator.new(paths: [ components_src_dir, pages_src_dir ],
283
+ extension: "html.erb")
266
284
  end
267
285
 
268
286
  c.store(
269
287
  "svg_locator",
270
- "Brut::FrontEnd::Component::TemplateLocator",
288
+ "Brut::FrontEnd::Templates::Locator",
271
289
  "Object to use to locate SVGs"
272
290
  ) do |svgs_src_dir|
273
- Brut::FrontEnd::Component::TemplateLocator.new(paths: svgs_src_dir,
274
- extension: "svg")
291
+ Brut::FrontEnd::Templates::Locator.new(paths: svgs_src_dir,
292
+ extension: "svg")
275
293
  end
276
294
 
277
295
  c.store(
@@ -291,15 +309,10 @@ class Brut::Framework::Config
291
309
 
292
310
  c.store(
293
311
  "instrumentation",
294
- Brut::Instrumentation::Basic,
312
+ Brut::Instrumentation::OpenTelemetry,
295
313
  "Interface for recording instrumentable events and subscribing to them",
296
- ) do |project_env|
297
- if project_env.production?
298
- Brut::Instrumentation::Basic.new
299
- else
300
- Brut::Instrumentation::Basic::TypeChecking.new
301
- end
302
- end
314
+ Brut::Instrumentation::OpenTelemetry.new
315
+ )
303
316
 
304
317
  # App can override
305
318
 
@@ -410,6 +423,36 @@ class Brut::Framework::Config
410
423
  allow_app_override: true,
411
424
  allow_nil: true,
412
425
  )
426
+
427
+ c.store(
428
+ "local_hostname",
429
+ String,
430
+ "If present, this is an additional host on which your app responds locally. Useful if you have local domain names set up for dev",
431
+ nil,
432
+ allow_app_override: true,
433
+ allow_nil: true
434
+ )
435
+
436
+
437
+ c.store(
438
+ "permitted_hosts",
439
+ Array,
440
+ "An array of hostnames or IPAddr objects representing which hosts this app will respond to",
441
+ ) do |local_hostname,project_env|
442
+ if project_env.production?
443
+ []
444
+ else
445
+ [
446
+ local_hostname,
447
+ "localhost",
448
+ ".localhost",
449
+ ".test",
450
+ IPAddr.new("0.0.0.0/0"),
451
+ IPAddr.new("::/0"),
452
+ ].compact
453
+ end
454
+ end
455
+
413
456
  end
414
457
  end
415
458
  end
@@ -1,6 +1,7 @@
1
1
  require "fileutils"
2
2
 
3
3
  module Brut
4
+ # Provides access to the singelton container that contains all of Brut's current configuration.
4
5
  def self.container(&block)
5
6
  @container ||= Brut::Framework::Container.new
6
7
  if !block.nil?
@@ -19,31 +20,49 @@ end
19
20
  # and can depend on other values in this container.
20
21
  #
21
22
  # There is no namespacing/hierarchy.
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
25
+ # configuration values or set your own.
22
26
  class Brut::Framework::Container
23
27
  def initialize
24
28
  @container = {}
25
29
  end
26
30
 
27
- # Store a named value for later.
31
+ # Store a named value for later. You can use this to store static values, or dynamic values that require the values of other stored
32
+ # values.
33
+ #
34
+ # @param [String] name The name of the value. This should be a string that is a valid Ruby identifier. If `type` is a boolean, this
35
+ # parameter must end in a question mark.
36
+ # @param [String] type Description of the type that the value should conform to. if this value is "boolean" or :boolean, then
37
+ # the value will be coerced into `true` or `false`. Otherwise, this serves as only documentation for now.
38
+ # @param [String] description Documentation as to what this value is for.
39
+ # @param [Object] value if given, this is the value to use. If the value you want is dynamically determined, or you want to create
40
+ # it lazily, pass a block.
41
+ # @param [true|false] allow_app_override if true, the app may override this value. Default is false, which means the app cannot.
42
+ # This is mostly useful for Brut internals to ensure the app doesnt' wreak havoc
43
+ # on things it should not mess with.
44
+ # @param [true|false] allow_nil if true, this value may be nil and if `allow_app_override` is true, the app can override the value
45
+ # to be `nil`. The default is false, which means `nil` is not allowed. Generally, you don't want
46
+ # `nil`. `nil` is no good for nobody.
47
+ # @yield [*any] Yields any existing configuration values to the block as *positional parameters*.
48
+ # The names of the parameters must match the name of another configuration value.
49
+ # @yieldreturn [Object] the value to use for this configuration option. This is memoized, so the block will not be called again.
28
50
  #
29
- # name:: The name of the value. This should be a string that is a valid Ruby identifier.
30
- # type:: Description of the type that the value should conform to. if this value is "boolean" or :boolean, then
31
- # the value will be coerced into `true` or `false`. Otherwise, this serves as documentation for now.
32
- # description:: Documentation as to what this value is for.
33
- # value:: if given, this is the value to use.
34
- # block:: If value is omitted, block will be evaluated the first time the value is
35
- # fetched and is expected to return the value to use for all subsequent
36
- # requests.
51
+ # @example Storing a static value
52
+ # container.store("num_retries",Integer,"Number of times to retry",10)
37
53
  #
38
- # The block can receive parameters and those parameters names must
39
- # match other values stored in this container. Those values are passed in.
54
+ # @example Storing a dynamic value based on another one
55
+ # container.store("num_retries",Integer,"Number of times to retry",10)
56
+ # container.store("max_retry_ms",Integer,"Number of times to retry") { |num_retries|
57
+ # num_retries * 100
58
+ # }
40
59
  #
41
- # For example, if you have the value `project_root`, you can then set another
42
- # value called `tmp_dir` that uses `project_root` like so:
60
+ # @see #store_required_path
61
+ # @see #store_ensured_path
43
62
  #
44
- # ```
45
- # container.store("tmp_dir") { |project_root| project_root / "tmp" }
46
- # ```
63
+ # @raise [ArgumentError] if the name has already been specified, if this is a boolean and the name doesn't
64
+ # end in a question mark, or if this a `Pathname` and the name does not end in `_dir`
65
+ # or `_file`.
47
66
  def store(name,type,description,value=:use_block,allow_app_override: false,allow_nil: false,&block)
48
67
  # TODO: Check that value / block is used properly
49
68
  name = name.to_s
@@ -72,6 +91,16 @@ class Brut::Framework::Container
72
91
  self
73
92
  end
74
93
 
94
+ # 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
+ #
97
+ # @param [String|Symbol] name name of the value to override. Will be coerced to a String. This name must have been previously
98
+ # configured.
99
+ # @param [Object] value if given, this is the value to use. If omitted, the block is called
100
+ #
101
+ # @yield [*any] Yields any existing configuration values to the block as *positional parameters*.
102
+ # The names of the parameters must match the name of another configuration value.
103
+ # @yieldreturn [Object] the value to use for this configuration option. This is memoized, so the block will not be called again.
75
104
  def override(name,value=:use_block,&block)
76
105
  name = name.to_s
77
106
  if !@container[name]
@@ -88,46 +117,76 @@ class Brut::Framework::Container
88
117
  end
89
118
 
90
119
  # Store a value that represents a path that must exist. The value will
91
- # be assumed to be of type Pathname
120
+ # be assumed to be a `Pathname` and the `name` must end in `_dir` or `_file`.
121
+ # Note that the value's existence is not checked until it is requested. When it is,
122
+ # an exception will be raised if it does not exist.
123
+ #
124
+ # @param [Symbol|String] name of this value. Must end in `_dir` or `_file`.
125
+ # @param description [String] description documentation of what this value is for
126
+ # @param [Object] value if given, this is the value to use. If omitted, the block is called
127
+ #
128
+ # @yield [*any] Yields any existing configuration values to the block as *positional parameters*.
129
+ # The names of the parameters must match the name of another configuration value.
130
+ # @yieldreturn [Object] the value to use for this configuration option. This is memoized, so the block will not be called again.
92
131
  def store_required_path(name,description,value=:use_block,&block)
93
132
  self.store(name,Pathname,description,value,&block)
94
133
  @container[name][:required_path] = true
95
134
  self
96
135
  end
97
136
 
98
- # Store a value that represents a path that can be created if
99
- # it does not exist. The path won't be created until the value is
100
- # accessed the first time. The value will
101
- # be assumed to be of type Pathname
137
+ # Store a value that represents a path that will be created if it doesn't exist.
138
+ # The value will be assumed to be a `Pathname` and the `name` must end in `_dir` or `_file`.
139
+ #
140
+ # This is preferred over {#store_required_path} so that you don't have to have a bunch of `.keep` files hanging around
141
+ # just for your version control system. The path will be created as a directory whenever it is first accessed.
142
+ #
143
+ # @param [Symbol|String] name of this value. Must end in `_dir` or `_file` (though ending it in `_file` doesn't make much sense).
144
+ # @param description [String] description documentation of what this value is for
145
+ # @param [Object] value if given, this is the value to use. If omitted, the block is called
146
+ #
147
+ # @yield [*any] Yields any existing configuration values to the block as *positional parameters*.
148
+ # The names of the parameters must match the name of another configuration value.
149
+ # @yieldreturn [Object] the value to use for this configuration option. This is memoized, so the block will not be called again.
102
150
  def store_ensured_path(name,description,value=:use_block,&block)
103
151
  self.store(name,Pathname,description,value,&block)
104
152
  @container[name][:ensured_path] = true
105
153
  self
106
154
  end
107
155
 
108
- # Fetch a value by using its name as a method to instances of this class.
156
+ # Provides method-like access to configured values. Only configured values will respond and only
157
+ # if the accessor method is called without parameters and without a block. See {#fetch}.
158
+ #
159
+ # @example
160
+ # Brut.container.store("num_retries",Integer,"Number of times to retry",10)
161
+ # Brut.container.num_retries # => 10
109
162
  def method_missing(sym,*args,&block)
110
163
  if args.length == 0 && block.nil? && self.respond_to_missing?(sym)
111
- fetch_value(sym.to_s)
164
+ fetch(sym.to_s)
112
165
  else
113
166
  super.method_missing(sym,*args,&block)
114
167
  end
115
168
  end
116
169
 
117
- # Implemented to go along with method_missing
170
+ # Required for good decorum when overriding {#method_missing}.
171
+ #
172
+ # @param [String|Symbol] name the name of a previously-configured value.
173
+ #
174
+ # @return [true|false] true if `name` has been configured
118
175
  def respond_to_missing?(name,include_private=false)
119
176
  @container.key?(name.to_s)
120
177
  end
121
178
 
122
-
123
- # Fetch a value given a name.
179
+ # Fetch the value given a name. For lazily-defined values, this will call all necessary blocks needed to determine the value. Thus,
180
+ # any number of other blocks could be called, depending on what values are needed.
181
+ #
182
+ # @param name [Symbol|String] the name of the value to fetch.
183
+ #
184
+ # @return [Object] the configured value, if it has been configured. Note that if the value was defined with `allow_nil: true`
185
+ # passed to {#store}, `nil` could be returned.
186
+ # @raise [KeyError] if `name` has not been previously stored
187
+ # @raise [Brut::Framework::Errors::NotFound] if a path stored with {#store_required_path} does not exist
124
188
  def fetch(name)
125
- fetch_value(name.to_s)
126
- end
127
-
128
- private
129
-
130
- def fetch_value(name)
189
+ name = name.to_s
131
190
  # TODO: Provide a cleanr impl and better error checking if things go wrong
132
191
  x = @container.fetch(name)
133
192
 
@@ -158,11 +217,16 @@ private
158
217
  handle_path_values(name,x)
159
218
  x[:value]
160
219
  end
220
+ private
161
221
 
162
222
  def handle_path_values(name,contained_value)
163
223
  value = contained_value[:value]
164
224
  if contained_value[:required_path] && !Dir.exist?(value)
165
- raise "For value '#{name}', the directory is represents must exist, but does not: '#{value}'"
225
+ raise Brut::Framework::Errors::NotFound.new(
226
+ resource_name: value,
227
+ id: name,
228
+ contetx: "For value '#{name}', the directory is represents must exist, but does not: '#{value}'"
229
+ )
166
230
  end
167
231
  if contained_value[:ensured_path]
168
232
  FileUtils.mkdir_p value
@@ -1,9 +1,5 @@
1
+ # Raised when a method must be defined by a subclass. This is useful for making it clear
2
+ # which methods a subclass is expected to override and for which no default behavior
3
+ # makes sense.
1
4
  class Brut::Framework::Errors::AbstractMethod < Brut::Framework::Error
2
- def initialize(message=nil)
3
- if message.nil?
4
- super
5
- else
6
- super("Subclass must implement: #{message}")
7
- end
8
- end
9
5
  end
@@ -0,0 +1,12 @@
1
+ # Raised when an expected parameter in e.g. a path or a method invocation is
2
+ # not available. This allows classes to give a better error message than
3
+ # the one provided by standard library exceptions like `KeyError`
4
+ class Brut::Framework::Errors::MissingParameter < Brut::Framework::Error
5
+ # Create the exception
6
+ # @param [String|Symbol] missing_param the name of the missing parameter.
7
+ # @param [Array<String|Symbol>] params_received the parameters that were received in the context that generated this error
8
+ # @param [String] context Any additional context to understand the error
9
+ def initialize(missing_param, params_received:, context:)
10
+ super("Parameter '#{missing_param}' was not available. Received params: #{params_received.empty? ? 'no params' : "'" + params_received.join(', ') + "'"}. #{context}")
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # Raised when a path has been declared, but the class to handle it cannot be found in the app.
2
+ class Brut::Framework::Errors::NoClassForPath < Brut::Framework::Error
3
+ # Array of names that, if joined with `::` would name the class that could not be found
4
+ # @return [Array<String>] array of parts. For a class named `Auth::LoginPage`, would return `["Auth","LoginPage"]`
5
+ attr_reader :class_name_path
6
+ # The path template that the class that couldn't be found was intended to handle
7
+ # @return [String] a path template as given inside {Brut::Framework::App.routes}
8
+ attr_reader :path_template
9
+
10
+ # Create the exception
11
+ # @param [Array<String>] class_name_path array of names that, if joined with `::` would name the class that could not be found
12
+ # @param [String] path_template The path template that the class that couldn't be found was intended to handle
13
+ # @param [NameError] name_error The `NameError` that was caught
14
+ def initialize(class_name_path:, path_template:, name_error:)
15
+ @class_name_path = class_name_path
16
+ @path_template = path_template
17
+ module_message = if name_error.receiver == Module
18
+ "Could not find"
19
+ else
20
+ "Module '#{name_error.receiver}' did not have"
21
+ end
22
+ message = "Cannot find page class for route '#{path_template}', which should be #{class_name_path.join("::")}. #{module_message} the class or module '#{name_error.name}'"
23
+ super(message)
24
+ end
25
+ end
@@ -1,10 +1,20 @@
1
1
  # Indicates that a resource or database row does not exist.
2
2
  class Brut::Framework::Errors::NotFound < Brut::Framework::Error
3
- def initialize(resource_name:,id:,context:nil)
3
+ # @param [String] resource_name Name of the type of resource
4
+ # @param [String|Int] id Identifier of the resource. If present, search_terms is ignored.
5
+ # @param [Object] search_terms If provided, these are the search terms used. Will be converted to a string via `inspect`. Ignored if
6
+ # id is present.
7
+ # @param [String] context Any additional context about what went wrong
8
+ def initialize(resource_name:,id: nil, search_terms: nil,context:nil)
4
9
  if !context.nil?
5
10
  context = ": #{context}"
6
11
  end
7
- super("Could not find a #{resource_name} using ID #{id}#{context}")
12
+ fragment = if id.nil?
13
+ "Search '#{search_terms.inspect}'"
14
+ else
15
+ "ID '#{id}'"
16
+ end
17
+ super("Could not find a #{resource_name} using #{fragment}#{context}")
8
18
  end
9
19
  end
10
20
 
@@ -0,0 +1,14 @@
1
+ # Thrown when use of a feature of Brut is detected, but that
2
+ # feature is not yet implemented. This is advisory only and
3
+ # not a promise to ever implement that feature. It's mostly used
4
+ # when two APIs are very similar and one might expect both to
5
+ # support the same features, but for technical reasons one of the APIs does not.
6
+ class Brut::Framework::Errors::NotImplemented < Brut::Framework::Error
7
+ def initialize(message=nil)
8
+ if message.nil?
9
+ super
10
+ else
11
+ super("NOT IMPLEMENTED: #{message}")
12
+ end
13
+ end
14
+ end