brut 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/Gemfile.lock +66 -1
  4. data/README.md +36 -0
  5. data/Rakefile +22 -0
  6. data/brut.gemspec +7 -0
  7. data/doc-src/architecture.md +102 -0
  8. data/doc-src/assets.md +98 -0
  9. data/doc-src/forms.md +214 -0
  10. data/doc-src/handlers.md +83 -0
  11. data/doc-src/javascript.md +265 -0
  12. data/doc-src/keyword-injection.md +183 -0
  13. data/doc-src/pages.md +210 -0
  14. data/doc-src/route-hooks.md +59 -0
  15. data/docs-todo.md +32 -0
  16. data/lib/brut/back_end/seed_data.rb +5 -1
  17. data/lib/brut/back_end/validator.rb +1 -1
  18. data/lib/brut/back_end/validators/form_validator.rb +31 -6
  19. data/lib/brut/cli/app.rb +100 -4
  20. data/lib/brut/cli/app_runner.rb +38 -5
  21. data/lib/brut/cli/apps/build_assets.rb +4 -6
  22. data/lib/brut/cli/apps/db.rb +2 -7
  23. data/lib/brut/cli/apps/scaffold.rb +413 -7
  24. data/lib/brut/cli/apps/test.rb +14 -1
  25. data/lib/brut/cli/command.rb +141 -6
  26. data/lib/brut/cli/error.rb +30 -3
  27. data/lib/brut/cli/execution_results.rb +44 -6
  28. data/lib/brut/cli/executor.rb +21 -1
  29. data/lib/brut/cli/options.rb +29 -2
  30. data/lib/brut/cli/output.rb +47 -0
  31. data/lib/brut/cli.rb +26 -0
  32. data/lib/brut/factory_bot.rb +2 -0
  33. data/lib/brut/framework/app.rb +38 -5
  34. data/lib/brut/framework/config.rb +97 -54
  35. data/lib/brut/framework/container.rb +97 -33
  36. data/lib/brut/framework/errors/abstract_method.rb +3 -7
  37. data/lib/brut/framework/errors/missing_parameter.rb +12 -0
  38. data/lib/brut/framework/errors/no_class_for_path.rb +25 -0
  39. data/lib/brut/framework/errors/not_found.rb +12 -2
  40. data/lib/brut/framework/errors/not_implemented.rb +14 -0
  41. data/lib/brut/framework/errors.rb +18 -0
  42. data/lib/brut/framework/fussy_type_enforcement.rb +17 -12
  43. data/lib/brut/framework/mcp.rb +106 -49
  44. data/lib/brut/framework/project_environment.rb +9 -0
  45. data/lib/brut/framework.rb +1 -0
  46. data/lib/brut/front_end/asset_metadata.rb +7 -1
  47. data/lib/brut/front_end/component.rb +129 -38
  48. data/lib/brut/front_end/components/constraint_violations.rb +57 -0
  49. data/lib/brut/front_end/components/form_tag.rb +23 -32
  50. data/lib/brut/front_end/components/i18n_translations.rb +34 -1
  51. data/lib/brut/front_end/components/input.rb +3 -0
  52. data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -0
  53. data/lib/brut/front_end/components/inputs/radio_button.rb +41 -0
  54. data/lib/brut/front_end/components/inputs/select.rb +26 -2
  55. data/lib/brut/front_end/components/inputs/text_field.rb +27 -5
  56. data/lib/brut/front_end/components/inputs/textarea.rb +24 -4
  57. data/lib/brut/front_end/components/locale_detection.rb +8 -1
  58. data/lib/brut/front_end/components/page_identifier.rb +2 -0
  59. data/lib/brut/front_end/components/time.rb +95 -0
  60. data/lib/brut/front_end/components/traceparent.rb +22 -0
  61. data/lib/brut/front_end/download.rb +11 -0
  62. data/lib/brut/front_end/flash.rb +32 -0
  63. data/lib/brut/front_end/form.rb +109 -106
  64. data/lib/brut/front_end/forms/constraint_violation.rb +16 -11
  65. data/lib/brut/front_end/forms/input.rb +30 -42
  66. data/lib/brut/front_end/forms/input_declarations.rb +90 -0
  67. data/lib/brut/front_end/forms/input_definition.rb +45 -30
  68. data/lib/brut/front_end/forms/radio_button_group_input.rb +46 -0
  69. data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +29 -0
  70. data/lib/brut/front_end/forms/select_input.rb +47 -0
  71. data/lib/brut/front_end/forms/select_input_definition.rb +27 -0
  72. data/lib/brut/front_end/forms/validity_state.rb +23 -9
  73. data/lib/brut/front_end/handler.rb +27 -8
  74. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +4 -2
  75. data/lib/brut/front_end/handlers/instrumentation_handler.rb +99 -0
  76. data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -4
  77. data/lib/brut/front_end/handlers/missing_handler.rb +9 -0
  78. data/lib/brut/front_end/handling_results.rb +14 -4
  79. data/lib/brut/front_end/http_method.rb +13 -1
  80. data/lib/brut/front_end/http_status.rb +10 -0
  81. data/lib/brut/front_end/layouts/_internal.html.erb +68 -0
  82. data/lib/brut/front_end/middleware.rb +5 -0
  83. data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +15 -0
  84. data/lib/brut/front_end/middlewares/favicon.rb +16 -0
  85. data/lib/brut/front_end/middlewares/open_telemetry_span.rb +12 -0
  86. data/lib/brut/front_end/middlewares/reload_app.rb +27 -9
  87. data/lib/brut/front_end/page.rb +50 -11
  88. data/lib/brut/front_end/pages/_missing_page.html.erb +17 -0
  89. data/lib/brut/front_end/pages/missing_page.rb +36 -0
  90. data/lib/brut/front_end/request_context.rb +117 -8
  91. data/lib/brut/front_end/route_hook.rb +43 -1
  92. data/lib/brut/front_end/route_hooks/age_flash.rb +3 -2
  93. data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +8 -0
  94. data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +18 -10
  95. data/lib/brut/front_end/route_hooks/locale_detection.rb +11 -5
  96. data/lib/brut/front_end/route_hooks/setup_request_context.rb +7 -4
  97. data/lib/brut/front_end/routing.rb +138 -31
  98. data/lib/brut/front_end/session.rb +86 -7
  99. data/lib/brut/front_end/template.rb +17 -2
  100. data/lib/brut/front_end/templates/block_filter.rb +4 -3
  101. data/lib/brut/front_end/templates/erb_parser.rb +1 -1
  102. data/lib/brut/front_end/templates/escapable_filter.rb +2 -0
  103. data/lib/brut/front_end/templates/html_safe_string.rb +30 -2
  104. data/lib/brut/front_end/templates/locator.rb +60 -0
  105. data/lib/brut/i18n/base_methods.rb +4 -0
  106. data/lib/brut/i18n.rb +1 -0
  107. data/lib/brut/instrumentation/logger_span_exporter.rb +75 -0
  108. data/lib/brut/instrumentation/open_telemetry.rb +107 -0
  109. data/lib/brut/instrumentation.rb +4 -6
  110. data/lib/brut/junk_drawer.rb +54 -4
  111. data/lib/brut/sinatra_helpers.rb +42 -38
  112. data/lib/brut/spec_support/clock_support.rb +6 -0
  113. data/lib/brut/spec_support/component_support.rb +53 -26
  114. data/lib/brut/spec_support/e2e_test_server.rb +82 -0
  115. data/lib/brut/spec_support/enhanced_node.rb +45 -0
  116. data/lib/brut/spec_support/general_support.rb +14 -3
  117. data/lib/brut/spec_support/handler_support.rb +2 -0
  118. data/lib/brut/spec_support/matcher.rb +6 -3
  119. data/lib/brut/spec_support/matchers/be_page_for.rb +3 -2
  120. data/lib/brut/spec_support/matchers/have_constraint_violation.rb +22 -9
  121. data/lib/brut/spec_support/matchers/have_html_attribute.rb +9 -2
  122. data/lib/brut/spec_support/matchers/have_i18n_string.rb +24 -0
  123. data/lib/brut/spec_support/matchers/have_link_to.rb +14 -0
  124. data/lib/brut/spec_support/matchers/have_redirected_to.rb +30 -0
  125. data/lib/brut/spec_support/matchers/have_rendered.rb +4 -11
  126. data/lib/brut/spec_support/matchers/have_returned_http_status.rb +16 -8
  127. data/lib/brut/spec_support/rspec_setup.rb +182 -0
  128. data/lib/brut/spec_support.rb +8 -3
  129. data/lib/brut/version.rb +2 -1
  130. data/lib/brut.rb +28 -5
  131. data/lib/sequel/extensions/brut_instrumentation.rb +5 -27
  132. data/lib/sequel/extensions/brut_migrations.rb +18 -8
  133. data/lib/sequel/plugins/created_at.rb +2 -0
  134. data/lib/sequel/plugins/external_id.rb +39 -1
  135. data/lib/sequel/plugins/find_bang.rb +4 -1
  136. metadata +140 -13
  137. data/lib/brut/back_end/action.rb +0 -3
  138. data/lib/brut/back_end/result.rb +0 -46
  139. data/lib/brut/front_end/components/timestamp.rb +0 -33
  140. data/lib/brut/instrumentation/basic.rb +0 -66
  141. data/lib/brut/instrumentation/event.rb +0 -19
  142. data/lib/brut/instrumentation/http_event.rb +0 -5
  143. data/lib/brut/instrumentation/subscriber.rb +0 -41
