brut 0.0.1

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 (131) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/CODE_OF_CONDUCT.txt +99 -0
  4. data/Dockerfile.dx +32 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +133 -0
  7. data/LICENSE.txt +370 -0
  8. data/README.md +21 -0
  9. data/Rakefile +1 -0
  10. data/bin/bin_kit.rb +39 -0
  11. data/bin/rake +27 -0
  12. data/bin/setup +145 -0
  13. data/brut.gemspec +60 -0
  14. data/docker-compose.dx.yml +16 -0
  15. data/dx/build +26 -0
  16. data/dx/docker-compose.env +22 -0
  17. data/dx/dx.sh.lib +24 -0
  18. data/dx/exec +58 -0
  19. data/dx/prune +19 -0
  20. data/dx/setupkit.sh.lib +144 -0
  21. data/dx/show-help-in-app-container-then-wait.sh +38 -0
  22. data/dx/start +30 -0
  23. data/dx/stop +23 -0
  24. data/lib/brut/back_end/action.rb +3 -0
  25. data/lib/brut/back_end/result.rb +46 -0
  26. data/lib/brut/back_end/seed_data.rb +24 -0
  27. data/lib/brut/back_end/validator.rb +3 -0
  28. data/lib/brut/back_end/validators/form_validator.rb +37 -0
  29. data/lib/brut/cli/app.rb +130 -0
  30. data/lib/brut/cli/app_runner.rb +219 -0
  31. data/lib/brut/cli/apps/build_assets.rb +123 -0
  32. data/lib/brut/cli/apps/db.rb +279 -0
  33. data/lib/brut/cli/apps/scaffold.rb +256 -0
  34. data/lib/brut/cli/apps/test.rb +200 -0
  35. data/lib/brut/cli/command.rb +130 -0
  36. data/lib/brut/cli/error.rb +12 -0
  37. data/lib/brut/cli/execution_results.rb +81 -0
  38. data/lib/brut/cli/executor.rb +37 -0
  39. data/lib/brut/cli/options.rb +46 -0
  40. data/lib/brut/cli/output.rb +30 -0
  41. data/lib/brut/cli.rb +24 -0
  42. data/lib/brut/factory_bot.rb +20 -0
  43. data/lib/brut/framework/app.rb +55 -0
  44. data/lib/brut/framework/config.rb +415 -0
  45. data/lib/brut/framework/container.rb +190 -0
  46. data/lib/brut/framework/errors/abstract_method.rb +9 -0
  47. data/lib/brut/framework/errors/bug.rb +14 -0
  48. data/lib/brut/framework/errors/not_found.rb +10 -0
  49. data/lib/brut/framework/errors.rb +14 -0
  50. data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
  51. data/lib/brut/framework/mcp.rb +215 -0
  52. data/lib/brut/framework/project_environment.rb +18 -0
  53. data/lib/brut/framework.rb +13 -0
  54. data/lib/brut/front_end/asset_metadata.rb +76 -0
  55. data/lib/brut/front_end/component.rb +213 -0
  56. data/lib/brut/front_end/components/form_tag.rb +71 -0
  57. data/lib/brut/front_end/components/i18n_translations.rb +36 -0
  58. data/lib/brut/front_end/components/input.rb +13 -0
  59. data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
  60. data/lib/brut/front_end/components/inputs/select.rb +100 -0
  61. data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
  62. data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
  63. data/lib/brut/front_end/components/locale_detection.rb +25 -0
  64. data/lib/brut/front_end/components/page_identifier.rb +13 -0
  65. data/lib/brut/front_end/components/timestamp.rb +33 -0
  66. data/lib/brut/front_end/download.rb +23 -0
  67. data/lib/brut/front_end/flash.rb +57 -0
  68. data/lib/brut/front_end/form.rb +171 -0
  69. data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
  70. data/lib/brut/front_end/forms/input.rb +119 -0
  71. data/lib/brut/front_end/forms/input_definition.rb +100 -0
  72. data/lib/brut/front_end/forms/validity_state.rb +36 -0
  73. data/lib/brut/front_end/handler.rb +48 -0
  74. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
  75. data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
  76. data/lib/brut/front_end/handling_results.rb +14 -0
  77. data/lib/brut/front_end/http_method.rb +33 -0
  78. data/lib/brut/front_end/http_status.rb +16 -0
  79. data/lib/brut/front_end/middleware.rb +7 -0
  80. data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
  81. data/lib/brut/front_end/page.rb +47 -0
  82. data/lib/brut/front_end/request_context.rb +82 -0
  83. data/lib/brut/front_end/route_hook.rb +15 -0
  84. data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
  85. data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
  86. data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
  87. data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
  88. data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
  89. data/lib/brut/front_end/routing.rb +236 -0
  90. data/lib/brut/front_end/session.rb +56 -0
  91. data/lib/brut/front_end/template.rb +32 -0
  92. data/lib/brut/front_end/templates/block_filter.rb +60 -0
  93. data/lib/brut/front_end/templates/erb_engine.rb +26 -0
  94. data/lib/brut/front_end/templates/erb_parser.rb +84 -0
  95. data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
  96. data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
  97. data/lib/brut/i18n/base_methods.rb +168 -0
  98. data/lib/brut/i18n/for_cli.rb +4 -0
  99. data/lib/brut/i18n/for_html.rb +4 -0
  100. data/lib/brut/i18n/http_accept_language.rb +68 -0
  101. data/lib/brut/i18n.rb +6 -0
  102. data/lib/brut/instrumentation/basic.rb +66 -0
  103. data/lib/brut/instrumentation/event.rb +19 -0
  104. data/lib/brut/instrumentation/http_event.rb +5 -0
  105. data/lib/brut/instrumentation/subscriber.rb +41 -0
  106. data/lib/brut/instrumentation.rb +11 -0
  107. data/lib/brut/junk_drawer.rb +88 -0
  108. data/lib/brut/sinatra_helpers.rb +183 -0
  109. data/lib/brut/spec_support/component_support.rb +49 -0
  110. data/lib/brut/spec_support/flash_support.rb +7 -0
  111. data/lib/brut/spec_support/general_support.rb +18 -0
  112. data/lib/brut/spec_support/handler_support.rb +7 -0
  113. data/lib/brut/spec_support/matcher.rb +9 -0
  114. data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
  115. data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
  116. data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
  117. data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
  118. data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
  119. data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
  120. data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
  121. data/lib/brut/spec_support/session_support.rb +3 -0
  122. data/lib/brut/spec_support.rb +12 -0
  123. data/lib/brut/version.rb +3 -0
  124. data/lib/brut.rb +38 -0
  125. data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
  126. data/lib/sequel/extensions/brut_migrations.rb +98 -0
  127. data/lib/sequel/plugins/created_at.rb +14 -0
  128. data/lib/sequel/plugins/external_id.rb +45 -0
  129. data/lib/sequel/plugins/find_bang.rb +13 -0
  130. data/lib/sequel/plugins.rb +3 -0
  131. metadata +484 -0
