brut 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/CODE_OF_CONDUCT.txt +99 -0
- data/Dockerfile.dx +32 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +370 -0
- data/README.md +21 -0
- data/Rakefile +1 -0
- data/bin/bin_kit.rb +39 -0
- data/bin/rake +27 -0
- data/bin/setup +145 -0
- data/brut.gemspec +60 -0
- data/docker-compose.dx.yml +16 -0
- data/dx/build +26 -0
- data/dx/docker-compose.env +22 -0
- data/dx/dx.sh.lib +24 -0
- data/dx/exec +58 -0
- data/dx/prune +19 -0
- data/dx/setupkit.sh.lib +144 -0
- data/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/dx/start +30 -0
- data/dx/stop +23 -0
- data/lib/brut/back_end/action.rb +3 -0
- data/lib/brut/back_end/result.rb +46 -0
- data/lib/brut/back_end/seed_data.rb +24 -0
- data/lib/brut/back_end/validator.rb +3 -0
- data/lib/brut/back_end/validators/form_validator.rb +37 -0
- data/lib/brut/cli/app.rb +130 -0
- data/lib/brut/cli/app_runner.rb +219 -0
- data/lib/brut/cli/apps/build_assets.rb +123 -0
- data/lib/brut/cli/apps/db.rb +279 -0
- data/lib/brut/cli/apps/scaffold.rb +256 -0
- data/lib/brut/cli/apps/test.rb +200 -0
- data/lib/brut/cli/command.rb +130 -0
- data/lib/brut/cli/error.rb +12 -0
- data/lib/brut/cli/execution_results.rb +81 -0
- data/lib/brut/cli/executor.rb +37 -0
- data/lib/brut/cli/options.rb +46 -0
- data/lib/brut/cli/output.rb +30 -0
- data/lib/brut/cli.rb +24 -0
- data/lib/brut/factory_bot.rb +20 -0
- data/lib/brut/framework/app.rb +55 -0
- data/lib/brut/framework/config.rb +415 -0
- data/lib/brut/framework/container.rb +190 -0
- data/lib/brut/framework/errors/abstract_method.rb +9 -0
- data/lib/brut/framework/errors/bug.rb +14 -0
- data/lib/brut/framework/errors/not_found.rb +10 -0
- data/lib/brut/framework/errors.rb +14 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
- data/lib/brut/framework/mcp.rb +215 -0
- data/lib/brut/framework/project_environment.rb +18 -0
- data/lib/brut/framework.rb +13 -0
- data/lib/brut/front_end/asset_metadata.rb +76 -0
- data/lib/brut/front_end/component.rb +213 -0
- data/lib/brut/front_end/components/form_tag.rb +71 -0
- data/lib/brut/front_end/components/i18n_translations.rb +36 -0
- data/lib/brut/front_end/components/input.rb +13 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
- data/lib/brut/front_end/components/inputs/select.rb +100 -0
- data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
- data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
- data/lib/brut/front_end/components/locale_detection.rb +25 -0
- data/lib/brut/front_end/components/page_identifier.rb +13 -0
- data/lib/brut/front_end/components/timestamp.rb +33 -0
- data/lib/brut/front_end/download.rb +23 -0
- data/lib/brut/front_end/flash.rb +57 -0
- data/lib/brut/front_end/form.rb +171 -0
- data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
- data/lib/brut/front_end/forms/input.rb +119 -0
- data/lib/brut/front_end/forms/input_definition.rb +100 -0
- data/lib/brut/front_end/forms/validity_state.rb +36 -0
- data/lib/brut/front_end/handler.rb +48 -0
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
- data/lib/brut/front_end/handling_results.rb +14 -0
- data/lib/brut/front_end/http_method.rb +33 -0
- data/lib/brut/front_end/http_status.rb +16 -0
- data/lib/brut/front_end/middleware.rb +7 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
- data/lib/brut/front_end/page.rb +47 -0
- data/lib/brut/front_end/request_context.rb +82 -0
- data/lib/brut/front_end/route_hook.rb +15 -0
- data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
- data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
- data/lib/brut/front_end/routing.rb +236 -0
- data/lib/brut/front_end/session.rb +56 -0
- data/lib/brut/front_end/template.rb +32 -0
- data/lib/brut/front_end/templates/block_filter.rb +60 -0
- data/lib/brut/front_end/templates/erb_engine.rb +26 -0
- data/lib/brut/front_end/templates/erb_parser.rb +84 -0
- data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
- data/lib/brut/i18n/base_methods.rb +168 -0
- data/lib/brut/i18n/for_cli.rb +4 -0
- data/lib/brut/i18n/for_html.rb +4 -0
- data/lib/brut/i18n/http_accept_language.rb +68 -0
- data/lib/brut/i18n.rb +6 -0
- data/lib/brut/instrumentation/basic.rb +66 -0
- data/lib/brut/instrumentation/event.rb +19 -0
- data/lib/brut/instrumentation/http_event.rb +5 -0
- data/lib/brut/instrumentation/subscriber.rb +41 -0
- data/lib/brut/instrumentation.rb +11 -0
- data/lib/brut/junk_drawer.rb +88 -0
- data/lib/brut/sinatra_helpers.rb +183 -0
- data/lib/brut/spec_support/component_support.rb +49 -0
- data/lib/brut/spec_support/flash_support.rb +7 -0
- data/lib/brut/spec_support/general_support.rb +18 -0
- data/lib/brut/spec_support/handler_support.rb +7 -0
- data/lib/brut/spec_support/matcher.rb +9 -0
- data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
- data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
- data/lib/brut/spec_support/session_support.rb +3 -0
- data/lib/brut/spec_support.rb +12 -0
- data/lib/brut/version.rb +3 -0
- data/lib/brut.rb +38 -0
- data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
- data/lib/sequel/extensions/brut_migrations.rb +98 -0
- data/lib/sequel/plugins/created_at.rb +14 -0
- data/lib/sequel/plugins/external_id.rb +45 -0
- data/lib/sequel/plugins/find_bang.rb +13 -0
- data/lib/sequel/plugins.rb +3 -0
- 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,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
|