brut 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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