brut 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1fd649346396f27b70064c601a639187c08fec7d38f4efc973c3a9839d2c163
|
4
|
+
data.tar.gz: a06518da1bb198c89acfd13dca787fabb302546097e08d8917bf1c500d399f39
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ecde01ea2f1cdc821e24e835fe18a7420848601d469435c74abe3d020187fccc73004ce063f7c242f663796d2470145981e6ace8a742ae885568abb0533eb74
|
7
|
+
data.tar.gz: da0e5c69737041a6ff25366c4fd4cb41214f97b1ada9c78f878544f380f8370bcd155f8f7e6b77d52b50a77b6270b08847e5b1e10bec657b464d289d84747c4f
|
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,12 +1,16 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
brut (0.0.
|
4
|
+
brut (0.0.2)
|
5
|
+
concurrent-ruby
|
5
6
|
dotenv
|
6
7
|
factory_bot
|
7
8
|
faker
|
8
9
|
i18n
|
10
|
+
irb
|
9
11
|
nokogiri
|
12
|
+
opentelemetry-exporter-otlp
|
13
|
+
opentelemetry-sdk
|
10
14
|
ostruct
|
11
15
|
prism
|
12
16
|
rack-protection
|
@@ -42,6 +46,7 @@ GEM
|
|
42
46
|
bigdecimal (3.1.8)
|
43
47
|
concurrent-ruby (1.3.4)
|
44
48
|
connection_pool (2.4.1)
|
49
|
+
date (3.4.1)
|
45
50
|
diff-lcs (1.5.1)
|
46
51
|
dotenv (3.1.4)
|
47
52
|
drb (2.2.1)
|
@@ -49,8 +54,33 @@ GEM
|
|
49
54
|
activesupport (>= 5.0.0)
|
50
55
|
faker (3.5.1)
|
51
56
|
i18n (>= 1.8.11, < 2)
|
57
|
+
google-protobuf (4.29.3)
|
58
|
+
bigdecimal
|
59
|
+
rake (>= 13)
|
60
|
+
google-protobuf (4.29.3-aarch64-linux)
|
61
|
+
bigdecimal
|
62
|
+
rake (>= 13)
|
63
|
+
google-protobuf (4.29.3-arm64-darwin)
|
64
|
+
bigdecimal
|
65
|
+
rake (>= 13)
|
66
|
+
google-protobuf (4.29.3-x86-linux)
|
67
|
+
bigdecimal
|
68
|
+
rake (>= 13)
|
69
|
+
google-protobuf (4.29.3-x86_64-darwin)
|
70
|
+
bigdecimal
|
71
|
+
rake (>= 13)
|
72
|
+
google-protobuf (4.29.3-x86_64-linux)
|
73
|
+
bigdecimal
|
74
|
+
rake (>= 13)
|
75
|
+
googleapis-common-protos-types (1.18.0)
|
76
|
+
google-protobuf (>= 3.18, < 5.a)
|
52
77
|
i18n (1.14.6)
|
53
78
|
concurrent-ruby (~> 1.0)
|
79
|
+
io-console (0.8.0)
|
80
|
+
irb (1.15.1)
|
81
|
+
pp (>= 0.6.0)
|
82
|
+
rdoc (>= 4.0.0)
|
83
|
+
reline (>= 0.4.2)
|
54
84
|
logger (1.6.1)
|
55
85
|
minitest (5.25.1)
|
56
86
|
mustermann (3.0.3)
|
@@ -67,8 +97,33 @@ GEM
|
|
67
97
|
racc (~> 1.4)
|
68
98
|
nokogiri (1.16.7-x86_64-linux)
|
69
99
|
racc (~> 1.4)
|
100
|
+
opentelemetry-api (1.4.0)
|
101
|
+
opentelemetry-common (0.21.0)
|
102
|
+
opentelemetry-api (~> 1.0)
|
103
|
+
opentelemetry-exporter-otlp (0.29.1)
|
104
|
+
google-protobuf (>= 3.18)
|
105
|
+
googleapis-common-protos-types (~> 1.3)
|
106
|
+
opentelemetry-api (~> 1.1)
|
107
|
+
opentelemetry-common (~> 0.20)
|
108
|
+
opentelemetry-sdk (~> 1.2)
|
109
|
+
opentelemetry-semantic_conventions
|
110
|
+
opentelemetry-registry (0.3.1)
|
111
|
+
opentelemetry-api (~> 1.1)
|
112
|
+
opentelemetry-sdk (1.7.0)
|
113
|
+
opentelemetry-api (~> 1.1)
|
114
|
+
opentelemetry-common (~> 0.20)
|
115
|
+
opentelemetry-registry (~> 0.2)
|
116
|
+
opentelemetry-semantic_conventions
|
117
|
+
opentelemetry-semantic_conventions (1.10.1)
|
118
|
+
opentelemetry-api (~> 1.0)
|
70
119
|
ostruct (0.6.1)
|
120
|
+
pp (0.6.2)
|
121
|
+
prettyprint
|
122
|
+
prettyprint (0.2.0)
|
71
123
|
prism (1.2.0)
|
124
|
+
psych (5.2.3)
|
125
|
+
date
|
126
|
+
stringio
|
72
127
|
racc (1.8.1)
|
73
128
|
rack (3.1.8)
|
74
129
|
rack-protection (4.0.0)
|
@@ -79,6 +134,11 @@ GEM
|
|
79
134
|
rackup (2.2.1)
|
80
135
|
rack (>= 3)
|
81
136
|
rake (13.2.1)
|
137
|
+
rdiscount (2.2.7.3)
|
138
|
+
rdoc (6.12.0)
|
139
|
+
psych (>= 4.0.0)
|
140
|
+
reline (0.6.0)
|
141
|
+
io-console (~> 0.5)
|
82
142
|
rexml (3.3.9)
|
83
143
|
rspec (3.13.0)
|
84
144
|
rspec-core (~> 3.13.0)
|
@@ -105,6 +165,7 @@ GEM
|
|
105
165
|
rack-protection (= 4.0.0)
|
106
166
|
rack-session (>= 2.0.0, < 3)
|
107
167
|
tilt (~> 2.0)
|
168
|
+
stringio (3.1.3)
|
108
169
|
temple (0.10.3)
|
109
170
|
tilt (2.4.0)
|
110
171
|
tzinfo (2.0.6)
|
@@ -112,6 +173,7 @@ GEM
|
|
112
173
|
tzinfo-data (1.2024.2)
|
113
174
|
tzinfo (>= 1.0.0)
|
114
175
|
uri (1.0.2)
|
176
|
+
yard (0.9.37)
|
115
177
|
zeitwerk (2.7.1)
|
116
178
|
|
117
179
|
PLATFORMS
|
@@ -127,7 +189,10 @@ DEPENDENCIES
|
|
127
189
|
brut!
|
128
190
|
bundler
|
129
191
|
rake
|
192
|
+
rdiscount
|
193
|
+
rdoc
|
130
194
|
rspec (~> 3.0)
|
195
|
+
yard
|
131
196
|
|
132
197
|
BUNDLED WITH
|
133
198
|
2.5.23
|
data/README.md
CHANGED
@@ -19,3 +19,39 @@ Or install it yourself as:
|
|
19
19
|
|
20
20
|
$ gem install brut
|
21
21
|
|
22
|
+
## Developing
|
23
|
+
|
24
|
+
The dev environment is managed by Docker and you are encouraged to use this. It's set up so you can edit your code on your computer
|
25
|
+
with your editor, but all commands are run inside Docker, which should be more consistent across developer workstations.
|
26
|
+
|
27
|
+
1. On Windows, setup WSL2
|
28
|
+
2. Install Docker
|
29
|
+
3. Build the image(s) you will use to start containers where development happens:
|
30
|
+
|
31
|
+
dx/build
|
32
|
+
4. Start up the dev environment
|
33
|
+
|
34
|
+
dx/start
|
35
|
+
5. At this point, you can "log in" to the virtual machine/docker container via:
|
36
|
+
|
37
|
+
dx/exec bash
|
38
|
+
6. From there, you have a UNIX prompt where you can run all commands. You can also run any command via:
|
39
|
+
|
40
|
+
dx/exec ls -l # or whatever
|
41
|
+
|
42
|
+
This is how you can configure your editor to issue commands and access their output.
|
43
|
+
|
44
|
+
7. To set everything up:
|
45
|
+
|
46
|
+
dx/exec bin/setup --no-credentials
|
47
|
+
|
48
|
+
The `--no-credentials` means that you will not be able to push to GitHub or RubyGems from within the Docker container. This
|
49
|
+
ability is only needed by maintainers to push new versions of the gem. You can push to GitHub from your computer.
|
50
|
+
|
51
|
+
## Docs
|
52
|
+
|
53
|
+
* See {file:doc-src/architecture.md Architecture}
|
54
|
+
* See {file:doc-src/pages.md Pages and Components}
|
55
|
+
* See {file:doc-src/forms.md Forms}
|
56
|
+
* See {file:docs-src/keyword-injection.md Keyword Injection}
|
57
|
+
* See {file:docs-src/route-hooks.md Route Hooks}
|
data/Rakefile
CHANGED
@@ -1 +1,23 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
require "pathname"
|
4
|
+
|
5
|
+
docs_dir = (Pathname(__FILE__).dirname / "docs").expand_path.to_s
|
6
|
+
desc "Generate YARD doc"
|
7
|
+
task :docs do
|
8
|
+
files = Dir["doc-src/*.md"].map { |file|
|
9
|
+
"--files #{file}"
|
10
|
+
}.join(" ")
|
11
|
+
system "bundle exec yard doc -o '#{docs_dir}' #{files} -m markdown -M rdiscount --backtrace"
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Show YARD status"
|
15
|
+
task :stats do
|
16
|
+
system "bundle exec yard stats --list-undoc"
|
17
|
+
end
|
18
|
+
|
19
|
+
desc "Clean up droppings"
|
20
|
+
task :clean do
|
21
|
+
FileUtils.rm_rf docs_dir, verbose: true
|
22
|
+
end
|
23
|
+
|
data/brut.gemspec
CHANGED
@@ -34,9 +34,11 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
35
35
|
spec.require_paths = ["lib"]
|
36
36
|
|
37
|
+
spec.add_runtime_dependency "irb"
|
37
38
|
spec.add_runtime_dependency "dotenv"
|
38
39
|
spec.add_runtime_dependency "ostruct" # squelch some warning - this is not used
|
39
40
|
spec.add_runtime_dependency "factory_bot"
|
41
|
+
spec.add_runtime_dependency "concurrent-ruby"
|
40
42
|
spec.add_runtime_dependency "faker"
|
41
43
|
spec.add_runtime_dependency "i18n"
|
42
44
|
spec.add_runtime_dependency "nokogiri"
|
@@ -52,9 +54,14 @@ Gem::Specification.new do |spec|
|
|
52
54
|
spec.add_runtime_dependency "tzinfo"
|
53
55
|
spec.add_runtime_dependency "tzinfo-data"
|
54
56
|
spec.add_runtime_dependency "zeitwerk"
|
57
|
+
spec.add_runtime_dependency "opentelemetry-sdk"
|
58
|
+
spec.add_runtime_dependency "opentelemetry-exporter-otlp"
|
55
59
|
|
56
60
|
spec.add_development_dependency "activesupport"
|
57
61
|
spec.add_development_dependency "rspec", "~> 3.0"
|
58
62
|
spec.add_development_dependency "bundler"
|
59
63
|
spec.add_development_dependency "rake"
|
64
|
+
spec.add_development_dependency "yard"
|
65
|
+
spec.add_development_dependency "rdiscount"
|
66
|
+
spec.add_development_dependency "rdoc"
|
60
67
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# Brut's High Level Architecture
|
2
|
+
|
3
|
+
Brut attempts to have parity with the web platform in general, and how web browsers work. For example, a web browser shows you a web
|
4
|
+
page, thus to do this in Brut, you create a page class.
|
5
|
+
|
6
|
+
Brut's core ethos is to captialize on fundamental knowledge you must already posses to build a web app, such as HTML, JavaScript, the
|
7
|
+
Web Platform, SQL, etc.
|
8
|
+
|
9
|
+
The best way to understand Brut is to break down how a request is handled.
|
10
|
+
|
11
|
+
## HTTP Requests at a High Level
|
12
|
+
|
13
|
+
1. HTTP Request is received
|
14
|
+
2. If the request's route is mapped, handle it and return the result
|
15
|
+
3. Otherwise, 404
|
16
|
+
|
17
|
+
This pretty much describes every web app server in the world. Let's dig into step 2
|
18
|
+
|
19
|
+
## Pages, Forms, Actions
|
20
|
+
|
21
|
+
HTML allows for exactly two ways to interact with a server: Issuing a `GET` for a resource (like a web page), or submitting a form via
|
22
|
+
a `GET` or `POST`.
|
23
|
+
|
24
|
+
1. HTTP Request is received
|
25
|
+
2. If the route is a mapped page, render that page
|
26
|
+
3. If the route is a configured form, submit that form handle it
|
27
|
+
4. If the route is an asset like CSS or an image, send that.
|
28
|
+
5. Otherwise, 404
|
29
|
+
|
30
|
+
A browser can use JavaScript to submit other requests, and Brut handles those, too:
|
31
|
+
|
32
|
+
1. HTTP Request is received
|
33
|
+
2. If the route is a mapped page, render that page (See {file:doc-src/pages.md})
|
34
|
+
3. If the route is a configured form, submit that form handle it (See {file:doc-src/forms.md})
|
35
|
+
4. If the route is a configured action, perform it and return the results. (See {file:doc-src/handlers.md})
|
36
|
+
5. If the route is an asset like CSS or an image, send that. (See {file:doc-src/assets.md})
|
37
|
+
6. Otherwise, 404
|
38
|
+
|
39
|
+
Before we dig deeper, it's worth pointing out at this point that *Brut is not an MVC framework*, mostly because there are no
|
40
|
+
controllers. *Models* in the MVC sense are instead *pages* or *components*—classes that provide all dynamic behavior and data needed
|
41
|
+
to render some HTML. Brut uses ERB templates to generate HTML and you could think of this as the view, if you want.
|
42
|
+
|
43
|
+
## Back End
|
44
|
+
|
45
|
+
Most of Brut is concerned with what it calls the *front end*, which is everything about receiving an HTTP request and producing the
|
46
|
+
reponse. But you can almost never make a web app with no back end. You almost always need a database and a place to execute logic
|
47
|
+
outside of a web browser. Brut refers to this as the *back end*.
|
48
|
+
|
49
|
+
Since web back ends are less constrained to protocols, like a front end is to HTTP and other Web APIs, Brut provides a lot less for
|
50
|
+
the back end and puts many fewer restrictions on it. This ends up being a good thing, since the back-end of most web apps are were
|
51
|
+
most of the differentiation in behavior and logic tend to be.
|
52
|
+
|
53
|
+
Brut provides access to a SQL database via the Sequel Ruby library. Brut provides some integration with Sidekiq, to allow running
|
54
|
+
background jobs. Brut also provides a CLI library for creating one-off tasks (like you'd use Rake for in a Rail apps).
|
55
|
+
|
56
|
+
### SQL
|
57
|
+
|
58
|
+
Almost all web apps that have a database use SQL. And Postgres is a great choice. This is the SQl database that Brut supports,
|
59
|
+
though support for other databases may be added later. This is because almost all of Brut's SQL integration is via the Sequel library
|
60
|
+
and it supports many databases.
|
61
|
+
|
62
|
+
Brut provides some configuration for Sequel to make managing your data easier and to better-support practices that you often want
|
63
|
+
to follow. For example:
|
64
|
+
|
65
|
+
* All tables have a primary key of type `int` by default.
|
66
|
+
* All tables have a `created_at` field that is set on row insertion.
|
67
|
+
* Timestamps use `timestamp with time zone`.
|
68
|
+
* You must provide a comment to document all tables.
|
69
|
+
* You can have Brut manage an external unique key for each table, so you can keep your primary keys private.
|
70
|
+
* `find!` is available on Sequel models, working like `find` does in Rails.
|
71
|
+
|
72
|
+
See {file:doc-src/sql.md} for more details.
|
73
|
+
|
74
|
+
Brut also uses Sequel's model support to provide access to your database. It is configured to namespace all models in the `DB::`
|
75
|
+
namespace, so if you have a table named `widgets`, Brut will expect `DB::Widget` to be defined to access that table.
|
76
|
+
|
77
|
+
The reason for this is that it is often confusing when an app conflates the concept of a database table and a domain model, it can be
|
78
|
+
difficult to manage the code. If the model to access the `widgets` table were called, simply, `Widget`, then you would lose a great
|
79
|
+
class name to use for modeling the widget as a domain object.
|
80
|
+
|
81
|
+
### Sidekiq
|
82
|
+
|
83
|
+
Sidekiq can be added to any app without much fanfare. That said, Brut provides a few convieniences:
|
84
|
+
|
85
|
+
* `bin/run-sidekiq` is provided to run Sidekiq alongside your web app
|
86
|
+
* {Brut::SpecSupport::RSpecSetup} well arrange for Sidekiq to be set up in a useful way during tests:
|
87
|
+
- For non-E2E tests, jobs are cleared between test runs.
|
88
|
+
- During E2E tests, actual Sidekiq is used, as started by `/bin/run-sidekiq`, and jobs are cleared between tests.
|
89
|
+
|
90
|
+
### CLI / Tasks
|
91
|
+
|
92
|
+
Rake is not a great tool for task automation, mostly because it exposes a cumbersome command line interface that relies on environment
|
93
|
+
variables, square brackets, and commas. It's often easier to create a full-blown command line app.
|
94
|
+
|
95
|
+
Brut's {Brut::CLI::App} can form the basis for any command line app or task you need for your system. It provides access to Brut
|
96
|
+
internals and your app, as needed, and shares much of its startup code with your web app, ensuring parity for all code shared.
|
97
|
+
|
98
|
+
Brut's CLI support also allows for an expedient definition of a subcommand-style UI that behaves like a canonical UNIX command line
|
99
|
+
app, without having to write a lot of code. It wraps `OptionParser`, so if you are familiar with this library that's part of Ruby,
|
100
|
+
you will be familiar with Brut's CLI API.
|
101
|
+
|
102
|
+
See {file:doc-src/cli.md}.
|
data/doc-src/assets.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Assets - CSS, JavaScript, Images
|
2
|
+
|
3
|
+
To learn about Brut's JavaScript API support, see {file:doc-src/javascript.md}. This page is about how requests for assets are
|
4
|
+
managed.
|
5
|
+
|
6
|
+
At a high level, all assets are served out of the *public folder*, which is in `app/public`. Brut copies files into this folder as
|
7
|
+
part of the build and development process.
|
8
|
+
|
9
|
+
Currently, Brut supports JavaScript, CSS, Fonts, and Images. These are all copied and/or processed from a source location into
|
10
|
+
`app/public` via {Brut::CLI::Apps::BuildAssets}:
|
11
|
+
|
12
|
+
* JavaScript - From `app/src/front_end/js`, bundled to `app/public/js`.
|
13
|
+
* CSS - From `app/src/front_end/css`, bundled to `app/public/css`.
|
14
|
+
* Fonts - From `app/src/front_end/fonts`, bundled to `app/public/css` (yes, they are bundled to `css` as that is the only reason to have a built step for fonts - see below).
|
15
|
+
* Images - From `app/src/front_end/iamges` to `app/public/images`.
|
16
|
+
|
17
|
+
## Images
|
18
|
+
|
19
|
+
Images are the simplest. Images in Brut are not hashed, so they are essentially synced from `app/src/front_end/images` to
|
20
|
+
`app/public/images`. Your CDN should arrange for cache invalidation.
|
21
|
+
|
22
|
+
## SVGs
|
23
|
+
|
24
|
+
SVGs are treated specially. They are located in `app/src/front_end/svgs`. To use an svg, your ERB should use the
|
25
|
+
{Brut::FrontEnd::Component::Helpers#svg} to inline the SVG into the page. You can put SVGs intended to be linked-to in
|
26
|
+
`app/src/front_end/images`, but for SVGs to be used as icons, for example, place them in `app/src/front_end/svgs` and use the `svg`
|
27
|
+
helper.
|
28
|
+
|
29
|
+
## JavaScript
|
30
|
+
|
31
|
+
JavaScript is currently managed by ESBuild. No fancy options are used nor currently possible. By default, there is a single entry
|
32
|
+
point for all your JavaScript, located in `app/src/front_end/javascript/index.js`. This is compiled into `app/public/js/app-«HASH».js`, for example `app/public/js/app-EAALH2IQ.js`. A sourcemap is included. Third party JS can be referenced and is assumed to be in `node_modules`.
|
33
|
+
|
34
|
+
This should be sufficient for most apps, however you can use additional entry points. See {file:doc-src/javascript.md} for how to set
|
35
|
+
this up. Also see "Hashing" below for how hashing works and is managed.
|
36
|
+
|
37
|
+
## CSS
|
38
|
+
|
39
|
+
CSS is also managed by ESBuild. There is a single entry point located in `app/src/front_end/css/index.css`, and this is compiled into
|
40
|
+
`app/public/css/styles-«HASH».css`, for example `app/public/css/styles-EAALH2IQ.css`. A sourcemap is included. Third party CSS can be
|
41
|
+
referenced and is assumed to be in `node_modules`.
|
42
|
+
|
43
|
+
Currently, there is no support for multiple CSS entry points - your entire app's CSS is expected to be in (or referenced by)
|
44
|
+
`index.css`.
|
45
|
+
|
46
|
+
To do that, Brut assumes you will use standard APIs, namely `@import`, and this is how you can bring in third party CSS as well as to
|
47
|
+
manage your app's CSS in multiple files:
|
48
|
+
|
49
|
+
@import "bootstrap/index.css";
|
50
|
+
@import "colors.css";
|
51
|
+
|
52
|
+
html { font-size: 20px; }
|
53
|
+
|
54
|
+
## Fonts
|
55
|
+
|
56
|
+
ESBuild will handle fonts when CSS is built. Fonts are hashed. You should place fonts in `app/src/front_end/fonts`, however this is
|
57
|
+
merely a convention. ESBuild will find your font as long as you properly use `url(...)` to reference it.
|
58
|
+
|
59
|
+
To follow the convention, here is how you might write your CSS:
|
60
|
+
|
61
|
+
/* index.css */
|
62
|
+
@font-face {
|
63
|
+
font-family: "Monaspace Xenon";
|
64
|
+
src: url("../fonts/monaspace-xenon.ttf") format("truetype");
|
65
|
+
font-display: swap;
|
66
|
+
}
|
67
|
+
|
68
|
+
ESBuild treats the relative path in `url` as relative to where the file being procssed is, thus it will expect to find
|
69
|
+
`app/src/front_end/fonts/monaspace-xenon.ttf`. While it's not relevant to you where it's copied, the file will be hashed and copied
|
70
|
+
to `app/public/css` and the `url(..)` will be adjusted, for example:
|
71
|
+
|
72
|
+
@font-face {
|
73
|
+
font-family: "Monaspace Xenon";
|
74
|
+
src: url("./monaspace-xenon-VZ5IIHXZ.ttf") format("truetype");
|
75
|
+
font-display: swap;
|
76
|
+
}
|
77
|
+
|
78
|
+
## Hashing
|
79
|
+
|
80
|
+
Hashing is on in development, testing, and production, as a way to minimize differences between the three environments. The way Brut
|
81
|
+
manages this is via the file `app/config/asset_metadata.json`. This file maps the logical name of an asset to its hashed name. For
|
82
|
+
example:
|
83
|
+
|
84
|
+
{
|
85
|
+
"asset_metadata": {
|
86
|
+
".js": {
|
87
|
+
"/js/app.js": "/js/app-L6VPFHLG.js"
|
88
|
+
},
|
89
|
+
".css": {
|
90
|
+
"/css/styles.css": "/css/styles-PHUHEJY3.css"
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
{Brut::FrontEnd::Component::Helpers#asset_path} accepts the logical name (`/js/app.js`) and returns the actual name
|
96
|
+
`/js/app-L6VPFHLG.js`). `asset_metadata.json` is managed by `bin/build-assets`.
|
97
|
+
|
98
|
+
Note that the fonts are not present in this file since they are only needed by CSS, and ESBuild handles the translation there.
|
data/doc-src/forms.md
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
# Forms
|
2
|
+
|
3
|
+
Brut's form support is designed to have parity with the Web Platform and allow you to both generate HTML and pasrse the results of a
|
4
|
+
form submission from a single source of truth. Brut's forms also allow the unification of client-side and server-side constraint
|
5
|
+
violations so that you can provide client-side validations but not require JavaScript for all validations.
|
6
|
+
|
7
|
+
## High Level Overview of Forms
|
8
|
+
|
9
|
+
The purpose of a form is to allow the website visitor to submit data to you. The Web Platform and web browser have deep support for
|
10
|
+
this, but the core way this works is that you have a `<form>` element that contains form elements like `<input>` or `<select>` elements and, when this form is submitted, your server can access the values of each element and take whatever action is necessary.
|
11
|
+
|
12
|
+
The lifecycle of this process looks like so:
|
13
|
+
|
14
|
+
1. HTML is generated, including a form with elements based on a definition you provide.
|
15
|
+
2. The visitor submits the form.
|
16
|
+
3. If there are constraint violations that the browser can detect, the form will not be submitted to your server.
|
17
|
+
4. If all constraints are satisfied (or the visitor bypasses client-side constraints), the server receives the form submission.
|
18
|
+
5. A {Brut::FrontEnd::Handler} is triggered to process that submission. The handler should re-check the client-side constraints and
|
19
|
+
can perform further server-side checks.
|
20
|
+
6. The handler will decide what the website visitor will experience next (e.g. re-render the form, proceed to another page, etc).
|
21
|
+
|
22
|
+
As a developer, you must write four pieces of code:
|
23
|
+
|
24
|
+
* Call {Brut::SinatraHelpers::ClassMethods.form} to declare the route.
|
25
|
+
* Create a subclass of {Brut::FrontEnd::Form} (whose name is determined by the route name). This class declares the inputs of your
|
26
|
+
form.
|
27
|
+
* Create a subclass of {Brut::FrontEnd::Handler} (whose name is determined by the route name). This class processes the form.
|
28
|
+
* ERB to generate the form. The classes in {Brut::FrontEnd::Components::Inputs} have methods like {Brut::FrontEnd::Components::Inputs::TextField.for_form_input} that will generate HTML for you.
|
29
|
+
|
30
|
+
## Concepts
|
31
|
+
|
32
|
+
Brut tries to create concepts that have a direct analog to the web platform.
|
33
|
+
|
34
|
+
These basic concepts form the basis for Brut's form support:
|
35
|
+
|
36
|
+
* *Form* is an HTML `<form>`
|
37
|
+
* *Input* is an HTML `<input>`, `<select>`, `<textarea>`, etc.
|
38
|
+
* *Input Name* is the name of an input, as defined by the `name` attribute.
|
39
|
+
* *Submitting a form* is when the browser submits a form to the form's action. This is done with an HTTP GET or HTTP POST only. No
|
40
|
+
other HTTP methods can submit a form.
|
41
|
+
* *Constraint Violation* describes invalid data in an input.
|
42
|
+
|
43
|
+
Building on these, Brut specifies how it manages Forms, Inputs, and Submission:
|
44
|
+
|
45
|
+
* *Form Class* defines the inputs a specific form will have.
|
46
|
+
* *Input Definition* defines the input for a given input name.
|
47
|
+
* *Form Object* holds the values and constraint violations of all inputs.
|
48
|
+
* *Handler* is a class that processes a form submission. It's `handle!` method can access the Form Object representing a submission.
|
49
|
+
* *Server-Side Constraint Violation* describes invalid data that required server processing to determine.
|
50
|
+
|
51
|
+
## Basic Workflow for Handling a Form
|
52
|
+
|
53
|
+
### Define Your Form
|
54
|
+
|
55
|
+
In Brut, a *form* class (or *form object*) holds metadata about the form in question. Namely, it defines all of the inputs and any
|
56
|
+
client-side constraints. For example, here is a form to create a new widget, where the name must be at least 3 characters and is
|
57
|
+
required, and there is an optional description:
|
58
|
+
|
59
|
+
class NewWidgetForm < AppForm
|
60
|
+
input :name, minlength: 3
|
61
|
+
input :description, required: false
|
62
|
+
end
|
63
|
+
|
64
|
+
This form class provides a few features:
|
65
|
+
|
66
|
+
* It can be used to generate HTML. For example, the "name" field will generate `<input type="text" name="name" required minlength="3">`
|
67
|
+
* It holds the data submitted to the server, serving as an allowlist of which parameters are accepted. For example, if the browser
|
68
|
+
submits "name", "description", and "price", since "price" is not an input of this form, the server will discard that value. Your code
|
69
|
+
will only have access to "name" and "description"
|
70
|
+
* It can validate the client-side constraints on the server. If a visitor submits the form, bypassing client-side constraint
|
71
|
+
validations, and "name" is only two characters, the form object will see that the "name" field has a violation.
|
72
|
+
|
73
|
+
### Define Your Handler
|
74
|
+
|
75
|
+
Handlers are like controller methods in Rails - they receive the data from a request and process it. Unlike a Rails controller, a
|
76
|
+
Handler is a normal class. It implements the method `handle`. The method signature you use for `handle` determines what data will
|
77
|
+
be passed into it. The return value of `handle` determines what happens after processing is complete.
|
78
|
+
|
79
|
+
Suppose that creating a widget requires that the name be unique. If it's not, we re-render the page containing the form and show the
|
80
|
+
user the errors. Suppose that the page in question is `/new_widget`, which would be the class `NewWidgetPage`.
|
81
|
+
|
82
|
+
class NewWidgetHandler < AppHandler
|
83
|
+
def handle(form:)
|
84
|
+
if !form.constraint_violations?
|
85
|
+
if DB::Widget[name: form.name]
|
86
|
+
form.server_side_constraint_violation(input_name: :name, key: :not_unique)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
if form.constraint_violations?
|
91
|
+
return NewWidgetPage.new(form:)
|
92
|
+
end
|
93
|
+
|
94
|
+
DB::Widget.create(name: form.name, description: form.description)
|
95
|
+
redirect_to(WidgetsPage)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
{file:doc-src/pages.md Pages} provides more information about what `NewWidgetPage` and `WidgetsPage` might be or do, but the logic in
|
101
|
+
the handler is, hopefully, clear. If our form is free of client-side constraint violations, we check to see if there is another
|
102
|
+
widget with the name from the form. If there is, we set a server-side constraint violation.
|
103
|
+
|
104
|
+
After that, if the form has any constraint violations (server-side or client-side), we return `NewWidgetPage` initialized with our
|
105
|
+
existing form. This will allow that page to generate HTML that includes information about the constraint violations detected.
|
106
|
+
|
107
|
+
If there aren't constraint violations, we create a widget in the database, then redirect to `WidgetsPage`. {Brut::FrontEnd::HandlingResults#redirect_to} is a convienience method for figuring out the URL for a given page.
|
108
|
+
|
109
|
+
### Generate HTML
|
110
|
+
|
111
|
+
In this example, `NewWidgetPage` would be generating the HTML form. It's class might look like so:
|
112
|
+
|
113
|
+
class NewWidgetPage < AppPage
|
114
|
+
|
115
|
+
attr_reader :form
|
116
|
+
|
117
|
+
def initialize(form: nil)
|
118
|
+
@form ||= NewWidgetForm.new
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
Here is the most direct way to use the form object to render HTML.
|
123
|
+
|
124
|
+
<%= form_tag for: form do %>
|
125
|
+
<%= component(Brut::FrontEnd::Components::TextField.for_form_input(form:, input_name: :name)) %>
|
126
|
+
<%= component(Brut::FrontEnd::Components::Textarea.for_form_input(form:, input_name: :description)) %>
|
127
|
+
<button>Save</button>
|
128
|
+
<% end %>
|
129
|
+
|
130
|
+
`for_form_input` uses the metadata in the form, along with the name of the field, to generate the appropriate HTML. This will only
|
131
|
+
generate an `<input>` tag, so you have complete flexibility to style it however you like.
|
132
|
+
|
133
|
+
You can also use `constraint_violations` to render any errors:
|
134
|
+
|
135
|
+
<%= form_tag for: form do %>
|
136
|
+
|
137
|
+
<%= component(Brut::FrontEnd::Components::TextField.for_form_input(form:, input_name: :name)) %>
|
138
|
+
<%= constraint_violations(form:, input_name: :name) %>
|
139
|
+
|
140
|
+
<%= component(Brut::FrontEnd::Components::Textarea.for_form_input(form:, input_name: :description)) %>
|
141
|
+
<%= constraint_violations(form:, input_name: :description) %>
|
142
|
+
|
143
|
+
<button>Save</button>
|
144
|
+
<% end %>
|
145
|
+
|
146
|
+
## Multiple Inputs with the Same Name
|
147
|
+
|
148
|
+
The HTTP spec allows for any number of inputs with the same name. All values are submitted. Rack, upon which Brut is based, further
|
149
|
+
provides a way to access such duplicate names as an array, using a naming convention.
|
150
|
+
|
151
|
+
Brut forms support this via `array: true` when defining an input:
|
152
|
+
|
153
|
+
class NewWidgetForm < AppForm
|
154
|
+
input :name, minlength: 3, array: true
|
155
|
+
input :description, required: false, array: true
|
156
|
+
end
|
157
|
+
|
158
|
+
When you do this, a call to `for_form_input` will require an index, as will any other method you use to interact with the form, such
|
159
|
+
as `server_side_constraint_violation`. When the HTML is generated, the `name=` of the `<input>` will use Rack's naming convention:
|
160
|
+
|
161
|
+
<input type="text" name="name[]" ...>
|
162
|
+
|
163
|
+
To access the values, you can pass an index to the generated method name, or use the `_each` form:
|
164
|
+
|
165
|
+
def handle!(form:)
|
166
|
+
form.name(1) # get the second value for the 'name' input
|
167
|
+
form.name_each do |value,i|
|
168
|
+
value # is the value of the input, empty string if omitted
|
169
|
+
i # is the zero-based index
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
When generating HTML, the form object will generate any number of inputs that you request:
|
174
|
+
|
175
|
+
<%= form_tag for: form do %>
|
176
|
+
|
177
|
+
<% 10.times do |index| %>
|
178
|
+
<%= component(Brut::FrontEnd::Components::TextField.for_form_input(form:, input_name: :name, index:)) %>
|
179
|
+
<%= constraint_violations(form:, input_name: :name) %>
|
180
|
+
<% end %>
|
181
|
+
|
182
|
+
<button>Save</button>
|
183
|
+
<% end %>
|
184
|
+
|
185
|
+
## Styling and Client-Side Behavior
|
186
|
+
|
187
|
+
While browsers have long-supported client-side constraint validations, there are a few complications that make them hard to use in
|
188
|
+
practice. Brut provides solutions for most of these issues and allows you to unify your error reporting into a single user experien
|
189
|
+
ce, regardless of where the constraint violation was identified. This does, however, require JavaScript. But, it is entirely opt-in.
|
190
|
+
|
191
|
+
### Issue: Blank Forms Match `:invalid` Pseudo-Class
|
192
|
+
|
193
|
+
If an input is required, it will match the `:invalid` pseudo class if it has no value, even if a user has not interacted with the
|
194
|
+
input. While Brut cannot change this behavior, it *does* allow you to have better control.
|
195
|
+
|
196
|
+
If you surround your `<form>` with the `<brut-form>` custom element, that element will add `data-submitted` to the `<form>` element
|
197
|
+
when submission is attempted. This means that your CSS can target something like `form[data-submitted] input:invalid` so that any
|
198
|
+
styling for constraint violations will only show up if the user has attempted to submit the form.
|
199
|
+
|
200
|
+
### Issue: App-Controled Messaging for Client-Side Constraint Violations
|
201
|
+
|
202
|
+
While it's not currently possible to control the browser's UI around client-side constraint violations, Brut does allow you to provide
|
203
|
+
your own error messages and UX when this happens. This means you can unify your client-side and server-side messaging so it looks the
|
204
|
+
same no matter what.
|
205
|
+
|
206
|
+
When a field is detected to be invalid, `<brut-form>` will locate a `<brut-cv-messages>` custom element and provide it with the
|
207
|
+
`ValidityState` of the input. This will create one `<brut-cv>` custom element for each constraint violation. The `<brut-cv>` custom
|
208
|
+
element will use its `key=` attribute to locate the appropriate `<brut-i18n-translation>` custom element, which your server should've
|
209
|
+
rendered with the appropriate error messages.
|
210
|
+
|
211
|
+
This has the effect of inserting a localized message you control into the DOM wherever you want it for reporting the error to the
|
212
|
+
user.
|
213
|
+
|
214
|
+
|