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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/CODE_OF_CONDUCT.txt +99 -0
- data/Dockerfile.dx +32 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +133 -0
- data/LICENSE.txt +370 -0
- data/README.md +21 -0
- data/Rakefile +1 -0
- data/bin/bin_kit.rb +39 -0
- data/bin/rake +27 -0
- data/bin/setup +145 -0
- data/brut.gemspec +60 -0
- data/docker-compose.dx.yml +16 -0
- data/dx/build +26 -0
- data/dx/docker-compose.env +22 -0
- data/dx/dx.sh.lib +24 -0
- data/dx/exec +58 -0
- data/dx/prune +19 -0
- data/dx/setupkit.sh.lib +144 -0
- data/dx/show-help-in-app-container-then-wait.sh +38 -0
- data/dx/start +30 -0
- data/dx/stop +23 -0
- data/lib/brut/back_end/action.rb +3 -0
- data/lib/brut/back_end/result.rb +46 -0
- data/lib/brut/back_end/seed_data.rb +24 -0
- data/lib/brut/back_end/validator.rb +3 -0
- data/lib/brut/back_end/validators/form_validator.rb +37 -0
- data/lib/brut/cli/app.rb +130 -0
- data/lib/brut/cli/app_runner.rb +219 -0
- data/lib/brut/cli/apps/build_assets.rb +123 -0
- data/lib/brut/cli/apps/db.rb +279 -0
- data/lib/brut/cli/apps/scaffold.rb +256 -0
- data/lib/brut/cli/apps/test.rb +200 -0
- data/lib/brut/cli/command.rb +130 -0
- data/lib/brut/cli/error.rb +12 -0
- data/lib/brut/cli/execution_results.rb +81 -0
- data/lib/brut/cli/executor.rb +37 -0
- data/lib/brut/cli/options.rb +46 -0
- data/lib/brut/cli/output.rb +30 -0
- data/lib/brut/cli.rb +24 -0
- data/lib/brut/factory_bot.rb +20 -0
- data/lib/brut/framework/app.rb +55 -0
- data/lib/brut/framework/config.rb +415 -0
- data/lib/brut/framework/container.rb +190 -0
- data/lib/brut/framework/errors/abstract_method.rb +9 -0
- data/lib/brut/framework/errors/bug.rb +14 -0
- data/lib/brut/framework/errors/not_found.rb +10 -0
- data/lib/brut/framework/errors.rb +14 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +50 -0
- data/lib/brut/framework/mcp.rb +215 -0
- data/lib/brut/framework/project_environment.rb +18 -0
- data/lib/brut/framework.rb +13 -0
- data/lib/brut/front_end/asset_metadata.rb +76 -0
- data/lib/brut/front_end/component.rb +213 -0
- data/lib/brut/front_end/components/form_tag.rb +71 -0
- data/lib/brut/front_end/components/i18n_translations.rb +36 -0
- data/lib/brut/front_end/components/input.rb +13 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +8 -0
- data/lib/brut/front_end/components/inputs/select.rb +100 -0
- data/lib/brut/front_end/components/inputs/text_field.rb +63 -0
- data/lib/brut/front_end/components/inputs/textarea.rb +51 -0
- data/lib/brut/front_end/components/locale_detection.rb +25 -0
- data/lib/brut/front_end/components/page_identifier.rb +13 -0
- data/lib/brut/front_end/components/timestamp.rb +33 -0
- data/lib/brut/front_end/download.rb +23 -0
- data/lib/brut/front_end/flash.rb +57 -0
- data/lib/brut/front_end/form.rb +171 -0
- data/lib/brut/front_end/forms/constraint_violation.rb +39 -0
- data/lib/brut/front_end/forms/input.rb +119 -0
- data/lib/brut/front_end/forms/input_definition.rb +100 -0
- data/lib/brut/front_end/forms/validity_state.rb +36 -0
- data/lib/brut/front_end/handler.rb +48 -0
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +11 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +22 -0
- data/lib/brut/front_end/handling_results.rb +14 -0
- data/lib/brut/front_end/http_method.rb +33 -0
- data/lib/brut/front_end/http_status.rb +16 -0
- data/lib/brut/front_end/middleware.rb +7 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +31 -0
- data/lib/brut/front_end/page.rb +47 -0
- data/lib/brut/front_end/request_context.rb +82 -0
- data/lib/brut/front_end/route_hook.rb +15 -0
- data/lib/brut/front_end/route_hooks/age_flash.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +17 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +46 -0
- data/lib/brut/front_end/route_hooks/locale_detection.rb +24 -0
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +11 -0
- data/lib/brut/front_end/routing.rb +236 -0
- data/lib/brut/front_end/session.rb +56 -0
- data/lib/brut/front_end/template.rb +32 -0
- data/lib/brut/front_end/templates/block_filter.rb +60 -0
- data/lib/brut/front_end/templates/erb_engine.rb +26 -0
- data/lib/brut/front_end/templates/erb_parser.rb +84 -0
- data/lib/brut/front_end/templates/escapable_filter.rb +18 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +40 -0
- data/lib/brut/i18n/base_methods.rb +168 -0
- data/lib/brut/i18n/for_cli.rb +4 -0
- data/lib/brut/i18n/for_html.rb +4 -0
- data/lib/brut/i18n/http_accept_language.rb +68 -0
- data/lib/brut/i18n.rb +6 -0
- data/lib/brut/instrumentation/basic.rb +66 -0
- data/lib/brut/instrumentation/event.rb +19 -0
- data/lib/brut/instrumentation/http_event.rb +5 -0
- data/lib/brut/instrumentation/subscriber.rb +41 -0
- data/lib/brut/instrumentation.rb +11 -0
- data/lib/brut/junk_drawer.rb +88 -0
- data/lib/brut/sinatra_helpers.rb +183 -0
- data/lib/brut/spec_support/component_support.rb +49 -0
- data/lib/brut/spec_support/flash_support.rb +7 -0
- data/lib/brut/spec_support/general_support.rb +18 -0
- data/lib/brut/spec_support/handler_support.rb +7 -0
- data/lib/brut/spec_support/matcher.rb +9 -0
- data/lib/brut/spec_support/matchers/be_a_bug.rb +14 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +14 -0
- data/lib/brut/spec_support/matchers/be_routing_for.rb +11 -0
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +56 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +69 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +20 -0
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +27 -0
- data/lib/brut/spec_support/session_support.rb +3 -0
- data/lib/brut/spec_support.rb +12 -0
- data/lib/brut/version.rb +3 -0
- data/lib/brut.rb +38 -0
- data/lib/sequel/extensions/brut_instrumentation.rb +37 -0
- data/lib/sequel/extensions/brut_migrations.rb +98 -0
- data/lib/sequel/plugins/created_at.rb +14 -0
- data/lib/sequel/plugins/external_id.rb +45 -0
- data/lib/sequel/plugins/find_bang.rb +13 -0
- data/lib/sequel/plugins.rb +3 -0
- 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
|