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
|
@@ -3,8 +3,6 @@ require "uri"
|
|
|
3
3
|
# Holds the registered routes for this app.
|
|
4
4
|
class Brut::FrontEnd::Routing
|
|
5
5
|
|
|
6
|
-
include SemanticLogger::Loggable
|
|
7
|
-
|
|
8
6
|
def initialize
|
|
9
7
|
@routes = Set.new
|
|
10
8
|
end
|
|
@@ -21,6 +19,10 @@ class Brut::FrontEnd::Routing
|
|
|
21
19
|
new_routes = @routes.map { |route|
|
|
22
20
|
if route.class == Route
|
|
23
21
|
route.class.new(route.http_method,route.path_template)
|
|
22
|
+
elsif route.class == MissingPage || route.class == MissingHandler || route.class == MissingForm
|
|
23
|
+
route.class.new(route.path_template,route.exception)
|
|
24
|
+
elsif route.class == MissingPath
|
|
25
|
+
route.class.new(route.method,route.path_template,route.exception)
|
|
24
26
|
else
|
|
25
27
|
route.class.new(route.path_template)
|
|
26
28
|
end
|
|
@@ -35,28 +37,60 @@ class Brut::FrontEnd::Routing
|
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def register_page(path)
|
|
38
|
-
route =
|
|
40
|
+
route = begin
|
|
41
|
+
PageRoute.new(path)
|
|
42
|
+
rescue Brut::Framework::Errors::NoClassForPath => ex
|
|
43
|
+
if Brut.container.project_env.development?
|
|
44
|
+
MissingPage.new(path,ex)
|
|
45
|
+
else
|
|
46
|
+
raise ex
|
|
47
|
+
end
|
|
48
|
+
end
|
|
39
49
|
@routes << route
|
|
40
50
|
add_routing_method(route)
|
|
41
51
|
route
|
|
42
52
|
end
|
|
43
53
|
|
|
44
54
|
def register_form(path)
|
|
45
|
-
route =
|
|
55
|
+
route = begin
|
|
56
|
+
FormRoute.new(path)
|
|
57
|
+
rescue Brut::Framework::Errors::NoClassForPath => ex
|
|
58
|
+
if Brut.container.project_env.development?
|
|
59
|
+
MissingForm.new(path,ex)
|
|
60
|
+
else
|
|
61
|
+
raise ex
|
|
62
|
+
end
|
|
63
|
+
end
|
|
46
64
|
@routes << route
|
|
47
65
|
add_routing_method(route)
|
|
48
66
|
route
|
|
49
67
|
end
|
|
50
68
|
|
|
51
69
|
def register_handler_only(path)
|
|
52
|
-
route =
|
|
70
|
+
route = begin
|
|
71
|
+
FormHandlerRoute.new(path)
|
|
72
|
+
rescue Brut::Framework::Errors::NoClassForPath => ex
|
|
73
|
+
if Brut.container.project_env.development?
|
|
74
|
+
MissingHandler.new(path,ex)
|
|
75
|
+
else
|
|
76
|
+
raise ex
|
|
77
|
+
end
|
|
78
|
+
end
|
|
53
79
|
@routes << route
|
|
54
80
|
add_routing_method(route)
|
|
55
81
|
route
|
|
56
82
|
end
|
|
57
83
|
|
|
58
84
|
def register_path(path, method:)
|
|
59
|
-
route =
|
|
85
|
+
route = begin
|
|
86
|
+
Route.new(method, path)
|
|
87
|
+
rescue Brut::Framework::Errors::NoClassForPath => ex
|
|
88
|
+
if Brut.container.project_env.development?
|
|
89
|
+
MissingPath.new(method,path,ex)
|
|
90
|
+
else
|
|
91
|
+
raise ex
|
|
92
|
+
end
|
|
93
|
+
end
|
|
60
94
|
@routes << route
|
|
61
95
|
add_routing_method(route)
|
|
62
96
|
route
|
|
@@ -73,7 +107,11 @@ class Brut::FrontEnd::Routing
|
|
|
73
107
|
handler_class_match || form_class_match
|
|
74
108
|
}
|
|
75
109
|
if !route
|
|
76
|
-
|
|
110
|
+
if handler_class.ancestors.include?(Brut::FrontEnd::Form)
|
|
111
|
+
raise ArgumentError,"There is no configured route for the form #{handler_class} and/or the handler class for this form doesn't exist"
|
|
112
|
+
else
|
|
113
|
+
raise ArgumentError,"There is no configured route for #{handler_class}"
|
|
114
|
+
end
|
|
77
115
|
end
|
|
78
116
|
route
|
|
79
117
|
end
|
|
@@ -116,8 +154,6 @@ class Brut::FrontEnd::Routing
|
|
|
116
154
|
|
|
117
155
|
class Route
|
|
118
156
|
|
|
119
|
-
include SemanticLogger::Loggable
|
|
120
|
-
|
|
121
157
|
attr_reader :handler_class, :path_template, :http_method
|
|
122
158
|
|
|
123
159
|
def initialize(method,path_template)
|
|
@@ -133,24 +169,37 @@ class Brut::FrontEnd::Routing
|
|
|
133
169
|
@handler_class = self.locate_handler_class(self.suffix,self.preposition)
|
|
134
170
|
end
|
|
135
171
|
|
|
172
|
+
def path_params
|
|
173
|
+
@path_template.split(/\//).map { |path_part|
|
|
174
|
+
if path_part =~ /^:(.+)$/
|
|
175
|
+
$1.to_sym
|
|
176
|
+
else
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
}.compact
|
|
180
|
+
end
|
|
181
|
+
|
|
136
182
|
def path(**query_string_params)
|
|
137
183
|
path = @path_template.split(/\//).map { |path_part|
|
|
138
184
|
if path_part =~ /^:(.+)$/
|
|
139
185
|
param_name = $1.to_sym
|
|
140
186
|
if !query_string_params.key?(param_name)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
raise ArgumentError,"path for #{@handler_class} requires '#{param_name}' as a path parameter, but it was not specified to #path. Got #{query_string_params_for_message}"
|
|
187
|
+
raise Brut::Framework::Errors::MissingParameter.new(
|
|
188
|
+
param_name,
|
|
189
|
+
params_received: query_string_params.keys,
|
|
190
|
+
context: ":#{param_name} was used as a path parameter for #{@handler_class} (path '#{@path_template}')"
|
|
191
|
+
)
|
|
147
192
|
end
|
|
148
193
|
query_string_params.delete(param_name)
|
|
149
194
|
else
|
|
150
195
|
path_part
|
|
151
196
|
end
|
|
152
197
|
}
|
|
153
|
-
|
|
198
|
+
joined_path = path.join("/")
|
|
199
|
+
if joined_path == ""
|
|
200
|
+
joined_path = "/"
|
|
201
|
+
end
|
|
202
|
+
uri = URI(joined_path)
|
|
154
203
|
uri.query = URI.encode_www_form(query_string_params)
|
|
155
204
|
uri
|
|
156
205
|
end
|
|
@@ -162,7 +211,7 @@ class Brut::FrontEnd::Routing
|
|
|
162
211
|
private
|
|
163
212
|
def locate_handler_class(suffix,preposition, on_missing: :raise)
|
|
164
213
|
if @path_template == "/"
|
|
165
|
-
return Module.const_get("HomePage")
|
|
214
|
+
return Module.const_get("HomePage") # XXX Needs error handling
|
|
166
215
|
end
|
|
167
216
|
path_parts = @path_template.split(/\//)[1..-1]
|
|
168
217
|
|
|
@@ -184,20 +233,20 @@ class Brut::FrontEnd::Routing
|
|
|
184
233
|
array
|
|
185
234
|
}
|
|
186
235
|
part_names[-1] += suffix
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
236
|
+
begin
|
|
237
|
+
part_names.inject(Module) { |mod,path_element|
|
|
238
|
+
mod.const_get(path_element,mod == Module)
|
|
239
|
+
}
|
|
240
|
+
rescue NameError => ex
|
|
241
|
+
if on_missing == :raise
|
|
242
|
+
raise Brut::Framework::Errors::NoClassForPath.new(
|
|
243
|
+
class_name_path: part_names,
|
|
244
|
+
path_template: @path_template,
|
|
245
|
+
name_error: ex,
|
|
246
|
+
)
|
|
247
|
+
else
|
|
248
|
+
nil
|
|
249
|
+
end
|
|
201
250
|
end
|
|
202
251
|
end
|
|
203
252
|
|
|
@@ -206,6 +255,64 @@ class Brut::FrontEnd::Routing
|
|
|
206
255
|
|
|
207
256
|
end
|
|
208
257
|
|
|
258
|
+
class MissingPage < Route
|
|
259
|
+
attr_reader :exception
|
|
260
|
+
def initialize(path_template,ex)
|
|
261
|
+
@http_method = Brut::FrontEnd::HttpMethod.new(:get)
|
|
262
|
+
@path_template = path_template
|
|
263
|
+
@handler_class = begin
|
|
264
|
+
page_class = Class.new(Brut::FrontEnd::Pages::MissingPage)
|
|
265
|
+
compressed_class_name = ex.class_name_path.join
|
|
266
|
+
Module.const_set(:"BrutMissingPages#{compressed_class_name}",page_class)
|
|
267
|
+
page_class
|
|
268
|
+
end
|
|
269
|
+
@exception = ex
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
class MissingHandler < Route
|
|
274
|
+
attr_reader :exception
|
|
275
|
+
def initialize(path_template,ex)
|
|
276
|
+
@http_method = Brut::FrontEnd::HttpMethod.new(:post)
|
|
277
|
+
@path_template = path_template
|
|
278
|
+
@handler_class = begin
|
|
279
|
+
handler_class = Class.new(Brut::FrontEnd::Handlers::MissingHandler)
|
|
280
|
+
compressed_class_name = ex.class_name_path.join
|
|
281
|
+
Module.const_set(:"BrutMissingHandlers#{compressed_class_name}",handler_class)
|
|
282
|
+
handler_class
|
|
283
|
+
end
|
|
284
|
+
@exception = ex
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
class MissingPath < Route
|
|
289
|
+
attr_reader :exception
|
|
290
|
+
def initialize(method,path_template,ex)
|
|
291
|
+
@http_method = Brut::FrontEnd::HttpMethod.new(method)
|
|
292
|
+
@path_template = path_template
|
|
293
|
+
@handler_class = begin
|
|
294
|
+
handler_class = Class.new(Brut::FrontEnd::Handlers::MissingHandler)
|
|
295
|
+
compressed_class_name = ex.class_name_path.join
|
|
296
|
+
Module.const_set(:"BrutMissingHandlers#{compressed_class_name}",handler_class)
|
|
297
|
+
handler_class
|
|
298
|
+
end
|
|
299
|
+
@exception = ex
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
class MissingForm < MissingHandler
|
|
304
|
+
attr_reader :form_class
|
|
305
|
+
def initialize(path_template,ex)
|
|
306
|
+
super
|
|
307
|
+
@form_class = begin
|
|
308
|
+
form_class = Class.new(Brut::FrontEnd::Handlers::MissingHandler::Form)
|
|
309
|
+
compressed_class_name = ex.class_name_path.join
|
|
310
|
+
Module.const_set(:"BrutMissingForms#{compressed_class_name}",form_class)
|
|
311
|
+
form_class
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
|
|
209
316
|
class PageRoute < Route
|
|
210
317
|
def initialize(path_template)
|
|
211
318
|
super(Brut::FrontEnd::HttpMethod.new(:get),path_template)
|
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
# Generally, this can act like a Hash for setting and accessing values stored in the session.
|
|
3
3
|
# It provides a few useful additions:
|
|
4
4
|
#
|
|
5
|
-
# * Your app can extend this to provide an app-specific API around the session.
|
|
5
|
+
# * Your app can extend this to provide an app-specific API around the session, using `Brut.container.override("session_class",«your class»)`.
|
|
6
6
|
# * There is direct access to commonly-used data stored in the session, such as the flash.
|
|
7
7
|
class Brut::FrontEnd::Session
|
|
8
|
+
# Create the session based on the session provided by Rack.
|
|
9
|
+
#
|
|
10
|
+
# @param [Rack session] rack_session the session as provided by Rack. This is treated as a Hash.
|
|
8
11
|
def initialize(rack_session:)
|
|
9
12
|
@rack_session = rack_session
|
|
10
13
|
end
|
|
11
14
|
|
|
15
|
+
# Return the interpretation of the `Accept-Language` header that was set by (#http_accept_language=).
|
|
16
|
+
#
|
|
17
|
+
# @return [Brut::I18n::HTTPAcceptLanguage] Never returns `nil`. If the value is corrupted or invalid, an instance that uses English
|
|
18
|
+
# will be returned.
|
|
12
19
|
def http_accept_language = Brut::I18n::HTTPAcceptLanguage.from_session(self[:__brut_http_accept_language])
|
|
20
|
+
|
|
21
|
+
# Set the `Accept-Language` for the session, as an {Brut::I18n::HTTPAcceptLanguage}
|
|
22
|
+
# @param [Brut::I18n::HTTPAcceptLanguage] http_accept_language
|
|
13
23
|
def http_accept_language=(http_accept_language)
|
|
14
24
|
self[:__brut_http_accept_language] = http_accept_language.for_session
|
|
15
25
|
end
|
|
16
26
|
|
|
17
|
-
# Get the timezone as reported by the browser,
|
|
18
|
-
#
|
|
27
|
+
# Get the timezone as reported by the browser, or nil if there isn't one or the browser sent and invalid value
|
|
28
|
+
#
|
|
29
|
+
# @return [TZInfo::Timezone|nil]
|
|
19
30
|
def timezone_from_browser
|
|
20
31
|
tz_name = self[:__brut_timezone_from_browser]
|
|
21
32
|
if tz_name.nil?
|
|
@@ -24,13 +35,14 @@ class Brut::FrontEnd::Session
|
|
|
24
35
|
begin
|
|
25
36
|
TZInfo::Timezone.get(tz_name)
|
|
26
37
|
rescue TZInfo::InvalidTimezoneIdentifier => ex
|
|
27
|
-
|
|
38
|
+
Brut.container.instrumentation.record_exception(ex, class: self.class)
|
|
28
39
|
nil
|
|
29
40
|
end
|
|
30
41
|
end
|
|
31
42
|
|
|
32
|
-
# Set the timezone as reported by the browser.
|
|
33
|
-
#
|
|
43
|
+
# Set the timezone as reported by the browser.
|
|
44
|
+
#
|
|
45
|
+
# @param timezone [TZInfo::Timezone|String] The timezone, or name of a timezone suitable for use with `TZInfo::Timezone`.
|
|
34
46
|
def timezone_from_browser=(timezone)
|
|
35
47
|
if timezone.kind_of?(TZInfo::Timezone)
|
|
36
48
|
timezone = timezone.name
|
|
@@ -38,18 +50,85 @@ class Brut::FrontEnd::Session
|
|
|
38
50
|
self[:__brut_timezone_from_browser] = timezone
|
|
39
51
|
end
|
|
40
52
|
|
|
53
|
+
# Set the session timezone, regardless of what the browser reports.
|
|
54
|
+
#
|
|
55
|
+
# @param timezone [TZInfo::Timezone|String|nil] The timezone, or name of a timezone suitable for use with `TZInfo::Timezone`. Use
|
|
56
|
+
# `nil` to clear this value and use the browser's time zone.
|
|
57
|
+
def timezone=(timezone)
|
|
58
|
+
if timezone.kind_of?(TZInfo::Timezone)
|
|
59
|
+
timezone = timezone.name
|
|
60
|
+
end
|
|
61
|
+
self[:__brut_timezone_override] = timezone
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get the session timezone. This is the preferred way to get a timezone for the current session. Always returns a value, based on
|
|
65
|
+
# the following logic:
|
|
66
|
+
#
|
|
67
|
+
# 1. If {#timezone=} has been called with a value, that time zone is returned.
|
|
68
|
+
# 2. If {#timezone=} has been given no value or `nil`, and {#timezone_from_browser} returns a value, that value is used.
|
|
69
|
+
# 3. If {#timezone_from_browser} returns `nil`, `ENV["TZ"]` is used, assuming it is a valid time zone.
|
|
70
|
+
# 4. If 'ENV["TZ"]` is blank or invalid, UTC is returned.
|
|
71
|
+
#
|
|
72
|
+
# It is in your best interest to ensure that each session has a valid time zone.
|
|
73
|
+
#
|
|
74
|
+
# @return [TZInfo::Timezone]
|
|
75
|
+
def timezone
|
|
76
|
+
tz_name = self[:__brut_timezone_override]
|
|
77
|
+
timezone = nil
|
|
78
|
+
if !tz_name.nil?
|
|
79
|
+
begin
|
|
80
|
+
timezone = TZInfo::Timezone.get(tz_name)
|
|
81
|
+
rescue TZInfo::InvalidTimezoneIdentifier => ex
|
|
82
|
+
Brut.container.instrumentation.record_exception(ex, class: self.class, invalid_tz_name: tz_name)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
if timezone.nil?
|
|
86
|
+
timezone = self.timezone_from_browser
|
|
87
|
+
end
|
|
88
|
+
if timezone.nil?
|
|
89
|
+
begin
|
|
90
|
+
timezone = if ENV["TZ"]
|
|
91
|
+
TZInfo::Timezone.get(ENV["TZ"])
|
|
92
|
+
else
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
rescue TZInfo::InvalidTimezoneIdentifier => ex
|
|
96
|
+
Brut.container.instrumentation.record_exception(ex, class: self.class, invalid_env_tz: ENV['TZ'])
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
if timezone.nil?
|
|
101
|
+
timezone = TZInfo::Timezone.get("UTC")
|
|
102
|
+
end
|
|
103
|
+
timezone
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Access the underlying session directly
|
|
107
|
+
#
|
|
108
|
+
# @param [Symbol|String] key the key to use. Coerced into a string.
|
|
109
|
+
# @return [Object] whatever value, including `nil`, is in the session for this key.
|
|
41
110
|
def[](key) = @rack_session[key.to_s]
|
|
42
111
|
|
|
112
|
+
# Set the session value for the key.
|
|
113
|
+
#
|
|
114
|
+
# @param [Symbol|String] key the key to use. Coerced into a string.
|
|
115
|
+
# @param [Object] value Value to use. Note that this value may be coerced into a string in a way that may not work for your use case.
|
|
116
|
+
# You are encouraged to send in a string. If you want to store rich data in the session, maybe don't? But if you must, add a
|
|
117
|
+
# method to do the marshalling in your app's subclass of this
|
|
43
118
|
def[]=(key,value)
|
|
44
119
|
@rack_session[key.to_s] = value
|
|
45
120
|
end
|
|
46
121
|
|
|
122
|
+
# Delete a key from the session. This is preferred to setting the value to `nil`
|
|
47
123
|
def delete(key) = @rack_session.delete(key.to_s)
|
|
48
124
|
|
|
49
|
-
# Access the flash, as an instance of whatever class has been configured.
|
|
125
|
+
# Access the flash, as an instance of whatever class has been configured. Note that this returns a copy of the flash, so any changes
|
|
126
|
+
# will not be stored in the session unless you call (#flash=) after changing it. Generally, this isn't a big deal as Brut handles
|
|
127
|
+
# this for you.
|
|
50
128
|
def flash
|
|
51
129
|
Brut.container.flash_class.from_h(self[:__brut_flash])
|
|
52
130
|
end
|
|
131
|
+
# Set the flash.
|
|
53
132
|
def flash=(new_flash)
|
|
54
133
|
self[:__brut_flash] = new_flash.to_h
|
|
55
134
|
end
|
|
@@ -1,31 +1,46 @@
|
|
|
1
1
|
require "temple"
|
|
2
2
|
|
|
3
|
+
# Holds code related to rendering ERB templates
|
|
3
4
|
module Brut::FrontEnd::Templates
|
|
4
5
|
autoload(:HTMLSafeString,"brut/front_end/templates/html_safe_string")
|
|
5
6
|
autoload(:ERBParser,"brut/front_end/templates/erb_parser")
|
|
6
7
|
autoload(:EscapableFilter,"brut/front_end/templates/escapable_filter")
|
|
7
8
|
autoload(:BlockFilter,"brut/front_end/templates/block_filter")
|
|
8
9
|
autoload(:ERBEngine,"brut/front_end/templates/erb_engine")
|
|
10
|
+
autoload(:Locator,"brut/front_end/templates/locator")
|
|
9
11
|
end
|
|
10
12
|
|
|
11
|
-
# Handles rendering HTML templates
|
|
13
|
+
# Handles rendering HTML templates written in ERB. This is a light wrapper around `Tilt`.
|
|
14
|
+
# This also configured a few customizations to allow a Rails-like rendering of ERB:
|
|
15
|
+
#
|
|
16
|
+
# * HTML escaping by default
|
|
17
|
+
# * Helpers that return {Brut::FrontEnd::Templates::HTMLSafeString}s won't be escaped
|
|
18
|
+
#
|
|
19
|
+
# @see https://github.com/rtomayko/tilt
|
|
12
20
|
class Brut::FrontEnd::Template
|
|
13
21
|
|
|
22
|
+
# @!visibility private
|
|
23
|
+
# This sets up global state somewhere, even though we aren't using `TempleTemplate`
|
|
24
|
+
# anywhere.
|
|
14
25
|
TempleTemplate = Temple::Templates::Tilt(Brut::FrontEnd::Templates::ERBEngine,
|
|
15
26
|
register_as: "html.erb")
|
|
16
27
|
|
|
28
|
+
attr_reader :template_file_path
|
|
29
|
+
|
|
17
30
|
# Wraps a string that is deemed safe to insert into
|
|
18
31
|
# HTML without escaping it. This allows stuff like
|
|
19
32
|
# <%= component(SomeComponent) %> to work without
|
|
20
33
|
# having to remember to <%== all the time.
|
|
21
34
|
def initialize(template_file_path)
|
|
22
|
-
@
|
|
35
|
+
@template_file_path = template_file_path
|
|
36
|
+
@tilt_template = Tilt.new(@template_file_path)
|
|
23
37
|
end
|
|
24
38
|
|
|
25
39
|
def render_template(...)
|
|
26
40
|
@tilt_template.render(...)
|
|
27
41
|
end
|
|
28
42
|
|
|
43
|
+
# Convienience method to escape HTML in the canonical way.
|
|
29
44
|
def self.escape_html(string)
|
|
30
45
|
Brut::FrontEnd::Templates::EscapableFilter.escape_html(string)
|
|
31
46
|
end
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
# https://github.com/hanami/view/blob/main/lib/hanami/view/erb/filters/block.rb
|
|
1
|
+
# Allows rendering blocks in ERB the way Rails' helpers like `form_with` do.
|
|
2
|
+
# This is a slightly modified copy of Hanami's `Filters::Block`.
|
|
4
3
|
#
|
|
4
|
+
# @see https://github.com/hanami/view/blob/main/lib/hanami/view/erb/filters/block.rb
|
|
5
5
|
class Brut::FrontEnd::Templates::BlockFilter < Temple::Filter
|
|
6
6
|
END_LINE_RE = /\bend\b/
|
|
7
7
|
|
|
8
|
+
# @!visibility private
|
|
8
9
|
def on_erb_block(escape, code, content)
|
|
9
10
|
tmp = unique_name
|
|
10
11
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
#
|
|
3
3
|
# https://github.com/hanami/view/blob/main/lib/hanami/view/erb/parser.rb
|
|
4
4
|
#
|
|
5
|
-
# That is licensed MIT and thus so is this.
|
|
5
|
+
# That is licensed MIT and thus so is this file.
|
|
6
6
|
#
|
|
7
7
|
# Avoid changes to this file so it can be kept updated with Hanami.
|
|
8
8
|
class Brut::FrontEnd::Templates::ERBParser < Temple::Parser
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
class Brut::FrontEnd::Templates::EscapableFilter < Temple::Filters::Escapable
|
|
4
4
|
using Brut::FrontEnd::Templates::HTMLSafeString::Refinement
|
|
5
5
|
|
|
6
|
+
# @!visibility private
|
|
6
7
|
def initialize(opts = {})
|
|
7
8
|
opts[:escape_code] ||= "::Brut::FrontEnd::Templates::EscapableFilter.escape_html((%s))"
|
|
8
9
|
super(opts)
|
|
9
10
|
end
|
|
10
11
|
|
|
12
|
+
# @!visibility private
|
|
11
13
|
def self.escape_html(html)
|
|
12
14
|
if html.kind_of?(Brut::FrontEnd::Templates::HTMLSafeString)
|
|
13
15
|
html.string
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
# A wrapper around a string to indicate it is HTML-safe and
|
|
2
|
-
# can be rendered directly without escaping.
|
|
2
|
+
# can be rendered directly without escaping. This was done to avoid adding methods on `String` and the internal state
|
|
3
|
+
# required to make something like `"foo".html_safe!` work.
|
|
3
4
|
class Brut::FrontEnd::Templates::HTMLSafeString
|
|
5
|
+
# This can be used via `using` to add `html_safe!` and `html_safe?` method to `String` when they might be more convienient
|
|
6
|
+
# than using {Brut::FrontEnd::Templates::HTMLSafeString} directly.
|
|
4
7
|
module Refinement
|
|
5
8
|
refine String do
|
|
6
9
|
def html_safe! = Brut::FrontEnd::Templates::HTMLSafeString.from_string(self)
|
|
7
10
|
def html_safe? = false
|
|
8
11
|
end
|
|
9
12
|
end
|
|
13
|
+
using Refinement
|
|
14
|
+
|
|
15
|
+
# @return [String] the underlying string being wrapped
|
|
10
16
|
attr_reader :string
|
|
17
|
+
|
|
18
|
+
# Create an HTML safe string based on the parameter. It's recommended to use {.from_string} instead.
|
|
19
|
+
#
|
|
20
|
+
# @param [String] string A string that is considered safe to put directly into a web page without escaping.
|
|
11
21
|
def initialize(string)
|
|
12
22
|
@string = string
|
|
13
23
|
end
|
|
14
24
|
|
|
15
|
-
#
|
|
25
|
+
# Creates an HTML Safe string based on the parameter, properly handling if a HTML safe string is being passed.
|
|
26
|
+
#
|
|
27
|
+
# @param [String|Brut::FrontEnd::Templates::HTMLSafeString] string_or_html_safe_string the value to turn into an HTML safe string.
|
|
28
|
+
#
|
|
29
|
+
# @return [Brut::FrontEnd::Templates::HTMLSafeString] if `string_or_html_safe_string` is already HTML safe, returns it. Otherwise,
|
|
30
|
+
# wraps the string as HTML safe.
|
|
16
31
|
def self.from_string(string_or_html_safe_string)
|
|
17
32
|
if string_or_html_safe_string.kind_of?(self)
|
|
18
33
|
string_or_html_safe_string
|
|
@@ -24,12 +39,25 @@ class Brut::FrontEnd::Templates::HTMLSafeString
|
|
|
24
39
|
# This must be convertible to a string
|
|
25
40
|
def to_s = @string
|
|
26
41
|
def to_str = @string
|
|
42
|
+
# Matches the protocol in {Brut::FrontEnd::Templates::HTMLSafeString::Refinement}
|
|
43
|
+
# @return [Brut::FrontEnd::Templates::HTMLSafeString] self
|
|
27
44
|
def html_safe! = self
|
|
45
|
+
# Matches the protocol in {Brut::FrontEnd::Templates::HTMLSafeString::Refinement}
|
|
46
|
+
# @return [true|false] true
|
|
28
47
|
def html_safe? = true
|
|
48
|
+
|
|
49
|
+
# Return a new instance that has called `capitalize` on the underlying string
|
|
29
50
|
def capitalize = self.class.new(@string.capitalize)
|
|
51
|
+
# Return a new instance that has called `downcase` on the underlying string
|
|
30
52
|
def downcase = self.class.new(@string.downcase)
|
|
53
|
+
# Return a new instance that has called `upcase` on the underlying string
|
|
31
54
|
def upcase = self.class.new(@string.upcase)
|
|
32
55
|
|
|
56
|
+
# Returns the concatenation of two strings. If the other is HTML safe, then this returns an HTML safe string.
|
|
57
|
+
# If the other is not, this returns a normal unsafe string.
|
|
58
|
+
#
|
|
59
|
+
# @param [String|Brut::FrontEnd::Templates::HTMLSafeString] other
|
|
60
|
+
# @return [String|Brut::FrontEnd::Templates::HTMLSafeString] A safe or unsafe string, depending on what was passed.
|
|
33
61
|
def +(other)
|
|
34
62
|
if other.html_safe?
|
|
35
63
|
self.class.new(@string + other.to_s)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Locates a template, based on a name, configured paths, and an extension. This class forms both an API
|
|
2
|
+
# for template location ({#locate}) as well as an implementation that is conventional with Brut apps.
|
|
3
|
+
class Brut::FrontEnd::Templates::Locator
|
|
4
|
+
# Create a locator that will search the given paths and require that template
|
|
5
|
+
# files have the given extension
|
|
6
|
+
#
|
|
7
|
+
# @param [Pathname|String|Array<Pathname|String>] paths one or more paths that will be searched for templates
|
|
8
|
+
# @param [String] extension file extension, without the dot, of the name of files that are considered templates
|
|
9
|
+
def initialize(paths:, extension:)
|
|
10
|
+
@paths = Array(paths).map { |path| Pathname(path) }
|
|
11
|
+
@extension = extension
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Given a base name, which may or may not be nested paths, returns the path to the template
|
|
15
|
+
# for this file. There must be exactly one template that matches.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
#
|
|
19
|
+
# locator = Locator.new(
|
|
20
|
+
# paths: [
|
|
21
|
+
# Brut.container.app_src_dir / "front_end" / "components",
|
|
22
|
+
# Brut.container.app_src_dir / "front_end" / "other_components",
|
|
23
|
+
# ],
|
|
24
|
+
# extension: "html.erb"
|
|
25
|
+
# )
|
|
26
|
+
#
|
|
27
|
+
# # Suppose app/src/front_end/components/foo.html.erb exists
|
|
28
|
+
# path = locator.locate("foo")
|
|
29
|
+
# # => "app/src/front_end/components/foo.html.erb"
|
|
30
|
+
#
|
|
31
|
+
# # Suppose app/src/front_end/components/bar/blah.html.erb exists
|
|
32
|
+
# path = locator.locate("bar/blah")
|
|
33
|
+
# # => "app/src/front_end/components/bar/blah.html.erb"
|
|
34
|
+
#
|
|
35
|
+
# # Suppose both app/src/front_end/components/bar/blah.html.erb and
|
|
36
|
+
# # app/src/front_end/other_components/bar/blah.html.erb
|
|
37
|
+
# # both exist
|
|
38
|
+
# path = locator.locate("bar/blah")
|
|
39
|
+
# # => raises an error since there are two matches
|
|
40
|
+
#
|
|
41
|
+
# @param [String] base_name the base name of a file that is expected to have a template. This is searched relative to the paths
|
|
42
|
+
# provided to the constructor, so it may have nested paths
|
|
43
|
+
# @return [String] path to the template for the given `base_name`
|
|
44
|
+
# @raises StandardError if zero or more than one templates are found
|
|
45
|
+
def locate(base_name)
|
|
46
|
+
paths_to_try = @paths.map { |path|
|
|
47
|
+
path / "#{base_name}.#{@extension}"
|
|
48
|
+
}
|
|
49
|
+
paths_found = paths_to_try.select { |path|
|
|
50
|
+
path.exist?
|
|
51
|
+
}
|
|
52
|
+
if paths_found.empty?
|
|
53
|
+
raise "Could not locate template for #{base_name}. Tried: #{paths_to_try.map(&:to_s).join(', ')}"
|
|
54
|
+
end
|
|
55
|
+
if paths_found.length > 1
|
|
56
|
+
raise "Found more than one valid pat for #{base_name}. You must rename your files to disambiguate them. These paths were all found: #{paths_found.map(&:to_s).join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
return paths_found[0]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -130,6 +130,10 @@ module Brut::I18n::BaseMethods
|
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
132
|
|
|
133
|
+
def l(date_like, format: :default)
|
|
134
|
+
::I18n::l(date_like,format: format)
|
|
135
|
+
end
|
|
136
|
+
|
|
133
137
|
def this_field_value
|
|
134
138
|
@__this_field_value ||= ::I18n.t("general.cv.this_field", raise: true)
|
|
135
139
|
end
|
data/lib/brut/i18n.rb
CHANGED