brut 0.19.2 → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 765659b6fbd1258a4b5615cfea8e7d273fbebd4336ceb6518367bc5ad329bcb8
4
- data.tar.gz: 35777088ab7cc473a3ef5459087349846586798154e9f699e52b3b5be22c64db
3
+ metadata.gz: de61901270558680112b8b90f787067add7f777204219016020adcc90c15564d
4
+ data.tar.gz: 4437fee31bc132bd93fe419ac72df5bc1b1e7bb67f506a4e36ab126dc7395b85
5
5
  SHA512:
6
- metadata.gz: 946899aa325e088a414c88e63b6bd58c7278380e90929f886905b9cd100c2a98d672d570a036e0edf66953719dccb4b6bd1dd5525f885fcc2421ea82da142ece
7
- data.tar.gz: 9bfe8cef4bd6e4effbca418ab446796fed352999f21b404d6904ec5ccd048bff9ed18de6f9fd47e8d45ad7045b73bf782375ca7bed13a1022d2af0e1e976bde5
6
+ metadata.gz: fcc0ef80b40ea5703298cdbaa71f86f3ec8d22b96f8bc4e9cea336f41b09bbca109b211eea365b202759f8bca4c07ace5c35d2a6f638a7d2231f5d0534090556
7
+ data.tar.gz: c4bb547d99089ca6d567e744c39539fda20ff85b21c45dd2419ab7d0bcfc0e265a9004006973e7bb4f96be35f41d21354efd71502f77e2276140dbd8badf29a3
@@ -91,9 +91,9 @@ This is to ensure that any images your code references will end up in the public
91
91
  def description = "Builds a single CSS file suitable for sending to the browser"
92
92
 
93
93
  def detailed_description = %{
94
- This produces a hashed file in every environment, in order to keep environments consistent and reduce differences. If your CSS file references images, fonts, or other assets via url() or other CSS functions, those files will be hashed and copied into the output directory where CSS is served.
94
+ This produces a hashed file in every environment, in order to keep environments consistent and reduce differences. If your CSS file references images, fonts, or other assets via `url()` or other CSS functions, those files will be hashed and copied into the output directory where CSS is served.
95
95
 
96
- To ensure this happens correctly, your url() or other function must reference the file as a relative file from where your actual source CSS file is located. For example, a font named some-font.ttf would be in app/src/front_end/fonts and to reference this from app/src/front_end/css/index.css you'd use the url "../fonts/some-font.ttf"
96
+ To ensure this happens correctly, your `url()` or other function must reference the file as a relative file from where your actual source CSS file is located. For example, a font named `some-font.ttf` would be in `app/src/front_end/fonts`. To reference this from `app/src/front_end/css/index.css` you'd use `url("../fonts/some-font.ttf")`
97
97
  }
98
98
 
99
99
  def run
@@ -51,20 +51,20 @@ class Brut::CLI::Apps::DB < Brut::CLI::Commands::BaseCommand
51
51
  rows = [
52
52
  [
53
53
  "Database Server",
54
- server_up ? theme.success.render("✅ UP") : theme.error.render("❌ DOWN")
54
+ server_up ? theme.success.render("✅ UP") : theme.error.render("❌ DOWN"),
55
55
  ],
56
56
  ]
57
57
  if server_up
58
58
  rows << [
59
59
  "Database #{theme.code.render(database_name)}",
60
- database_exists ? theme.success.render("✅ Exists") : theme.error.render("❌ DOES NOT EXIST")
60
+ database_exists ? theme.success.render("✅ Exists") : theme.error.render("❌ DOES NOT EXIST"),
61
61
  ]
62
62
  end
63
63
  if database_exists
64
64
  if migration_files.empty? && migrations_run.empty?
65
65
  rows << [
66
66
  "Migrations",
67
- "✅ NO MIGRATION FILES TO RUN"
67
+ "✅ NO MIGRATION FILES TO RUN",
68
68
  ]
69
69
  else
70
70
  migration_files.each do |filename|
@@ -19,8 +19,16 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
19
19
  def default_rack_env = "development"
20
20
 
21
21
  def run
22
+ options.set_default(:deploy, true)
23
+ puts "Logging in to Heroku Container Registry"
24
+ command = %{heroku container:login}
25
+ system!(command)
22
26
  execute_result = Brut::CLI::ExecuteResult.new do
23
- delegate_to_command(Brut::CLI::Apps::Deploy::Build.new)
27
+ delegate_to_command(
28
+ Brut::CLI::Apps::Deploy::Build.new(
29
+ push: options.deploy? ? "registry.heroku.com/#{Brut.container.app_id}/%{name}": false
30
+ )
31
+ )
24
32
  end
25
33
  if execute_result.failed?
26
34
  puts theme.error.render("Build failed.")
@@ -28,47 +36,17 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
28
36
  puts theme.error.render("Error message from build: #{error_message}")
29
37
  end
30
38
  end
31
- options.set_default(:deploy, true)
32
- version = ""
33
- git_guess = %{git rev-parse HEAD}
34
- system!(git_guess) do |output|
35
- version << output
36
- end
37
- version.strip!.chomp!
38
- if version == ""
39
- error "Attempt to use git via command '#{git_guess}' to figure out the version failed"
40
- return 1
41
- end
42
- short_version = version[0..7]
39
+ names = []
43
40
  app_docker_files = AppDockerImages.new(
44
41
  project_root: Brut.container.project_root,
45
42
  organization: Brut.container.app_organization,
46
43
  app_id: Brut.container.app_id,
47
- short_version:
44
+ short_version: "NA"
48
45
  )
