brut 0.0.1

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