@@ -0,0 +1,415 @@
1
+ require_relative "project_environment"
2
+ require "pathname"
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.
8
+ class Brut::Framework::Config
9
+
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
+
30
+ def configure!
31
+ Brut.container do |c|
32
+ # Brut Stuff that should not be changed
33
+
34
+ c.store_ensured_path(
35
+ "tmp_dir",
36
+ "Temporary directory where ephemeral files can do"
37
+ ) do |project_root|
38
+ project_root / "tmp"
39
+ end
40
+
41
+ c.store(
42
+ "project_env",
43
+ Brut::Framework::ProjectEnvironment,
44
+ "The environment of the running app, e.g. dev/test/prod",
45
+ Brut::Framework::ProjectEnvironment.new(ENV["RACK_ENV"])
46
+ )
47
+
48
+ c.store_ensured_path(
49
+ "log_dir",
50
+ "Path where log files may be written"
51
+ ) do |project_root|
52
+ project_root / "logs"
53
+ end
54
+
55
+ c.store_ensured_path(
56
+ "public_root_dir",
57
+ "Path to the root of all public files"
58
+ ) do |project_root|
59
+ project_root / "app" / "public"
60
+ end
61
+
62
+ c.store_ensured_path(
63
+ "images_root_dir",
64
+ "Path to the root of all images"
65
+ ) do |public_root_dir|
66
+ public_root_dir / "static" / "images"
67
+ end
68
+
69
+ c.store_ensured_path(
70
+ "css_bundle_output_dir",
71
+ "Path where bundled CSS is written for use in web pages"
72
+ ) do |public_root_dir|
73
+ public_root_dir / "css"
74
+ end
75
+
76
+ c.store_ensured_path(
77
+ "js_bundle_output_dir",
78
+ "Path where bundled JS is written for use in web pages"
79
+ ) do |public_root_dir|
80
+ public_root_dir / "js"
81
+ end
82
+
83
+ c.store(
84
+ "database_url",
85
+ String,
86
+ "URL to the primary database - generally avoid this and use sequel_db_handle"
87
+ ) do
88
+ ENV.fetch("DATABASE_URL")
89
+ end
90
+
91
+ c.store(
92
+ "sequel_db_handle",
93
+ Object,
94
+ "Handle to the database",
95
+ ) do |database_url|
96
+ Sequel.connect(database_url)
97
+ end
98
+
99
+ c.store_ensured_path(
100
+ "app_src_dir",
101
+ "Path to root of where all the app's source files are"
102
+ ) do |project_root|
103
+ project_root / "app" / "src"
104
+ end
105
+
106
+ c.store_ensured_path(
107
+ "app_specs_dir",
108
+ "Path to root of where all the app's specs/tests are"
109
+ ) do |project_root|
110
+ project_root / "specs"
111
+ end
112
+
113
+ c.store_ensured_path(
114
+ "js_specs_dir",
115
+ "Path to root of where all JS-based specs/tests are",
116
+ ) do |app_specs_dir|
117
+ app_specs_dir / "front_end" / "js"
118
+ end
119
+
120
+ c.store_required_path(
121
+ "front_end_src_dir",
122
+ "Path to the root of the front end layer for the app"
123
+ ) do |app_src_dir|
124
+ app_src_dir / "front_end"
125
+ end
126
+
127
+ c.store_required_path(
128
+ "components_src_dir",
129
+ "Path to where components classes and templates are stored"
130
+ ) do |front_end_src_dir|
131
+ front_end_src_dir / "components"
132
+ end
133
+
134
+ c.store_required_path(
135
+ "components_specs_dir",
136
+ "Path to where tests of components classes are stored",
137
+ ) do |app_specs_dir|
138
+ app_specs_dir / "front_end" / "components"
139
+ end
140
+
141
+ c.store_required_path(
142
+ "forms_src_dir",
143
+ "Path to where form classes are stored"
144
+ ) do |front_end_src_dir|
145
+ front_end_src_dir / "forms"
146
+ end
147
+
148
+ c.store_required_path(
149
+ "handlers_src_dir",
150
+ "Path to where handlers are stored"
151
+ ) do |front_end_src_dir|
152
+ front_end_src_dir / "handlers"
153
+ end
154
+
155
+ c.store_required_path(
156
+ "svgs_src_dir",
157
+ "Path to where svgs are stored"
158
+ ) do |front_end_src_dir|
159
+ front_end_src_dir / "svgs"
160
+ end
161
+
162
+ c.store_required_path(
163
+ "images_src_dir",
164
+ "Path to where images are stored"
165
+ ) do |front_end_src_dir|
166
+ front_end_src_dir / "images"
167
+ end
168
+
169
+ c.store_required_path(
170
+ "pages_src_dir",
171
+ "Path to where page classes and templates are stored"
172
+ ) do |front_end_src_dir|
173
+ front_end_src_dir / "pages"
174
+ end
175
+
176
+ c.store_required_path(
177
+ "pages_specs_dir",
178
+ "Path to where tests of page classes are stored",
179
+ ) do |app_specs_dir|
180
+ app_specs_dir / "front_end" / "pages"
181
+ end
182
+
183
+ c.store_required_path(
184
+ "layouts_src_dir",
185
+ "Path to where layout classes and templates are stored"
186
+ ) do |front_end_src_dir|
187
+ front_end_src_dir / "layouts"
188
+ end
189
+
190
+ c.store_required_path(
191
+ "js_src_dir",
192
+ "Path to where JS files are",
193
+ ) do |front_end_src_dir|
194
+ front_end_src_dir / "js"
195
+ end
196
+
197
+ c.store_required_path(
198
+ "back_end_src_dir",
199
+ "Path to the root of the back end layer for the app"
200
+ ) do |app_src_dir|
201
+ app_src_dir / "back_end"
202
+ end
203
+
204
+ c.store_ensured_path(
205
+ "data_models_src_dir",
206
+ "Path to the root of all data modeling",
207
+ ) do |back_end_src_dir|
208
+ back_end_src_dir / "data_models"
209
+ end
210
+
211
+ c.store_ensured_path(
212
+ "migrations_dir",
213
+ "Path to the DB migrations",
214
+ ) do |data_models_src_dir|
215
+ data_models_src_dir / "migrations"
216
+ end
217
+
218
+ c.store_ensured_path(
219
+ "db_seeds_dir",
220
+ "Path to the seed data for the DB",
221
+ ) do |data_models_src_dir|
222
+ data_models_src_dir / "seed"
223
+ end
224
+
225
+ c.store_ensured_path(
226
+ "config_dir",
227
+ "Path to where configuration files are stores"
228
+ ) do |project_root|
229
+ project_root / "app" / "config"
230
+ end
231
+
232
+ c.store(
233
+ "asset_metadata_file",
234
+ Pathname,
235
+ "Path to the asset metadata file, used to manage hashed asset names"
236
+ ) do |config_dir|
237
+ config_dir / "asset_metadata.json"
238
+ end
239
+
240
+
241
+ c.store(
242
+ "layout_locator",
243
+ "Brut::FrontEnd::Component::TemplateLocator",
244
+ "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")
248
+ end
249
+
250
+ c.store(
251
+ "page_locator",
252
+ "Brut::FrontEnd::Component::TemplateLocator",
253
+ "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")
257
+ end
258
+
259
+ c.store(
260
+ "component_locator",
261
+ "Brut::FrontEnd::Component::TemplateLocator",
262
+ "Object to use to locate templates for components"
263
+ ) do |components_src_dir, pages_src_dir|
264
+ Brut::FrontEnd::Component::TemplateLocator.new(paths: [ components_src_dir, pages_src_dir ],
265
+ extension: "html.erb")
266
+ end
267
+
268
+ c.store(
269
+ "svg_locator",
270
+ "Brut::FrontEnd::Component::TemplateLocator",
271
+ "Object to use to locate SVGs"
272
+ ) do |svgs_src_dir|
273
+ Brut::FrontEnd::Component::TemplateLocator.new(paths: svgs_src_dir,
274
+ extension: "svg")
275
+ end
276
+
277
+ c.store(
278
+ "asset_path_resolver",
279
+ "Brut::FrontEnd::Component::AssetPathResolver",
280
+ "Object to use to resolve logical asset paths to actual asset paths"
281
+ ) do |asset_metadata_file|
282
+ Brut::FrontEnd::Component::AssetPathResolver.new(metadata_file: asset_metadata_file)
283
+ end
284
+
285
+ c.store(
286
+ "routing",
287
+ "Brut::FrontEnd::Routing",
288
+ "Routing for all registered routes of this app",
289
+ Brut::FrontEnd::Routing.new
290
+ )
291
+
292
+ c.store(
293
+ "instrumentation",
294
+ Brut::Instrumentation::Basic,
295
+ "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
303
+
304
+ # App can override
305
+
306
+ c.store(
307
+ "external_id_prefix",
308
+ String,
309
+ "String to use as a prefix for external ids in tables using the external_id feature. Nil means the feature is disabled",
310
+ nil,
311
+ allow_app_override: true,
312
+ allow_nil: true,
313
+ )
314
+
315
+ c.store(
316
+ "debug_zeitwerk?",
317
+ :boolean,
318
+ "If true, Zeitwerk's loading will be logged for debugging purposes. Do not enable this in production",
319
+ false,
320
+ allow_app_override: true,
321
+ )
322
+
323
+ c.store(
324
+ "session_class",
325
+ Class,
326
+ "Class to use when wrapping the Rack session",
327
+ Brut::FrontEnd::Session,
328
+ allow_app_override: true,
329
+ )
330
+
331
+ c.store(
332
+ "flash_class",
333
+ Class,
334
+ "Class to use to represent the Flash",
335
+ Brut::FrontEnd::Flash,
336
+ allow_app_override: true,
337
+ )
338
+
339
+ c.store(
340
+ "semantic_logger_appenders",
341
+ { Hash => "if only one appender is needed", Array => "to configure multiple appenders" },
342
+ "List of appenders to be configured for SemanticLogger",
343
+ allow_app_override: true
344
+ ) do |project_env,log_dir|
345
+ appenders = if project_env.development?
346
+ [
347
+ { formatter: :color, io: $stdout },
348
+ { file_name: (log_dir / "development.log").to_s },
349
+ ]
350
+ end
351
+ if appenders.nil?
352
+ appenders = { file_name: (log_dir / "#{project_env}.log").to_s }
353
+ end
354
+ if appenders.nil?
355
+ appenders = { io: $stdout }
356
+ end
357
+ appenders
358
+ end
359
+
360
+ c.store(
361
+ "eager_load_classes?",
362
+ :boolean,
363
+ "If true, classes are eagerly loaded upon startup",
364
+ true,
365
+ allow_app_override: true
366
+ )
367
+
368
+ c.store(
369
+ "auto_reload_classes?",
370
+ :boolean,
371
+ "If true, classes are reloaded with each request. Useful only really for development",
372
+ allow_app_override: true
373
+ ) do |project_env|
374
+ no_reload_in_dev = ENV["BRUT_NO_RELOAD_IN_DEV"] == "true"
375
+ if project_env.development?
376
+ !no_reload_in_dev
377
+ else
378
+ false
379
+ end
380
+ end
381
+
382
+ c.store(
383
+ "log_level",
384
+ String,
385
+ "Log level to control how much logging is happening",
386
+ allow_app_override: true,
387
+ ) do
388
+ ENV["LOG_LEVEL"] || "debug"
389
+ end
390
+
391
+ c.store(
392
+ "csp_class",
393
+ Class,
394
+ "Route Hook to use for setting the Content-Security-Policy header",
395
+ allow_app_override: true,
396
+ allow_nil: true,
397
+ ) do |project_env|
398
+ if project_env.development?
399
+ Brut::FrontEnd::RouteHooks::CSPNoInlineScripts
400
+ else
401
+ Brut::FrontEnd::RouteHooks::CSPNoInlineStylesOrScripts
402
+ end
403
+ end
404
+
405
+ c.store(
406
+ "csp_reporting_class",
407
+ Class,
408
+ "Route Hook to use for setting the Content-Security-Policy-Report-Only header",
409
+ Brut::FrontEnd::RouteHooks::CSPNoInlineStylesOrScripts::ReportOnly,
410
+ allow_app_override: true,
411
+ allow_nil: true,
412
+ )
413
+ end
414
+ end
415
+ end
@@ -0,0 +1,190 @@
1
+ require "fileutils"
2
+
3
+ module Brut
4
+ def self.container(&block)
5
+ @container ||= Brut::Framework::Container.new
6
+ if !block.nil?
7
+ block.(@container)
8
+ end
9
+ @container
10
+ end
11
+ end
12
+
13
+ # This is a basic container for shared context, configuration,
14
+ # and objects. This allows easily sharing cross-cutting information
15
+ # such as project root, environment, and other objects.
16
+ #
17
+ # This can be used to store configuration values, re-usable objects,
18
+ # or anything else that is needed in the app. Values are fetched lazily
19
+ # and can depend on other values in this container.
20
+ #
21
+ # There is no namespacing/hierarchy.
22
+ class Brut::Framework::Container
23
+ def initialize
24
+ @container = {}
25
+ end
26
+
27
+ # Store a named value for later.
28
+ #
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.
37
+ #
38
+ # The block can receive parameters and those parameters names must
39
+ # match other values stored in this container. Those values are passed in.
40
+ #
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:
43
+ #
44
+ # ```
45
+ # container.store("tmp_dir") { |project_root| project_root / "tmp" }
46
+ # ```
47
+ def store(name,type,description,value=:use_block,allow_app_override: false,allow_nil: false,&block)
48
+ # TODO: Check that value / block is used properly
49
+ name = name.to_s
50
+ if type == "boolean"
51
+ type = :boolean
52
+ end
53
+ if type == :boolean
54
+ if name !~ /\?$/
55
+ raise ArgumentError, "#{name} is a boolean so must end with a question mark"
56
+ end
57
+ end
58
+ self.validate_name!(name:,type:,allow_app_override:)
59
+ if value == :use_block
60
+ derive_with = block
61
+ @container[name] = { value: nil, derive_with: derive_with }
62
+ else
63
+ if type == :boolean
64
+ value = !!value
65
+ end
66
+ @container[name] = { value: value }
67
+ end
68
+ @container[name][:description] = description
69
+ @container[name][:type] = type
70
+ @container[name][:allow_app_override] = allow_app_override
71
+ @container[name][:allow_nil] = allow_nil
72
+ self
73
+ end
74
+
75
+ def override(name,value=:use_block,&block)
76
+ name = name.to_s
77
+ if !@container[name]
78
+ raise ArgumentError,"#{name} has not been specified so you cannot override it"
79
+ end
80
+ if !@container[name][:allow_app_override]
81
+ raise ArgumentError,"#{name} does not allow the app to override it"
82
+ end
83
+ if value == :use_block
84
+ @container[name] = { value: nil, derive_with: block }
85
+ else
86
+ @container[name] = { value: value }
87
+ end
88
+ end
89
+
90
+ # Store a value that represents a path that must exist. The value will
91
+ # be assumed to be of type Pathname
92
+ def store_required_path(name,description,value=:use_block,&block)
93
+ self.store(name,Pathname,description,value,&block)
94
+ @container[name][:required_path] = true
95
+ self
96
+ end
97
+
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
102
+ def store_ensured_path(name,description,value=:use_block,&block)
103
+ self.store(name,Pathname,description,value,&block)
104
+ @container[name][:ensured_path] = true
105
+ self
106
+ end
107
+
108
+ # Fetch a value by using its name as a method to instances of this class.
109
+ def method_missing(sym,*args,&block)
110
+ if args.length == 0 && block.nil? && self.respond_to_missing?(sym)
111
+ fetch_value(sym.to_s)
112
+ else
113
+ super.method_missing(sym,*args,&block)
114
+ end
115
+ end
116
+
117
+ # Implemented to go along with method_missing
118
+ def respond_to_missing?(name,include_private=false)
119
+ @container.key?(name.to_s)
120
+ end
121
+
122
+
123
+ # Fetch a value given a name.
124
+ def fetch(name)
125
+ fetch_value(name.to_s)
126
+ end
127
+
128
+ private
129
+
130
+ def fetch_value(name)
131
+ # TODO: Provide a cleanr impl and better error checking if things go wrong
132
+ x = @container.fetch(name)
133
+
134
+ value = x[:value]
135
+
136
+ if !value.nil? || (x[:allow_nil] && value.nil?)
137
+ handle_path_values(name,x)
138
+ return value
139
+ end
140
+
141
+ deriver = x[:derive_with]
142
+ if deriver.nil?
143
+ raise "Something is seriously wrong. '#{name}' was stored in container without a derive_with value"
144
+ end
145
+
146
+ parameters = deriver.parameters(lambda: true)
147
+ args = parameters.map { |param_description| param_description[1] }.map { |name_of_dependent_object| self.send(name_of_dependent_object) }
148
+ value = deriver.(*args)
149
+ if x[:type] == :boolean
150
+ value = !!value
151
+ end
152
+ x[:value] = value
153
+ if x[:value].nil?
154
+ if !x[:allow_nil]
155
+ raise "Something is wrong: #{name} had no value"
156
+ end
157
+ end
158
+ handle_path_values(name,x)
159
+ x[:value]
160
+ end
161
+
162
+ def handle_path_values(name,contained_value)
163
+ value = contained_value[:value]
164
+ if contained_value[:required_path] && !Dir.exist?(value)
165
+ raise "For value '#{name}', the directory is represents must exist, but does not: '#{value}'"
166
+ end
167
+ if contained_value[:ensured_path]
168
+ FileUtils.mkdir_p value
169
+ end
170
+ end
171
+
172
+ PATHNAME_NAME_REGEXP = /_(dir|file)$/
173
+
174
+ def validate_name!(name:,type:,allow_app_override:)
175
+ if @container.key?(name)
176
+ if allow_app_override
177
+ raise ArgumentError.new("Name '#{name}' has already been specified - to override it, use Brut.container.override")
178
+ else
179
+ raise ArgumentError.new("Name '#{name}' has already been specified - you cannot override it")
180
+ end
181
+ end
182
+ if type.to_s == "Pathname"
183
+ if name != "project_root"
184
+ if !name.match(PATHNAME_NAME_REGEXP)
185
+ raise ArgumentError.new("Name '#{name}' is a Pathname, and must end in '_dir' or '_file'")
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,9 @@
1
+ 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
+ end
@@ -0,0 +1,14 @@
1
+ # Thrown when a codepath should never have been allowed
2
+ # to occur. This is useful is signaling that the system
3
+ # has some sort of bug in its integration. For example,
4
+ # attempting to perform an action that the UI should've
5
+ # prevented
6
+ class Brut::Framework::Errors::Bug < Brut::Framework::Error
7
+ def initialize(message=nil)
8
+ if message.nil?
9
+ super
10
+ else
11
+ super("BUG: #{message}")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ # Indicates that a resource or database row does not exist.
2
+ class Brut::Framework::Errors::NotFound < Brut::Framework::Error
3
+ def initialize(resource_name:,id:,context:nil)
4
+ if !context.nil?
5
+ context = ": #{context}"
6
+ end
7
+ super("Could not find a #{resource_name} using ID #{id}#{context}")
8
+ end
9
+ end
10
+
@@ -0,0 +1,14 @@
1
+ module Brut
2
+ module Framework
3
+ module Errors
4
+ autoload(:Bug,"brut/framework/errors/bug")
5
+ autoload(:NotFound,"brut/framework/errors/not_found")
6
+ autoload(:AbstractMethod,"brut/framework/errors/abstract_method")
7
+ def bug!(message=nil)
8
+ raise Brut::Framework::Errors::Bug,message
9
+ end
10
+ end
11
+ class Error < StandardError
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,50 @@
1
+ # Include this to enable methods to help with type checking. Generally, you should not use this
2
+ # unless there is a real concern that someone will pass the wrong type in and it would not be obvious
3
+ # that they made this mistake. Of note, this is preferred for widely used classes instead of trying
4
+ # to convert arguments to whatever type the class needs.
5
+ module Brut::Framework::FussyTypeEnforcement
6
+ # Perform basic type checking, ideally inside a constructor when assigning ivars
7
+ #
8
+ # value:: the value that was given
9
+ # type_descriptor:: a class or an array of allowed values. If a class, value must be kind_of? that class. If an array,
10
+ # value must be one of the values in the array.
11
+ # variable_name_for_error_message:: the name of the variable so that error messages make sense
12
+ # required:: if true, the value may not be nil. If false, nil values are allowed and no real type checking is done. Note that a
13
+ # string that is blank counts as nil, so a require string must not be blank.
14
+ # coerce:: if given, this is the symbol that will be used to coerce the value before type checking
15
+ def type!(value,type_descriptor,variable_name_for_error_message, required: false, coerce: false)
16
+
17
+ value_blank = value.nil? || ( value.kind_of?(String) && value.strip == "" )
18
+
19
+ if !required && value_blank
20
+ return value
21
+ end
22
+
23
+ if required && value_blank
24
+ raise ArgumentError.new("'#{variable_name_for_error_message}' must have a value")
25
+ end
26
+
27
+ if type_descriptor.kind_of?(Class)
28
+ coerced_value = coerce ? value.send(coerce) : value
29
+ if !coerced_value.kind_of?(type_descriptor)
30
+ class_description = if coerce
31
+ "but was a #{value.class}, coerced to a #{coerced_value.class} via #{coerce}"
32
+ else
33
+ "but was a #{value.class}"
34
+ end
35
+ raise ArgumentError.new("'#{variable_name_for_error_message}' must be a #{type_descriptor}, #{class_description} (value as a string is #{value})")
36
+ end
37
+ value = coerced_value
38
+ elsif type_descriptor.kind_of?(Array)
39
+ if !type_descriptor.include?(value)
40
+ description_of_values = type_descriptor.map { |value|
41
+ "#{value} (a #{value.class})"
42
+ }.join(", ")
43
+ raise ArgumentError.new("'#{variable_name_for_error_message}' must be one of #{description_of_values}, but was a #{value.class} (value as a string is #{value})")
44
+ end
45
+ else
46
+ raise ArgumentError.new("Use of type! with a #{type_descriptor.class} (#{type_descriptor}) is not supported")
47
+ end
48
+ value
49
+ end
50
+ end