brut 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/Gemfile.lock +66 -1
  4. data/README.md +36 -0
  5. data/Rakefile +22 -0
  6. data/brut.gemspec +7 -0
  7. data/doc-src/architecture.md +102 -0
  8. data/doc-src/assets.md +98 -0
  9. data/doc-src/forms.md +214 -0
  10. data/doc-src/handlers.md +83 -0
  11. data/doc-src/javascript.md +265 -0
  12. data/doc-src/keyword-injection.md +183 -0
  13. data/doc-src/pages.md +210 -0
  14. data/doc-src/route-hooks.md +59 -0
  15. data/docs-todo.md +32 -0
  16. data/lib/brut/back_end/seed_data.rb +5 -1
  17. data/lib/brut/back_end/validator.rb +1 -1
  18. data/lib/brut/back_end/validators/form_validator.rb +31 -6
  19. data/lib/brut/cli/app.rb +100 -4
  20. data/lib/brut/cli/app_runner.rb +38 -5
  21. data/lib/brut/cli/apps/build_assets.rb +4 -6
  22. data/lib/brut/cli/apps/db.rb +2 -7
  23. data/lib/brut/cli/apps/scaffold.rb +413 -7
  24. data/lib/brut/cli/apps/test.rb +14 -1
  25. data/lib/brut/cli/command.rb +141 -6
  26. data/lib/brut/cli/error.rb +30 -3
  27. data/lib/brut/cli/execution_results.rb +44 -6
  28. data/lib/brut/cli/executor.rb +21 -1
  29. data/lib/brut/cli/options.rb +29 -2
  30. data/lib/brut/cli/output.rb +47 -0
  31. data/lib/brut/cli.rb +26 -0
  32. data/lib/brut/factory_bot.rb +2 -0
  33. data/lib/brut/framework/app.rb +38 -5
  34. data/lib/brut/framework/config.rb +97 -54
  35. data/lib/brut/framework/container.rb +97 -33
  36. data/lib/brut/framework/errors/abstract_method.rb +3 -7
  37. data/lib/brut/framework/errors/missing_parameter.rb +12 -0
  38. data/lib/brut/framework/errors/no_class_for_path.rb +25 -0
  39. data/lib/brut/framework/errors/not_found.rb +12 -2
  40. data/lib/brut/framework/errors/not_implemented.rb +14 -0
  41. data/lib/brut/framework/errors.rb +18 -0
  42. data/lib/brut/framework/fussy_type_enforcement.rb +17 -12
  43. data/lib/brut/framework/mcp.rb +106 -49
  44. data/lib/brut/framework/project_environment.rb +9 -0
  45. data/lib/brut/framework.rb +1 -0
  46. data/lib/brut/front_end/asset_metadata.rb +7 -1
  47. data/lib/brut/front_end/component.rb +129 -38
  48. data/lib/brut/front_end/components/constraint_violations.rb +57 -0
  49. data/lib/brut/front_end/components/form_tag.rb +23 -32
  50. data/lib/brut/front_end/components/i18n_translations.rb +34 -1
  51. data/lib/brut/front_end/components/input.rb +3 -0
  52. data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -0
  53. data/lib/brut/front_end/components/inputs/radio_button.rb +41 -0
  54. data/lib/brut/front_end/components/inputs/select.rb +26 -2
  55. data/lib/brut/front_end/components/inputs/text_field.rb +27 -5
  56. data/lib/brut/front_end/components/inputs/textarea.rb +24 -4
  57. data/lib/brut/front_end/components/locale_detection.rb +8 -1
  58. data/lib/brut/front_end/components/page_identifier.rb +2 -0
  59. data/lib/brut/front_end/components/time.rb +95 -0
  60. data/lib/brut/front_end/components/traceparent.rb +22 -0
  61. data/lib/brut/front_end/download.rb +11 -0
  62. data/lib/brut/front_end/flash.rb +32 -0
  63. data/lib/brut/front_end/form.rb +109 -106
  64. data/lib/brut/front_end/forms/constraint_violation.rb +16 -11
  65. data/lib/brut/front_end/forms/input.rb +30 -42
  66. data/lib/brut/front_end/forms/input_declarations.rb +90 -0
  67. data/lib/brut/front_end/forms/input_definition.rb +45 -30
  68. data/lib/brut/front_end/forms/radio_button_group_input.rb +46 -0
  69. data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +29 -0
  70. data/lib/brut/front_end/forms/select_input.rb +47 -0
  71. data/lib/brut/front_end/forms/select_input_definition.rb +27 -0
  72. data/lib/brut/front_end/forms/validity_state.rb +23 -9
  73. data/lib/brut/front_end/handler.rb +27 -8
  74. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +4 -2
  75. data/lib/brut/front_end/handlers/instrumentation_handler.rb +99 -0
  76. data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -4
  77. data/lib/brut/front_end/handlers/missing_handler.rb +9 -0
  78. data/lib/brut/front_end/handling_results.rb +14 -4
  79. data/lib/brut/front_end/http_method.rb +13 -1
  80. data/lib/brut/front_end/http_status.rb +10 -0
  81. data/lib/brut/front_end/layouts/_internal.html.erb +68 -0
  82. data/lib/brut/front_end/middleware.rb +5 -0
  83. data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +15 -0
  84. data/lib/brut/front_end/middlewares/favicon.rb +16 -0
  85. data/lib/brut/front_end/middlewares/open_telemetry_span.rb +12 -0
  86. data/lib/brut/front_end/middlewares/reload_app.rb +27 -9
  87. data/lib/brut/front_end/page.rb +50 -11
  88. data/lib/brut/front_end/pages/_missing_page.html.erb +17 -0
  89. data/lib/brut/front_end/pages/missing_page.rb +36 -0
  90. data/lib/brut/front_end/request_context.rb +117 -8
  91. data/lib/brut/front_end/route_hook.rb +43 -1
  92. data/lib/brut/front_end/route_hooks/age_flash.rb +3 -2
  93. data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +8 -0
  94. data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +18 -10
  95. data/lib/brut/front_end/route_hooks/locale_detection.rb +11 -5
  96. data/lib/brut/front_end/route_hooks/setup_request_context.rb +7 -4
  97. data/lib/brut/front_end/routing.rb +138 -31
  98. data/lib/brut/front_end/session.rb +86 -7
  99. data/lib/brut/front_end/template.rb +17 -2
  100. data/lib/brut/front_end/templates/block_filter.rb +4 -3
  101. data/lib/brut/front_end/templates/erb_parser.rb +1 -1
  102. data/lib/brut/front_end/templates/escapable_filter.rb +2 -0
  103. data/lib/brut/front_end/templates/html_safe_string.rb +30 -2
  104. data/lib/brut/front_end/templates/locator.rb +60 -0
  105. data/lib/brut/i18n/base_methods.rb +4 -0
  106. data/lib/brut/i18n.rb +1 -0
  107. data/lib/brut/instrumentation/logger_span_exporter.rb +75 -0
  108. data/lib/brut/instrumentation/open_telemetry.rb +107 -0
  109. data/lib/brut/instrumentation.rb +4 -6
  110. data/lib/brut/junk_drawer.rb +54 -4
  111. data/lib/brut/sinatra_helpers.rb +42 -38
  112. data/lib/brut/spec_support/clock_support.rb +6 -0
  113. data/lib/brut/spec_support/component_support.rb +53 -26
  114. data/lib/brut/spec_support/e2e_test_server.rb +82 -0
  115. data/lib/brut/spec_support/enhanced_node.rb +45 -0
  116. data/lib/brut/spec_support/general_support.rb +14 -3
  117. data/lib/brut/spec_support/handler_support.rb +2 -0
  118. data/lib/brut/spec_support/matcher.rb +6 -3
  119. data/lib/brut/spec_support/matchers/be_page_for.rb +3 -2
  120. data/lib/brut/spec_support/matchers/have_constraint_violation.rb +22 -9
  121. data/lib/brut/spec_support/matchers/have_html_attribute.rb +9 -2
  122. data/lib/brut/spec_support/matchers/have_i18n_string.rb +24 -0
  123. data/lib/brut/spec_support/matchers/have_link_to.rb +14 -0
  124. data/lib/brut/spec_support/matchers/have_redirected_to.rb +30 -0
  125. data/lib/brut/spec_support/matchers/have_rendered.rb +4 -11
  126. data/lib/brut/spec_support/matchers/have_returned_http_status.rb +16 -8
  127. data/lib/brut/spec_support/rspec_setup.rb +182 -0
  128. data/lib/brut/spec_support.rb +8 -3
  129. data/lib/brut/version.rb +2 -1
  130. data/lib/brut.rb +28 -5
  131. data/lib/sequel/extensions/brut_instrumentation.rb +5 -27
  132. data/lib/sequel/extensions/brut_migrations.rb +18 -8
  133. data/lib/sequel/plugins/created_at.rb +2 -0
  134. data/lib/sequel/plugins/external_id.rb +39 -1
  135. data/lib/sequel/plugins/find_bang.rb +4 -1
  136. metadata +140 -13
  137. data/lib/brut/back_end/action.rb +0 -3
  138. data/lib/brut/back_end/result.rb +0 -46
  139. data/lib/brut/front_end/components/timestamp.rb +0 -33
  140. data/lib/brut/instrumentation/basic.rb +0 -66
  141. data/lib/brut/instrumentation/event.rb +0 -19
  142. data/lib/brut/instrumentation/http_event.rb +0 -5
  143. data/lib/brut/instrumentation/subscriber.rb +0 -41
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c756b30db51ff455ac587492ee974356c0dbe4084483fc562854ce682cca8014
4
- data.tar.gz: 7ce5250e1bfac5f9cd61f3329399a5896f83ce4af9457882190a9e089cf35fcd
3
+ metadata.gz: c1fd649346396f27b70064c601a639187c08fec7d38f4efc973c3a9839d2c163
4
+ data.tar.gz: a06518da1bb198c89acfd13dca787fabb302546097e08d8917bf1c500d399f39
5
5
  SHA512:
