brut 0.0.13 → 0.0.21
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/Gemfile.lock +4 -6
- data/brut.gemspec +1 -3
- data/lib/brut/back_end/seed_data.rb +19 -2
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
- data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
- data/lib/brut/back_end/sidekiq.rb +2 -1
- data/lib/brut/back_end/validator.rb +5 -1
- data/lib/brut/back_end.rb +9 -0
- data/lib/brut/cli/apps/scaffold.rb +16 -24
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/factory_bot.rb +0 -5
- data/lib/brut/framework/app.rb +70 -5
- data/lib/brut/framework/config.rb +9 -46
- data/lib/brut/framework/container.rb +3 -2
- data/lib/brut/framework/errors.rb +12 -4
- data/lib/brut/framework/mcp.rb +63 -2
- data/lib/brut/framework/project_environment.rb +6 -2
- data/lib/brut/framework.rb +1 -1
- data/lib/brut/front_end/asset_path_resolver.rb +15 -0
- data/lib/brut/front_end/component.rb +101 -246
- data/lib/brut/front_end/components/constraint_violations.rb +10 -10
- data/lib/brut/front_end/components/form_tag.rb +17 -29
- data/lib/brut/front_end/components/i18n_translations.rb +12 -13
- data/lib/brut/front_end/components/input.rb +0 -1
- data/lib/brut/front_end/components/inputs/csrf_token.rb +3 -3
- data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
- data/lib/brut/front_end/components/inputs/select.rb +13 -20
- data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
- data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
- data/lib/brut/front_end/components/locale_detection.rb +2 -2
- data/lib/brut/front_end/components/page_identifier.rb +3 -5
- data/lib/brut/front_end/components/{time.rb → time_tag.rb} +14 -11
- data/lib/brut/front_end/components/traceparent.rb +5 -6
- data/lib/brut/front_end/http_method.rb +4 -0
- data/lib/brut/front_end/inline_svg_locator.rb +21 -0
- data/lib/brut/front_end/layout.rb +19 -0
- data/lib/brut/front_end/page.rb +52 -40
- data/lib/brut/front_end/request_context.rb +13 -0
- data/lib/brut/front_end/routing.rb +8 -3
- data/lib/brut/front_end.rb +32 -0
- data/lib/brut/i18n/base_methods.rb +51 -11
- data/lib/brut/i18n/for_back_end.rb +8 -0
- data/lib/brut/i18n/for_cli.rb +5 -1
- data/lib/brut/i18n/for_html.rb +9 -1
- data/lib/brut/i18n/http_accept_language.rb +47 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/open_telemetry.rb +25 -0
- data/lib/brut/instrumentation.rb +3 -5
- data/lib/brut/sinatra_helpers.rb +13 -7
- data/lib/brut/spec_support/component_support.rb +27 -13
- data/lib/brut/spec_support/e2e_support.rb +4 -0
- data/lib/brut/spec_support/general_support.rb +3 -0
- data/lib/brut/spec_support/handler_support.rb +6 -1
- data/lib/brut/spec_support/matcher.rb +1 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +3 -1
- data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
- data/lib/brut/spec_support/rspec_setup.rb +1 -0
- data/lib/brut/spec_support.rb +5 -4
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +7 -50
- metadata +14 -49
- data/doc-src/architecture.md +0 -102
- data/doc-src/assets.md +0 -98
- data/doc-src/forms.md +0 -214
- data/doc-src/handlers.md +0 -83
- data/doc-src/javascript.md +0 -265
- data/doc-src/keyword-injection.md +0 -183
- data/doc-src/pages.md +0 -210
- data/doc-src/route-hooks.md +0 -59
- data/lib/brut/front_end/template.rb +0 -47
- data/lib/brut/front_end/templates/block_filter.rb +0 -61
- data/lib/brut/front_end/templates/erb_engine.rb +0 -26
- data/lib/brut/front_end/templates/erb_parser.rb +0 -84
- data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
- data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
- data/lib/brut/front_end/templates/locator.rb +0 -60
@@ -21,10 +21,11 @@ class Brut::FrontEnd::Components::Inputs::Select < Brut::FrontEnd::Components::I
|
|
21
21
|
option_text_attribute:,
|
22
22
|
index: nil,
|
23
23
|
html_attributes: {})
|
24
|
+
html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
|
24
25
|
default_html_attributes = {}
|
25
26
|
index ||= 0
|
26
27
|
input = form.input(input_name, index:)
|
27
|
-
default_html_attributes[
|
28
|
+
default_html_attributes[:required] = input.required
|
28
29
|
if !form.new? && !input.valid?
|
29
30
|
default_html_attributes["data-invalid"] = true
|
30
31
|
input.validity_state.each do |constraint,violated|
|
@@ -61,36 +62,28 @@ class Brut::FrontEnd::Components::Inputs::Select < Brut::FrontEnd::Components::I
|
|
61
62
|
@selected_value = selected_value
|
62
63
|
@value_attribute = value_attribute
|
63
64
|
@option_text_attribute = option_text_attribute
|
65
|
+
@html_attributes = html_attributes
|
64
66
|
|
65
|
-
html_attributes[
|
66
|
-
@sanitized_attributes = html_attributes.map { |key,value|
|
67
|
-
[
|
68
|
-
key.to_s.gsub(/[\s\"\'>\/=]/,"-"),
|
69
|
-
value
|
70
|
-
]
|
71
|
-
}.select { |key,value|
|
72
|
-
!value.nil?
|
73
|
-
}.to_h
|
67
|
+
@html_attributes[:name] = name
|
74
68
|
end
|
75
69
|
|
76
|
-
def
|
77
|
-
|
78
|
-
|
70
|
+
def view_template
|
71
|
+
select(**@html_attributes) {
|
72
|
+
if @include_blank
|
73
|
+
option(**@include_blank.option_attributes) {
|
74
|
+
@include_blank.text_content
|
75
|
+
}
|
76
|
+
end
|
77
|
+
options = @options.each do |option|
|
79
78
|
value = option.send(@value_attribute)
|
80
79
|
option_attributes = { value: value }
|
81
80
|
if value == @selected_value
|
82
81
|
option_attributes[:selected] = true
|
83
82
|
end
|
84
|
-
|
83
|
+
option(**option_attributes) {
|
85
84
|
option.send(@option_text_attribute)
|
86
85
|
}
|
87
|
-
}
|
88
|
-
if @include_blank
|
89
|
-
options.unshift(html_tag(:option,**@include_blank.option_attributes) {
|
90
|
-
@include_blank.text_content
|
91
|
-
})
|
92
86
|
end
|
93
|
-
options.join("\n")
|
94
87
|
}
|
95
88
|
end
|
96
89
|
private
|
@@ -9,40 +9,39 @@ class Brut::FrontEnd::Components::Inputs::TextField < Brut::FrontEnd::Components
|
|
9
9
|
# @param [Hash] html_attributes any additional HTML attributes to include on the `<input>` element.
|
10
10
|
def self.for_form_input(form:, input_name:, index: nil, html_attributes: {})
|
11
11
|
default_html_attributes = {}
|
12
|
-
html_attributes = html_attributes.map { |key,value| [ key.
|
12
|
+
html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
|
13
13
|
input = form.input(input_name, index:)
|
14
14
|
|
15
|
-
default_html_attributes[
|
16
|
-
default_html_attributes[
|
17
|
-
default_html_attributes[
|
18
|
-
default_html_attributes[
|
15
|
+
default_html_attributes[:required] = input.required
|
16
|
+
default_html_attributes[:pattern] = input.pattern
|
17
|
+
default_html_attributes[:type] = input.type
|
18
|
+
default_html_attributes[:name] = if input.array?
|
19
19
|
"#{input.name}[]"
|
20
20
|
else
|
21
21
|
input.name
|
22
22
|
end
|
23
23
|
|
24
24
|
if input.max
|
25
|
-
default_html_attributes[
|
25
|
+
default_html_attributes[:max] = input.max
|
26
26
|
end
|
27
27
|
if input.maxlength
|
28
|
-
default_html_attributes[
|
28
|
+
default_html_attributes[:maxlength] = input.maxlength
|
29
29
|
end
|
30
30
|
if input.min
|
31
|
-
default_html_attributes[
|
31
|
+
default_html_attributes[:min] = input.min
|
32
32
|
end
|
33
33
|
if input.minlength
|
34
|
-
default_html_attributes[
|
35
|
-
end
|
34
|
+
default_html_attributes[:minlength] = input.minlength end
|
36
35
|
if input.step
|
37
|
-
default_html_attributes[
|
36
|
+
default_html_attributes[:step] = input.step
|
38
37
|
end
|
39
38
|
value = input.value
|
40
39
|
|
41
40
|
if input.type == "checkbox"
|
42
|
-
default_html_attributes[
|
43
|
-
default_html_attributes[
|
41
|
+
default_html_attributes[:value] = (index || true).to_s
|
42
|
+
default_html_attributes[:checked] = value == "true"
|
44
43
|
else
|
45
|
-
default_html_attributes[
|
44
|
+
default_html_attributes[:value] = value.nil? ? nil : value.to_s
|
46
45
|
end
|
47
46
|
if !form.new? && !input.valid?
|
48
47
|
default_html_attributes["data-invalid"] = true
|
@@ -55,30 +54,16 @@ class Brut::FrontEnd::Components::Inputs::TextField < Brut::FrontEnd::Components
|
|
55
54
|
Brut::FrontEnd::Components::Inputs::TextField.new(default_html_attributes.merge(html_attributes))
|
56
55
|
end
|
57
56
|
|
57
|
+
def invalid? = @attributes["data-invalid"] == true
|
58
|
+
|
58
59
|
# Create an instance
|
59
60
|
#
|
60
61
|
# @param [Hash] attributes HTML attributes to put on the element.
|
61
62
|
def initialize(attributes)
|
62
|
-
@
|
63
|
-
[
|
64
|
-
key.to_s.gsub(/[\s\"\'>\/=]/,"-"),
|
65
|
-
value
|
66
|
-
]
|
67
|
-
}.select { |key,value|
|
68
|
-
!value.nil?
|
69
|
-
}.to_h
|
63
|
+
@attributes = attributes
|
70
64
|
end
|
71
65
|
|
72
|
-
def
|
73
|
-
|
74
|
-
if value == true
|
75
|
-
key
|
76
|
-
elsif value == false
|
77
|
-
""
|
78
|
-
else
|
79
|
-
REXML::Attribute.new(key,value).to_string
|
80
|
-
end
|
81
|
-
}.join(" ")
|
82
|
-
"<input #{attribute_string}>"
|
66
|
+
def view_template
|
67
|
+
input(**@attributes)
|
83
68
|
end
|
84
69
|
end
|
@@ -8,22 +8,23 @@ class Brut::FrontEnd::Components::Inputs::Textarea < Brut::FrontEnd::Components:
|
|
8
8
|
# @param [Integer] index if this input is part of an array, this is the index into that array. This is used to get the input's value.
|
9
9
|
# @param [Hash] html_attributes any additional HTML attributes to include on the `<textarea>` element.
|
10
10
|
def self.for_form_input(form:, input_name:, index: nil, html_attributes: {})
|
11
|
+
html_attributes = html_attributes.map { |key,value| [ key.to_sym, value ] }.to_h
|
11
12
|
default_html_attributes = {}
|
12
13
|
|
13
14
|
index ||= 0
|
14
15
|
input = form.input(input_name, index:)
|
15
16
|
|
16
|
-
default_html_attributes[
|
17
|
-
default_html_attributes[
|
17
|
+
default_html_attributes[:required] = input.required
|
18
|
+
default_html_attributes[:name] = if input.array?
|
18
19
|
"#{input.name}[]"
|
19
20
|
else
|
20
21
|
input.name
|
21
22
|
end
|
22
23
|
if input.maxlength
|
23
|
-
default_html_attributes[
|
24
|
+
default_html_attributes[:maxlength] = input.maxlength
|
24
25
|
end
|
25
26
|
if input.minlength
|
26
|
-
default_html_attributes[
|
27
|
+
default_html_attributes[:minlength] = input.minlength
|
27
28
|
end
|
28
29
|
if !form.new? && !input.valid?
|
29
30
|
default_html_attributes["data-invalid"] = true
|
@@ -41,31 +42,15 @@ class Brut::FrontEnd::Components::Inputs::Textarea < Brut::FrontEnd::Components:
|
|
41
42
|
# @param [Hash] attributes HTML attributes to put on the element.
|
42
43
|
# @param [String] value the value to place inside the text area
|
43
44
|
def initialize(attributes, value)
|
44
|
-
@
|
45
|
-
|
46
|
-
key.to_s.gsub(/[\s\"\'>\/=]/,"-"),
|
47
|
-
value
|
48
|
-
]
|
49
|
-
}.select { |key,value|
|
50
|
-
!value.nil?
|
51
|
-
}.to_h
|
52
|
-
@value = value
|
45
|
+
@attributes = attributes
|
46
|
+
@value = value
|
53
47
|
end
|
54
48
|
|
55
|
-
def
|
49
|
+
def invalid? = @attributes["data-invalid"] == true
|
56
50
|
|
57
|
-
def
|
58
|
-
|
59
|
-
|
60
|
-
key
|
61
|
-
elsif value == false
|
62
|
-
""
|
63
|
-
else
|
64
|
-
REXML::Attribute.new(key,value).to_string
|
65
|
-
end
|
66
|
-
}.join(" ")
|
67
|
-
%{
|
68
|
-
<textarea #{attribute_string}>#{ @value }</textarea>
|
51
|
+
def view_template
|
52
|
+
textarea(**@attributes) {
|
53
|
+
@value
|
69
54
|
}
|
70
55
|
end
|
71
56
|
end
|
@@ -13,7 +13,7 @@ class Brut::FrontEnd::Components::LocaleDetection < Brut::FrontEnd::Component
|
|
13
13
|
@url = Brut::FrontEnd::Handlers::LocaleDetectionHandler.routing
|
14
14
|
end
|
15
15
|
|
16
|
-
def
|
16
|
+
def view_template
|
17
17
|
attributes = {
|
18
18
|
"url" => @url,
|
19
19
|
}
|
@@ -27,6 +27,6 @@ class Brut::FrontEnd::Components::LocaleDetection < Brut::FrontEnd::Component
|
|
27
27
|
attributes["show-warnings"] = true
|
28
28
|
end
|
29
29
|
|
30
|
-
|
30
|
+
brut_locale_detection(**attributes)
|
31
31
|
end
|
32
32
|
end
|
@@ -1,15 +1,13 @@
|
|
1
|
-
require "rexml"
|
2
|
-
|
3
1
|
# Renders a `<meta>` tag that contains the name of the page. This is useful for end to end tests to assert that they are on a specific page before continuing with the test. It can eliminate a lot of confusion when a test fails.
|
4
2
|
class Brut::FrontEnd::Components::PageIdentifier < Brut::FrontEnd::Component
|
5
3
|
def initialize(page_name)
|
6
4
|
@page_name = page_name
|
7
5
|
end
|
8
6
|
|
9
|
-
def
|
7
|
+
def view_template
|
10
8
|
if Brut.container.project_env.production?
|
11
|
-
return
|
9
|
+
return nil
|
12
10
|
end
|
13
|
-
|
11
|
+
meta(name: "class", content: @page_name)
|
14
12
|
end
|
15
13
|
end
|
@@ -1,6 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
class Brut::FrontEnd::Components::Time < Brut::FrontEnd::Component
|
1
|
+
# Renders a date or timestamp accessibly, using the `<time>` element. Likely you will use this via the {Brut::FrontEnd::Component#time_tag} method. This will account for the current request's time zone. See {Clock}.
|
2
|
+
class Brut::FrontEnd::Components::TimeTag < Brut::FrontEnd::Component
|
4
3
|
include Brut::I18n::ForHTML
|
5
4
|
# Creates the component
|
6
5
|
# @param timestamp [Time] the timestamp you wish to render. Mutually exclusive with `date`.
|
@@ -21,13 +20,18 @@ class Brut::FrontEnd::Components::Time < Brut::FrontEnd::Component
|
|
21
20
|
skip_year_if_same: true,
|
22
21
|
skip_dow_if_not_this_week: true,
|
23
22
|
attribute_format: :iso_8601,
|
24
|
-
|
25
|
-
|
23
|
+
clock: :from_request_context,
|
24
|
+
**only_contains_class
|
26
25
|
)
|
27
26
|
require_exactly_one!(timestamp:,date:)
|
28
27
|
|
29
28
|
@date_only = timestamp.nil?
|
30
29
|
@timestamp = timestamp || date
|
30
|
+
@clock = if clock == :from_request_context
|
31
|
+
Brut::FrontEnd::RequestContext.current[:clock]
|
32
|
+
else
|
33
|
+
clock
|
34
|
+
end
|
31
35
|
|
32
36
|
formats = [ format ]
|
33
37
|
use_no_year = skip_year_if_same && @timestamp.year == Time.now.year
|
@@ -69,21 +73,20 @@ class Brut::FrontEnd::Components::Time < Brut::FrontEnd::Component
|
|
69
73
|
@format = found_format.to_sym
|
70
74
|
@attribute_format = attribute_format.to_sym
|
71
75
|
@class_attribute = only_contains_class[:class] || ""
|
72
|
-
@contents = contents
|
73
76
|
end
|
74
77
|
|
75
|
-
def
|
78
|
+
def view_template
|
76
79
|
adjusted_value = if @date_only
|
77
80
|
@timestamp
|
78
81
|
else
|
79
|
-
clock.in_time_zone(@timestamp)
|
82
|
+
@clock.in_time_zone(@timestamp)
|
80
83
|
end
|
81
84
|
|
82
85
|
datetime_attribute = ::I18n.l(adjusted_value,format: @attribute_format)
|
83
86
|
|
84
|
-
|
85
|
-
if
|
86
|
-
|
87
|
+
time(class: @class_attribute, datetime: datetime_attribute) do
|
88
|
+
if block_given?
|
89
|
+
yield
|
87
90
|
else
|
88
91
|
::I18n.l(adjusted_value,format: @format)
|
89
92
|
end
|
@@ -8,15 +8,14 @@ class Brut::FrontEnd::Components::Traceparent < Brut::FrontEnd::Component
|
|
8
8
|
@traceparent = carrier["traceparent"]
|
9
9
|
end
|
10
10
|
|
11
|
-
def
|
11
|
+
def view_template
|
12
12
|
attributes = {
|
13
|
-
name: "traceparent"
|
13
|
+
name: "traceparent",
|
14
|
+
content: @traceparent,
|
14
15
|
}
|
15
|
-
if
|
16
|
-
attributes[:content] = @traceparent
|
17
|
-
else
|
16
|
+
if !@traceparent
|
18
17
|
attributes["data-no-traceparent"] = "no traceparent was available - this component may have been rendered outside of an existing OpenTelemetry context"
|
19
18
|
end
|
20
|
-
|
19
|
+
meta(**attributes)
|
21
20
|
end
|
22
21
|
end
|
@@ -1,5 +1,9 @@
|
|
1
|
+
require "phlex"
|
2
|
+
|
1
3
|
# Wrapper around an HTTP Method, ensuring it contains only a valid value.
|
2
4
|
class Brut::FrontEnd::HttpMethod
|
5
|
+
include Phlex::SGML::SafeObject
|
6
|
+
|
3
7
|
# Create an HTTP method from a string.
|
4
8
|
#
|
5
9
|
# @param [String|Symbol] string a string containing an HTTP method name. Case insensitive, and can be a symbol.
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Brut::FrontEnd::InlineSvgLocator
|
2
|
+
def initialize(paths:)
|
3
|
+
@paths = Array(paths).map { |path| Pathname(path) }
|
4
|
+
end
|
5
|
+
|
6
|
+
def locate(base_name)
|
7
|
+
paths_to_try = @paths.map { |path|
|
8
|
+
path / "#{base_name}.svg"
|
9
|
+
}
|
10
|
+
paths_found = paths_to_try.select { |path|
|
11
|
+
path.exist?
|
12
|
+
}
|
13
|
+
if paths_found.empty?
|
14
|
+
raise "Could not locate SVG for #{base_name}. Tried: #{paths_to_try.map(&:to_s).join(', ')}"
|
15
|
+
end
|
16
|
+
if paths_found.length > 1
|
17
|
+
raise "Found more than one valid path for #{base_name}. You must rename your files to disambiguate them. These paths were all found: #{paths_found.map(&:to_s).join(', ')}"
|
18
|
+
end
|
19
|
+
return paths_found[0]
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# A layout is common HTML that surrounds different pages. For example, it would hold your
|
2
|
+
# DOCTYPE, `<head>`, and possibly any common `<body>` elements that every page needs.
|
3
|
+
#
|
4
|
+
# A layout is a Phlex component but it must contain a call to `yield` somewhere in the
|
5
|
+
# implementation of `view_template`.
|
6
|
+
#
|
7
|
+
# This base class contains helper methods needed for implementing a layout.
|
8
|
+
class Brut::FrontEnd::Layout < Brut::FrontEnd::Component
|
9
|
+
# Get the actual path of an asset managed by Brut. This handles
|
10
|
+
# locating the asset's URL as well as ensuring the hash is properly
|
11
|
+
# inserted into the filename.
|
12
|
+
#
|
13
|
+
# @param [String] path the path to an asset, such as `/css/styles.css`.
|
14
|
+
#
|
15
|
+
# @return [String] the actual path to the current version of that asset.
|
16
|
+
#
|
17
|
+
# @see Brut::FrontEnd::AssetPathResolver
|
18
|
+
def asset_path(path) = Brut.container.asset_path_resolver.resolve(path)
|
19
|
+
end
|
data/lib/brut/front_end/page.rb
CHANGED
@@ -1,73 +1,71 @@
|
|
1
|
-
# A
|
2
|
-
#
|
1
|
+
# A Page backs a web page, which handles rendering everything in a browser window when a URL is requested.
|
2
|
+
# Technically, a page is identical to a {Brut::FrontEnd::Component}, except that a page has a layout.
|
3
|
+
# A {Brut::FrontEnd::Layout} is common HTML that surrounds your page's HTML.
|
4
|
+
# Your page is a Phlex component, but instead of implementing `view_template`, you
|
5
|
+
# implement {#page_template} to ensure the layout is used.
|
3
6
|
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
7
|
+
# To create a page, after defining a route, subclass this class (or, more likely, your app's `AppPage`) and
|
8
|
+
# provide an initializer that accepts keyword arguments. The names of these arguments will be used to locate the
|
9
|
+
# values that Brut will pass in when creating your page object.
|
7
10
|
#
|
8
|
-
#
|
9
|
-
# app. For example, if you have a page named `Auth::LoginPage`, it would expected to be in
|
10
|
-
# `app/src/front_end/pages/auth/login_page.rb`. Thus, Brut will also expect
|
11
|
-
# `app/src/front_end/pages/auth/login_page.html.erb` to exist as well. That ERB file is used with an instance of your
|
12
|
-
# pages's class to render the page's HTML.
|
11
|
+
# Consult Brut's documentation on keyword injection to know what values you may use and how values are located.
|
13
12
|
#
|
14
13
|
# @see Brut::FrontEnd::Component
|
14
|
+
# @see Brut::FrontEnd::Layout
|
15
15
|
class Brut::FrontEnd::Page < Brut::FrontEnd::Component
|
16
16
|
include Brut::FrontEnd::HandlingResults
|
17
|
-
using Brut::FrontEnd::Templates::HTMLSafeString::Refinement
|
18
17
|
|
19
|
-
# Returns the name of the layout for this page. This string is used to find
|
20
|
-
#
|
18
|
+
# Returns the name of the layout for this page. This string is used to find a class named
|
19
|
+
# `«camelized-layout»Layout` in your app. The default value is "default", meaning that the class
|
20
|
+
# `DefaultLayout` will be used.
|
21
21
|
#
|
22
|
-
# Note that the layout can be dynamic. It is requested when {#
|
22
|
+
# Note that the layout can be dynamic. It is requested when {#page_template} is called, so you can override this
|
23
23
|
# method and use any ivar set in your constructor to change what layout is used.
|
24
24
|
#
|
25
|
+
# If your page does not need a layout, you have two options:
|
26
|
+
#
|
27
|
+
# * Create your own blank layout named, e.g. `BlankLayout` and have this method return `"blank"`.
|
28
|
+
# * Implement `view_template` instead of `page_template`, thus overriding this class' implementation that uses
|
29
|
+
# layouts.
|
30
|
+
#
|
25
31
|
# @return [String] The name of the layout. May not be `nil`.
|
26
32
|
def layout = "default"
|
27
33
|
|
28
|
-
# Called after the page is created, but before {#
|
34
|
+
# Called after the page is created, but before {#page_template} is called. This allows you to do any pre-flight checks and potentially
|
29
35
|
# redirect the user or produce an error.
|
30
36
|
#
|
31
|
-
# @return [URI|Brut::FrontEnd::HttpStatus|Object] If you return a `URI` (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#redirect_to}), the user is redirected and
|
37
|
+
# @return [URI|Brut::FrontEnd::HttpStatus|Object] If you return a `URI` (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#redirect_to}), the user is redirected and no HTML is generated. If you return a {Brut::FrontEnd::HttpStatus} (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#http_status}), HTML generation is skipped and that status is returned with no content. If anything else is returned, HTML is generated normal.
|
32
38
|
def before_render = nil
|
33
39
|
|
34
|
-
#
|
40
|
+
# Core method of this class. Do not override. This handles the use of {#before_render} and is what Brut
|
41
|
+
# calls to possibly render the page.
|
35
42
|
def handle!
|
36
43
|
case before_render
|
37
44
|
in URI => uri
|
38
|
-
Brut.container.instrumentation.add_event("before_render got a URI", uri: uri)
|
39
45
|
uri
|
40
46
|
in Brut::FrontEnd::HttpStatus => http_status
|
41
|
-
Brut.container.instrumentation.add_event("before_render got status", http_status: http_status)
|
42
47
|
http_status
|
43
48
|
else
|
44
|
-
|
49
|
+
self.call
|
45
50
|
end
|
46
51
|
end
|
47
52
|
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
|
53
|
-
# system and skip all ERB processing. Unlike {Brut::FrontEnd::Component#render}, overriding this method does not provide access to injected data from the request context.
|
54
|
-
#
|
55
|
-
# @return [Brut::FrontEnd::Templates::HTMLSafeString] string containing the page's full HTML.
|
56
|
-
def render
|
57
|
-
layout_template = Brut.container.layout_locator.locate(self.layout).
|
58
|
-
then { |layout_erb_file| Brut::FrontEnd::Template.new(layout_erb_file) }
|
59
|
-
|
60
|
-
template = Brut.container.page_locator.locate(self.template_name).
|
61
|
-
then { |erb_file| Brut::FrontEnd::Template.new(erb_file) }
|
62
|
-
|
63
|
-
Brut.container.instrumentation.add_event("templates found", layout: layout_template.template_file_path, page: template.template_file_path)
|
53
|
+
# Override this method to produce your page's HTML. You are intended to call Phlex
|
54
|
+
# methods here. Anything you can do inside the Phlex-standard `view_template` method, you can
|
55
|
+
# do here. The only difference is that this will all be rendered in the context of your configured
|
56
|
+
# {#layout}.
|
57
|
+
def page_template = abstract_method!
|
64
58
|
|
65
|
-
|
66
|
-
|
67
|
-
|
59
|
+
# Phlex's API to produce markup. Do not override this or you will lose your layout.
|
60
|
+
# This implementation locates the configured layout, renders it, and renders {#page_template}
|
61
|
+
# inside.
|
62
|
+
def view_template
|
63
|
+
with_layout do
|
64
|
+
page_template
|
68
65
|
end
|
69
66
|
end
|
70
67
|
|
68
|
+
|
71
69
|
# @return [String] name of this page for use in debugging or for whatever reason you may want to dynamically refer to the page's name. The default value is the class name.
|
72
70
|
def self.page_name = self.name
|
73
71
|
|
@@ -79,7 +77,21 @@ class Brut::FrontEnd::Page < Brut::FrontEnd::Component
|
|
79
77
|
|
80
78
|
private
|
81
79
|
|
82
|
-
|
80
|
+
# Locates the layout class and uses it to render itself, along
|
81
|
+
# with the block given.
|
82
|
+
#
|
83
|
+
# @!visibility private
|
84
|
+
def with_layout(&block)
|
85
|
+
layout_class = Module.const_get(
|
86
|
+
layout_class = RichString.new([
|
87
|
+
self.layout,
|
88
|
+
"layout"
|
89
|
+
].join("_")).camelize
|
90
|
+
)
|
91
|
+
render layout_class.new(page_name:,&block)
|
92
|
+
end
|
93
|
+
|
94
|
+
|
83
95
|
|
84
96
|
end
|
85
97
|
|
@@ -8,6 +8,19 @@
|
|
8
8
|
# and create an initializer for it that accepts the `clock:` keyword argument, the managed instance of {Clock} will be passed into it
|
9
9
|
# when Brut creates an instance of the class.
|
10
10
|
class Brut::FrontEnd::RequestContext
|
11
|
+
|
12
|
+
def self.current
|
13
|
+
Thread.current.thread_variable_get(:request_context)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Create an instance of klass injected with the request context.
|
17
|
+
def self.inject(klass, request_params: nil)
|
18
|
+
self.current.then { |request_context|
|
19
|
+
request_context.as_constructor_args(klass,request_params:)
|
20
|
+
}.then { |constructor_args|
|
21
|
+
klass.new(**constructor_args)
|
22
|
+
}
|
23
|
+
end
|
11
24
|
# Create a new RequestContext based on some of the information provided by Rack
|
12
25
|
#
|
13
26
|
# @param [Hash] env the Rack `env` object, as available to any middleware
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require "uri"
|
2
|
+
require "phlex"
|
2
3
|
|
3
4
|
# Holds the registered routes for this app.
|
4
5
|
class Brut::FrontEnd::Routing
|
@@ -217,12 +218,16 @@ private
|
|
217
218
|
joined_path = joined_path + "#" + URI.encode_uri_component(anchor)
|
218
219
|
end
|
219
220
|
uri = URI(joined_path)
|
220
|
-
|
221
|
-
|
221
|
+
query_string = URI.encode_www_form(query_string_params)
|
222
|
+
if query_string.to_s.strip != ""
|
223
|
+
uri.query = query_string
|
224
|
+
end
|
225
|
+
|
226
|
+
uri.extend(Phlex::SGML::SafeObject)
|
222
227
|
end
|
223
228
|
|
224
229
|
def url(**query_string_params)
|
225
|
-
request_context =
|
230
|
+
request_context = Brut::FrontEnd::RequestContext.current
|
226
231
|
path = self.path(**query_string_params)
|
227
232
|
host = if request_context
|
228
233
|
request_context[:host]
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# In Brut, the _front end_ is considered anything that interacts directly
|
2
|
+
# with a web browser or HTTP. This includes rendering HTML, managing
|
3
|
+
# JavaScript and CSS, and processing form submissions. It contrasts to
|
4
|
+
# {Brut::BackEnd}, which handles the business logic and database.
|
5
|
+
#
|
6
|
+
# The entire front-end is based on Rack, so you should be able to achieve anything you need to.
|
7
|
+
module Brut::FrontEnd
|
8
|
+
autoload(:AssetMetadata, "brut/front_end/asset_metadata")
|
9
|
+
autoload(:AssetPathResolver, "brut/front_end/asset_path_resolver")
|
10
|
+
autoload(:Component, "brut/front_end/component")
|
11
|
+
autoload(:Components, "brut/front_end/component")
|
12
|
+
autoload(:Download, "brut/front_end/download")
|
13
|
+
autoload(:Flash, "brut/front_end/flash")
|
14
|
+
autoload(:Form, "brut/front_end/form")
|
15
|
+
autoload(:GenericResponse, "brut/front_end/generic_response")
|
16
|
+
autoload(:Handler, "brut/front_end/handler")
|
17
|
+
autoload(:Handlers, "brut/front_end/handler")
|
18
|
+
autoload(:HandlingResults, "brut/front_end/handling_results")
|
19
|
+
autoload(:HttpMethod, "brut/front_end/http_method")
|
20
|
+
autoload(:HttpStatus, "brut/front_end/http_status")
|
21
|
+
autoload(:InlineSvgLocator, "brut/front_end/inline_svg_locator")
|
22
|
+
autoload(:Layout, "brut/front_end/layout")
|
23
|
+
autoload(:Middleware, "brut/front_end/middleware")
|
24
|
+
autoload(:Middlewares, "brut/front_end/middleware")
|
25
|
+
autoload(:Page, "brut/front_end/page")
|
26
|
+
autoload(:Pages, "brut/front_end/page")
|
27
|
+
autoload(:RequestContext, "brut/front_end/request_context")
|
28
|
+
autoload(:RouteHook, "brut/front_end/route_hook")
|
29
|
+
autoload(:RouteHooks, "brut/front_end/route_hook")
|
30
|
+
autoload(:Routing, "brut/front_end/routing")
|
31
|
+
autoload(:Session, "brut/front_end/session")
|
32
|
+
end
|