brut 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|