6
- metadata.gz: 35f7f592155864b2bc178c2c8267967c95a141da0105c07e2969c3a646435fb23101a3070e686aebd5fc797e1938155e7e8d4f6af029bb93eb208350f4774014
7
- data.tar.gz: db78b9b4ac1be3561a599cae098a2ae4c005a027d0f9c768e8d766add2c736ee87614505bc131750cb1b3af8d99b916054c427c48556ba8d2f5422c002125de8
6
+ metadata.gz: 0ecde01ea2f1cdc821e24e835fe18a7420848601d469435c74abe3d020187fccc73004ce063f7c242f663796d2470145981e6ace8a742ae885568abb0533eb74
7
+ data.tar.gz: da0e5c69737041a6ff25366c4fd4cb41214f97b1ada9c78f878544f380f8370bcd155f8f7e6b77d52b50a77b6270b08847e5b1e10bec657b464d289d84747c4f
data/.gitignore CHANGED
@@ -5,3 +5,9 @@
5
5
  # from inside the dev environment. As such, these are real secrets and
6
6
  # should never be checked in
7
7
  /dx/credentials
8
+
9
+ # Don't know what this is for, so not checking it in.
10
+ /.yardoc
11
+
12
+ # Ignore the docs for now
13
+ /docs
data/Gemfile.lock CHANGED
@@ -1,12 +1,16 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- brut (0.0.1)
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
+