brut 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/Gemfile.lock +66 -1
- data/README.md +36 -0
- data/Rakefile +22 -0
- data/brut.gemspec +7 -0
- data/doc-src/architecture.md +102 -0
- data/doc-src/assets.md +98 -0
- data/doc-src/forms.md +214 -0
- data/doc-src/handlers.md +83 -0
- data/doc-src/javascript.md +265 -0
- data/doc-src/keyword-injection.md +183 -0
- data/doc-src/pages.md +210 -0
- data/doc-src/route-hooks.md +59 -0
- data/docs-todo.md +32 -0
- data/lib/brut/back_end/seed_data.rb +5 -1
- data/lib/brut/back_end/validator.rb +1 -1
- data/lib/brut/back_end/validators/form_validator.rb +31 -6
- data/lib/brut/cli/app.rb +100 -4
- data/lib/brut/cli/app_runner.rb +38 -5
- data/lib/brut/cli/apps/build_assets.rb +4 -6
- data/lib/brut/cli/apps/db.rb +2 -7
- data/lib/brut/cli/apps/scaffold.rb +413 -7
- data/lib/brut/cli/apps/test.rb +14 -1
- data/lib/brut/cli/command.rb +141 -6
- data/lib/brut/cli/error.rb +30 -3
- data/lib/brut/cli/execution_results.rb +44 -6
- data/lib/brut/cli/executor.rb +21 -1
- data/lib/brut/cli/options.rb +29 -2
- data/lib/brut/cli/output.rb +47 -0
- data/lib/brut/cli.rb +26 -0
- data/lib/brut/factory_bot.rb +2 -0
- data/lib/brut/framework/app.rb +38 -5
- data/lib/brut/framework/config.rb +97 -54
- data/lib/brut/framework/container.rb +97 -33
- data/lib/brut/framework/errors/abstract_method.rb +3 -7
- data/lib/brut/framework/errors/missing_parameter.rb +12 -0
- data/lib/brut/framework/errors/no_class_for_path.rb +25 -0
- data/lib/brut/framework/errors/not_found.rb +12 -2
- data/lib/brut/framework/errors/not_implemented.rb +14 -0
- data/lib/brut/framework/errors.rb +18 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +17 -12
- data/lib/brut/framework/mcp.rb +106 -49
- data/lib/brut/framework/project_environment.rb +9 -0
- data/lib/brut/framework.rb +1 -0
- data/lib/brut/front_end/asset_metadata.rb +7 -1
- data/lib/brut/front_end/component.rb +129 -38
- data/lib/brut/front_end/components/constraint_violations.rb +57 -0
- data/lib/brut/front_end/components/form_tag.rb +23 -32
- data/lib/brut/front_end/components/i18n_translations.rb +34 -1
- data/lib/brut/front_end/components/input.rb +3 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -0
- data/lib/brut/front_end/components/inputs/radio_button.rb +41 -0
- data/lib/brut/front_end/components/inputs/select.rb +26 -2
- data/lib/brut/front_end/components/inputs/text_field.rb +27 -5
- data/lib/brut/front_end/components/inputs/textarea.rb +24 -4
- data/lib/brut/front_end/components/locale_detection.rb +8 -1
- data/lib/brut/front_end/components/page_identifier.rb +2 -0
- data/lib/brut/front_end/components/time.rb +95 -0
- data/lib/brut/front_end/components/traceparent.rb +22 -0
- data/lib/brut/front_end/download.rb +11 -0
- data/lib/brut/front_end/flash.rb +32 -0
- data/lib/brut/front_end/form.rb +109 -106
- data/lib/brut/front_end/forms/constraint_violation.rb +16 -11
- data/lib/brut/front_end/forms/input.rb +30 -42
- data/lib/brut/front_end/forms/input_declarations.rb +90 -0
- data/lib/brut/front_end/forms/input_definition.rb +45 -30
- data/lib/brut/front_end/forms/radio_button_group_input.rb +46 -0
- data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +29 -0
- data/lib/brut/front_end/forms/select_input.rb +47 -0
- data/lib/brut/front_end/forms/select_input_definition.rb +27 -0
- data/lib/brut/front_end/forms/validity_state.rb +23 -9
- data/lib/brut/front_end/handler.rb +27 -8
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +4 -2
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +99 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -4
- data/lib/brut/front_end/handlers/missing_handler.rb +9 -0
- data/lib/brut/front_end/handling_results.rb +14 -4
- data/lib/brut/front_end/http_method.rb +13 -1
- data/lib/brut/front_end/http_status.rb +10 -0
- data/lib/brut/front_end/layouts/_internal.html.erb +68 -0
- data/lib/brut/front_end/middleware.rb +5 -0
- data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +15 -0
- data/lib/brut/front_end/middlewares/favicon.rb +16 -0
- data/lib/brut/front_end/middlewares/open_telemetry_span.rb +12 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +27 -9
- data/lib/brut/front_end/page.rb +50 -11
- data/lib/brut/front_end/pages/_missing_page.html.erb +17 -0
- data/lib/brut/front_end/pages/missing_page.rb +36 -0
- data/lib/brut/front_end/request_context.rb +117 -8
- data/lib/brut/front_end/route_hook.rb +43 -1
- data/lib/brut/front_end/route_hooks/age_flash.rb +3 -2
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +18 -10
- data/lib/brut/front_end/route_hooks/locale_detection.rb +11 -5
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +7 -4
- data/lib/brut/front_end/routing.rb +138 -31
- data/lib/brut/front_end/session.rb +86 -7
- data/lib/brut/front_end/template.rb +17 -2
- data/lib/brut/front_end/templates/block_filter.rb +4 -3
- data/lib/brut/front_end/templates/erb_parser.rb +1 -1
- data/lib/brut/front_end/templates/escapable_filter.rb +2 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +30 -2
- data/lib/brut/front_end/templates/locator.rb +60 -0
- data/lib/brut/i18n/base_methods.rb +4 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/logger_span_exporter.rb +75 -0
- data/lib/brut/instrumentation/open_telemetry.rb +107 -0
- data/lib/brut/instrumentation.rb +4 -6
- data/lib/brut/junk_drawer.rb +54 -4
- data/lib/brut/sinatra_helpers.rb +42 -38
- data/lib/brut/spec_support/clock_support.rb +6 -0
- data/lib/brut/spec_support/component_support.rb +53 -26
- data/lib/brut/spec_support/e2e_test_server.rb +82 -0
- data/lib/brut/spec_support/enhanced_node.rb +45 -0
- data/lib/brut/spec_support/general_support.rb +14 -3
- data/lib/brut/spec_support/handler_support.rb +2 -0
- data/lib/brut/spec_support/matcher.rb +6 -3
- data/lib/brut/spec_support/matchers/be_page_for.rb +3 -2
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +22 -9
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +9 -2
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +24 -0
- data/lib/brut/spec_support/matchers/have_link_to.rb +14 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +30 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +4 -11
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +16 -8
- data/lib/brut/spec_support/rspec_setup.rb +182 -0
- data/lib/brut/spec_support.rb +8 -3
- data/lib/brut/version.rb +2 -1
- data/lib/brut.rb +28 -5
- data/lib/sequel/extensions/brut_instrumentation.rb +5 -27
- data/lib/sequel/extensions/brut_migrations.rb +18 -8
- data/lib/sequel/plugins/created_at.rb +2 -0
- data/lib/sequel/plugins/external_id.rb +39 -1
- data/lib/sequel/plugins/find_bang.rb +4 -1
- metadata +140 -13
- data/lib/brut/back_end/action.rb +0 -3
- data/lib/brut/back_end/result.rb +0 -46
- data/lib/brut/front_end/components/timestamp.rb +0 -33
- data/lib/brut/instrumentation/basic.rb +0 -66
- data/lib/brut/instrumentation/event.rb +0 -19
- data/lib/brut/instrumentation/http_event.rb +0 -5
- data/lib/brut/instrumentation/subscriber.rb +0 -41
data/lib/brut/framework/app.rb
CHANGED
@@ -1,16 +1,27 @@
|
|
1
|
-
# An "App" in Brut paralance is the collection of source code and
|
1
|
+
# An "App" in Brut paralance is the collection of source code and configuration that is needed to operate
|
2
2
|
# a website. This includes everything needed to serve HTTP requests, but also includes ancillary
|
3
|
-
# tasks and any related files required for the app to exist and function.
|
3
|
+
# tasks and any related files required for the app to exist and function. Your app will have an `App` class that subclasses this
|
4
|
+
# class.
|
5
|
+
#
|
6
|
+
# When your app is initialized, Brut will have been configured, but access to internal resources may not be available. It is here
|
7
|
+
# that you can override configuration values or do any other setup before everything boots.
|
4
8
|
class Brut::Framework::App
|
9
|
+
include Brut::Framework::Errors
|
5
10
|
|
6
|
-
# An identifier for this app that can be used as a hostname
|
7
|
-
def id
|
11
|
+
# An identifier for this app that can be used as a hostname. Your app must provide this.
|
12
|
+
def id
|
13
|
+
abstract_method!
|
14
|
+
end
|
8
15
|
|
9
16
|
# An identifier for the app's 'organization' that can be used as a hostname.
|
10
17
|
# This isn't relevant in all contexts, but is useful for deploys or other
|
11
18
|
# actions where an app needs to exist inside some organizational context.
|
12
19
|
def organization = id
|
13
20
|
|
21
|
+
# Call this in your app's definition to define your app's routes. The contents of the block will be evaluated in the context of
|
22
|
+
# {Brut::SinatraHelpers::ClassMethods}, and the methods there are generally the ones you should be calling.
|
23
|
+
#
|
24
|
+
# You can call this multiple times and the routes will be concatenated together.
|
14
25
|
def self.routes(&block)
|
15
26
|
@routes_blocks ||= []
|
16
27
|
if block.nil?
|
@@ -19,6 +30,14 @@ class Brut::Framework::App
|
|
19
30
|
@routes_blocks << block
|
20
31
|
end
|
21
32
|
end
|
33
|
+
|
34
|
+
# Add a Rack middleware to your app. Middlewares are configured in the order in which you call this method.
|
35
|
+
#
|
36
|
+
# @param [Class] middleware a class that implements [Rack Middleware](https://github.com/rack/rack/blob/main/SPEC.rdoc).
|
37
|
+
# @param [Array] args arguments to be given to the `middleware` class' initializer.
|
38
|
+
# @param [block] block a block that is given to the initializer of the `middleware` class.
|
39
|
+
#
|
40
|
+
# @return [Array] if no parameters are given, returns all the currently-configured middleware.
|
22
41
|
def self.middleware(middleware=nil,*args,&block)
|
23
42
|
@middlewares ||= []
|
24
43
|
if middleware.nil? && args.empty? && block.nil?
|
@@ -27,6 +46,13 @@ class Brut::Framework::App
|
|
27
46
|
@middlewares << [ middleware, args, block ]
|
28
47
|
end
|
29
48
|
end
|
49
|
+
|
50
|
+
# Configure a {Brut::FrontEnd::RouteHook} to be called before each request.
|
51
|
+
#
|
52
|
+
# @param [String] klass_name The name of the class that extends {Brut::FrontEnd::RouteHook} and implements #before. This uses the
|
53
|
+
# name (not the class itself) to avoid loading issues.
|
54
|
+
#
|
55
|
+
# @return [Array] If no parameters given, returns all configured before hooks.
|
30
56
|
def self.before(klass_name=nil)
|
31
57
|
@before ||= []
|
32
58
|
if klass_name.nil?
|
@@ -35,6 +61,13 @@ class Brut::Framework::App
|
|
35
61
|
@before << klass_name
|
36
62
|
end
|
37
63
|
end
|
64
|
+
|
65
|
+
# Configure a {Brut::FrontEnd::RouteHook} to be called after each request.
|
66
|
+
#
|
67
|
+
# @param [String] klass_name The name of the class that extends {Brut::FrontEnd::RouteHook} and implements #after. This uses the
|
68
|
+
# name (not the class itself) to avoid loading issues.
|
69
|
+
#
|
70
|
+
# @return [Array] If no parameters given, returns all configured after hooks.
|
38
71
|
def self.after(klass_name=nil)
|
39
72
|
@after ||= []
|
40
73
|
if klass_name.nil?
|
@@ -48,7 +81,7 @@ class Brut::Framework::App
|
|
48
81
|
# code required *after* Brut has been set up and started. You can rely on the
|
49
82
|
# database being available. Any attempts to override configuration values
|
50
83
|
# may not succeed. This is called after the framework has booted, but before
|
51
|
-
# your apps routes are set up.
|
84
|
+
# your apps' routes are set up.
|
52
85
|
def boot!
|
53
86
|
end
|
54
87
|
|
@@ -1,32 +1,12 @@
|
|
1
1
|
require_relative "project_environment"
|
2
2
|
require "pathname"
|
3
3
|
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# but is a class and thus invokable, so that the configuration can
|
7
|
-
# be controlled.
|
4
|
+
# Holds configuration for the framework and your app. In general, you should not interact with this class, however it's source code
|
5
|
+
# is a good reference for what is configured by default by Brut.
|
8
6
|
class Brut::Framework::Config
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
def initialize(string)
|
13
|
-
if string.to_s.match?(PATH_REGEXP)
|
14
|
-
@string = string
|
15
|
-
else
|
16
|
-
raise ArgumentError.new("Value must be only lower case letters, digits, and may have at most one underscore: '#{string}'")
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
def to_str = @string
|
21
|
-
def to_s = self.to_str
|
22
|
-
end
|
23
|
-
|
24
|
-
class AppId < DockerPathComponent
|
25
|
-
end
|
26
|
-
|
27
|
-
class AppOrganizationName < DockerPathComponent
|
28
|
-
end
|
29
|
-
|
8
|
+
# Configures all defaults. In general, this attempts to be lazy in setting things up, so calling this should not attempt to make a
|
9
|
+
# connection to your database.
|
30
10
|
def configure!
|
31
11
|
Brut.container do |c|
|
32
12
|
# Brut Stuff that should not be changed
|
@@ -110,6 +90,13 @@ class Brut::Framework::Config
|
|
110
90
|
project_root / "specs"
|
111
91
|
end
|
112
92
|
|
93
|
+
c.store_ensured_path(
|
94
|
+
"e2e_specs_dir",
|
95
|
+
"Path to the root of all end-to-end tests"
|
96
|
+
) do |app_specs_dir|
|
97
|
+
app_specs_dir / "e2e"
|
98
|
+
end
|
99
|
+
|
113
100
|
c.store_ensured_path(
|
114
101
|
"js_specs_dir",
|
115
102
|
"Path to root of where all JS-based specs/tests are",
|
@@ -117,49 +104,56 @@ class Brut::Framework::Config
|
|
117
104
|
app_specs_dir / "front_end" / "js"
|
118
105
|
end
|
119
106
|
|
120
|
-
c.
|
107
|
+
c.store_ensured_path(
|
121
108
|
"front_end_src_dir",
|
122
109
|
"Path to the root of the front end layer for the app"
|
123
110
|
) do |app_src_dir|
|
124
111
|
app_src_dir / "front_end"
|
125
112
|
end
|
126
113
|
|
127
|
-
c.
|
114
|
+
c.store_ensured_path(
|
128
115
|
"components_src_dir",
|
129
116
|
"Path to where components classes and templates are stored"
|
130
117
|
) do |front_end_src_dir|
|
131
118
|
front_end_src_dir / "components"
|
132
119
|
end
|
133
120
|
|
134
|
-
c.
|
121
|
+
c.store_ensured_path(
|
135
122
|
"components_specs_dir",
|
136
123
|
"Path to where tests of components classes are stored",
|
137
124
|
) do |app_specs_dir|
|
138
125
|
app_specs_dir / "front_end" / "components"
|
139
126
|
end
|
140
127
|
|
141
|
-
c.
|
128
|
+
c.store_ensured_path(
|
142
129
|
"forms_src_dir",
|
143
130
|
"Path to where form classes are stored"
|
144
131
|
) do |front_end_src_dir|
|
145
132
|
front_end_src_dir / "forms"
|
146
133
|
end
|
147
134
|
|
148
|
-
c.
|
135
|
+
c.store_ensured_path(
|
149
136
|
"handlers_src_dir",
|
150
137
|
"Path to where handlers are stored"
|
151
138
|
) do |front_end_src_dir|
|
152
139
|
front_end_src_dir / "handlers"
|
153
140
|
end
|
154
141
|
|
155
|
-
c.
|
142
|
+
c.store_ensured_path(
|
143
|
+
"handlers_specs_dir",
|
144
|
+
"Path to where tests of handler classes are stored",
|
145
|
+
) do |app_specs_dir|
|
146
|
+
app_specs_dir / "front_end" / "handlers"
|
147
|
+
end
|
148
|
+
|
149
|
+
c.store_ensured_path(
|
156
150
|
"svgs_src_dir",
|
157
151
|
"Path to where svgs are stored"
|
158
152
|
) do |front_end_src_dir|
|
159
153
|
front_end_src_dir / "svgs"
|
160
154
|
end
|
161
155
|
|
162
|
-
c.
|
156
|
+
c.store_ensured_path(
|
163
157
|
"images_src_dir",
|
164
158
|
"Path to where images are stored"
|
165
159
|
) do |front_end_src_dir|
|
@@ -194,7 +188,7 @@ class Brut::Framework::Config
|
|
194
188
|
front_end_src_dir / "js"
|
195
189
|
end
|
196
190
|
|
197
|
-
c.
|
191
|
+
c.store_ensured_path(
|
198
192
|
"back_end_src_dir",
|
199
193
|
"Path to the root of the back end layer for the app"
|
200
194
|
) do |app_src_dir|
|
@@ -229,6 +223,13 @@ class Brut::Framework::Config
|
|
229
223
|
project_root / "app" / "config"
|
230
224
|
end
|
231
225
|
|
226
|
+
c.store_ensured_path(
|
227
|
+
"i18n_locales_dir",
|
228
|
+
"Path to where I18N locale files are stored"
|
229
|
+
) do |config_dir|
|
230
|
+
config_dir / "i18n"
|
231
|
+
end
|
232
|
+
|
232
233
|
c.store(
|
233
234
|
"asset_metadata_file",
|
234
235
|
Pathname,
|
@@ -240,38 +241,55 @@ class Brut::Framework::Config
|
|
240
241
|
|
241
242
|
c.store(
|
242
243
|
"layout_locator",
|
243
|
-
"Brut::FrontEnd::
|
244
|
+
"Brut::FrontEnd::Templates::Locator",
|
244
245
|
"Object to use to locate templates for layouts"
|
245
|
-
) do |layouts_src_dir|
|
246
|
-
|
247
|
-
|
246
|
+
) do |layouts_src_dir,project_env,brut_internal_dir|
|
247
|
+
paths = if project_env.development?
|
248
|
+
[ layouts_src_dir, brut_internal_dir / "lib" / "brut" / "front_end" / "layouts" ]
|
249
|
+
else
|
250
|
+
layouts_src_dir
|
251
|
+
end
|
252
|
+
Brut::FrontEnd::Templates::Locator.new(paths: paths,
|
253
|
+
extension: "html.erb")
|
254
|
+
end
|
255
|
+
|
256
|
+
c.store_required_path(
|
257
|
+
"brut_internal_dir",
|
258
|
+
"Location to where the Brut gem is installed."
|
259
|
+
) do
|
260
|
+
(Pathname(__FILE__).dirname / ".." / ".." / "..").expand_path
|
248
261
|
end
|
249
262
|
|
250
263
|
c.store(
|
251
264
|
"page_locator",
|
252
|
-
"Brut::FrontEnd::
|
265
|
+
"Brut::FrontEnd::Templates::Locator",
|
253
266
|
"Object to use to locate templates for pages"
|
254
|
-
) do |pages_src_dir|
|
255
|
-
|
256
|
-
|
267
|
+
) do |pages_src_dir,project_env,brut_internal_dir|
|
268
|
+
paths = if project_env.development?
|
269
|
+
[ pages_src_dir, brut_internal_dir / "lib" / "brut" / "front_end" / "pages" ]
|
270
|
+
else
|
271
|
+
pages_src_dir
|
272
|
+
end
|
273
|
+
Brut::FrontEnd::Templates::Locator.new(paths: paths,
|
274
|
+
extension: "html.erb")
|
257
275
|
end
|
258
276
|
|
259
277
|
c.store(
|
260
278
|
"component_locator",
|
261
|
-
"Brut::FrontEnd::
|
279
|
+
"Brut::FrontEnd::Templates::Locator",
|
262
280
|
"Object to use to locate templates for components"
|
263
281
|
) do |components_src_dir, pages_src_dir|
|
264
|
-
Brut::FrontEnd::
|
265
|
-
|
282
|
+
Brut::FrontEnd::Templates::Locator.new(paths: [ components_src_dir, pages_src_dir ],
|
283
|
+
extension: "html.erb")
|
266
284
|
end
|
267
285
|
|
268
286
|
c.store(
|
269
287
|
"svg_locator",
|
270
|
-
"Brut::FrontEnd::
|
288
|
+
"Brut::FrontEnd::Templates::Locator",
|
271
289
|
"Object to use to locate SVGs"
|
272
290
|
) do |svgs_src_dir|
|
273
|
-
Brut::FrontEnd::
|
274
|
-
|
291
|
+
Brut::FrontEnd::Templates::Locator.new(paths: svgs_src_dir,
|
292
|
+
extension: "svg")
|
275
293
|
end
|
276
294
|
|
277
295
|
c.store(
|
@@ -291,15 +309,10 @@ class Brut::Framework::Config
|
|
291
309
|
|
292
310
|
c.store(
|
293
311
|
"instrumentation",
|
294
|
-
Brut::Instrumentation::
|
312
|
+
Brut::Instrumentation::OpenTelemetry,
|
295
313
|
"Interface for recording instrumentable events and subscribing to them",
|
296
|
-
|
297
|
-
|
298
|
-
Brut::Instrumentation::Basic.new
|
299
|
-
else
|
300
|
-
Brut::Instrumentation::Basic::TypeChecking.new
|
301
|
-
end
|
302
|
-
end
|
314
|
+
Brut::Instrumentation::OpenTelemetry.new
|
315
|
+
)
|
303
316
|
|
304
317
|
# App can override
|
305
318
|
|
@@ -410,6 +423,36 @@ class Brut::Framework::Config
|
|
410
423
|
allow_app_override: true,
|
411
424
|
allow_nil: true,
|
412
425
|
)
|
426
|
+
|
427
|
+
c.store(
|
428
|
+
"local_hostname",
|
429
|
+
String,
|
430
|
+
"If present, this is an additional host on which your app responds locally. Useful if you have local domain names set up for dev",
|
431
|
+
nil,
|
432
|
+
allow_app_override: true,
|
433
|
+
allow_nil: true
|
434
|
+
)
|
435
|
+
|
436
|
+
|
437
|
+
c.store(
|
438
|
+
"permitted_hosts",
|
439
|
+
Array,
|
440
|
+
"An array of hostnames or IPAddr objects representing which hosts this app will respond to",
|
441
|
+
) do |local_hostname,project_env|
|
442
|
+
if project_env.production?
|
443
|
+
[]
|
444
|
+
else
|
445
|
+
[
|
446
|
+
local_hostname,
|
447
|
+
"localhost",
|
448
|
+
".localhost",
|
449
|
+
".test",
|
450
|
+
IPAddr.new("0.0.0.0/0"),
|
451
|
+
IPAddr.new("::/0"),
|
452
|
+
].compact
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
413
456
|
end
|
414
457
|
end
|
415
458
|
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
require "fileutils"
|
2
2
|
|
3
3
|
module Brut
|
4
|
+
# Provides access to the singelton container that contains all of Brut's current configuration.
|
4
5
|
def self.container(&block)
|
5
6
|
@container ||= Brut::Framework::Container.new
|
6
7
|
if !block.nil?
|
@@ -19,31 +20,49 @@ end
|
|
19
20
|
# and can depend on other values in this container.
|
20
21
|
#
|
21
22
|
# There is no namespacing/hierarchy.
|
23
|
+
#
|
24
|
+
# In general, you should not create instances of this class, but you may need to access it via {Brut.container} in order to obtain
|
25
|
+
# configuration values or set your own.
|
22
26
|
class Brut::Framework::Container
|
23
27
|
def initialize
|
24
28
|
@container = {}
|
25
29
|
end
|
26
30
|
|
27
|
-
# Store a named value for later.
|
31
|
+
# Store a named value for later. You can use this to store static values, or dynamic values that require the values of other stored
|
32
|
+
# values.
|
33
|
+
#
|
34
|
+
# @param [String] name The name of the value. This should be a string that is a valid Ruby identifier. If `type` is a boolean, this
|
35
|
+
# parameter must end in a question mark.
|
36
|
+
# @param [String] type Description of the type that the value should conform to. if this value is "boolean" or :boolean, then
|
37
|
+
# the value will be coerced into `true` or `false`. Otherwise, this serves as only documentation for now.
|
38
|
+
# @param [String] description Documentation as to what this value is for.
|
39
|
+
# @param [Object] value if given, this is the value to use. If the value you want is dynamically determined, or you want to create
|
40
|
+
# it lazily, pass a block.
|
41
|
+
# @param [true|false] allow_app_override if true, the app may override this value. Default is false, which means the app cannot.
|
42
|
+
# This is mostly useful for Brut internals to ensure the app doesnt' wreak havoc
|
43
|
+
# on things it should not mess with.
|
44
|
+
# @param [true|false] allow_nil if true, this value may be nil and if `allow_app_override` is true, the app can override the value
|
45
|
+
# to be `nil`. The default is false, which means `nil` is not allowed. Generally, you don't want
|
46
|
+
# `nil`. `nil` is no good for nobody.
|
47
|
+
# @yield [*any] Yields any existing configuration values to the block as *positional parameters*.
|
48
|
+
# The names of the parameters must match the name of another configuration value.
|
49
|
+
# @yieldreturn [Object] the value to use for this configuration option. This is memoized, so the block will not be called again.
|
28
50
|
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
# the value will be coerced into `true` or `false`. Otherwise, this serves as documentation for now.
|
32
|
-
# description:: Documentation as to what this value is for.
|
33
|
-
# value:: if given, this is the value to use.
|
34
|
-
# block:: If value is omitted, block will be evaluated the first time the value is
|
35
|
-
# fetched and is expected to return the value to use for all subsequent
|
36
|
-
# requests.
|
51
|
+
# @example Storing a static value
|
52
|
+
# container.store("num_retries",Integer,"Number of times to retry",10)
|
37
53
|
#
|
38
|
-
#
|
39
|
-
#
|
54
|
+
# @example Storing a dynamic value based on another one
|
55
|
+
# container.store("num_retries",Integer,"Number of times to retry",10)
|
56
|
+
# container.store("max_retry_ms",Integer,"Number of times to retry") { |num_retries|
|
57
|
+
# num_retries * 100
|
58
|
+
# }
|
40
59
|
#
|
41
|
-
#
|
42
|
-
#
|
60
|
+
# @see #store_required_path
|
61
|
+
# @see #store_ensured_path
|
43
62
|
#
|
44
|
-
#
|
45
|
-
#
|
46
|
-
#
|
63
|
+
# @raise [ArgumentError] if the name has already been specified, if this is a boolean and the name doesn't
|
64
|
+
# end in a question mark, or if this a `Pathname` and the name does not end in `_dir`
|
65
|
+
# or `_file`.
|
47
66
|
def store(name,type,description,value=:use_block,allow_app_override: false,allow_nil: false,&block)
|
48
67
|
# TODO: Check that value / block is used properly
|
49
68
|
name = name.to_s
|
@@ -72,6 +91,16 @@ class Brut::Framework::Container
|
|
72
91
|
self
|
73
92
|
end
|
74
93
|
|
94
|
+
# Called by your app to override an existing value. The value must be overridable (see {#store}). Generally, you should call this
|
95
|
+
# in the initializer of your {Brut::Framework::App} subclass. Calling this after the fact may not have the affect you want.
|
96
|
+
#
|
97
|
+
# @param [String|Symbol] name name of the value to override. Will be coerced to a String. This name must have been previously
|
98
|
+
# configured.
|
99
|
+
# @param [Object] value if given, this is the value to use. If omitted, the block is called
|
100
|
+
#
|
101
|
+
# @yield [*any] Yields any existing configuration values to the block as *positional parameters*.
|
102
|
+
# The names of the parameters must match the name of another configuration value.
|
103
|
+
# @yieldreturn [Object] the value to use for this configuration option. This is memoized, so the block will not be called again.
|
75
104
|
def override(name,value=:use_block,&block)
|
76
105
|
name = name.to_s
|
77
106
|
if !@container[name]
|
@@ -88,46 +117,76 @@ class Brut::Framework::Container
|
|
88
117
|
end
|
89
118
|
|
90
119
|
# Store a value that represents a path that must exist. The value will
|
91
|
-
# be assumed to be
|
120
|
+
# be assumed to be a `Pathname` and the `name` must end in `_dir` or `_file`.
|
121
|
+
# Note that the value's existence is not checked until it is requested. When it is,
|
122
|
+
# an exception will be raised if it does not exist.
|
123
|
+
#
|
124
|
+
# @param [Symbol|String] name of this value. Must end in `_dir` or `_file`.
|
125
|
+
# @param description [String] description documentation of what this value is for
|
126
|
+
# @param [Object] value if given, this is the value to use. If omitted, the block is called
|
127
|
+
#
|
128
|
+
# @yield [*any] Yields any existing configuration values to the block as *positional parameters*.
|
129
|
+
# The names of the parameters must match the name of another configuration value.
|
130
|
+
# @yieldreturn [Object] the value to use for this configuration option. This is memoized, so the block will not be called again.
|
92
131
|
def store_required_path(name,description,value=:use_block,&block)
|
93
132
|
self.store(name,Pathname,description,value,&block)
|
94
133
|
@container[name][:required_path] = true
|
95
134
|
self
|
96
135
|
end
|
97
136
|
|
98
|
-
# Store a value that represents a path that
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
137
|
+
# Store a value that represents a path that will be created if it doesn't exist.
|
138
|
+
# The value will be assumed to be a `Pathname` and the `name` must end in `_dir` or `_file`.
|
139
|
+
#
|
140
|
+
# This is preferred over {#store_required_path} so that you don't have to have a bunch of `.keep` files hanging around
|
141
|
+
# just for your version control system. The path will be created as a directory whenever it is first accessed.
|
142
|
+
#
|
143
|
+
# @param [Symbol|String] name of this value. Must end in `_dir` or `_file` (though ending it in `_file` doesn't make much sense).
|
144
|
+
# @param description [String] description documentation of what this value is for
|
145
|
+
# @param [Object] value if given, this is the value to use. If omitted, the block is called
|
146
|
+
#
|
147
|
+
# @yield [*any] Yields any existing configuration values to the block as *positional parameters*.
|
148
|
+
# The names of the parameters must match the name of another configuration value.
|
149
|
+
# @yieldreturn [Object] the value to use for this configuration option. This is memoized, so the block will not be called again.
|
102
150
|
def store_ensured_path(name,description,value=:use_block,&block)
|
103
151
|
self.store(name,Pathname,description,value,&block)
|
104
152
|
@container[name][:ensured_path] = true
|
105
153
|
self
|
106
154
|
end
|
107
155
|
|
108
|
-
#
|
156
|
+
# Provides method-like access to configured values. Only configured values will respond and only
|
157
|
+
# if the accessor method is called without parameters and without a block. See {#fetch}.
|
158
|
+
#
|
159
|
+
# @example
|
160
|
+
# Brut.container.store("num_retries",Integer,"Number of times to retry",10)
|
161
|
+
# Brut.container.num_retries # => 10
|
109
162
|
def method_missing(sym,*args,&block)
|
110
163
|
if args.length == 0 && block.nil? && self.respond_to_missing?(sym)
|
111
|
-
|
164
|
+
fetch(sym.to_s)
|
112
165
|
else
|
113
166
|
super.method_missing(sym,*args,&block)
|
114
167
|
end
|
115
168
|
end
|
116
169
|
|
117
|
-
#
|
170
|
+
# Required for good decorum when overriding {#method_missing}.
|
171
|
+
#
|
172
|
+
# @param [String|Symbol] name the name of a previously-configured value.
|
173
|
+
#
|
174
|
+
# @return [true|false] true if `name` has been configured
|
118
175
|
def respond_to_missing?(name,include_private=false)
|
119
176
|
@container.key?(name.to_s)
|
120
177
|
end
|
121
178
|
|
122
|
-
|
123
|
-
#
|
179
|
+
# Fetch the value given a name. For lazily-defined values, this will call all necessary blocks needed to determine the value. Thus,
|
180
|
+
# any number of other blocks could be called, depending on what values are needed.
|
181
|
+
#
|
182
|
+
# @param name [Symbol|String] the name of the value to fetch.
|
183
|
+
#
|
184
|
+
# @return [Object] the configured value, if it has been configured. Note that if the value was defined with `allow_nil: true`
|
185
|
+
# passed to {#store}, `nil` could be returned.
|
186
|
+
# @raise [KeyError] if `name` has not been previously stored
|
187
|
+
# @raise [Brut::Framework::Errors::NotFound] if a path stored with {#store_required_path} does not exist
|
124
188
|
def fetch(name)
|
125
|
-
|
126
|
-
end
|
127
|
-
|
128
|
-
private
|
129
|
-
|
130
|
-
def fetch_value(name)
|
189
|
+
name = name.to_s
|
131
190
|
# TODO: Provide a cleanr impl and better error checking if things go wrong
|
132
191
|
x = @container.fetch(name)
|
133
192
|
|
@@ -158,11 +217,16 @@ private
|
|
158
217
|
handle_path_values(name,x)
|
159
218
|
x[:value]
|
160
219
|
end
|
220
|
+
private
|
161
221
|
|
162
222
|
def handle_path_values(name,contained_value)
|
163
223
|
value = contained_value[:value]
|
164
224
|
if contained_value[:required_path] && !Dir.exist?(value)
|
165
|
-
raise
|
225
|
+
raise Brut::Framework::Errors::NotFound.new(
|
226
|
+
resource_name: value,
|
227
|
+
id: name,
|
228
|
+
contetx: "For value '#{name}', the directory is represents must exist, but does not: '#{value}'"
|
229
|
+
)
|
166
230
|
end
|
167
231
|
if contained_value[:ensured_path]
|
168
232
|
FileUtils.mkdir_p value
|
@@ -1,9 +1,5 @@
|
|
1
|
+
# Raised when a method must be defined by a subclass. This is useful for making it clear
|
2
|
+
# which methods a subclass is expected to override and for which no default behavior
|
3
|
+
# makes sense.
|
1
4
|
class Brut::Framework::Errors::AbstractMethod < Brut::Framework::Error
|
2
|
-
def initialize(message=nil)
|
3
|
-
if message.nil?
|
4
|
-
super
|
5
|
-
else
|
6
|
-
super("Subclass must implement: #{message}")
|
7
|
-
end
|
8
|
-
end
|
9
5
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# Raised when an expected parameter in e.g. a path or a method invocation is
|
2
|
+
# not available. This allows classes to give a better error message than
|
3
|
+
# the one provided by standard library exceptions like `KeyError`
|
4
|
+
class Brut::Framework::Errors::MissingParameter < Brut::Framework::Error
|
5
|
+
# Create the exception
|
6
|
+
# @param [String|Symbol] missing_param the name of the missing parameter.
|
7
|
+
# @param [Array<String|Symbol>] params_received the parameters that were received in the context that generated this error
|
8
|
+
# @param [String] context Any additional context to understand the error
|
9
|
+
def initialize(missing_param, params_received:, context:)
|
10
|
+
super("Parameter '#{missing_param}' was not available. Received params: #{params_received.empty? ? 'no params' : "'" + params_received.join(', ') + "'"}. #{context}")
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# Raised when a path has been declared, but the class to handle it cannot be found in the app.
|
2
|
+
class Brut::Framework::Errors::NoClassForPath < Brut::Framework::Error
|
3
|
+
# Array of names that, if joined with `::` would name the class that could not be found
|
4
|
+
# @return [Array<String>] array of parts. For a class named `Auth::LoginPage`, would return `["Auth","LoginPage"]`
|
5
|
+
attr_reader :class_name_path
|
6
|
+
# The path template that the class that couldn't be found was intended to handle
|
7
|
+
# @return [String] a path template as given inside {Brut::Framework::App.routes}
|
8
|
+
attr_reader :path_template
|
9
|
+
|
10
|
+
# Create the exception
|
11
|
+
# @param [Array<String>] class_name_path array of names that, if joined with `::` would name the class that could not be found
|
12
|
+
# @param [String] path_template The path template that the class that couldn't be found was intended to handle
|
13
|
+
# @param [NameError] name_error The `NameError` that was caught
|
14
|
+
def initialize(class_name_path:, path_template:, name_error:)
|
15
|
+
@class_name_path = class_name_path
|
16
|
+
@path_template = path_template
|
17
|
+
module_message = if name_error.receiver == Module
|
18
|
+
"Could not find"
|
19
|
+
else
|
20
|
+
"Module '#{name_error.receiver}' did not have"
|
21
|
+
end
|
22
|
+
message = "Cannot find page class for route '#{path_template}', which should be #{class_name_path.join("::")}. #{module_message} the class or module '#{name_error.name}'"
|
23
|
+
super(message)
|
24
|
+
end
|
25
|
+
end
|
@@ -1,10 +1,20 @@
|
|
1
1
|
# Indicates that a resource or database row does not exist.
|
2
2
|
class Brut::Framework::Errors::NotFound < Brut::Framework::Error
|
3
|
-
|
3
|
+
# @param [String] resource_name Name of the type of resource
|
4
|
+
# @param [String|Int] id Identifier of the resource. If present, search_terms is ignored.
|
5
|
+
# @param [Object] search_terms If provided, these are the search terms used. Will be converted to a string via `inspect`. Ignored if
|
6
|
+
# id is present.
|
7
|
+
# @param [String] context Any additional context about what went wrong
|
8
|
+
def initialize(resource_name:,id: nil, search_terms: nil,context:nil)
|
4
9
|
if !context.nil?
|
5
10
|
context = ": #{context}"
|
6
11
|
end
|
7
|
-
|
12
|
+
fragment = if id.nil?
|
13
|
+
"Search '#{search_terms.inspect}'"
|
14
|
+
else
|
15
|
+
"ID '#{id}'"
|
16
|
+
end
|
17
|
+
super("Could not find a #{resource_name} using #{fragment}#{context}")
|
8
18
|
end
|
9
19
|
end
|
10
20
|
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Thrown when use of a feature of Brut is detected, but that
|
2
|
+
# feature is not yet implemented. This is advisory only and
|
3
|
+
# not a promise to ever implement that feature. It's mostly used
|
4
|
+
# when two APIs are very similar and one might expect both to
|
5
|
+
# support the same features, but for technical reasons one of the APIs does not.
|
6
|
+
class Brut::Framework::Errors::NotImplemented < Brut::Framework::Error
|
7
|
+
def initialize(message=nil)
|
8
|
+
if message.nil?
|
9
|
+
super
|
10
|
+
else
|
11
|
+
super("NOT IMPLEMENTED: #{message}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|