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