brut 0.0.1 → 0.0.2
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/.gitignore +6 -0
- data/Gemfile.lock +66 -1
- data/README.md +36 -0
- data/Rakefile +22 -0
- data/brut.gemspec +7 -0
- data/doc-src/architecture.md +102 -0
- data/doc-src/assets.md +98 -0
- data/doc-src/forms.md +214 -0
- data/doc-src/handlers.md +83 -0
- data/doc-src/javascript.md +265 -0
- data/doc-src/keyword-injection.md +183 -0
- data/doc-src/pages.md +210 -0
- data/doc-src/route-hooks.md +59 -0
- data/docs-todo.md +32 -0
- data/lib/brut/back_end/seed_data.rb +5 -1
- data/lib/brut/back_end/validator.rb +1 -1
- data/lib/brut/back_end/validators/form_validator.rb +31 -6
- data/lib/brut/cli/app.rb +100 -4
- data/lib/brut/cli/app_runner.rb +38 -5
- data/lib/brut/cli/apps/build_assets.rb +4 -6
- data/lib/brut/cli/apps/db.rb +2 -7
- data/lib/brut/cli/apps/scaffold.rb +413 -7
- data/lib/brut/cli/apps/test.rb +14 -1
- data/lib/brut/cli/command.rb +141 -6
- data/lib/brut/cli/error.rb +30 -3
- data/lib/brut/cli/execution_results.rb +44 -6
- data/lib/brut/cli/executor.rb +21 -1
- data/lib/brut/cli/options.rb +29 -2
- data/lib/brut/cli/output.rb +47 -0
- data/lib/brut/cli.rb +26 -0
- data/lib/brut/factory_bot.rb +2 -0
- data/lib/brut/framework/app.rb +38 -5
- data/lib/brut/framework/config.rb +97 -54
- data/lib/brut/framework/container.rb +97 -33
- data/lib/brut/framework/errors/abstract_method.rb +3 -7
- data/lib/brut/framework/errors/missing_parameter.rb +12 -0
- data/lib/brut/framework/errors/no_class_for_path.rb +25 -0
- data/lib/brut/framework/errors/not_found.rb +12 -2
- data/lib/brut/framework/errors/not_implemented.rb +14 -0
- data/lib/brut/framework/errors.rb +18 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +17 -12
- data/lib/brut/framework/mcp.rb +106 -49
- data/lib/brut/framework/project_environment.rb +9 -0
- data/lib/brut/framework.rb +1 -0
- data/lib/brut/front_end/asset_metadata.rb +7 -1
- data/lib/brut/front_end/component.rb +129 -38
- data/lib/brut/front_end/components/constraint_violations.rb +57 -0
- data/lib/brut/front_end/components/form_tag.rb +23 -32
- data/lib/brut/front_end/components/i18n_translations.rb +34 -1
- data/lib/brut/front_end/components/input.rb +3 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -0
- data/lib/brut/front_end/components/inputs/radio_button.rb +41 -0
- data/lib/brut/front_end/components/inputs/select.rb +26 -2
- data/lib/brut/front_end/components/inputs/text_field.rb +27 -5
- data/lib/brut/front_end/components/inputs/textarea.rb +24 -4
- data/lib/brut/front_end/components/locale_detection.rb +8 -1
- data/lib/brut/front_end/components/page_identifier.rb +2 -0
- data/lib/brut/front_end/components/time.rb +95 -0
- data/lib/brut/front_end/components/traceparent.rb +22 -0
- data/lib/brut/front_end/download.rb +11 -0
- data/lib/brut/front_end/flash.rb +32 -0
- data/lib/brut/front_end/form.rb +109 -106
- data/lib/brut/front_end/forms/constraint_violation.rb +16 -11
- data/lib/brut/front_end/forms/input.rb +30 -42
- data/lib/brut/front_end/forms/input_declarations.rb +90 -0
- data/lib/brut/front_end/forms/input_definition.rb +45 -30
- data/lib/brut/front_end/forms/radio_button_group_input.rb +46 -0
- data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +29 -0
- data/lib/brut/front_end/forms/select_input.rb +47 -0
- data/lib/brut/front_end/forms/select_input_definition.rb +27 -0
- data/lib/brut/front_end/forms/validity_state.rb +23 -9
- data/lib/brut/front_end/handler.rb +27 -8
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +4 -2
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +99 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -4
- data/lib/brut/front_end/handlers/missing_handler.rb +9 -0
- data/lib/brut/front_end/handling_results.rb +14 -4
- data/lib/brut/front_end/http_method.rb +13 -1
- data/lib/brut/front_end/http_status.rb +10 -0
- data/lib/brut/front_end/layouts/_internal.html.erb +68 -0
- data/lib/brut/front_end/middleware.rb +5 -0
- data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +15 -0
- data/lib/brut/front_end/middlewares/favicon.rb +16 -0
- data/lib/brut/front_end/middlewares/open_telemetry_span.rb +12 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +27 -9
- data/lib/brut/front_end/page.rb +50 -11
- data/lib/brut/front_end/pages/_missing_page.html.erb +17 -0
- data/lib/brut/front_end/pages/missing_page.rb +36 -0
- data/lib/brut/front_end/request_context.rb +117 -8
- data/lib/brut/front_end/route_hook.rb +43 -1
- data/lib/brut/front_end/route_hooks/age_flash.rb +3 -2
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +18 -10
- data/lib/brut/front_end/route_hooks/locale_detection.rb +11 -5
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +7 -4
- data/lib/brut/front_end/routing.rb +138 -31
- data/lib/brut/front_end/session.rb +86 -7
- data/lib/brut/front_end/template.rb +17 -2
- data/lib/brut/front_end/templates/block_filter.rb +4 -3
- data/lib/brut/front_end/templates/erb_parser.rb +1 -1
- data/lib/brut/front_end/templates/escapable_filter.rb +2 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +30 -2
- data/lib/brut/front_end/templates/locator.rb +60 -0
- data/lib/brut/i18n/base_methods.rb +4 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/logger_span_exporter.rb +75 -0
- data/lib/brut/instrumentation/open_telemetry.rb +107 -0
- data/lib/brut/instrumentation.rb +4 -6
- data/lib/brut/junk_drawer.rb +54 -4
- data/lib/brut/sinatra_helpers.rb +42 -38
- data/lib/brut/spec_support/clock_support.rb +6 -0
- data/lib/brut/spec_support/component_support.rb +53 -26
- data/lib/brut/spec_support/e2e_test_server.rb +82 -0
- data/lib/brut/spec_support/enhanced_node.rb +45 -0
- data/lib/brut/spec_support/general_support.rb +14 -3
- data/lib/brut/spec_support/handler_support.rb +2 -0
- data/lib/brut/spec_support/matcher.rb +6 -3
- data/lib/brut/spec_support/matchers/be_page_for.rb +3 -2
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +22 -9
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +9 -2
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +24 -0
- data/lib/brut/spec_support/matchers/have_link_to.rb +14 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +30 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +4 -11
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +16 -8
- data/lib/brut/spec_support/rspec_setup.rb +182 -0
- data/lib/brut/spec_support.rb +8 -3
- data/lib/brut/version.rb +2 -1
- data/lib/brut.rb +28 -5
- data/lib/sequel/extensions/brut_instrumentation.rb +5 -27
- data/lib/sequel/extensions/brut_migrations.rb +18 -8
- data/lib/sequel/plugins/created_at.rb +2 -0
- data/lib/sequel/plugins/external_id.rb +39 -1
- data/lib/sequel/plugins/find_bang.rb +4 -1
- metadata +140 -13
- data/lib/brut/back_end/action.rb +0 -3
- data/lib/brut/back_end/result.rb +0 -46
- data/lib/brut/front_end/components/timestamp.rb +0 -33
- data/lib/brut/instrumentation/basic.rb +0 -66
- data/lib/brut/instrumentation/event.rb +0 -19
- data/lib/brut/instrumentation/http_event.rb +0 -5
- data/lib/brut/instrumentation/subscriber.rb +0 -41
data/doc-src/pages.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# Pages and Components
|
|
2
|
+
|
|
3
|
+
A website is made up of pages. Even a web *app* has pages. Thus, the most basic way to create dynamic behavior with Brut is the
|
|
4
|
+
*page*. In Brut, a page is a URL, a subclass of {Brut::FrontEnd::Page}, and an ERB template. The template is rendered in the context
|
|
5
|
+
of an instance of the class whenever the URL is requested.
|
|
6
|
+
|
|
7
|
+
## Overview of Rendering Pages
|
|
8
|
+
|
|
9
|
+
In your `app.rb`, you first declare a page using the {Brut::SinatraHelpers::ClassMethods#page} method (inside the block given to {Brut::Framework::App.routes}):
|
|
10
|
+
|
|
11
|
+
class App < Brut::Framework::App
|
|
12
|
+
|
|
13
|
+
routes do
|
|
14
|
+
|
|
15
|
+
page "/sign_in"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
The name of the route must start with a `/` and the rest of the route determines the name of the page. In the example above, Brut
|
|
20
|
+
will expect to find a class named `SignInPage`, which should be located in `app/src/front_end/pages`. You can create it with
|
|
21
|
+
`bin/scaffold`:
|
|
22
|
+
|
|
23
|
+
> bin/scaffold page SignInPage
|
|
24
|
+
|
|
25
|
+
The page class must subclass {Brut::FrontEnd::Page}. You write a constructor to accept whatever your page needs to assemble the data
|
|
26
|
+
needed to render it.
|
|
27
|
+
|
|
28
|
+
The HTML is generated based on an ERB file located in `app/src/front_end/pages`. In this example, that's
|
|
29
|
+
`app/src/front_end/pages/sign_in_page.html.erb`. That ERB is executed with an instance of the page class as context. That means you
|
|
30
|
+
can references any methods or ivars.
|
|
31
|
+
|
|
32
|
+
## Creating a Page Class
|
|
33
|
+
|
|
34
|
+
{Brut::FrontEnd::Page#render} is called by Brut and expects to receive HTML, which it will send as the body of the response. `render`
|
|
35
|
+
manages the ERB rendering, so the bulk of your page's logic will be in other methods. The constructor is where any data you need is
|
|
36
|
+
provided.
|
|
37
|
+
|
|
38
|
+
A page's `initialize` method must accept only keyword arguments and those keywords are used by Brut to determine what data needs to be
|
|
39
|
+
passed in. This technique, called *{file:doc-src/keyword-injection.md Keyword Injection}*, is used in several other places in Brut.
|
|
40
|
+
|
|
41
|
+
When the page's URL is requested, an instance of your page class is created, passing in the values needed. Beyond that, however, your page class is a normal Ruby class. You can save data to instance variables, implement attributes or other methods. Basically, whatever logic your page needs to render will exist in the page class.
|
|
42
|
+
|
|
43
|
+
## Implementing Your ERB
|
|
44
|
+
|
|
45
|
+
ERB is part of the Ruby standard library that allows the creation of dynamic templates. Brut's ERB is close to this implementation, but has a few additional features that make working with HTML a bit safer.
|
|
46
|
+
|
|
47
|
+
### Your Page is the Only Context
|
|
48
|
+
|
|
49
|
+
The instance of your page class is the only context available to the ERB. There is no set of global helpers that are injected, nor
|
|
50
|
+
are there any modules added when the page is rendered. Anything you need to do in your ERB must be available to the page class you've
|
|
51
|
+
created.
|
|
52
|
+
|
|
53
|
+
The reason for this is to keep things simple. If your page's HTML needs access to a lot of stuff, your page class will be the source
|
|
54
|
+
of truth as to where that stuff comes from. You will always be able to figure out where methods are defined by looking at the page
|
|
55
|
+
class or any of its ancestors. You are free to `include` as many modules as you like, or dump lots of methods into your `AppPage`
|
|
56
|
+
base class. But you don't have to.
|
|
57
|
+
|
|
58
|
+
### All Strings are HTML-Escaped
|
|
59
|
+
|
|
60
|
+
Any instance of a `String` is HTML-escaped by Brut before being inserted into the HTML being generated by the ERB. This is, generally, what you want to have happen. If you have a string that you believe is safe and does not need to be escaped, you can prevent HTML-escaping in one of two ways:
|
|
61
|
+
|
|
62
|
+
* Call {Brut::FrontEnd::Component#html_safe!} on the string
|
|
63
|
+
|
|
64
|
+
<%= html_safe!(get_value) %>
|
|
65
|
+
|
|
66
|
+
* Wrap the string in a {Brut::FrontEnd::Templates::HTMLSafeString}.
|
|
67
|
+
|
|
68
|
+
def get_value
|
|
69
|
+
Brut::FrontEnd::Templates::HTMLSafeString.from_string(some_value)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
This is what `html_safe!` does and this is what Brut does internally. Brut doesn't monkey-patch `String`. A `String` is always
|
|
73
|
+
considered unsafe, and an `HTMLSafeString` is always considered safe.
|
|
74
|
+
|
|
75
|
+
Both of these methods are verbose, but this is on purpose. You generally don't want un-escaped HTML going into your HTML, but if you
|
|
76
|
+
do, you may find it better to create a component (see below), or use {Brut::FrontEnd::Component::Helpers#html_tag} to generate markup.
|
|
77
|
+
|
|
78
|
+
### Pages Have Layouts
|
|
79
|
+
|
|
80
|
+
The page's ERB is rendered in the context of a *layout*. A Layout works similar to how it works in Rails. It's intended to hold HTML
|
|
81
|
+
needed by every page of your app. A minimal layout might look like so:
|
|
82
|
+
|
|
83
|
+
<!DOCTYPE html>
|
|
84
|
+
<html>
|
|
85
|
+
<head>
|
|
86
|
+
<meta charset="utf-8">
|
|
87
|
+
<%= component(Brut::FrontEnd::Components::PageIdentifier.new(self.page_name)) %>
|
|
88
|
+
<link rel="preload" as="style" href="<%= asset_path('/css/styles.css') %>">
|
|
89
|
+
<link rel="stylesheet" href="<%= asset_path('/css/styles.css') %>">
|
|
90
|
+
<script defer src="<%= asset_path('/js/app.js') %>"></script>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
<%= yield %>
|
|
94
|
+
</body>
|
|
95
|
+
</html>
|
|
96
|
+
|
|
97
|
+
The layout is rendered in the context of the page class, so every page must provide whatever features are needed by the layout. This
|
|
98
|
+
is usually done by adding globally-used functions to `AppPage`, which is the base class of all your app's pages.
|
|
99
|
+
|
|
100
|
+
A page can use another layout by overriding {Brut::FrontEnd::Page#layout}. The string returned must match a file in
|
|
101
|
+
`app/src/front_end/layouts/`.
|
|
102
|
+
|
|
103
|
+
### There Are No Partials
|
|
104
|
+
|
|
105
|
+
Rails partials are not part of ERB, and Brut does not include this feature. Instead, you would use *Components*.
|
|
106
|
+
|
|
107
|
+
## Decompose and Re-Use with Components
|
|
108
|
+
|
|
109
|
+
*Components* in Brut are very similar to the View Components library, though somewhat simpler. A component is a class and an ERB
|
|
110
|
+
template. That template is rendered in the context of an instance of the class. That class is created the same way a page is and has
|
|
111
|
+
a `render` method that works just like a page's.
|
|
112
|
+
|
|
113
|
+
This is all because {Brut::FrontEnd::Page} extends {Brut::FrontEnd::Component}. A page adds the concept of a layout, but generally a
|
|
114
|
+
page and a component are the same thing.
|
|
115
|
+
|
|
116
|
+
### Using Components
|
|
117
|
+
|
|
118
|
+
The main difference from your perspective is that generally *you* create instances of components. This means that your page must have
|
|
119
|
+
access to any data a component needs. When you've created your component instance, use {Brut::FrontEnd::Component#component} to
|
|
120
|
+
render it:
|
|
121
|
+
|
|
122
|
+
<%= component(Button.new(type: :danger, label: "Cancel Subscription")) %>
|
|
123
|
+
|
|
124
|
+
### Components with Templates
|
|
125
|
+
|
|
126
|
+
The most common way to use a component is with an ERB template. It is expected to be in `app/src/front_end/components`. For the
|
|
127
|
+
hypothetical `Button` component above, Brut would expect `app/src/front_end/components/button.html.erb` to exist as a template.
|
|
128
|
+
|
|
129
|
+
Just like a page, the component's ERB is rendered in the context of the component only.
|
|
130
|
+
|
|
131
|
+
Sometimes, components are simple enough that you don't need HTML.
|
|
132
|
+
|
|
133
|
+
### Components That Render Themselves
|
|
134
|
+
|
|
135
|
+
While overriding `render` in a page is generally discouraged, {Brut::FrontEnd::Component#render} can be overridden if you want to
|
|
136
|
+
generate HTML yourself. The best way to do that is with {Brut::FrontEnd::Component::Helpers#html_tag}, though you can always return a
|
|
137
|
+
`String` or {Brut::FrontEnd::Templates::HTMLSafeString} that you've created yourself. Just remeber that all `String` instances will be
|
|
138
|
+
HTML-escaped.
|
|
139
|
+
|
|
140
|
+
As an example, here is how you might have a Markdown component that renders Markdown as HTML:
|
|
141
|
+
|
|
142
|
+
class MarkdownComponent < AppComponent
|
|
143
|
+
def initialize(markdown:)
|
|
144
|
+
@markdown = markdown
|
|
145
|
+
@renderer = Redcarpet::Markdown.new(
|
|
146
|
+
Redcarpet::Render::HTML.new(
|
|
147
|
+
filter_html: true,
|
|
148
|
+
no_images: true,
|
|
149
|
+
no_styles: true,
|
|
150
|
+
safe_links_only: true,
|
|
151
|
+
),
|
|
152
|
+
fenced_code_blocks: true,
|
|
153
|
+
autolink: true,
|
|
154
|
+
quote: true,
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def render
|
|
159
|
+
html_safe!(@renderer.render(@markdown.to_s))
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
### Global Components
|
|
165
|
+
|
|
166
|
+
While a component is just a class with an initializer, sometimes you need a component that is generally useful on any page, but that
|
|
167
|
+
you don't want to initialize. For example, if your component needs access to the flash, but your page does not, you don't want your
|
|
168
|
+
page to require a flash just to pass to the component. In that case, a *global component* can be used and Brut will instantiate it.
|
|
169
|
+
|
|
170
|
+
{Brut::FrontEnd::Component#component} can be given a class, and Brut will use keyword injection to create it. Consider a generic
|
|
171
|
+
flash component:
|
|
172
|
+
|
|
173
|
+
class GlobalFlash < AppComponent
|
|
174
|
+
|
|
175
|
+
attr_reader :flash
|
|
176
|
+
|
|
177
|
+
def initialize(flash:)
|
|
178
|
+
@flash = flash
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
You can use this anywhere like so:
|
|
183
|
+
|
|
184
|
+
<%= component(GlobalFlash) %>
|
|
185
|
+
|
|
186
|
+
Because the flash is availble from the {Brut::FrontEnd::RequestContext}, Brut can create this component when needed.
|
|
187
|
+
|
|
188
|
+
## Testing Pages and Components
|
|
189
|
+
|
|
190
|
+
Pages and Components are classes with a constructor you create and a well-defined primary method called `render`. This means you can
|
|
191
|
+
test them conventionally, since they can be directly created in your tests.
|
|
192
|
+
|
|
193
|
+
That said, you likely want to test the generated HTML and not the methods of the class. All tests of a page or component have the
|
|
194
|
+
methods in {Brut::SpecSupport::ComponentSupport} available. Of particular interest is
|
|
195
|
+
{Brut::SpecSupport::ComponentSupport#render_and_parse}.
|
|
196
|
+
|
|
197
|
+
This method accepts an instance of a page or component, parses the resulting HTML with Nokogiri, and returns a
|
|
198
|
+
{Brut::SpecSupport::EnhancedNode}, which wraps a `Nokogiri::XML::Node` or `Nokogiri::XML::Element`, depending on what was parsed. You
|
|
199
|
+
can then use Nokogiri's API locate elements and assert on them.
|
|
200
|
+
|
|
201
|
+
To keep close to the web platform, it's recommended to use CSS selectors either via {Brut::SpecSupport::EnhancedNode#e} or {Brut::SpecSupport::EnhancedNode#e!} (both of which wrap Nokogiri's `css` method).
|
|
202
|
+
|
|
203
|
+
Instead of creating another API for accessing HTML content, the Nokogiri API should be used directly. You can create custom matchers
|
|
204
|
+
as needed for common assertions. Brut includes a few that you will find useful:
|
|
205
|
+
|
|
206
|
+
* `have_link_to`
|
|
207
|
+
* `have_html_attribute`
|
|
208
|
+
* `have_i18n_string`
|
|
209
|
+
|
|
210
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Route and Page Hooks
|
|
2
|
+
|
|
3
|
+
Route and page hooks allow you to perform logic or redirect the visitor before a page is rendered or action handled.
|
|
4
|
+
|
|
5
|
+
## Route Hooks
|
|
6
|
+
|
|
7
|
+
Route hooks are objects that are run before or after a request has been handled. They are useful for setting up cross-cutting code
|
|
8
|
+
that you don't want to have inside a page or handler.
|
|
9
|
+
|
|
10
|
+
To use one, call either {Brut::Framework::App.before} or {Brut::Framework::App.after}, passing it the *name* of a class to use as the
|
|
11
|
+
hook (i.e. a `String`).
|
|
12
|
+
|
|
13
|
+
Then, implement that class, extending {Brut::FrontEnd::RouteHook}, and provide either {Brut::FrontEnd::RouteHook#before} or {Brut::FrontEnd::RouteHook#after}. As discussed in {file:doc-src/keyword-injection.md Keyword Injection}, your hook can be passed some managed values to allow it to work.
|
|
14
|
+
|
|
15
|
+
In general, a hook will allow the request to continue or not, but using one of the following methods as the return value:
|
|
16
|
+
|
|
17
|
+
* {Brut::FrontEnd::HandlingResults#redirect_to} to redirect the user instead of rendering the page or handling the request.
|
|
18
|
+
* {Brut::FrontEnd::HandlingResults#http_status} to return an HTTP status instead of rendering the page or handling the request.
|
|
19
|
+
* {Brut::FrontEnd::RouteHook#continue} to proceed with the request.
|
|
20
|
+
|
|
21
|
+
## Page Hooks
|
|
22
|
+
|
|
23
|
+
Sometimes, the behavior you want to manage before a page is rendered is specific to a page and not cross-cutting. Because a page
|
|
24
|
+
exepcts to render HTML, you cannot easily put such code in your page class.
|
|
25
|
+
|
|
26
|
+
If you implement {Brut::FrontEnd::Page#before_render}, you can skip page rendering entirely and redirect the user or send an error. A
|
|
27
|
+
good example of this would be a set of admin pages where the logged-in site visitor must possess some roles in order to see the page.
|
|
28
|
+
|
|
29
|
+
A page hook expects one of these return values:
|
|
30
|
+
|
|
31
|
+
* `URI` - redirect the visitor instead of rendering the page.
|
|
32
|
+
* {Brut::FrontEnd::HttpStatus} - Send the browser this status code instead of rendering the page.
|
|
33
|
+
* Anything else - render the page as normal
|
|
34
|
+
|
|
35
|
+
Thus, the lifecycle of a page is:
|
|
36
|
+
|
|
37
|
+
1. "Before" Route Hooks
|
|
38
|
+
2. Page Initializer, injected as described in {file:doc-src/keyword-injection.md}
|
|
39
|
+
3. Page's `before_render`, called with no arguments.
|
|
40
|
+
4. Page's ERB generates HTML
|
|
41
|
+
5. "After" Route Hooks
|
|
42
|
+
|
|
43
|
+
## Handler Hooks
|
|
44
|
+
|
|
45
|
+
Like page hooks, handler hooks are called before handling logic. Implement `before_handle`. It's arguments must be a subset of the
|
|
46
|
+
arguments passed to `handle`. Thus, any value needed by `before_handle` must be declared as a keyword argument to `handle` as well.
|
|
47
|
+
|
|
48
|
+
If `before_handle` returns `nil`, `handle` is then called. Otherwise, `handle` is skipped and the return value of `before_handle` is
|
|
49
|
+
interpreted as the return value of `handle`. See {Brut::FrontEnd::Handler#handle}.
|
|
50
|
+
|
|
51
|
+
This makes the lifecycle of a handler as such:
|
|
52
|
+
|
|
53
|
+
1. "Before" Route Hooks
|
|
54
|
+
2. Handler Initializer, called with no argument.
|
|
55
|
+
3. Handler's `handle!`, injected with arguments as described in {file:doc-src/keyword-injections.md}
|
|
56
|
+
1. `handle!` calls `before_handle`, passing the arguments in.
|
|
57
|
+
2. `handle!` calls `handle`, passing the arguments in.
|
|
58
|
+
4. "After" Route Hooks
|
|
59
|
+
|
data/docs-todo.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# DOCS strategy
|
|
2
|
+
|
|
3
|
+
## Four types:
|
|
4
|
+
|
|
5
|
+
* Tutorials
|
|
6
|
+
* HOWTO
|
|
7
|
+
* Explanations
|
|
8
|
+
* Reference
|
|
9
|
+
|
|
10
|
+
Ideally, these can interlink, espeically to avoid reference having to have too much stuff in it.
|
|
11
|
+
|
|
12
|
+
## Tutorials
|
|
13
|
+
|
|
14
|
+
* SHORT - Making a blog from zero to done
|
|
15
|
+
* LONG - Agile Web Development with Brut
|
|
16
|
+
|
|
17
|
+
## HOWTO
|
|
18
|
+
|
|
19
|
+
* Create, validate, and process a form
|
|
20
|
+
* Implement Omniauth
|
|
21
|
+
* ???
|
|
22
|
+
|
|
23
|
+
## Explanations
|
|
24
|
+
|
|
25
|
+
* Brut overall architecture
|
|
26
|
+
* Brut guiding principles
|
|
27
|
+
* ???
|
|
28
|
+
|
|
29
|
+
## Reference
|
|
30
|
+
|
|
31
|
+
* API Docs
|
|
32
|
+
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
require_relative "../factory_bot"
|
|
2
2
|
module Brut
|
|
3
|
-
module
|
|
3
|
+
module BackEnd
|
|
4
|
+
# Base class and manager of Seed Data for the app. Seed Data is data used for development. It is not for populating e.g.
|
|
5
|
+
# reference data or other stuff in production.
|
|
6
|
+
#
|
|
7
|
+
# Seed Data uses FactoryBot.
|
|
4
8
|
class SeedData
|
|
5
9
|
def self.inherited(seed_data_klass)
|
|
6
10
|
@classes ||= []
|
|
@@ -1,13 +1,38 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
1
|
+
# Provides a very light DSL to declaring server-side validations for your
|
|
2
|
+
# {Brut::FrontEnd::Form} subclass. Unlike Active Record, these validations aren't mixed-into another object.
|
|
3
|
+
# Your subclass of this class is a standalone object that will operate on a form.
|
|
4
|
+
#
|
|
5
|
+
# @example
|
|
6
|
+
# # Suppose you are creating a widget with a name and description.
|
|
7
|
+
# # The form requires name, but not description, however if
|
|
8
|
+
# # the user initiates a "publish" action from that form, the description is
|
|
9
|
+
# # required. This cannot be managed with HTML alone.
|
|
10
|
+
# class WidgetPublishValidator < Brut::BackEnd::Validators::FormValidator
|
|
11
|
+
# validate :description, required: true, minlength: 10
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# # Then, in your back-end logic somewhere
|
|
15
|
+
#
|
|
16
|
+
# validator = WidgetPublishValidator.new
|
|
17
|
+
# validator.validate(form)
|
|
18
|
+
# if form.constraint_violations?
|
|
19
|
+
# # return back to the user
|
|
20
|
+
# else
|
|
21
|
+
# # proceed with business logic
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
5
24
|
class Brut::BackEnd::Validators::FormValidator
|
|
6
|
-
|
|
25
|
+
# Called inside the subclass body to indicate that a given form input should be validated based on the given options
|
|
26
|
+
# @param [String|Symbol] input_name name of the input of the form
|
|
27
|
+
# @param [Hash] options options describing the validation to perform.
|
|
28
|
+
def self.validate(input_name,options)
|
|
7
29
|
@@validations ||= {}
|
|
8
|
-
@@validations[
|
|
30
|
+
@@validations[input_name] = options
|
|
9
31
|
end
|
|
10
32
|
|
|
33
|
+
# Validate the given form, calling {Brut::FrontEnd::Form#server_side_constraint_violation} on each validate failure found.
|
|
34
|
+
#
|
|
35
|
+
# @param [Brut::FrontEnd::Form] form the form to validate
|
|
11
36
|
def validate(form)
|
|
12
37
|
@@validations.each do |attribute,options|
|
|
13
38
|
value = form.send(attribute)
|
data/lib/brut/cli/app.rb
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
require "optparse"
|
|
2
2
|
require_relative "../junk_drawer"
|
|
3
|
+
|
|
4
|
+
# Base class for all Brut-powered CLI Apps. Your subclass will call or override methods to declare the UI of your CLI app, including
|
|
5
|
+
# the commands it provides and options it recognizes. These mostly help to provide command-line documentation for your app, but also
|
|
6
|
+
# provide basic help with accessing the command line arguments and options. Internally, this uses Ruby's `OptionParser`.
|
|
7
|
+
#
|
|
8
|
+
# The types of CLIs this framework supports are *command suites* where a single CLI app has several subcommands, similar to `git`. As
|
|
9
|
+
# such, the CLI has several parts that you can configure:
|
|
10
|
+
#
|
|
11
|
+
# ```
|
|
12
|
+
# cli_app [global options] «subcommand» [command options] [arguments]
|
|
13
|
+
# ```
|
|
14
|
+
#
|
|
15
|
+
# * Global options appear between the CLI app's executable and the name of the subcommand. These affect any command. A common example is `--log-level`. These are configured with {Brut::CLI::App.on} or {Brut::CLI::App.option_parser}.
|
|
16
|
+
# * Subcommand is a single string representing the command to execute. The available commands are returned by {Brut::CLI::App.commands} although it's more conventional to declare inner classes of your app that extend {Brut::CLI::Command}.
|
|
17
|
+
# * Command options appear after the subcommand and apply only to that sub command. They are declared with {Brut::CLI::Command.on} or {Brut::CLI::Command.opts}.
|
|
18
|
+
# * Arguments are any additional values present on the command line. They are defined per-command and can be documented via {Brut::CLI::Command.args}.
|
|
3
19
|
class Brut::CLI::App
|
|
4
20
|
include Brut::CLI::ExecutionResults
|
|
5
21
|
include Brut::I18n::ForCLI
|
|
6
22
|
|
|
23
|
+
# Returns a list of {Brut::CLI::Command} classes that each represent the subcommands your CLI app accepts.
|
|
24
|
+
# By default, this will look for all internal classes that extend {Brut::CLI::Command} and use them as your subcommands.
|
|
25
|
+
# This means that you don't need to override this method and can instead define classes inside your app subclass.
|
|
7
26
|
def self.commands
|
|
8
27
|
self.constants.map { |name|
|
|
9
28
|
self.const_get(name)
|
|
@@ -11,6 +30,11 @@ class Brut::CLI::App
|
|
|
11
30
|
constant.kind_of?(Class) && constant.ancestors.include?(Brut::CLI::Command) && constant.instance_methods.include?(:execute)
|
|
12
31
|
}
|
|
13
32
|
end
|
|
33
|
+
|
|
34
|
+
# Call this to set the one-line description of your command line app.
|
|
35
|
+
#
|
|
36
|
+
# @param new_description [String] When present, sets the description of this app. When omitted, returns the current description.
|
|
37
|
+
# @return [String] the current description (if called with no parameters)
|
|
14
38
|
def self.description(new_description=nil)
|
|
15
39
|
if new_description.nil?
|
|
16
40
|
return @description.to_s
|
|
@@ -18,21 +42,77 @@ class Brut::CLI::App
|
|
|
18
42
|
@description = new_description
|
|
19
43
|
end
|
|
20
44
|
end
|
|
45
|
+
|
|
46
|
+
# Call this for each environment variable your *app* responds to. These would be variables that affect any of the subcommands. For
|
|
47
|
+
# command-specific environment variables, see {Brut::CLI::Command.env_var}.
|
|
48
|
+
#
|
|
49
|
+
# @param var_name [String] Declares that this app recognizes this environment variable.
|
|
50
|
+
# @param purpose [String] An explanation for how this environment variable affects the app. Used in documentation.
|
|
51
|
+
def self.env_var(var_name,purpose:)
|
|
52
|
+
env_vars[var_name] = purpose
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Access all configured environment variables.
|
|
56
|
+
# @!visibility private
|
|
57
|
+
def self.env_vars
|
|
58
|
+
@env_vars ||= {
|
|
59
|
+
"BRUT_CLI_RAISE_ON_ERROR" => "if set, shows backtrace on errors"
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Specify the default command to use when no subcommand is given.
|
|
64
|
+
#
|
|
65
|
+
# @param new_command_name [String] if present, sets the name of the command to run when none is given on the command line. When omitted, returns the currently configured name. The default is `help`.
|
|
21
66
|
def self.default_command(new_command_name=nil)
|
|
22
67
|
if new_command_name.nil?
|
|
23
68
|
return @default_command || "help"
|
|
24
69
|
else
|
|
25
|
-
@default_command = new_command_name.
|
|
70
|
+
@default_command = new_command_name.to_s
|
|
26
71
|
end
|
|
27
72
|
end
|
|
73
|
+
|
|
74
|
+
# Provides access to an `OptionParser` you can use to declare flags and switches that should be accepted globally. The way to use
|
|
75
|
+
# this is to call `.on` and provide a description for an option as you would to `OptionParser`. The only difference is that you
|
|
76
|
+
# should not pass a block to this. When the command line is parsed, the results will be placed into a {Brut::CLI::Options}
|
|
77
|
+
# instance made available to your command.
|
|
78
|
+
#
|
|
79
|
+
# @return [OptionParser]
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
#
|
|
83
|
+
# class MyApp < Brut::CLI::App
|
|
84
|
+
#
|
|
85
|
+
# opts.on("--dry-run","Don't change anything, just pretend")
|
|
86
|
+
#
|
|
87
|
+
# end
|
|
28
88
|
def self.opts
|
|
29
89
|
self.option_parser
|
|
30
90
|
end
|
|
91
|
+
|
|
92
|
+
# Returns the configured `OptionParser` used to parse global portion of the command line.
|
|
93
|
+
# If you don't want to call {.opts}, you can create and return
|
|
94
|
+
# a fully-formed `OptionParser` by overriding this method. By default, it will create one with a conventional banner.
|
|
95
|
+
#
|
|
96
|
+
# @return [OptionParser]
|
|
31
97
|
def self.option_parser
|
|
32
98
|
@option_parser ||= OptionParser.new do |opts|
|
|
33
99
|
opts.banner = "%{app} %{global_options} commands [command options] [args]"
|
|
34
100
|
end
|
|
35
101
|
end
|
|
102
|
+
|
|
103
|
+
# Call this if your CLI requires a project environment as context for what it does. For example, a command to analyze the database
|
|
104
|
+
# needs to know if it should operate on development, test, or production. When called, this will do a few things:
|
|
105
|
+
#
|
|
106
|
+
# * Your app will recognize `--env=ENVIRONMENT` as a global option
|
|
107
|
+
# * Your app will recognize the `RACK_ENV` environment variable.
|
|
108
|
+
#
|
|
109
|
+
# When your app executes, the project environment will be determined as follows:
|
|
110
|
+
#
|
|
111
|
+
# 1. If `--env` was on the command line, that is the environment used
|
|
112
|
+
# 2. If `RACK_ENV` is set in the environment, that is used
|
|
113
|
+
# 3. Otherwise the value given to the `default:` parameter of this method is used.
|
|
114
|
+
#
|
|
115
|
+
# @param default [String] name of the environment to use if none was specified. `nil` should be used to require the environment to be specified explicitly.
|
|
36
116
|
def self.requires_project_env(default: "development")
|
|
37
117
|
default_message = if default.nil?
|
|
38
118
|
""
|
|
@@ -42,16 +122,26 @@ class Brut::CLI::App
|
|
|
42
122
|
opts.on("--env=ENVIRONMENT","Project environment#{default_message}")
|
|
43
123
|
@default_env = ENV["RACK_ENV"] || default
|
|
44
124
|
@requires_project_env = true
|
|
125
|
+
self.env_var("RACK_ENV",purpose: "default project environment when --env is omitted")
|
|
45
126
|
end
|
|
46
127
|
|
|
128
|
+
# Returns the default project env, based on the logic described in {.requires_project_env}
|
|
47
129
|
def self.default_env = @default_env
|
|
130
|
+
# Returns true if this app requires a project env
|
|
48
131
|
def self.requires_project_env? = @requires_project_env
|
|
49
132
|
|
|
133
|
+
# Call this if your app must operate before the Brut framework starts up.
|
|
50
134
|
def self.configure_only!
|
|
51
135
|
@configure_only = true
|
|
52
136
|
end
|
|
53
137
|
def self.configure_only? = !!@configure_only
|
|
54
138
|
|
|
139
|
+
# Create the App. This is called by {Brut::CLI::AppRunner}.
|
|
140
|
+
#
|
|
141
|
+
# @param [Brut::CLI::Options] global_options global options specified on the command line
|
|
142
|
+
# @param [Brut::CLI::Output] out IO to use to send output to the standard output
|
|
143
|
+
# @param [Brut::CLI::Output] err IO to use to send output to the standard error
|
|
144
|
+
# @param [Brut::CLI::Executor] executor Class to use to execute child processes instead of e.g. `system`.
|
|
55
145
|
def initialize(global_options:,out:,err:,executor:)
|
|
56
146
|
@global_options = global_options
|
|
57
147
|
@out = out
|
|
@@ -62,12 +152,14 @@ class Brut::CLI::App
|
|
|
62
152
|
end
|
|
63
153
|
end
|
|
64
154
|
|
|
155
|
+
# @!visibility private
|
|
65
156
|
def set_env_if_needed
|
|
66
157
|
if self.class.requires_project_env?
|
|
67
158
|
ENV["RACK_ENV"] = options.env
|
|
68
159
|
end
|
|
69
160
|
end
|
|
70
161
|
|
|
162
|
+
# @!visibility private
|
|
71
163
|
def load_env(project_root:)
|
|
72
164
|
if !ENV["RACK_ENV"]
|
|
73
165
|
ENV["RACK_ENV"] = "development"
|
|
@@ -81,15 +173,19 @@ class Brut::CLI::App
|
|
|
81
173
|
end
|
|
82
174
|
end
|
|
83
175
|
|
|
176
|
+
# Called before anything else happens. You can override this to perform any setup or other checking before Brut is started up.
|
|
84
177
|
def before_execute
|
|
85
178
|
end
|
|
86
179
|
|
|
180
|
+
# Called after all setup has been executed. Brut will have been started/loaded. This will *not* be called if anything
|
|
181
|
+
# caused execution to be aborted.
|
|
87
182
|
def after_bootstrap
|
|
88
183
|
end
|
|
89
184
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
185
|
+
# Executes the command. Called by {Brut::CLI::AppRunner}.
|
|
186
|
+
#
|
|
187
|
+
# @param [Brut::CLI::Command] command the command to run, based on what was on the command line
|
|
188
|
+
# @param [Pathname] project_root root of the Brut app's project files.
|
|
93
189
|
def execute!(command,project_root:)
|
|
94
190
|
before_execute
|
|
95
191
|
set_env_if_needed
|
data/lib/brut/cli/app_runner.rb
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
module Brut
|
|
2
2
|
module CLI
|
|
3
|
+
# Manages the execution of a CLI app that extends {Brut::CLI::App}. Generally you won't use this class directly, but will call
|
|
4
|
+
# {Brut::CLI.app}.
|
|
3
5
|
class AppRunner
|
|
4
6
|
include Brut::CLI::ExecutionResults
|
|
5
7
|
|
|
8
|
+
# Create the app runner with a class and project root
|
|
9
|
+
#
|
|
10
|
+
# @param [Class] app_klass The class of your app that extends {Brut::CLI::App}
|
|
11
|
+
# @param [Pathname] project_root path to the root of the project
|
|
6
12
|
def initialize(app_klass:,project_root:)
|
|
7
13
|
@app_klass = app_klass
|
|
8
14
|
@project_root = project_root
|
|
9
15
|
end
|
|
10
16
|
|
|
17
|
+
# Runs the app, based on the CLI arguments and UNIX environment provided at the time of execution.
|
|
11
18
|
def run!
|
|
12
19
|
app_klass = @app_klass
|
|
13
20
|
out = Brut::CLI::Output.new(io: $stdout,prefix: "[ #{$0} ] ")
|
|
@@ -36,7 +43,7 @@ module Brut
|
|
|
36
43
|
show_global_help(app_klass:,out:)
|
|
37
44
|
else
|
|
38
45
|
command_option_parser = command_klass.option_parser
|
|
39
|
-
show_command_help(global_option_parser:,command_option_parser:,command_klass:,out:)
|
|
46
|
+
show_command_help(global_option_parser:,command_option_parser:,command_klass:,out:,app_klass:)
|
|
40
47
|
end
|
|
41
48
|
end
|
|
42
49
|
return result.to_i
|
|
@@ -52,7 +59,11 @@ module Brut
|
|
|
52
59
|
end
|
|
53
60
|
|
|
54
61
|
command_argv = remaining_argv[1..-1] || []
|
|
55
|
-
|
|
62
|
+
begin
|
|
63
|
+
args = command_option_parser.parse!(command_argv,into:command_options)
|
|
64
|
+
rescue OptionParser::ParseError => ex
|
|
65
|
+
raise Brut::CLI::InvalidOption.new(ex, context: command_klass)
|
|
66
|
+
end
|
|
56
67
|
|
|
57
68
|
cli_app = app_klass.new(global_options:, out:, err:, executor:)
|
|
58
69
|
cmd = command_klass.new(command_options:Brut::CLI::Options.new(command_options),global_options:, args:, out:, err:, executor:)
|
|
@@ -133,13 +144,22 @@ module Brut
|
|
|
133
144
|
option_parser.summarize do |line|
|
|
134
145
|
out.puts_no_prefix line
|
|
135
146
|
end
|
|
147
|
+
out.puts_no_prefix
|
|
148
|
+
out.puts_no_prefix "ENVIRONMENT VARIABLES"
|
|
149
|
+
out.puts_no_prefix
|
|
150
|
+
max_length = app_klass.env_vars.keys.map(&:length).max
|
|
151
|
+
printf_string = " %-#{max_length}s - %s\n"
|
|
152
|
+
app_klass.env_vars.keys.sort.each do |var_name|
|
|
153
|
+
out.printf_no_prefix(printf_string,var_name,app_klass.env_vars[var_name])
|
|
154
|
+
end
|
|
155
|
+
out.puts_no_prefix
|
|
136
156
|
if app_klass.commands.any?
|
|
137
157
|
out.puts_no_prefix
|
|
138
158
|
out.puts_no_prefix "COMMANDS"
|
|
139
159
|
out.puts_no_prefix
|
|
140
160
|
max_length = [ 4, app_klass.commands.map { |_| _.command_name.to_s.length }.max ].max
|
|
141
161
|
printf_string = " %-#{max_length}s - %s%s\n"
|
|
142
|
-
|
|
162
|
+
out.printf_no_prefix printf_string, "help", "Get help on a command",""
|
|
143
163
|
app_klass.commands.sort_by(&:command_name).each do |command|
|
|
144
164
|
default_message = if command.name_matches?(app_klass.default_command)
|
|
145
165
|
" (default)"
|
|
@@ -152,13 +172,13 @@ module Brut
|
|
|
152
172
|
else
|
|
153
173
|
command.description
|
|
154
174
|
end
|
|
155
|
-
|
|
175
|
+
out.printf_no_prefix printf_string, command.command_name, command.description, default_message
|
|
156
176
|
end
|
|
157
177
|
end
|
|
158
178
|
out.puts_no_prefix
|
|
159
179
|
end
|
|
160
180
|
|
|
161
|
-
def show_command_help(global_option_parser:,command_option_parser:,command_klass:,out:)
|
|
181
|
+
def show_command_help(global_option_parser:,command_option_parser:,command_klass:,out:,app_klass:)
|
|
162
182
|
banner = command_option_parser.banner % {
|
|
163
183
|
app: $0,
|
|
164
184
|
global_options: global_option_parser.top.list.length == 0 ? "" : "[global options]",
|
|
@@ -179,6 +199,16 @@ module Brut
|
|
|
179
199
|
global_option_parser.summarize do |line|
|
|
180
200
|
out.puts_no_prefix line
|
|
181
201
|
end
|
|
202
|
+
out.puts_no_prefix
|
|
203
|
+
out.puts_no_prefix "ENVIRONMENT VARIABLES"
|
|
204
|
+
out.puts_no_prefix
|
|
205
|
+
all_vars = app_klass.env_vars.merge(command_klass.env_vars)
|
|
206
|
+
max_length = all_vars.keys.map(&:length).max
|
|
207
|
+
printf_string = " %-#{max_length}s - %s\n"
|
|
208
|
+
all_vars.keys.sort.each do |var_name|
|
|
209
|
+
out.printf_no_prefix(printf_string,var_name,all_vars[var_name])
|
|
210
|
+
end
|
|
211
|
+
out.puts_no_prefix
|
|
182
212
|
if command_option_parser.top.list.length > 0
|
|
183
213
|
out.puts_no_prefix
|
|
184
214
|
out.puts_no_prefix "COMMAND OPTIONS"
|
|
@@ -208,11 +238,14 @@ module Brut
|
|
|
208
238
|
option_parser.on("--verbose","Set log level to '#{log_levels[0]}', which will produce maximum output") do
|
|
209
239
|
ENV["LOG_LEVEL"] = log_levels[0]
|
|
210
240
|
end
|
|
241
|
+
app_klass.env_var("LOG_LEVEL",purpose: "log level if --log-level or --verbose is omitted")
|
|
211
242
|
|
|
212
243
|
hash = {}
|
|
213
244
|
remaining_argv = option_parser.order!(into:hash)
|
|
214
245
|
global_options = Brut::CLI::Options.new(hash)
|
|
215
246
|
[ continue_execution, remaining_argv, global_options, option_parser ]
|
|
247
|
+
rescue OptionParser::ParseError => ex
|
|
248
|
+
raise Brut::CLI::InvalidOption.new(ex, context: :global)
|
|
216
249
|
end
|
|
217
250
|
end
|
|
218
251
|
end
|