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.
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
+