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
data/doc-src/handlers.md
DELETED
@@ -1,83 +0,0 @@
|
|
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
|
-
|
data/doc-src/javascript.md
DELETED
@@ -1,265 +0,0 @@
|
|
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
|
-
|
@@ -1,183 +0,0 @@
|
|
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.
|