49
- names = []
50
- puts "Logging in to Heroku Container Registry"
51
- command = %{heroku container:login}
52
- system!(command)
53
- app_docker_files.each do |name:, image_name:|
54
- heroku_image_name = "registry.heroku.com/#{Brut.container.app_id}/#{name}"
55
- puts "Tagging '#{image_name}' with '#{heroku_image_name}' for Heroku"
56
- command = %{docker tag #{image_name} #{heroku_image_name}}
57
- system!(command)
58
- begin
59
- puts "Pushing '#{heroku_image_name}'"
60
- command = %{docker push #{heroku_image_name}}
61
- system!(command)
62
- rescue Brut::CLI::SystemExecError => ex
63
- error "Failed to push image '#{heroku_image_name}' to Heroku"
64
- if options.log_level != "debug"
65
- error "Could be you must re-authenticate to Heroku."
66
- error "Try re-running with --log-level=debug to see more details"
67
- end
68
- return 1
69
- end
46
+ app_docker_files.each do |name:, cmd:, dockerfile:|
70
47
  names << name
71
48
  end
49
+
72
50
  deploy_command = "heroku container:release #{names.join(' ')} -a #{Brut.container.app_id}"
73
51
  if options.deploy?
74
52
  puts "Deploying images to Heroku"
@@ -142,7 +120,13 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
142
120
  def description = Docker.new.description
143
121
  def opts = Docker.new.opts
144
122
  def default_rack_env = Docker.new.default_rack_env
145
- def run = delegate_to_command(Docker.new)
123
+
124
+ def initialize(push: false)
125
+ @push = push
126
+ end
127
+ def run
128
+ delegate_to_command(Docker.new(push: @push))
129
+ end
146
130
 
147
131
  def commands = []
148
132
 
@@ -155,6 +139,10 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
155
139
  ]
156
140
  def default_rack_env = "development"
157
141
 
142
+ def initialize(push: false)
143
+ @push = push
144
+ end
145
+
158
146
  def run
159
147
  if !options.skip_checks?
160
148
  execute_result = Brut::CLI::ExecuteResult.new do
@@ -177,8 +165,6 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
177
165
  error "Attempt to use git via command '#{git_guess}' to figure out the version failed"
178
166
  return 1
179
167
  end
180
- short_version = version[0..7]
181
-
182
168
  short_version = version[0..7]
183
169
  app_docker_files = AppDockerImages.new(
184
170
  project_root: Brut.container.project_root,
@@ -219,12 +205,16 @@ class Brut::CLI::Apps::Deploy < Brut::CLI::Commands::BaseCommand
219
205
  puts
220
206
  rows = []
221
207
  items = []
208
+ push_or_load = @push ? "--push" : "--load"
222
209
  app_docker_files.each do |name:, image_name:, dockerfile:|
210
+ if @push && @push.kind_of?(String)
211
+ image_name = @push % { name: name }
212
+ end
223
213
  rows << [ name, theme.code.render(image_name) ]
224
- command = %{docker build --build-arg app_git_sha1=#{version} --file #{Brut.container.project_root}/#{dockerfile} --platform #{options.platform} --tag #{image_name} . 2>&1}
214
+ command = %{docker buildx build --provenance=false --build-arg app_git_sha1=#{version} --file #{Brut.container.project_root}/#{dockerfile} --platform #{options.platform} #{push_or_load} --tag #{image_name} . 2>&1}
225
215
  items << theme.code.render(theme.wrap(command, first_indent: false, indent: 7, newline: " \\\n"))
226
216
  if !options.dry_run?
227
- puts theme.subheader.render("Building '#{name}' image")
217
+ puts theme.subheader.render("Building #{@push ? 'and pushing' : '' } '#{name}' image")
228
218
  system!(command)
229
219
  end
230
220
  end
@@ -53,12 +53,12 @@ class Brut::CLI::Apps::New::App < Brut::CLI::Commands::BaseCommand
53
53
  [
54
54
  "--app-id=ID",
55
55
  Brut::CLI::Apps::New::AppId,
56
- "App identifier, which must be able to be used as a hostname or other Internet identifier. Derived from your app name, if omitted"
56
+ "App identifier, which must be able to be used as a hostname or other Internet identifier. Derived from your app name, if omitted",
57
57
  ],
58
58
  [
59
59
  "--organization=ORG",
60
60
  Brut::CLI::Apps::New::Organization,
61
- "Organization name, e.g. what you'd use for GitHub. Defaults to the app-id value"
61
+ "Organization name, e.g. what you'd use for GitHub. Defaults to the app-id value",
62
62
  ],
63
63
  [
64
64
  "--[no-]interactive",
@@ -67,7 +67,7 @@ class Brut::CLI::Apps::New::App < Brut::CLI::Commands::BaseCommand
67
67
  [
68
68
  "--prefix=PREFIX",
69
69
  Brut::CLI::Apps::New::Prefix,
70
- "Two-character prefix for external IDs and autonomous custom elements. Derived from your app-id, if omitted."
70
+ "Two-character prefix for external IDs and autonomous custom elements. Derived from your app-id, if omitted.",
71
71
  ],
72
72
  [
73
73
  "--segments=SEGMENTS",
@@ -76,11 +76,11 @@ class Brut::CLI::Apps::New::App < Brut::CLI::Commands::BaseCommand
76
76
  ],
77
77
  [
78
78
  "--dry-run",
79
- "Only show what would happen, don't actually do anything"
79
+ "Only show what would happen, don't actually do anything",
80
80
  ],
81
81
  [
82
82
  "--[no-]demo",
83
- "Include, or not, additional files that demonstrate Brut's features (default is true for now)"
83
+ "Include, or not, additional files that demonstrate Brut's features (default is true for now)",
84
84
  ],
85
85
  ]
86
86
 
@@ -272,7 +272,7 @@ class Brut::CLI::Apps::New::App < Brut::CLI::Commands::BaseCommand
272
272
  ],
273
273
  [
274
274
  "--dry-run",
275
- "Only show what would happen, don't actually do anything"
275
+ "Only show what would happen, don't actually do anything",
276
276
  ],
277
277
  ]
278
278
 
@@ -319,7 +319,7 @@ class Brut::CLI::Apps::New::App < Brut::CLI::Commands::BaseCommand
319
319
 
320
320
  puts "Adding #{segment_name} to this app"
321
321
  segment.add!
322
- segment.output_post_add_messaging(stdout:)
322
+ segment.output_post_add_messaging(stdout: execution_context.stdout)
323
323
  0
324
324
  end
325
325
  end
@@ -19,6 +19,11 @@ class Brut::CLI::Apps::New::Ops::InsertCodeInMethod < Brut::CLI::Apps::New::Ops:
19
19
  if !@file.exist? && @ignore_if_file_not_found
20
20
  return
21
21
  end
22
+ if dry_run?
23
+ op = @class_method ? "::" : "#"
24
+ puts "Would add this code to #{@class_name}#{op}#{@method_name} in #{@file}:\n\n#{@code}\n\n"
25
+ return
26
+ end
22
27
  method_node = find_method(class_name: @class_name, method_name: @method_name, class_method: @class_method)
23
28
 
24
29
  insertion_point = if @where == :start
@@ -16,9 +16,6 @@ class Brut::CLI::Apps::New::Segments::Heroku < Brut::CLI::Apps::New::Base
16
16
  end
17
17
  end
18
18
 
19
- def output_post_add_messaging(stdout:)
20
- end
21
-
22
19
  def <=>(other)
23
20
  if self.class == other.class
24
21
  0
@@ -75,6 +75,10 @@ class Brut::CLI::Apps::New::Segments::Sidekiq < Brut::CLI::Apps::New::Base
75
75
  file: @project_root / "Gemfile",
76
76
  content: "# Sidekiq is used for background jobs\ngem \"sidekiq\"\n"
77
77
  ),
78
+ Brut::CLI::Apps::New::Ops::AppendToFile.new(
79
+ file: @project_root / "Gemfile",
80
+ content: "# Sets up OTel middelware for Sidekiq \ngem \"opentelemetry-instrumentation-sidekiq\"\n"
81
+ ),
78
82
  Brut::CLI::Apps::New::Ops::AppendToFile.new(
79
83
  file: @project_root / ".env.development",
80
84
  content: %{
@@ -110,7 +114,7 @@ SIDEKIQ_BASIC_AUTH_PASSWORD=password
110
114
  Brut::CLI::Apps::New::Ops::InsertIntoFile.new(
111
115
  file: @project_root / "specs" / "spec_helper.rb",
112
116
  before_line: "require \"brut/spec_support\"",
113
- content: "require \"sidekiq/testing\""
117
+ content: "Sidekiq.testing!(:fake)"
114
118
  ),
115
119
  Brut::CLI::Apps::New::Ops::InsertIntoFile.new(
116
120
  file: @project_root / "config.ru",
@@ -142,10 +146,9 @@ SIDEKIQ_BASIC_AUTH_PASSWORD=password
142
146
  code: "@sidekiq_segment.boot!"
143
147
  ),
144
148
  Brut::CLI::Apps::New::Ops::InsertCodeInMethod.new(
145
- file: project_root / "deploy" / "heroku_config.rb",
149
+ file: project_root / "deploy" / "docker_config.rb",
146
150
  class_name: "HerokuConfig",
147
151
  method_name: "additional_images",
148
- class_method: true,
149
152
  ignore_if_file_not_found: true,
150
153
  code: %{
151
154
  {
@@ -4,6 +4,10 @@ require "brut/cli"
4
4
  class Brut::CLI::Apps::Scaffold < Brut::CLI::Commands::BaseCommand
5
5
  def description = "Create scaffolds of various files to help develop more quckly"
6
6
 
7
+ def commands
8
+ super - [ Brut::CLI::Apps::Scaffold::BaseCommand ]
9
+ end
10
+
7
11
  class BaseCommand < Brut::CLI::Commands::BaseCommand
8
12
  def bootstrap? = false
9
13
  def default_rack_env = "development"
@@ -111,7 +115,7 @@ end}
111
115
  def args_description = "test_name"
112
116
  def name = "e2e_test"
113
117
 
114
- def opts = [
118
+ def opts = super + [
115
119
  ["--path PATH","Path within the e2e tests to create the file"],
116
120
  ]
117
121
 
@@ -193,7 +197,7 @@ end}
193
197
  def args_description = "ComponentName"
194
198
  def detailed_description = "New components go in the `components/` folder of your app, however using --page will create a 'page private' component. To do that, the component name must be an inner class of an existing page, for example HomePage::Welcome. This component goes in a sub-folder inside the `pages/` area of your app"
195
199
 
196
- def opts = [
200
+ def opts = super + [
197
201
  [ "--page","If set, this component is for a specific page and won't go with the other components"],
198
202
  ]
199
203
 
@@ -384,7 +388,7 @@ end}
384
388
  error " The page may not render properly the first time you load it"
385
389
  end
386
390
 
387
- routes_editor = RoutesEditor.new(app_path:,out:)
391
+ routes_editor = RoutesEditor.new(app_path:,stdout: execution_context.stdout)
388
392
  routes_editor.add_route!(route_code:)
389
393
 
390
394
  if !routes_editor.found_routes?
@@ -419,7 +423,7 @@ end}
419
423
  end
420
424
  def description = "Create a handler for an action"
421
425
  def args_description = "action_route"
422
- def opts = [
426
+ def opts = super + [
423
427
  [ "--http-method=METHOD", "If present, the action will be a path available on the given route and this HTTP method. If omitted, this will create an action available via POST" ],
424
428
  ]
425
429
 
@@ -528,7 +532,7 @@ end}
528
532
  execution_context.stdout.printf printf_string,handler_class_name, handler_source_path.relative_path_from(Brut.container.project_root)
529
533
  execution_context.stdout.printf printf_string,"Spec", handler_spec_path.relative_path_from(Brut.container.project_root)
530
534
 
531
- routes_editor = RoutesEditor.new(app_path:,out:)
535
+ routes_editor = RoutesEditor.new(app_path:,stdout: execution_context.stdout)
532
536
  routes_editor.add_route!(route_code:)
533
537
 
534
538
  if form
@@ -633,7 +637,8 @@ describe("#{description}", () => {
633
637
 
634
638
  def run
635
639
  if argv.length == 0
636
- return abort_execution("You must provide a model name")
640
+ puts "You must provide one or more model names"
641
+ return 1
637
642
  end
638
643
  db_module = ModuleName.from_string("DB")
639
644
  actions = argv.map { |arg|
@@ -703,9 +708,9 @@ end
703
708
  end
704
709
 
705
710
  class RoutesEditor
706
- def initialize(app_path:,out:)
711
+ def initialize(app_path:,stdout:)
707
712
  @app_path = app_path
708
- @out = out
713
+ @stdout = stdout
709
714
  @found_routes = false
710
715
  @routes_existed = false
711
716
  end
@@ -726,7 +731,7 @@ end
726
731
  end
727
732
  if in_routes && line =~ /^ end\s*$/
728
733
  if !@routes_existed
729
- @out.puts "Inserted route into #{@app_path.relative_path_from(Brut.container.project_root)}"
734
+ @stdout.puts "Inserted route into #{@app_path.relative_path_from(Brut.container.project_root)}"
730
735
  file.puts " #{route_code}"
731
736
  end
732
737
  @found_routes = true
@@ -90,6 +90,7 @@ Runs all non end-to-end tests for the app, or runs a subset of non-end-to-end te
90
90
  [ "E2E_RECORD_VIDEOS","If set to 'true', videos of each test run are saved in `./tmp/e2e-videos`" ],
91
91
  [ "E2E_SLOW_MO","If set to, will attempt to slow operations down by this many milliseconds" ],
92
92
  [ "E2E_TIMEOUT_MS","ms to wait for any browser activity before failing the test. And here you didn't think you'd get away without using sleep in browse-based tests?" ],
93
+ [ "E2E_STARTUP_TIMEOUT_SEC","seconds to wait for the test server to start before assuming something went wrong" ],
93
94
  ]
94
95
 
95
96
  def rspec_cli_args = "--tag e2e"
@@ -102,11 +103,17 @@ Runs all end-to-end tests for the app, or runs a subset of end-to-end tests usin
102
103
  private
103
104
 
104
105
  def run_tests
105
- require "brut/spec_support/e2e_test_server"
106
- Brut::SpecSupport::E2ETestServer.instance.start
107
- super
108
- ensure
109
- Brut::SpecSupport::E2ETestServer.instance.stop
106
+ test_server = Brut::SpecSupport::E2ETestServer.new(
107
+ bin_dir: Brut.container.project_root / "bin",
108
+ start_timeout_seconds: ENV["E2E_STARTUP_TIMEOUT_SEC"]
109
+ )
110
+ begin
111
+ require "brut/spec_support/e2e_test_server"
112
+ test_server.start
113
+ super
114
+ ensure
115
+ test_server.stop
116
+ end
110
117
  end
111
118
  end
112
119
  class Js < Brut::CLI::Commands::BaseCommand
@@ -0,0 +1,109 @@
1
+ class Brut::CLI::Commands::HelpInMarkdown < Brut::CLI::Commands::BaseCommand
2
+ def description = "Get help for the app or a command, in Markdown"
3
+ attr_accessor :option_parser
4
+
5
+ def initialize(command,option_parser)
6
+ @command = command
7
+ @option_parser = option_parser
8
+ end
9
+
10
+ def commands = []
11
+
12
+ def run
13
+ if env["BRUT_HELP_IN_MARKDOWN_COMMANDS_ONLY"]
14
+ @command.commands.sort_by(&:name).each do |command|
15
+ puts command.name
16
+ end
17
+ return 0
18
+ end
19
+ cli = [@command.name ]
20
+ cmd = @command
21
+ while cmd.parent_command
22
+ cmd = cmd.parent_command
23
+ cli.unshift cmd.name
24
+ end
25
+ invocation = cli.join(" ")
26
+ puts "# `#{invocation}`"
27
+ puts
28
+ puts @command.description
29
+ puts
30
+
31
+ usage = invocation
32
+
33
+ options = @option_parser.top.list
34
+ if options.size > 0
35
+ usage << theme.weak.render(" [options]")
36
+ end
37
+ if @command.commands.any?
38
+ usage << theme.code.render(" command")
39
+ end
40
+ if @command.args_description
41
+ usage << " #{@command.args_description}"
42
+ end
43
+ puts
44
+ puts "## USAGE"
45
+ puts
46
+ puts " " + usage
47
+ puts
48
+ if @command.detailed_description
49
+ puts
50
+ puts "## DESCRIPTION"
51
+ puts
52
+ puts @command.detailed_description.gsub(/ +/," ").strip
53
+ puts
54
+ end
55
+ if options.size > 0
56
+ puts
57
+ puts "## OPTIONS"
58
+ puts
59
+
60
+ options.each do |option|
61
+ switches = option.long.map { |switch|
62
+ if option.arg
63
+ if option.arg[0] == "="
64
+ "#{switch.strip}#{theme.weak.render(option.arg.strip)}"
65
+ else
66
+ "#{switch.strip}=#{theme.weak.render(option.arg.strip)}"
67
+ end
68
+ else
69
+ switch
70
+ end
71
+ } + option.short.map { |switch|
72
+ if option.arg
73
+ "#{switch} #{theme.weak.render(option.arg)}"
74
+ else
75
+ switch
76
+ end
77
+ }
78
+ puts "* `#{switches.join(", ")}` - #{option.desc.join(" ")}"
79
+ end
80
+ end
81
+ if @command.env_vars.any?
82
+ puts
83
+ puts "## ENVIRONMENT VARIABLES"
84
+ puts
85
+ @command.env_vars.sort_by(&:first).each do |env_var|
86
+ puts "* `#{env_var[0]}` - #{env_var[1]}"
87
+ end
88
+ end
89
+ if @command.commands.any?
90
+ commands_subpath = env["BRUT_HELP_IN_MARKDOWN_COMMAND_PATH"] || "commands"
91
+ puts
92
+ puts "## COMMANDS"
93
+ puts
94
+ @command.commands.sort_by(&:name).each do |command|
95
+ puts "### [`#{command.name}`](./#{commands_subpath}/#{command.name})"
96
+ puts
97
+ puts "#{command.description}"
98
+ if command.detailed_description
99
+ puts
100
+ puts command.detailed_description.gsub(/ +/," ").strip
101
+ end
102
+ end
103
+ end
104
+ 0
105
+ end
106
+
107
+ def bootstrap? = false
108
+ def default_rack_env = nil
109
+ end
@@ -2,6 +2,7 @@ module Brut::CLI::Commands
2
2
  autoload(:BaseCommand, "brut/cli/commands/base_command")
3
3
  autoload(:CompoundCommand, "brut/cli/commands/compound_command")
4
4
  autoload(:Help, "brut/cli/commands/help")
5
+ autoload(:HelpInMarkdown, "brut/cli/commands/help_in_markdown")
5
6
  autoload(:OutputError, "brut/cli/commands/output_error")
6
7
  autoload(:RaiseError, "brut/cli/commands/raise_error")
7
8
  autoload(:ExecutionContext, "brut/cli/commands/execution_context")
@@ -1,5 +1,6 @@
1
1
  require "logger"
2
2
  require "fileutils"
3
+ require "pathname"
3
4
  require "delegate"
4
5
 
5
6
  class Brut::CLI::Logger < SimpleDelegator
@@ -58,9 +59,9 @@ class Brut::CLI::Logger < SimpleDelegator
58
59
  end
59
60
 
60
61
  def log_file=(log_file)
61
- @log_file = log_file
62
62
  if log_file
63
- log_dir = log_file.dirname
63
+ @log_file = Pathname(log_file)
64
+ log_dir = @log_file.dirname
64
65
  if !log_dir.exist?
65
66
  FileUtils.mkdir_p(log_dir)
66
67
  end
@@ -69,6 +70,8 @@ class Brut::CLI::Logger < SimpleDelegator
69
70
  if @logger.level == ::Logger::DEBUG
70
71
  @stdout.puts "Logging to file #{@log_file}"
71
72
  end
73
+ else
74
+ @log_file = nil
72
75
  end
73
76
  end
74
77
 
@@ -30,6 +30,11 @@ class Brut::CLI::ParsedCommandLine
30
30
  # This should always succeed, however depending on the contents of the parameters, the value
31
31
  # for `#command` may be a command that outputs an error.
32
32
  def initialize(app_command:, argv:, env:)
33
+ help_command_class = if env["BRUT_HELP_IN_MARKDOWN"] == "true"
34
+ Brut::CLI::Commands::HelpInMarkdown
35
+ else
36
+ Brut::CLI::Commands::Help
37
+ end
33
38
  brut_provided_help_requested = false
34
39
  app_option_parser = new_option_parser(app_command.name) do |opts|
35
40
  opts.banner = app_command.description
@@ -51,7 +56,7 @@ class Brut::CLI::ParsedCommandLine
51
56
  end
52
57
 
53
58
  help_command = if brut_provided_help_requested
54
- Brut::CLI::Commands::Help.new(app_command,app_option_parser)
59
+ help_command_class.new(app_command,app_option_parser)
55
60
  end
56
61
 
57
62
  command = app_command
@@ -83,7 +88,7 @@ class Brut::CLI::ParsedCommandLine
83
88
  end
84
89
  remaining_argv = command_option_parser.parse!(remaining_argv, into: options)
85
90
  if brut_provided_help_requested
86
- help_command = Brut::CLI::Commands::Help.new(command,command_option_parser)
91
+ help_command = help_command_class.new(command,command_option_parser)
87
92
  elsif help_command
88
93
  help_command.option_parser = command_option_parser
89
94
  end
@@ -112,12 +117,16 @@ class Brut::CLI::ParsedCommandLine
112
117
  if !@options[:'log-file']
113
118
  log_file_path = if env["XDG_STATE_HOME"]
114
119
  Pathname(env["XDG_STATE_HOME"]) / "brut"
115
- elsif env["HOME"]
120
+ elsif env["HOME"] && File.writable?(env["HOME"])
116
121
  Pathname("#{env['HOME']}/.local/state/") / "brut"
117
122
  else
118
- Pathname("/tmp/") / "brut"
123
+ nil
119
124
  end
120
- @options[:'log-file'] = log_file_path / (app_command.name + ".log")
125
+ if log_file_path
126
+ @options[:'log-file'] = log_file_path / (app_command.name + ".log")
127
+ end
128
+ else
129
+ @options[:'log-file'] = Pathname(@options[:'log-file'])
121
130
  end
122
131
  if @options[:'log-stdout'].nil?
123
132
  @options[:'log-stdout'] = @options.verbose? || @options.debug?
@@ -143,8 +152,7 @@ private
143
152
  "Project environment, e.g. test, development, production. Default depends on the command")
144
153
  opts.on("--log-level=LOG_LEVEL", [ "debug", "info", "warn", "error", "fatal" ],
145
154
  "Log level, which should be debug, info, warn, error, or fatal. Defaults to error")
146
- opts.on("--verbose", "Set log level to debug, and show log messages on stdout")
147
- opts.on("--debug", "Set log level to debug, and show log messages on stdout")
155
+ opts.on("--debug", "--verbose", "Set log level to debug, and show log messages on stdout")
148
156
  opts.on("--quiet", "Set log level to error")
149
157
  opts.on("--log-file=FILE",
150
158
  "Path to a file where log messages are written. Defaults to $XDG_CACHE_HOME/brut/logs/#{app_name}.log")
@@ -439,6 +439,7 @@ private
439
439
  if defined?(OpenTelemetry::Instrumentation::Sidekiq)
440
440
  c.use 'OpenTelemetry::Instrumentation::Sidekiq', {
441
441
  span_naming: :job_class,
442
+ propagation_style: :child, # XXX: Configurable?
442
443
  }
443
444
  else
444
445
  SemanticLogger[self.class].info "OpenTelemetry::Instrumentation::Sidekiq is not loaded, so Sidekiq traces will not be captured"
@@ -4,7 +4,6 @@
4
4
  class Brut::FrontEnd::AssetMetadata
5
5
 
6
6
  # @param [String] asset_metadata_file to the asset metadata file
7
- # @param [IO] out IO on which to write messaging
8
7
  def initialize(asset_metadata_file:,logger: :use_default)
9
8
  @asset_metadata_file = asset_metadata_file
10
9
  @logger = if logger == :use_default
@@ -1,4 +1,16 @@
1
- # Extended by {Brut::FrontEnd::Form} to allow declaring inputs. Do not use this module directly. Instead, call {#input} or {#select}
1
+ # Extended by {Brut::FrontEnd::Form} to allow declaring inputs. This module creates methods per input on the form passed to your handlers. For example, if you have an `input :book_title`, then `form.book_title` will be available to access the value of the "book_title" input.
2
+ #
3
+ # There are two methods that could be created, per input. Examples below use
4
+ # `book_title` as the attribute name
5
+ #
6
+ # * `#book_title` - returns {Brut::FrontEnd::Forms::Input#value}, which is always a string.
7
+ # * `#book_title_coerced` - returns {Brut::FrontEnd::Forms::Input#typed_value}, which is always the correct type for the input **or `nil` if type coercion failed**. Only call this once you have checked for constraint violations
8
+ #
9
+ # For indexed parameters, the above methods require the index to be passed,
10
+ # e.g. `form.book_title_coerced(4)`. For non-indexed parameters, the index may
11
+ # not be passed.
12
+ #
13
+ # Do not use this module directly. Instead, call {#input} or {#select}
2
14
  # from within your form's class definition.
3
15
  module Brut::FrontEnd::Forms::InputDeclarations
4
16
  # Declares an input for this form, to be modeled via an HTML `<INPUT>` tag.
@@ -59,11 +71,22 @@ module Brut::FrontEnd::Forms::InputDeclarations
59
71
  end
60
72
  self.input(input_definition.name, index:).value
61
73
  end
74
+ define_method "#{input_definition.name}_coerced" do |index=nil|
75
+ if index.nil?
76
+ raise ArgumentError,"#{input_definition.name} is an array - you must provide an index to access one of its values"
77
+ end
78
+ self.input(input_definition.name, index:).typed_value
79
+ end
62
80
  define_method "#{input_definition.name}_each" do |&block|
63
81
  self.inputs(input_definition.name).each_with_index do |input,i|
64
82
  block.(input.value,i)
65
83
  end
66
84
  end
85
+ define_method "#{input_definition.name}_each_coerced" do |&block|
86
+ self.inputs(input_definition.name).each_with_index do |input,i|
87
+ block.(input.typed_value,i)
88
+ end
89
+ end
67
90
  else
68
91
  define_method input_definition.name do |index_that_should_be_omitted=nil|
69
92
  if !index_that_should_be_omitted.nil?
@@ -71,6 +94,12 @@ module Brut::FrontEnd::Forms::InputDeclarations
71
94
  end
72
95
  self.input(input_definition.name, index: 0).value
73
96
  end
97
+ define_method "#{input_definition.name}_coerced" do |index_that_should_be_omitted=nil|
98
+ if !index_that_should_be_omitted.nil?
99
+ raise ArgumentError,"#{input_definition.name} is not an array - do not provide an index when accessing its value"
100
+ end
101
+ self.input(input_definition.name, index: 0).typed_value
102
+ end
74
103
  end
75
104
  end
76
105
 
@@ -23,7 +23,8 @@ module Brut::FrontEnd
23
23
  include Brut::FrontEnd::HandlingResults
24
24
  include Brut::Framework::Errors
25
25
 
26
- # You must implement this to accept whatever parameters you need. See {Brut::FrontEnd::RequestContext} for how that works.
26
+ # You must implement this to perform whatever action your handler must perform. Any information from the request would've been given to your initializer. See {Brut::FrontEnd::RequestContext} for how that works.
27
+ #
27
28
  # The type of the return value determines what will happen:
28
29
  #
29
30
  # * Instance of `URI` - browser will redirect to this URI. Typically, you would do this by calling {Brut::FrontEnd::HandlingResults#redirect_to}.
@@ -39,7 +40,7 @@ module Brut::FrontEnd
39
40
  abstract_method!
40
41
  end
41
42
 
42
- # Override this to performa any checks before {#handle} is called. This should
43
+ # Override this to perform any checks before {#handle} is called. This should
43
44
  # return `nil` if {#handle} should proceed to be called. Generally, you don't need to override
44
45
  # this as {#handle} can include the logic. Where this is useful is to share cross-cutting logic
45
46
  # across other handlers.
@@ -48,8 +49,7 @@ module Brut::FrontEnd
48
49
  # {#handle} for what each return value means.
49
50
  def before_handle = nil
50
51
 
51
- # Called by Brut to handle the request. Do not override this. If your handler responds to `before_handle` that is called with the
52
- # same args as you have defined for {#handle}. If `before_handle` returns anything other than `nil`, that value is returned and
52
+ # Called by Brut to handle the request. Do not override this. If `before_handle` returns anything other than `nil`, that value is returned and
53
53
  # should be one of the values documented in {#handle}. If `before_handle` returns `nil`, {#handle} is called and whatever it
54
54
  # returns is returned here.
55
55
  def handle!(**args)
@@ -46,9 +46,9 @@ module Brut
46
46
  "member?" => "include?",
47
47
  },
48
48
  },
49
- "Style/EndlessMethod" => {
50
- "EnforcedStyle" => "allow_single_line",
51
- },
49
+ #"Style/EndlessMethod" => {
50
+ # "EnforcedStyle" => "allow_single_line",
51
+ #},
52
52
  "Style/For" => {
53
53
  "EnforcedStyle" => "each",
54
54
  },
@@ -15,9 +15,10 @@ class Brut::SpecSupport::E2ETestServer
15
15
  # from the given bin dir
16
16
  #
17
17
  # @param [Pathname] bin_dir path to where the app's Brut-provide CLI apps are installed
18
- def initialize(bin_dir:)
19
- @bin_dir = bin_dir
20
- @pid = nil
18
+ def initialize(bin_dir:, start_timeout_seconds: nil)
19
+ @bin_dir = bin_dir
20
+ @pid = nil
21
+ @start_timeout_seconds = start_timeout_seconds || 5
21
22
  end
22
23
 
23
24
  # Starts the server. Returns when the server has started
@@ -66,7 +67,7 @@ private
66
67
 
67
68
  def is_port_open?(ip, port)
68
69
  begin
69
- Timeout::timeout(5) do
70
+ Timeout::timeout(@start_timeout_seconds) do
70
71
  loop do
71
72
  begin
72
73
  logger.debug "Attemping to conenct to '#{ip}' on port '#{port}'"
@@ -47,6 +47,9 @@ class Brut::SpecSupport::Matchers::HaveConstraintViolation
47
47
  if !form.kind_of?(Brut::FrontEnd::Form)
48
48
  raise "#{self.class} only works with forms, not #{form.class}"
49
49
  end
50
+ if field.to_s == ""
51
+ raise "field is required"
52
+ end
50
53
  @form = form
51
54
  @field = field.to_s
52
55
  @key = key.to_s
@@ -112,7 +112,11 @@ private
112
112
  errors.each do
113
113
  $stderr.puts("FATAL Exception: #{it.exception.class}: #{it.exception.message}\n #{it.exception.backtrace.join("\n ")}")
114
114
  end
115
- exit 1
115
+ if errors.any?
116
+ exit 1
117
+ else
118
+ exit 0
119
+ end
116
120
  else
117
121
  errors.each { @queue.unshift(Brut::TUI::Events::Exception.new(it)) }
118
122
  end
@@ -29,7 +29,7 @@ class Brut::TUI::Script::ExecStep < Brut::TUI::Script::Step
29
29
  command: @command,
30
30
  strip_ansi: false,
31
31
  stdout: @stdout,
32
- stderr: @stderr
32
+ stderr: @stderr,
33
33
  })
34
34
  end
35
35
  def run!
data/lib/brut/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Brut
2
2
  # @!visibility private
3
- VERSION = "0.19.2"
3
+ VERSION = "0.20.1"
4
4
  end
@@ -0,0 +1,8 @@
1
+ Data Models go here. Create them with `brut scaffold db_model`
2
+
3
+ ```ruby
4
+ # db/widget.rb
5
+ class DB::Widget < AppDataModel
6
+ end
7
+ ```
8
+
@@ -16,7 +16,7 @@ if ARGV[0] == "help"
16
16
  exit
17
17
  end
18
18
 
19
- ENV["RACK_ENV"] = "development"
19
+ ENV["RACK_ENV"] ||= "development"
20
20
  if ENV["LOG_LEVEL"].to_s == ""
21
21
  ENV["LOG_LEVEL"] = "warn"
22
22
  end
@@ -19,7 +19,7 @@ echo "[ bin/release ] started"
19
19
  echo "[ bin/release ] Creating DB if needed"
20
20
  BRUT_CLI_RAISE_ON_ERROR=true bundle exec brut db create --env=production
21
21
  echo "[ bin/release ] Migrating DB if needed"
22
- BRUT_CLI_RAISE_ON_ERROR=true bundle exec burt db migrate --env=production
22
+ BRUT_CLI_RAISE_ON_ERROR=true bundle exec brut db migrate --env=production
23
23
 
24
24
  # Add additional commands here as needed
25
25
 
@@ -1,8 +1,5 @@
1
1
  # Insert developer-specifilc Bash customizations here.
2
- # This file is not checked into version control, so you
3
- # are safe to export EDITOR=vim and avoid any guff from
4
- # co-workers.
5
-
2
+ # This file is not checked into version control
6
3
  # Sets up a multi-line prompt since the working directory
7
4
  # may be very deep. Customize or change at your leisure.
8
5
  PS1='\[\e[35m\]docker-container\[\e[0m\] - \[\e[37m\]\w\n\[\e[0m\]> '
@@ -48,7 +48,7 @@ setup_playright_build_args() {
48
48
  require_command "grep"
49
49
  require_command "sed"
50
50
 
51
- if [ ! -e "${SCRIPT_DIR}"/Gemfile.lock ]; then
51
+ if [ ! -e "${SCRIPT_DIR}"/../Gemfile.lock ]; then
52
52
  log "Could not find Gemfile.lock, which is needed to determine the playwright-ruby-client version"
53
53
  log "Assuming your app is brand-new, this should be OK"
54
54
  echo "# When this file was created, there was no Gemfile.lock, so" >> "${SCRIPT_DIR}"/build.args
@@ -57,7 +57,7 @@ setup_playright_build_args() {
57
57
  echo "# encouraged to re-run \`dx/build\` to address this issue." >> "${SCRIPT_DIR}"/build.args
58
58
  echo PLAYWRIGHT_VERSION=latest >> "${SCRIPT_DIR}"/build.args
59
59
  else
60
- PLAYWRIGHT_VERSION=$(grep playwright-ruby-client Gemfile.lock | grep '(' | sed 's/^.*(//' | sed 's/).*$//' | grep -v ^=)
60
+ PLAYWRIGHT_VERSION=$(grep playwright-ruby-client $SCRIPT_DIR/../Gemfile.lock | grep '(' | sed 's/^.*(//' | sed 's/).*$//' | grep -v ^=)
61
61
  if [ -z "${PLAYWRIGHT_VERSION}" ]; then
62
62
  log "Could not find precise version of playwright-ruby-client from Gemfile.lock"
63
63
  log "This means that your playwright-ruby-client version and playwright NPM modules may be out of sync and may not work"
@@ -1,4 +1,7 @@
1
+ require "dotenv"
1
2
  ENV["RACK_ENV"] = "test"
3
+ Dotenv.load(".env.test.local", ".env.test")
4
+
2
5
  require_relative "../app/bootstrap"
3
6
  Bootstrap.new.bootstrap!
4
7
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brut
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.19.2
4
+ version: 0.20.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Bryant Copeland
@@ -475,16 +475,16 @@ dependencies:
475
475
  name: yard
476
476
  requirement: !ruby/object:Gem::Requirement
477
477
  requirements:
478
- - - ">="
478
+ - - '='
479
479
  - !ruby/object:Gem::Version
480
- version: '0'
480
+ version: 0.9.37
481
481
  type: :development
482
482
  prerelease: false
483
483
  version_requirements: !ruby/object:Gem::Requirement
484
484
  requirements:
485
- - - ">="
485
+ - - '='
486
486
  - !ruby/object:Gem::Version
487
- version: '0'
487
+ version: 0.9.37
488
488
  description: An opinionated web framework build on web standards
489
489
  email:
490
490
  - davec@thirdtank.com
@@ -548,6 +548,7 @@ files:
548
548
  - lib/brut/cli/commands/compound_command.rb
549
549
  - lib/brut/cli/commands/execution_context.rb
550
550
  - lib/brut/cli/commands/help.rb
551
+ - lib/brut/cli/commands/help_in_markdown.rb
551
552
  - lib/brut/cli/commands/output_error.rb
552
553
  - lib/brut/cli/commands/raise_error.rb
553
554
  - lib/brut/cli/error.rb
@@ -719,7 +720,6 @@ files:
719
720
  - lib/sequel/plugins/find_bang.rb
720
721
  - templates/Base/.dockerignore
721
722
  - templates/Base/.env.development.erb
722
- - templates/Base/.env.development.local
723
723
  - templates/Base/.env.test.erb
724
724
  - templates/Base/.gitignore
725
725
  - templates/Base/.projections.json
@@ -736,6 +736,7 @@ files:
736
736
  - templates/Base/app/src/app.rb.erb
737
737
  - templates/Base/app/src/back_end/data_models/app_data_model.rb
738
738
  - templates/Base/app/src/back_end/data_models/db.rb
739
+ - templates/Base/app/src/back_end/data_models/db/README.md
739
740
  - templates/Base/app/src/back_end/data_models/migrations/20240101130000_citext.rb
740
741
  - templates/Base/app/src/back_end/data_models/seed/seed_data.rb
741
742
  - templates/Base/app/src/front_end/components/app_component.rb
@@ -851,7 +852,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
851
852
  - !ruby/object:Gem::Version
852
853
  version: '0'
853
854
  requirements: []
854
- rubygems_version: 3.7.2
855
+ rubygems_version: 4.0.8
855
856
  specification_version: 4
856
857
  summary: Web Framework Built around Ruby, Web Standards, Simplicity, and Object-Orientation
857
858
  test_files: []
@@ -1,2 +0,0 @@
1
- # Place developer-specific overrides of .env.development in here,
2
- # e.g. API keys needed for local dev. DO NOT CHECK INTO VERSION CONTROL