brut 0.0.1

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