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.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +4 -6
  3. data/brut.gemspec +1 -3
  4. data/lib/brut/back_end/seed_data.rb +19 -2
  5. data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
  6. data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
  7. data/lib/brut/back_end/sidekiq.rb +2 -1
  8. data/lib/brut/back_end/validator.rb +5 -1
  9. data/lib/brut/back_end.rb +9 -0
  10. data/lib/brut/cli/apps/scaffold.rb +16 -24
  11. data/lib/brut/cli.rb +4 -3
  12. data/lib/brut/factory_bot.rb +0 -5
  13. data/lib/brut/framework/app.rb +70 -5
  14. data/lib/brut/framework/config.rb +9 -46
  15. data/lib/brut/framework/container.rb +3 -2
  16. data/lib/brut/framework/errors.rb +12 -4
  17. data/lib/brut/framework/mcp.rb +63 -2
  18. data/lib/brut/framework/project_environment.rb +6 -2
  19. data/lib/brut/framework.rb +1 -1
  20. data/lib/brut/front_end/asset_path_resolver.rb +15 -0
  21. data/lib/brut/front_end/component.rb +101 -246
  22. data/lib/brut/front_end/components/constraint_violations.rb +10 -10
  23. data/lib/brut/front_end/components/form_tag.rb +17 -29
  24. data/lib/brut/front_end/components/i18n_translations.rb +12 -13
  25. data/lib/brut/front_end/components/input.rb +0 -1
  26. data/lib/brut/front_end/components/inputs/csrf_token.rb +3 -3
  27. data/lib/brut/front_end/components/inputs/radio_button.rb +6 -6
  28. data/lib/brut/front_end/components/inputs/select.rb +13 -20
  29. data/lib/brut/front_end/components/inputs/text_field.rb +18 -33
  30. data/lib/brut/front_end/components/inputs/textarea.rb +11 -26
  31. data/lib/brut/front_end/components/locale_detection.rb +2 -2
  32. data/lib/brut/front_end/components/page_identifier.rb +3 -5
  33. data/lib/brut/front_end/components/{time.rb → time_tag.rb} +14 -11
  34. data/lib/brut/front_end/components/traceparent.rb +5 -6
  35. data/lib/brut/front_end/http_method.rb +4 -0
  36. data/lib/brut/front_end/inline_svg_locator.rb +21 -0
  37. data/lib/brut/front_end/layout.rb +19 -0
  38. data/lib/brut/front_end/page.rb +52 -40
  39. data/lib/brut/front_end/request_context.rb +13 -0
  40. data/lib/brut/front_end/routing.rb +8 -3
  41. data/lib/brut/front_end.rb +32 -0
  42. data/lib/brut/i18n/base_methods.rb +51 -11
  43. data/lib/brut/i18n/for_back_end.rb +8 -0
  44. data/lib/brut/i18n/for_cli.rb +5 -1
  45. data/lib/brut/i18n/for_html.rb +9 -1
  46. data/lib/brut/i18n/http_accept_language.rb +47 -0
  47. data/lib/brut/i18n.rb +1 -0
  48. data/lib/brut/instrumentation/open_telemetry.rb +25 -0
  49. data/lib/brut/instrumentation.rb +3 -5
  50. data/lib/brut/sinatra_helpers.rb +13 -7
  51. data/lib/brut/spec_support/component_support.rb +27 -13
  52. data/lib/brut/spec_support/e2e_support.rb +4 -0
  53. data/lib/brut/spec_support/general_support.rb +3 -0
  54. data/lib/brut/spec_support/handler_support.rb +6 -1
  55. data/lib/brut/spec_support/matcher.rb +1 -0
  56. data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
  57. data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
  58. data/lib/brut/spec_support/matchers/have_i18n_string.rb +3 -1
  59. data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
  60. data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
  61. data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
  62. data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
  63. data/lib/brut/spec_support/rspec_setup.rb +1 -0
  64. data/lib/brut/spec_support.rb +5 -4
  65. data/lib/brut/version.rb +1 -1
  66. data/lib/brut.rb +7 -50
  67. metadata +14 -49
  68. data/doc-src/architecture.md +0 -102
  69. data/doc-src/assets.md +0 -98
  70. data/doc-src/forms.md +0 -214
  71. data/doc-src/handlers.md +0 -83
  72. data/doc-src/javascript.md +0 -265
  73. data/doc-src/keyword-injection.md +0 -183
  74. data/doc-src/pages.md +0 -210
  75. data/doc-src/route-hooks.md +0 -59
  76. data/lib/brut/front_end/template.rb +0 -47
  77. data/lib/brut/front_end/templates/block_filter.rb +0 -61
  78. data/lib/brut/front_end/templates/erb_engine.rb +0 -26
  79. data/lib/brut/front_end/templates/erb_parser.rb +0 -84
  80. data/lib/brut/front_end/templates/escapable_filter.rb +0 -20
  81. data/lib/brut/front_end/templates/html_safe_string.rb +0 -68
  82. 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["required"] = input.required
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["name"] = name
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 render
77
- html_tag(:select,**@sanitized_attributes) {
78
- options = @options.map { |option|
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
- html_tag(:option,**option_attributes) {
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.to_s, value ] }.to_h
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["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?
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["max"] = input.max
25
+ default_html_attributes[:max] = input.max
26
26
  end
27
27
  if input.maxlength
28
- default_html_attributes["maxlength"] = input.maxlength
28
+ default_html_attributes[:maxlength] = input.maxlength
29
29
  end
30
30
  if input.min
31
- default_html_attributes["min"] = input.min
31
+ default_html_attributes[:min] = input.min
32
32
  end
33
33
  if input.minlength
34
- default_html_attributes["minlength"] = input.minlength
35
- end
34
+ default_html_attributes[:minlength] = input.minlength end
36
35
  if input.step
37
- default_html_attributes["step"] = input.step
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["value"] = (index || true).to_s
43
- default_html_attributes["checked"] = value == "true"
41
+ default_html_attributes[:value] = (index || true).to_s
42
+ default_html_attributes[:checked] = value == "true"
44
43
  else
45
- default_html_attributes["value"] = value
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
- @sanitized_attributes = attributes.map { |key,value|
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 render
73
- attribute_string = @sanitized_attributes.map { |key,value|
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["required"] = input.required
17
- default_html_attributes["name"] = if input.array?
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["maxlength"] = input.maxlength
24
+ default_html_attributes[:maxlength] = input.maxlength
24
25
  end
25
26
  if input.minlength
26
- default_html_attributes["minlength"] = input.minlength
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
- @sanitized_attributes = attributes.map { |key,value|
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 sanitized_attributes = @sanitized_attributes
49
+ def invalid? = @attributes["data-invalid"] == true
56
50
 
57
- def render
58
- attribute_string = @sanitized_attributes.map { |key,value|
59
- if value == true
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 render
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
- html_tag("brut-locale-detection",**attributes)
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 render
7
+ def view_template
10
8
  if Brut.container.project_env.production?
11
- return ""
9
+ return nil
12
10
  end
13
- html_tag(:meta, name: "class", content: @page_name)
11
+ meta(name: "class", content: @page_name)
14
12
  end
15
13
  end
@@ -1,6 +1,5 @@
1
- require "rexml"
2
- # Renders a date or timestamp accessibly, using the `<time>` element. Likely you will use this via the {Brut::FrontEnd::Component::Helpers#time_tag} method. This will account for the current request's time zone. See {Clock}.
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
- **only_contains_class,
25
- &contents
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 render(clock:)
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
- html_tag(:time, class: @class_attribute, datetime: datetime_attribute) do
85
- if @contents
86
- @contents.()
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 render
11
+ def view_template
12
12
  attributes = {
13
- name: "traceparent"
13
+ name: "traceparent",
14
+ content: @traceparent,
14
15
  }
15
- if @traceparent
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
- html_tag(:meta, **attributes)
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
@@ -1,73 +1,71 @@
1
- # A page backs a web page. A page renders everything in the browser window. Technically, it is exactly like a component except that
2
- # it can have a layout.
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
- # When subclassing this to create a page, your initializer's signature will determine what data
5
- # is required for your page to work. It can be anything, just keep in mind that any component your page uses may
6
- # require additional data.
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
- # If your page does not override {#render} (which, generally, it won't), an ERB file is expected to exist alongside it in the
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 an ERB file in `app/src/front_end/layouts`. Every page
20
- # must have a layout. If you wish to render a page with no layout, create an empty layout in your app and use that.
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 {#render} is called, so you can override this
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 {#render} is called. This allows you to do any pre-flight checks and potentially
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 {#render} is never called. If you return a {Brut::FrontEnd::HttpStatus} (mostly likely by returning the result of calling {Brut::FrontEnd::HandlingResults#http_status}), {#render} is skipped and that status is returned with no content. If anything else is returned, {#render} is called as normal.
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
- # @!visibility private
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
- render
49
+ self.call
45
50
  end
46
51
  end
47
52
 
48
- # The core method of a page, which overrides {Brut::FrontEnd::Component#render}. This is expected to return
49
- # a string to be sent as a response to an HTTP request. Generally, you should not call this method as it is
50
- # called by Brut when your page is requested.
51
- #
52
- # Also, generally don't override this unles you need to do something unusual. Overriding this will completely bypass the layout
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
- page = template.render_template(self).html_safe!
66
- layout_template.render_template(self) do
67
- page
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
- def template_name = RichString.new(self.class.name).underscorized.to_s.gsub(/^pages\//,"")
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
- uri.query = URI.encode_www_form(query_string_params)
221
- uri
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 = Thread.current.thread_variable_get(: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