brut 0.0.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.
Files changed (131) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/CODE_OF_CONDUCT.txt +99 -0
  4. data/Dockerfile.dx +32 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +133 -0
  7. data/LICENSE.txt +370 -0
  8. data/README.md +21 -0
  9. data/Rakefile +1 -0
  10. data/bin/bin_kit.rb +39 -0
  11. data/bin/rake +27 -0
  12. data/bin/setup +145 -0
  13. data/brut.gemspec +60 -0
  14. data/docker-compose.dx.yml +16 -0
  15. data/dx/build +26 -0
  16. data/dx/docker-compose.env +22 -0
  17. data/dx/dx.sh.lib +24 -0
  18. data/dx/exec +58 -0
  19. data/dx/prune +19 -0
  20. data/dx/setupkit.sh.lib +144 -0
  21. data/dx/show-help-in-app-container-then-wait.sh +38 -0
  22. data/dx/start +30 -0
  23. data/dx/stop +23 -0
  24. data/lib/brut/back_end/action.rb +3 -0
  25. data/lib/brut/back_end/result.rb +46 -0
  26. data/lib/brut/back_end/seed_data.rb +24 -0
  27. data/lib/brut/back_end/validator.rb +3 -0
  28. data/lib/brut/back_end/validators/form_validator.rb +37 -0
  29. data/lib/brut/cli/app.rb +130 -0
  30. data/lib/brut/cli/app_runner.rb +219 -0
  31. data/lib/brut/cli/apps/build_assets.rb +123 -0
  32. data/lib/brut/cli/apps/db.rb +279 -0
  33. data/lib/brut/cli/apps/scaffold.rb +256 -0
  34. data/lib/brut/cli/apps/test.rb +200 -0
  35. data/lib/brut/cli/command.rb +130 -0
  36. data/lib/brut/cli/error.rb +12 -0
  37. data/lib/brut/cli/execution_results.rb +81 -0
  38. data/lib/brut/cli/executor.rb +37 -0
  39. data/lib/brut/cli/options.rb +46 -0
  40. data/lib/brut/cli/output.rb +30 -0
  41. data/lib/brut/cli.rb +24 -0
  42. data/lib/brut/factory_bot.rb +20 -0
  43. data/lib/brut/framework/app.rb +55 -0
  44. data/lib/brut/framework/config.rb +415 -0
  45. data/lib/brut/framework/container.rb +190 -0
  46. data/lib/brut/framework/errors/abstract_method.rb +9 -0
  47. data/lib/brut/framework/errors/bug.rb +14 -0
  48. data/lib/brut/framework/errors/not_found.rb +10 -0
  49. data/lib/brut/framework/errors.rb +14 -0
  50. data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
  51. data/lib/brut/framework/mcp.rb +215 -0
  52. data/lib/brut/framework/project_environment.rb +18 -0
  53. data/lib/brut/framework.rb +13 -0
  54. data/lib/brut/front_end/asset_metadata.rb +76 -0
  55. data/lib/brut/front_end/component.rb +213 -0
  56. data/lib/brut/front_end/components/form_tag.rb +71 -0
  57. data/lib/brut/front_end/components/i18n_translations.rb +36 -0
  58. data/lib/brut/front_end/components/input.rb +13 -0
  59. data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
  60. data/lib/brut/front_end/components/inputs/select.rb +100 -0
  61. data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
  62. data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
  63. data/lib/brut/front_end/components/locale_detection.rb +25 -0
  64. data/lib/brut/front_end/components/page_identifier.rb +13 -0
  65. data/lib/brut/front_end/components/timestamp.rb +33 -0
  66. data/lib/brut/front_end/download.rb +23 -0
  67. data/lib/brut/front_end/flash.rb +57 -0
  68. data/lib/brut/front_end/form.rb +171 -0
  69. data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
  70. data/lib/brut/front_end/forms/input.rb +119 -0
  71. data/lib/brut/front_end/forms/input_definition.rb +100 -0
  72. data/lib/brut/front_end/forms/validity_state.rb +36 -0
  73. data/lib/brut/front_end/handler.rb +48 -0
  74. data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
  75. data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
  76. data/lib/brut/front_end/handling_results.rb +14 -0
  77. data/lib/brut/front_end/http_method.rb +33 -0
  78. data/lib/brut/front_end/http_status.rb +16 -0
  79. data/lib/brut/front_end/middleware.rb +7 -0
  80. data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
  81. data/lib/brut/front_end/page.rb +47 -0
  82. data/lib/brut/front_end/request_context.rb +82 -0
  83. data/lib/brut/front_end/route_hook.rb +15 -0
  84. data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
  85. data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
  86. data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
  87. data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
  88. data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
  89. data/lib/brut/front_end/routing.rb +236 -0
  90. data/lib/brut/front_end/session.rb +56 -0
  91. data/lib/brut/front_end/template.rb +32 -0
  92. data/lib/brut/front_end/templates/block_filter.rb +60 -0
  93. data/lib/brut/front_end/templates/erb_engine.rb +26 -0
  94. data/lib/brut/front_end/templates/erb_parser.rb +84 -0
  95. data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
  96. data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
  97. data/lib/brut/i18n/base_methods.rb +168 -0
  98. data/lib/brut/i18n/for_cli.rb +4 -0
  99. data/lib/brut/i18n/for_html.rb +4 -0
  100. data/lib/brut/i18n/http_accept_language.rb +68 -0
  101. data/lib/brut/i18n.rb +6 -0
  102. data/lib/brut/instrumentation/basic.rb +66 -0
  103. data/lib/brut/instrumentation/event.rb +19 -0
  104. data/lib/brut/instrumentation/http_event.rb +5 -0
  105. data/lib/brut/instrumentation/subscriber.rb +41 -0
  106. data/lib/brut/instrumentation.rb +11 -0
  107. data/lib/brut/junk_drawer.rb +88 -0
  108. data/lib/brut/sinatra_helpers.rb +183 -0
  109. data/lib/brut/spec_support/component_support.rb +49 -0
  110. data/lib/brut/spec_support/flash_support.rb +7 -0
  111. data/lib/brut/spec_support/general_support.rb +18 -0
  112. data/lib/brut/spec_support/handler_support.rb +7 -0
  113. data/lib/brut/spec_support/matcher.rb +9 -0
  114. data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
  115. data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
  116. data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
  117. data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
  118. data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
  119. data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
  120. data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
  121. data/lib/brut/spec_support/session_support.rb +3 -0
  122. data/lib/brut/spec_support.rb +12 -0
  123. data/lib/brut/version.rb +3 -0
  124. data/lib/brut.rb +38 -0
  125. data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
  126. data/lib/sequel/extensions/brut_migrations.rb +98 -0
  127. data/lib/sequel/plugins/created_at.rb +14 -0
  128. data/lib/sequel/plugins/external_id.rb +45 -0
  129. data/lib/sequel/plugins/find_bang.rb +13 -0
  130. data/lib/sequel/plugins.rb +3 -0
  131. metadata +484 -0