@@ -0,0 +1,83 @@
1
+ # Handlers
2
+
3
+ In Brut, a *handler* responds to an HTTP request that *isn't* inteded to render a web page. Primarily, a handler is used to process a
4
+ form submission, but a handler can also respond to an Ajax request. A handler is like a controller in Rails, however in Brut, a
5
+ handler is a class you implement that has a method that receives arguments and a return value that controls the response (unlike controllers in Rails).
6
+
7
+ ## Defining a Route to be Handled
8
+
9
+ There are three ways to define a route that requires a handler, `form`, `action`, and `path`.
10
+
11
+
12
+ class App < Brut::Framework::App
13
+
14
+ routes do
15
+
16
+ form "/new_widget"
17
+ action "/archive_widget/:id"
18
+ path "/payment_received", method: :get
19
+ end
20
+ end
21
+
22
+ `form` indicates you have a form you are going to render in HTML and process its submission. The use of `form` requires that you have
23
+ a {Brut::FrontEnd::Form} defined (see {file:doc-src/forms.md}) as well as a handler. The names are conventional based on the route.
24
+ In the example above, Brut will expect `NewWidgetForm` and `NewWidgetHandler` to exist.
25
+
26
+ You can create these with `bin/scaffold`:
27
+
28
+ bin/scaffold form NewWidget
29
+
30
+ `action` is used when you have a form to submit, but it has no user controls to collect the data to submit. This is akin to Rails'
31
+ `button_to` where the URL describes everything needed to handle a user's action. In the example above, you can imagine a form with a
32
+ method to `/archive_widget/<%= widget.id %>` that has a button labeled "Archive Widget". All that's needed is to submit to the
33
+ server.
34
+
35
+ In that case, only a handler is required. The name is again conventional. In this case, `ArchiveWidgetWithId`. You can create this
36
+ with `bin/scaffold`
37
+
38
+ bin/scaffold handler ArchiveWidthWithId
39
+
40
+ The last case, `path`, is for arbitray routes. It works the same way as `action`, but requires a `method:` to declare the HTTP
41
+ method.
42
+
43
+ ## Implementing a Handler
44
+
45
+ Regardless of how you declare a route, all handlers must inherit {Brut::FrontEnd::Handler} (though realistically, they will inherit
46
+ `AppHandler`, which inherets `Brut::FrontEnd::Handler`) and implement `handle`.
47
+
48
+ `handle` can accept keyword arguments that are injected by Brut according to the rules outlined in {file:/doc-src/keyword-injection.md Keyword Injection}. Note that handlers that process forms should declare `form:` as a keyword argument. They will be given an instantiated instance of their form, based on the values in the form submission from the browser.
49
+
50
+ The return value of the method determines what will happen:
51
+
52
+ * `URI` - the visitor is redirected to this URI. Typically, you'd achieve this with the {Brut::FrontEnd::HandlingResults#redirect_to}
53
+ helper.
54
+ * {Brut::FrontEnd::Component} - this component's HTML is rendered. Note that since {Brut::FrontEnd::Page} is a subclass of
55
+ `Brut::FrontEnd::Component}, returning a page instance will render that entire page. This is useful when re-rendering a page with
56
+ form errors.
57
+ - You can also return a two-element array with the first element being a component and the second being a `Brut::FrontEnd::HttpStatus`. This will render the component's HTML but use the given status as the HTTP status.
58
+ * {Brut::FrontEnd::HttpStatus} - this status is returned with no content. Typically, you'd achieve this with the {Brut::FrontEnd::HandlingResults#http_status}
59
+ * {Brut::FrontEnd::Download} - a file will be downloaded
60
+
61
+ ## Hooks
62
+
63
+ See {file:/doc-src/hooks.md} for more discussiong, but implementing `before_handle` will allow you to run code before `handle` is
64
+ called. This feature is mostly useful for a base class or module to share re-usable logic.
65
+
66
+ ## Testing Handlers
67
+
68
+ Since a handler is just a class, you can test it conventionally, but there are a few things to keep in mind that can make testing your
69
+ handler easier.
70
+
71
+ First is that you should call `handle!`, not `handle`. The public interface of a handler is `handle!`—`handle` is a template method.
72
+ `handle!` will call `before_render`, so you should still call `handle!`, even if you are testing the logic in `before_render`.
73
+
74
+ You can also simplify your expectations with the following matchers:
75
+
76
+ * `have_redirected_to` - asserts that the handler redirected to the given URL. It can be given a `URI` or a page class.
77
+ * `have_rendered`- asserts that the handler renders the given component or page. It expects the page or component class.
78
+ * `have_returned_http_status` - asserts that the handler returned the given HTTP status. This works for a lot of return values that
79
+ the handler can return:
80
+ - If the handler returned a `URI`, the matcher will match on a 302 and fail otherwise
81
+ - If the handler returns an HTTP status code, the matcher's code must match (or the code must be omitted)
82
+ - If the handler returns anything else, the matcher will match on a 200 and fail otherwise
83
+
@@ -0,0 +1,265 @@
1
+ # JavaScript and Front End Behavior
2
+
3
+ Brut does not prevent you from using any front-end framework you like. You can certainly install React and reference it in
4
+ `app/src/front_end/javascript/index.js`. However, Brut would like to humbly request that you not do this.
5
+
6
+ Brut is based around server-generated HTML and the web platform. This means that Brut would like you to use custom elements for any
7
+ client-side behavior you need, and to do so with progressive enhancement.
8
+
9
+ To that end, Brut includes BrutJS, which is a set of custom elements and ancillary JavaScript you can use to build client-side
10
+ behavior.
11
+
12
+ By default, your `index.js` will look like this:
13
+
14
+ import { BrutCustomElements } from "brut-js"
15
+ import Example from "./Example"
16
+
17
+ document.addEventListener("DOMContentLoaded", () => {
18
+ BrutCustomElements.define()
19
+ Example.define()
20
+ })
21
+
22
+ `BrutCustomElements` and `BrutCustomElements.define()` will set up the custom elements bundled with Brut. `Example` shows you how to
23
+ build your own custom elements.
24
+
25
+ ## Some Useful Brut Elements
26
+
27
+ Please refer to BrutJS's documentation for everything that is included, but here are a few highlights that you will find usefule.
28
+
29
+ ### Client Side Form Support
30
+
31
+ {file:doc-src/forms.md Forms} outlines Brut's server-side form support. Brut provides custom elements to allow you to unify client
32
+ and server side constraint validations, and make the process a bit easier to manage with CSS.
33
+
34
+ First, surrounding a form with a `<brut-form>` will place `data-submitted` onto the `<form>` *only* when the user attempts to submit the
35
+ form. You can use this in your CSS to prevent showing error messages before a user has submitted.
36
+
37
+ Second, you can use `<brut-cv-messages>` and `<brut-cv>` to control the error messages that are shown when the browser detects
38
+ constraint violations. This works with `<brut-i18n-translation>` to show translated strings.
39
+
40
+ Consider this ERB
41
+
42
+ <label>
43
+ <%=
44
+ component(
45
+ Brut::FrontEnd::Components::Inputs::TextField.for_form_input(form:, input_name: :name)
46
+ )
47
+ %>
48
+ <span>Name</span>
49
+ <%= constraint_violations(form:,input_name: :name) %>
50
+ </label>
51
+
52
+ {Brut::FrontEnd::Component::Helpers#constraint_violations} will render the built-in {Brut::FrontEnd::Components::ConstraintViolations}
53
+ component. Along with `for_form_input`, the following HTML will be generated:
54
+
55
+ <label>
56
+ <input type="text" name="name" required>
57
+ <span>Name</span>
58
+ <brut-cv-messages input-name="name">
59
+ </brut-cv-messages>
60
+ </label>
61
+
62
+ When any element of the form causes a validity event to be fired, `<brut-form>` will locate the appropriate `<brut-cv-messages>` and
63
+ insert the appropriate `<brut-cv>` elements. Suppose the user submitted this form. Since the `name` input is required, the form
64
+ submission wouldn't happen, and the resulting HTML would look like so:
65
+
66
+ <label>
67
+ <input type="text" name="name" required>
68
+ <span>Name</span>
69
+ <brut-cv-messages input-name="name">
70
+ <brut-cv input-name="name" key="valueMissing"></brut-cv>
71
+ </brut-cv-messages>
72
+ </label>
73
+
74
+ Now, assuming your layout used `<brut-i18n-translation>` custom elements, for example like so:
75
+
76
+ <brut-i18n-translation key="general.cv.fe.valueMissing">%{field} is required</brut-i18n-translation>
77
+
78
+ The `<brut-cv>` custom element will find this and replace its `textContent`, resulting in the following HTML:
79
+
80
+ <label>
81
+ <input type="text" name="name" required>
82
+ <span>Name</span>
83
+ <brut-cv-messages input-name="name">
84
+ <brut-cv input-name="name" key="valueMissing">
85
+ This field is required
86
+ </brut-cv>
87
+ </brut-cv-messages>
88
+ </label>
89
+
90
+ You can now use CSS to style client-side validations *and* control the content shown to the user. If there are server-side constraint
91
+ violations, The `ConstraintViolations` component would render them (as well as server-generated translations), for example if a name
92
+ was given, but it's taken already, `ConstraintViolations` would render this HTML:
93
+
94
+ <label>
95
+ <input type="text" name="name" required value="foo">
96
+ <span>Name</span>
97
+ <brut-cv-messages input-name="name">
98
+ <brut-cv input-name="name" server-side>
99
+ This value has been taken
100
+ </brut-cv>
101
+ </brut-cv-messages>
102
+ </label>
103
+
104
+ ### Confirming Dangerous Actions
105
+
106
+ Often, you want to use JavaScript to confirm the submission of a form whose action is considered dangerous to the user or hard to undo.
107
+ This can be achieved with `<brut-confirm-submit>`
108
+
109
+ <form>
110
+ <input type="text" name="name" required>
111
+ <brut-confirm-submit message="This will delete the app">
112
+ <button>Delete App</button>
113
+ </brut-confirm-submit>
114
+ </form>
115
+
116
+ By default, this will use the browser's built-in `window.confirm`, however you can also use a `<dialog>` element as well.
117
+
118
+ If the generated HTML includes a `<dialog>`, you can surround it with `<brut-confirmation-dialog>` to indicate it should be used for
119
+ confirmation. The `<dialog>` should have an `<h1>` where the message will be placed and two buttons, one with `value="ok"` and one
120
+ with `value="cancel"`.
121
+
122
+
123
+ <brut-confirmation-dialog>
124
+ <dialog>
125
+ <h1></h1>
126
+ <button value="ok">Do It!</button>
127
+ <button value="cancel">Nevermind</button>
128
+ </dialog>
129
+ </brut-confirmation-dialog>
130
+
131
+ When the `<brut-confirm-submit>`'s `<button>` is clicked, this `<dialog>` is shown with the message inserted. If the user hits the
132
+ button with `value` of `"ok"`, the form submission goes through. Otherwise, it doesn't. The dialog is then hidden.
133
+
134
+ ### Ajax Form Submission
135
+
136
+ To submit a form via Ajax, you can use `<brut-ajax-submit>` around the `<button>` that should submit the form with Ajax. This element
137
+ attempts to provide a fault-tolerant user experience and will set various attributes on itself to allow you to change styling during
138
+ the various phases of the request.
139
+
140
+ If the submission works, it will fire a `brut:submitok` event that your custom code can receive and do whatever makes the most sense
141
+ in that case.
142
+
143
+ If the submission fails with a 422, your server should return a series of `<brut-cv>` custom elements. If it does, this will be
144
+ inserted into the correct `<brut-cv-messages>` elements to dynamically create error messages. The element will then fire a
145
+ `brut:submitinvalid` event you can catch and handle to do something custom.
146
+
147
+ If the submission times out or fails in some other way, Brut will submit the form the old-fashioned way.
148
+
149
+ ### Client-Side, Accessible Tabs
150
+
151
+ Many UIs involve a set of tabs that switch between different views. While HTML has no built-in support for this, Brut's `<brut-tabs>`
152
+ custom element can captialize on the various ARIA roles required to design a tabbed interface and provide all the JavaScript behavior
153
+ necessary. See that element's documentation for an extended example.
154
+
155
+ ## Building Your Own Custom Elements
156
+
157
+ Because custom elements are part of the web platform, Brut encourages you to use them to add client-side behavior. As a
158
+ demonstration of this working, there is an `Example` element set up when you created your app. Assuming your app's prefix was `cc`
159
+ when you created it, the `Example` element works like so:
160
+
161
+ <cc-example transform="upper">
162
+ Here is some text
163
+ </cc-example>
164
+
165
+ When the browser renders this—assuming JavaScript is enabled—it will render the following:
166
+
167
+ <cc-example transform="upper">
168
+ HERE IS SOME TEXT
169
+ </cc-example>
170
+
171
+ You wouldn't want to do this, but this simple element demonstrates both how to make your own and that custom elements in your app are
172
+ properly configured.
173
+
174
+ This element is very close to a vanilla custom element, but it extends `BaseCustomElement`, which is provided by BrutJS, which includes
175
+ z few quality-of-life improvements. Let's walk through the code.
176
+
177
+ First, we extends `BaseCustomElement` (which extends `HTMLElement`) and define a static attribute, `tagName` that will be the
178
+ element's tag name you can use in your HTML:
179
+
180
+ import { BaseCustomElement } from "brut-js"
181
+
182
+ class Example extends BaseCustomElement {
183
+ static tagName = "cc-example"
184
+
185
+ Next, we'll define the attributes of our element using `observedAttributes`, which is part of the custom element spec (and not
186
+ specific to BrutJS):
187
+
188
+ static observedAttributes = [
189
+ "transform",
190
+ "show-warnings",
191
+ ]
192
+
193
+ The `show-warnings` attribute, if placed on the element's HTML, configures `this.logger` from `BaseCustomElement` to allow output of debug messages. This allows you to easily debug your element's behavior in development, but remove them from production. We'll see that in a bit.
194
+
195
+ Next, we'll set a private attribute to hold a default value for the `transform` HTML attribute. It can be named anything:
196
+
197
+ #transform = "upper"
198
+
199
+ Now, we want to know when `transform` changes. Normally, you'd implement `attributeChangedCallback` and check its `name` parameter.
200
+ `BaseCustomElement` allows you to do this more directly by creating a `xxxChangedCallback` method for each attribute in
201
+ `observedAttributes` that you want to know about. For `transform`, that means implementing `transformChangedCallback`:
202
+
203
+ transformChangedCallback({newValue}) {
204
+ this.#transform = newValue
205
+ }
206
+
207
+ Next, we implement the bulk of the element's behavior. Because there are many lifecycle events that may require modifying the
208
+ element, `BaseCustomElement` consolidates all of those events and calls the method `update()`. `update()` should be idempotent
209
+ and should examine the element's state (as well as the document's, if necessary) and update the element however it makes sense.
210
+
211
+ For this example, we want to grab the content, examine the value for `transform` and change the content:
212
+
213
+ update() {
214
+ const content = this.textContent
215
+ if (this.#transform == "upper") {
216
+ this.textContent = content.toLocaleUpperCase()
217
+ }
218
+ else if (this.#transform == "lower") {
219
+ this.textContent = content.toLocaleLowerCase()
220
+ }
221
+ else {
222
+ this.logger.info("We only support upper or lower, but got %s",this.#transform)
223
+ }
224
+ }
225
+
226
+ Notice the last line where we call `this.logger.info`. If `show-warnings` is omitted from the HTML, this message isn't shown anywhere. If `show-warnings` *is* present, this message will show up in the console. This can be useful for understanding why your element isn't working.
227
+
228
+ Lastly, we export the class:
229
+
230
+ }
231
+ export default Example
232
+
233
+ By virtue of having extended `BaseCustomElement`, the static `define()` method will set it up as a custom element with the browser.
234
+
235
+ ## Testing Custom Elements
236
+
237
+ Sometimes, system tests are sufficient to ensure your custom element code is working. If not, Brut (via BrutJS) provides a way to
238
+ test your element in isolation.
239
+
240
+ These tests use JSDom which, while not perfect, allows the tests to run reasonably quickly. Each test begins with `withHTML` to
241
+ define the markup that is being tested. This is followed by `test` which accepts a function that performs the test.
242
+
243
+ Rather than have a DSL to provide access to your element's state, you can use the browser's API (as provided by JSDom) to check
244
+ whatever it is you need to check:
245
+
246
+ import { withHTML } from "./SpecHelper.js"
247
+
248
+ describe("<cc-example>", () => {
249
+ withHTML(`
250
+ <cc-example>This is some Text</cc-example>
251
+ `).test("upper case by default", ({document,assert}) => {
252
+ const element = document.querySelector("cc-example")
253
+ assert.equal(element.textContent,"THIS IS SOME TEXT")
254
+ })
255
+ withHTML(`
256
+ <cc-example transform="lower">This is some Text</cc-example>
257
+ `).test("lower case when asked", ({document,assert}) => {
258
+ const element = document.querySelector("cc-example")
259
+ assert.equal(element.textContent,"this is some text")
260
+ })
261
+ })
262
+
263
+ Note that `test`'s second argument—the function that performs the test—is called with objects you can use to perform the test. In this
264
+ case, the `document` is passed in as-is the `assert` method.
265
+
@@ -0,0 +1,183 @@
1
+ # Keyword Injection
2
+
3
+ Brut is desiged around classes and objects, as compared to modules and DSLs. Almost everything you do when creating your app is to
4
+ create a class that has an initializer and implements one or more methods. But, these initalizers and methods often need information
5
+ from the request that Brut is managing.
6
+
7
+ In a basic Rack or Sinatra app, you would access stuff like query parameters or the session by using Rack's API. This can be tedious
8
+ and error-prone. Brut will inject certain values into your class based on the keyword arguments of the initializer or method.
9
+
10
+ For example, a {file:doc-src/pages.md Page} requires you to implement an initializer. That initializer's keyword arguments define what
11
+ information is needed. Brut provides that information when it creates the object. This is a form of dependency injection and it can simplify your code if used effectively.
12
+
13
+ Consider this route:
14
+
15
+ page "/widgets/:id"
16
+
17
+ Brut will expect to find `WidgetsByIdPage`. Your initializer can declare `id:` as a keyword arg and this will be passed when the
18
+ class is created:
19
+
20
+ class WidgetsByIdPage < AppPage
21
+ def initialize(id:)
22
+ @widget = DB::Widget.find(id)
23
+ end
24
+ end
25
+
26
+ There are many more values that can made available. For example, suppose you accept the query string parameter "compact" that
27
+ controls some rendering of the page. To access it, declare it as a keyword arg (being sure to set a default value since it may not be available):
28
+
29
+ class WidgetsByIdPage < AppPage
30
+ def initialize(id:, compact: false)
31
+ @widget = DB::Widget.find(id)
32
+ @compact = compact
33
+ end
34
+ end
35
+
36
+ ## Standard Injectible Information
37
+
38
+ In any request, the following information is available to be injected:
39
+
40
+ * `session:` - An instance of your app's {Brut::FrontEnd::Session} subclass for the current visitor's session.
41
+ * `flash:` - An instance of your app's {Brut::FrontEnd::Flash} subclass.
42
+ * `xhr:` - true if this was an Ajax request.
43
+ * `body:` - the body submitted, if any.
44
+ * `csrf_token:` - The current CSRF token.
45
+ * `clock:` - A {Clock} to be used to access the current time in the visitor's time zone.
46
+ * `http_*` - any parameter that starts with `http_` is assumed to be for an HTTP header. For example, `http_accept_language` would be
47
+ given the value for the "Accept-Language" header. See the section on HTTP headers below.
48
+ * `env:` - The Rack env. This is discouraged, but available if you can't get what you want directly
49
+
50
+ Depending on the context, other information is available:
51
+
52
+ * `form:` - If a form was submitted, this is the {Brut::FrontEnd::Form} subclass containing the data. See {file:doc-src/forms.md Forms}.
53
+ * Any query string paramter - Remember, these should have default values or Brut will raise an error if they are not provided.
54
+ * Any route parameter - These should not have default values, since they are required for Brut to match the route.
55
+
56
+ A {Brut::FrontEnd::RouteHook} is slightly different. Only the following data is available to be injected:
57
+
58
+ * `:request_context` - The current request context, thought it may be `nil` depending on when the hook runs
59
+ * `session:` - An instance of your app's {Brut::FrontEnd::Session} subclass for the current visitor's session.
60
+ * `:request` - The Rack request
61
+ * `:response` - The Rack response
62
+ * `env:` - The Rack env.
63
+
64
+ ### HTTP Headers
65
+
66
+ Since any header can be sent with a request, Brut allows you to access them, including non-standard ones. Rack (which is based on CGI), provides access to all HTTP headers in the `env` by taking the header name, replacing dashes ("-") with underscores ("\_"), and prepending `http_` to the name, then uppercasing it. Thus, "User-Agent" becomes `HTTP_USER_AGENT`.
67
+
68
+ Because Ruby parameters and variables must start with a lower-case letter, Brut uses the lowercased version of the Rack/CGI variable.
69
+ Thus, to receive the "User-Agent", you would declare the keyword parameter `http_user_agent`.
70
+
71
+ Further, because headers come from the client and may not be under your control, the value that is actually injected depends on a few
72
+ things:
73
+
74
+ * If your keyword arg is required, i.e. there is no default value:
75
+ - If the header was not provided, `nil` is injected.
76
+ - If the header *was* provided, it's value is injected, even if it's the empty string.
77
+ * If your keyword arg is optional, i.e. it has a default value
78
+ - If the header was not provided, no value is injected, and your code will receive the default value.
79
+ - If the header *was* provided, it's value is injected, even if it's the empty string.
80
+
81
+ ### Ordering and Disambiguation
82
+
83
+ You are discouraged from using builtin keys for your own data or request parameters. For example, you should not have a query string
84
+ parameter named `env` as this conflicts with the builtin `env` that Brut will inject.
85
+
86
+ Since you can inject your own data (see below), you are free to corrupt the request context. Please don't do this. Brut may actively
87
+ prevent this in the future.
88
+
89
+ You can also use the request context to put your own data that can be injected.
90
+
91
+ ## Injecting Custom Data
92
+
93
+ The general lifecycle of a request is that any before hook is run, then the page or action is triggered, then after actions. Thus, to
94
+ inject your own data, such as the currently authenticated visitor, you would use a before hook:
95
+
96
+ class AppSession < Brut::FrontEnd::Session
97
+ def logged_in?
98
+ !!self.authenticated_account
99
+ end
100
+ def authenticated_account
101
+ # look up the account data model
102
+ # based on e.g. self[:account_id]
103
+ end
104
+ end
105
+
106
+ class AuthBeforeHook < Brut::FrontEnd::RouteHook
107
+ def before(request_context:,session:,request:,env:)
108
+ if session.logged_in?
109
+ request_context[:authenticated_account] = session.authenticated_account
110
+ end
111
+ continue
112
+ end
113
+ end
114
+
115
+ Once you do this, you can use `authenticated_account:` as a keyword argument to any page, handler, or global component:
116
+
117
+ class DashboardPage < AppPage
118
+ def initialize(authenticated_account:)
119
+ @widgets = authenticated_account.widgets # e.g.
120
+ end
121
+ end
122
+
123
+ While this won't handle authorization for you, you can be sure that when `DashboardPage` is used, there is an `authenticated_account`
124
+ available.
125
+
126
+ ## `nil` and Empty Strings
127
+
128
+ When a keyword argument has no default value, Brut will require that value to exist and be available for injection. If the keyword is
129
+ not one of the canned always-available values, it will look in the request context, then in the query string.
130
+
131
+ If the request has the keyword as a key, *it will inject whatever value it finds, including `nil`*. In general, you should avoid
132
+ injecting `nil` when you actually intend to not have a value.
133
+
134
+ For example, the `AuthBeforeHook` above, you could implement it like so:
135
+
136
+ request_context[:authenticated_account] = session.authenticated_account
137
+
138
+ The problem is that if the visitor is not logged in, the `:authenticated_account` *will* have a value, and that value will be `nil`.
139
+ This is almost certainly not what you want.
140
+
141
+ For query string parameters, the HTTP spec says that they are strings. Thus, if a query string parameter is present in ther request
142
+ URL, it will *always* have a value and *never* be `nil`. If the paramter doesn't have a value after the `=` (e.g. for `foo` in `?foo=&bar=quux`), the value will be the empty string.
143
+
144
+ This means you must write code to explicitly handle the cases you care about.
145
+
146
+ ## When Values Aren't Available
147
+
148
+ When a value is not available for injection, and the keyword doesn't provide a default, Brut will raise an error. This is because
149
+ such a situation represents a design error.
150
+
151
+ For example, the `DashboardPage` above requires an `authenticated_account`. Your app should never route a logged-out visitor to that
152
+ page. This allows the `DashboardPage` to avoid having to check for `nil` and figure out what to do.
153
+
154
+ This is most relevant for query string parameters, since they can be easily manipulated by the visitor in their browser. Query string
155
+ parameters should always have a default value, even if it's `nil`.
156
+
157
+ *Path* parameters (like `:id` in `WidgetsByIdPage`) should *never* have a default value as their absence means a different URL was
158
+ requested. For example, `/widgets` would trigger a `WidgetsPage`. *Only* if the `:id` path parameter is present would the
159
+ `WidgetsByIdPage` be triggered, so it's safe to omit the default value for `id:` (and pointless to include one).
160
+
161
+ See {file:docs-src/route-hooks.md}
162
+
163
+ ## Design For Injection
164
+
165
+ Consider a method like so:
166
+
167
+ def create_widget(name:, organization: nil, quantity: 10)
168
+
169
+ Outside of Brut, the way to interpret this arguments is as follows:
170
+
171
+ * `name` is required
172
+ * `organization` is optional
173
+ * `quantity` has a default value of 10 if not provided
174
+
175
+ Any method or intializer that will be keyword-injected should be designed with this in mind. Thus, the following guidelines will be
176
+ helpful in managing your app:
177
+
178
+ * **Choose arguments based on the needs of the class:**
179
+ - If a value is optional, default it to either `nil` or a symbol that indicates what happens when the value is omitted
180
+ - If an optional value has a default, use that (this should be rare for pages, handlers, components, and hooks)
181
+ - Otherwise, do not provide a default for the keyword
182
+ * **Do not inject `nil` into the request context.** When your code requires a value for a keyword, you want to rely on that value being non-nil. Thus, avoid injecting `nil` into the request context. Brut will allow it as a sort-of escape hatch, but you should design your app to avoid it
183
+ * **Be careful injecting global data.** The request context instance is per request, but you could certainly put global data into it. For example, you may put an initialized API client into the request context as a convieniece. **Be careful** because your app is multi-threaded. Any object that is not scoped to the request must be thread-safe.