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.
- 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
|