@@ -0,0 +1,200 @@
1
+ require "shellwords"
2
+ require "brut/cli"
3
+
4
+ class Brut::CLI::Apps::Test < Brut::CLI::App
5
+ description "Run and audit tests of the app"
6
+ default_command :run
7
+
8
+ def before_execute
9
+ ENV["RACK_ENV"] = "test"
10
+ end
11
+
12
+ class Run < Brut::CLI::Command
13
+ description "Run non-e2e tests"
14
+ opts.on("--[no-]rebuild", "If true, test database is rebuilt before tests are run (default false)")
15
+ opts.on("--[no-]rebuild-after", "If true, test database is rebuilt after tests are run (default false)")
16
+ opts.on("--seed SEED", "Set the random seed to allow duplicating a test run")
17
+ args "specs_to_run..."
18
+
19
+ def rspec_command
20
+ parts = [
21
+ "bin/rspec",
22
+ "-I", Brut.container.app_specs_dir,
23
+ "-I", Brut.container.app_src_dir,
24
+ "-I lib/", # not needed when Brut is gemified
25
+ rspec_cli_args,
26
+ "-P \"**/*.spec.rb\"",
27
+ ]
28
+ if options.seed
29
+ parts << "--seed #{options.seed}"
30
+ end
31
+ parts.join(" ")
32
+ end
33
+
34
+ def rspec_cli_args = "--tag ~e2e"
35
+
36
+ def rebuild_by_default? = false
37
+ def rebuild_after_by_default? = false
38
+
39
+ def execute
40
+ Brut.container.sequel_db_handle.disconnect
41
+ if options.rebuild?(default: rebuild_by_default?)
42
+ out.puts "Rebuilding test database schema"
43
+ system! "bin/db rebuild --env=test"
44
+ end
45
+ if args.empty?
46
+ out.puts "Running all tests"
47
+ system! "#{rspec_command} #{Brut.container.app_specs_dir}/"
48
+ else
49
+ test_args = args.map { |_|
50
+ '"' + Shellwords.escape(_) + '"'
51
+ }.join(" ")
52
+ system! "#{rspec_command} #{test_args}"
53
+ end
54
+ if options.rebuild_after?(default: rebuild_after_by_default?)
55
+ out.puts "Re-Rebuilding test database schema"
56
+ system! "bin/db rebuild --env=test"
57
+ end
58
+ 0
59
+ end
60
+ end
61
+ class E2e < Run
62
+ description "Run e2e tests"
63
+ opts.on("--[no-]rebuild", "If true, test database is rebuilt before tests are run (default true)")
64
+ opts.on("--[no-]rebuild-after", "If true, test database is rebuilt after tests are run (default true)")
65
+ opts.on("--seed SEED", "Set the random seed to allow duplicating a test run")
66
+ args "specs_to_run..."
67
+
68
+ def rspec_cli_args = "--tag e2e"
69
+ def rebuild_by_default? = true
70
+ def rebuild_after_by_default? = true
71
+ end
72
+ class JS < Brut::CLI::Command
73
+ description "Run JavaScript unit tests"
74
+ opts.on("--[no-]build-assets","Build all assets before running the tests")
75
+ def execute
76
+ if options.build_assets?
77
+ system!({ "RACK_ENV" => "test" }, "bin/build-assets")
78
+ end
79
+ system!({ "NODE_DISABLE_COLORS" => "1" },"npx mocha #{Brut.container.js_specs_dir} --no-color --extension 'spec.js' --recursive")
80
+ 0
81
+ end
82
+ end
83
+ class Audit < Brut::CLI::Command
84
+ description "Audits all of the app's classes to see if test files exist"
85
+
86
+ opts.on("--ignore PATH[,PATH]","Ignore any files in these paths, relative to app root",Array)
87
+ opts.on("--type TYPE","Only audit this type of file")
88
+ opts.on("--show-scaffold","If set, shows the command to scaffold the missing tests")
89
+
90
+ def execute
91
+ app_files = Dir["#{Brut.container.app_src_dir}/**/*"].select { |file|
92
+ if file.start_with?(Brut.container.app_specs_dir.to_s)
93
+ false
94
+ elsif Pathname(file).extname != ".rb"
95
+ false
96
+ else
97
+ true
98
+ end
99
+ }
100
+ audit = app_files.map { |file|
101
+ Pathname(file)
102
+ }.select { |pathname|
103
+ relative_to_root = pathname.relative_path_from(Brut.container.project_root)
104
+ if options.ignore(default: []).include?(relative_to_root.to_s)
105
+ false
106
+ else
107
+ true
108
+ end
109
+ }.map { |pathname|
110
+ relative = pathname.relative_path_from(Brut.container.app_src_dir)
111
+ test_file = Brut.container.project_root / "specs" / relative.dirname / "#{relative.basename(relative.extname)}.spec.rb"
112
+ hash = {
113
+ source_file: pathname.relative_path_from(Brut.container.project_root),
114
+ test_file: test_file,
115
+ test_expected: true,
116
+ }
117
+ if pathname.fnmatch?( (Brut.container.components_src_dir / "**").to_s )
118
+ if pathname.basename.to_s == "app_component.rb"
119
+ hash[:type] = :infrastructure
120
+ hash[:test_expected] = false
121
+ else
122
+ hash[:type] = :component
123
+ end
124
+ elsif pathname.fnmatch?( (Brut.container.forms_src_dir / "**").to_s )
125
+ if pathname.basename.to_s == "app_form.rb"
126
+ hash[:type] = :infrastructure
127
+ else
128
+ hash[:type] = :form
129
+ end
130
+ hash[:test_expected] = false
131
+ elsif pathname.fnmatch?( (Brut.container.handlers_src_dir / "**").to_s )
132
+ if pathname.basename.to_s == "app_handler.rb"
133
+ hash[:type] = :infrastructure
134
+ hash[:test_expected] = false
135
+ else
136
+ hash[:type] = :handler
137
+ end
138
+ elsif pathname.fnmatch?( (Brut.container.pages_src_dir / "**").to_s )
139
+ if pathname.basename.to_s == "app_page.rb"
140
+ hash[:type] = :infrastructure
141
+ hash[:test_expected] = false
142
+ else
143
+ hash[:type] = :page
144
+ end
145
+ elsif pathname.fnmatch?( (Brut.container.back_end_src_dir / "**").to_s )
146
+ type = pathname.parent.basename.to_s
147
+ if pathname.basename.to_s == "app_#{type}.rb" ||
148
+ type == "back_end" ||
149
+ type == "seed" ||
150
+ type == "migrations" ||
151
+ pathname.basename.to_s == "app_data_model.rb" ||
152
+ pathname.basename.to_s == "db.rb"
153
+
154
+ hash[:type] = :infrastructure
155
+ hash[:test_expected] = false
156
+ else
157
+ hash[:type] = type.to_sym
158
+ end
159
+ else
160
+ hash[:type] = :other
161
+ hash[:test_expected] = false
162
+ end
163
+ hash
164
+ }.compact
165
+
166
+ files_missing = []
167
+ printed_header = false
168
+ audit.each do |file_audit|
169
+ if !file_audit[:test_file].exist?
170
+ if options.audit_type.nil? || file_audit[:type] == options.audit_type
171
+ if file_audit[:test_expected]
172
+ files_missing << file_audit[:source_file]
173
+ if !printed_header
174
+ out.puts "These files are missing tests:"
175
+ out.puts ""
176
+ printed_header = true
177
+ end
178
+ out.puts "#{file_audit[:type].to_s.ljust(15)} - #{file_audit[:source_file]}"
179
+ end
180
+ end
181
+ end
182
+ end
183
+ if files_missing.empty?
184
+ out.puts "All tests exists!"
185
+ 0
186
+ else
187
+ if options.show_scaffold?
188
+ out.puts
189
+ files_missing_args = files_missing.map { |file|
190
+ ' "' + Shellwords.escape(file.to_s) + '"'
191
+ }.join(" \\\n")
192
+
193
+ out.puts "Run this command to generate empty tests:\n\nbin/scaffold test \\\n#{files_missing_args}"
194
+ end
195
+ 1
196
+ end
197
+ end
198
+ end
199
+ end
200
+
@@ -0,0 +1,130 @@
1
+ require "optparse"
2
+ class Brut::CLI::Command
3
+ include Brut::CLI::ExecutionResults
4
+ include Brut::I18n::ForCLI
5
+
6
+ def self.description(new_description=nil)
7
+ if new_description.nil?
8
+ return @description.to_s
9
+ else
10
+ @description = new_description
11
+ end
12
+ end
13
+ def self.detailed_description(new_description=nil)
14
+ if new_description.nil?
15
+ if @detailed_description.nil?
16
+ return @detailed_description
17
+ end
18
+ return @detailed_description.to_s
19
+ else
20
+ @detailed_description = new_description
21
+ end
22
+ end
23
+ def self.args(new_args=nil)
24
+ if new_args.nil?
25
+ return @args.to_s
26
+ else
27
+ @args = new_args
28
+ end
29
+ end
30
+ def self.command_name = RichString.new(self.name.split(/::/).last).underscorized
31
+ def self.name_matches?(string)
32
+ self.command_name == string || self.command_name.to_s.gsub(/_/,"-") == string
33
+ end
34
+ def self.opts
35
+ self.option_parser
36
+ end
37
+ def self.option_parser
38
+ @option_parser ||= OptionParser.new do |opts|
39
+ opts.banner = "%{app} %{global_options} #{command_name} %{command_options} %{args}"
40
+ end
41
+ end
42
+
43
+ def self.requires_project_env(default: "development")
44
+ default_message = if default.nil?
45
+ ""
46
+ else
47
+ " (default '#{default}')"
48
+ end
49
+ opts.on("--env=ENVIRONMENT","Project environment#{default_message}")
50
+ @default_env = default
51
+ @requires_project_env = true
52
+ end
53
+
54
+ def self.default_env = @default_env
55
+ def self.requires_project_env? = @requires_project_env
56
+
57
+ def initialize(command_options:,global_options:, args:,out:,err:,executor:)
58
+ @command_options = command_options
59
+ @global_options = global_options
60
+ @args = args
61
+ @out = out
62
+ @err = err
63
+ @executor = executor
64
+ if self.class.default_env
65
+ @command_options.set_default(:env,self.class.default_env)
66
+ end
67
+ end
68
+
69
+ def system!(*args) = @executor.system!(*args)
70
+
71
+ def delegate_to_commands(*command_klasses)
72
+ result = nil
73
+ command_klasses.each do |command_klass|
74
+ result = delegate_to_command(command_klass)
75
+ if !result.ok?
76
+ err.puts "#{command_klass.command_name} failed"
77
+ return result
78
+ end
79
+ end
80
+ result
81
+ end
82
+
83
+ def delegate_to_command(command_klass)
84
+ command = command_klass.new(command_options: options, global_options:, args:, out:, err:, executor: @executor)
85
+ as_execution_result(command.execute)
86
+ end
87
+
88
+ def execute
89
+ raise Brut::Framework::Errors::AbstractMethod
90
+ end
91
+
92
+ def before_execute
93
+ end
94
+
95
+ def set_env_if_needed
96
+ if self.class.requires_project_env?
97
+ ENV["RACK_ENV"] = options.env
98
+ end
99
+ end
100
+
101
+ def handle_bootstrap_exception(ex)
102
+ raise ex
103
+ end
104
+
105
+ def bootstrap!(project_root:, configure_only:)
106
+ require "bundler"
107
+ Bundler.require(:default, ENV["RACK_ENV"].to_sym)
108
+ if configure_only
109
+ require "#{project_root}/app/pre_boot"
110
+ Brut::Framework.new(app: ::App.new)
111
+ else
112
+ require "#{project_root}/app/boot"
113
+ end
114
+ continue_execution
115
+ end
116
+
117
+ private
118
+
119
+ def options = @command_options
120
+ def global_options = @global_options
121
+ def args = @args
122
+ def out = @out
123
+ def err = @err
124
+
125
+ def puts(...)
126
+ warn("Your CLI apps should use out and err to produce terminal output, not puts", uplevel: 1)
127
+ Kernel.puts(...)
128
+ end
129
+
130
+ end
@@ -0,0 +1,12 @@
1
+ # Marker that means an expected error happens and
2
+ # we don't need to show the stack trace
3
+ class Brut::CLI::Error < StandardError
4
+ end
5
+ class Brut::CLI::SystemExecError < Brut::CLI::Error
6
+ attr_reader :command,:exit_status
7
+ def initialize(command,exit_status)
8
+ super("#{command} failed - exited #{exit_status}")
9
+ @command = command
10
+ @exit_status = exit_status
11
+ end
12
+ end
@@ -0,0 +1,81 @@
1
+ module Brut
2
+ module CLI
3
+ module ExecutionResults
4
+ class Result
5
+ attr_reader :message
6
+ def initialize(exit_status:,message:nil)
7
+ @exit_status = exit_status
8
+ @message = message
9
+ end
10
+
11
+ # Returns true if execution internal to the command should stop
12
+ def stop? = @exit_status != 0
13
+ # Returns true if the execution of the command succeeded or didn't error
14
+ def ok? = @exit_status == 0
15
+ # Returns the exit status to use for the CLI
16
+ def to_i = @exit_status
17
+ def show_usage? = false
18
+ end
19
+
20
+ # Stop execution, even though nothing is wrong
21
+ class Stop < Result
22
+ def initialize
23
+ super(exit_status: 0)
24
+ end
25
+ def stop? = true
26
+ end
27
+
28
+ class ShowCLIUsage < Stop
29
+ attr_reader :command_klass
30
+ def initialize(command_klass:)
31
+ super()
32
+ @command_klass = command_klass
33
+ end
34
+ def show_usage? = true
35
+ end
36
+
37
+ # Continue execution
38
+ class Continue < Result
39
+ def initialize
40
+ super(exit_status: 0)
41
+ end
42
+ end
43
+
44
+ # Abort execution immediately
45
+ class Abort < Result
46
+ def initialize(exit_status:1,message:nil)
47
+ if exit_status == 0
48
+ raise ArgumentError,"Do not use Abort for a zero exit status"
49
+ end
50
+ super(exit_status:,message:)
51
+ end
52
+ end
53
+ class CLIUsageError < Abort
54
+ def initialize(message:)
55
+ super(message:,exit_status:65)
56
+ end
57
+ def show_usage? = true
58
+ end
59
+
60
+ def stop_execution = Stop.new
61
+ def continue_execution = Continue.new
62
+ def abort_execution(message,exit_status:1) = Abort.new(message:,exit_status:)
63
+ def cli_usage_error(message) = CLIUsageError.new(message:)
64
+ def show_cli_usage(command_klass=nil) = ShowCLIUsage.new(command_klass:)
65
+
66
+ def as_execution_result(exit_status_or_execution_result)
67
+ if exit_status_or_execution_result.kind_of?(Numeric) || exit_status_or_execution_result.nil?
68
+ Result.new(exit_status: exit_status_or_execution_result.to_i)
69
+ elsif exit_status_or_execution_result == true
70
+ Result.new(exit_status: 0)
71
+ elsif exit_status_or_execution_result == false
72
+ Abort.new
73
+ elsif exit_status_or_execution_result.kind_of?(Result)
74
+ exit_status_or_execution_result
75
+ else
76
+ raise ArgumentError,"Your method returned a #{exit_status_or_execution_result.class} when it should return an exit status or one of the methods from ExecutionResults"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,37 @@
1
+ require "open3"
2
+ class Brut::CLI::Executor
3
+ def initialize(out:,err:)
4
+ @out = out
5
+ @err = err
6
+ end
7
+ def system!(*args)
8
+ @out.puts "Executing #{args}"
9
+ wait_thread = Open3.popen3(*args) do |_stdin,stdout,stderr,wait_thread|
10
+ o = stdout.read_nonblock(10, exception: false)
11
+ e = stderr.read_nonblock(10, exception: false)
12
+ while o || e
13
+ if o
14
+ if o != :wait_readable
15
+ @out.print o
16
+ @out.flush
17
+ end
18
+ o = stdout.read_nonblock(10, exception: false)
19
+ end
20
+ if e
21
+ if e != :wait_readable
22
+ @err.print e
23
+ @err.flush
24
+ end
25
+ e = stderr.read_nonblock(10, exception: false)
26
+ end
27
+ end
28
+ wait_thread
29
+ end
30
+ if wait_thread.value.success?
31
+ @out.puts "#{args} succeeded"
32
+ else
33
+ raise Brut::CLI::SystemExecError.new(*args,wait_thread.value.exitstatus)
34
+ end
35
+ true
36
+ end
37
+ end
@@ -0,0 +1,46 @@
1
+ # Convienience module to put into Hash to allow options
2
+ # parsed to be a bit more accessible
3
+ class Brut::CLI::Options
4
+ def initialize(parsed_options)
5
+ @parsed_options = parsed_options
6
+ @defaults = {}
7
+ end
8
+
9
+ def to_h = @parsed_options
10
+
11
+ def [](key) = @parsed_options[key]
12
+ def key?(key) = @parsed_options.key?(key)
13
+ def set_default(sym,default_value)
14
+ @defaults[sym] = default_value
15
+ end
16
+
17
+ def method_missing(sym,*args,&block)
18
+ boolean = false
19
+ if sym.to_s =~ /\?$/
20
+ sym = sym.to_s[0..-2].to_sym
21
+ boolean = true
22
+ end
23
+
24
+ sym_underscore = sym.to_s.gsub(/\-/,"_").to_sym
25
+ sym_dash = sym.to_s.gsub(/_/,"-").to_sym
26
+
27
+ value = if self.key?(sym_underscore)
28
+ self[sym_underscore]
29
+ elsif self.key?(sym_dash)
30
+ self[sym_dash]
31
+ elsif args[0].kind_of?(Hash) && args[0].key?(:default)
32
+ return args[0][:default]
33
+ elsif @defaults.key?(sym_underscore)
34
+ @defaults[sym_underscore]
35
+ elsif @defaults.key?(sym_dash)
36
+ @defaults[sym_dash]
37
+ else
38
+ nil
39
+ end
40
+ if boolean
41
+ !!value
42
+ else
43
+ value
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ class Brut::CLI::Output
2
+ def initialize(io:, prefix:)
3
+ @io = io
4
+ @prefix = prefix
5
+ @sync_status = @io.sync
6
+ end
7
+
8
+ def puts_no_prefix(*objects)
9
+ @io.puts(*objects)
10
+ end
11
+
12
+ def puts(*objects)
13
+ if objects.empty?
14
+ objects << ""
15
+ end
16
+ objects.each do |object|
17
+ @io.puts(@prefix + object.to_s)
18
+ end
19
+ nil
20
+ end
21
+
22
+ def print(*objects)
23
+ @io.print(*objects)
24
+ end
25
+
26
+ def flush
27
+ @io.flush
28
+ self
29
+ end
30
+ end
data/lib/brut/cli.rb ADDED
@@ -0,0 +1,24 @@
1
+ module Brut
2
+ module CLI
3
+
4
+ def self.app(app_klass, project_root:)
5
+ Brut::CLI::AppRunner.new(app_klass:,project_root:).run!
6
+ end
7
+ autoload(:App, "brut/cli/app")
8
+ autoload(:Command, "brut/cli/command")
9
+ autoload(:Error, "brut/cli/error")
10
+ autoload(:SystemExecError, "brut/cli/error")
11
+ autoload(:ExecutionResults, "brut/cli/execution_results")
12
+ autoload(:Options, "brut/cli/options")
13
+ autoload(:Output, "brut/cli/output")
14
+ autoload(:Executor, "brut/cli/executor")
15
+ autoload(:AppRunner, "brut/cli/app_runner")
16
+ module Apps
17
+ autoload(:DB,"brut/cli/apps/db")
18
+ autoload(:DB,"brut/cli/apps/test")
19
+ autoload(:DB,"brut/cli/apps/build_assets")
20
+ autoload(:DB,"brut/cli/apps/scaffold")
21
+ end
22
+ end
23
+ end
24
+ require_relative "i18n"
@@ -0,0 +1,20 @@
1
+ # Because FactoryBot 6.4.6 has a bug where it is not properly
2
+ # requiring active support, active supporot must be required first,
3
+ # then factory bot. When 6.4.7 is released, this can be removed. See Gemfile
4
+ require "active_support"
5
+ require "factory_bot"
6
+ require "faker"
7
+
8
+ class Brut::FactoryBot
9
+ def setup!
10
+ Faker::Config.locale = :en
11
+ FactoryBot.definition_file_paths = [
12
+ Brut.container.app_specs_dir / "factories"
13
+ ]
14
+ FactoryBot.define do
15
+ to_create { |instance| instance.save }
16
+ end
17
+ FactoryBot.find_definitions
18
+
19
+ end
20
+ end
@@ -0,0 +1,55 @@
1
+ # An "App" in Brut paralance is the collection of source code and configure that is needed to operate
2
+ # a website. This includes everything needed to serve HTTP requests, but also includes ancillary
3
+ # tasks and any related files required for the app to exist and function.
4
+ class Brut::Framework::App
5
+
6
+ # An identifier for this app that can be used as a hostname
7
+ def id = raise "Subclass must implement"
8
+
9
+ # An identifier for the app's 'organization' that can be used as a hostname.
10
+ # This isn't relevant in all contexts, but is useful for deploys or other
11
+ # actions where an app needs to exist inside some organizational context.
12
+ def organization = id
13
+
14
+ def self.routes(&block)
15
+ @routes_blocks ||= []
16
+ if block.nil?
17
+ @routes_blocks
18
+ else
19
+ @routes_blocks << block
20
+ end
21
+ end
22
+ def self.middleware(middleware=nil,*args,&block)
23
+ @middlewares ||= []
24
+ if middleware.nil? && args.empty? && block.nil?
25
+ @middlewares
26
+ else
27
+ @middlewares << [ middleware, args, block ]
28
+ end
29
+ end
30
+ def self.before(klass_name=nil)
31
+ @before ||= []
32
+ if klass_name.nil?
33
+ @before
34
+ else
35
+ @before << klass_name
36
+ end
37
+ end
38
+ def self.after(klass_name=nil)
39
+ @after ||= []
40
+ if klass_name.nil?
41
+ @after
42
+ else
43
+ @after << klass_name
44
+ end
45
+ end
46
+
47
+ # Override this to set up any runtime connections or execute other pre-flight
48
+ # code required *after* Brut has been set up and started. You can rely on the
49
+ # database being available. Any attempts to override configuration values
50
+ # may not succeed. This is called after the framework has booted, but before
51
+ # your apps routes are set up.
52
+ def boot!
53
+ end
54
+
55
+ end