brut 0.0.1 → 0.0.2
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 +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
|