brut 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +6 -0
- data/Gemfile.lock +66 -1
- data/README.md +36 -0
- data/Rakefile +22 -0
- data/brut.gemspec +7 -0
- data/doc-src/architecture.md +102 -0
- data/doc-src/assets.md +98 -0
- data/doc-src/forms.md +214 -0
- data/doc-src/handlers.md +83 -0
- data/doc-src/javascript.md +265 -0
- data/doc-src/keyword-injection.md +183 -0
- data/doc-src/pages.md +210 -0
- data/doc-src/route-hooks.md +59 -0
- data/docs-todo.md +32 -0
- data/lib/brut/back_end/seed_data.rb +5 -1
- data/lib/brut/back_end/validator.rb +1 -1
- data/lib/brut/back_end/validators/form_validator.rb +31 -6
- data/lib/brut/cli/app.rb +100 -4
- data/lib/brut/cli/app_runner.rb +38 -5
- data/lib/brut/cli/apps/build_assets.rb +4 -6
- data/lib/brut/cli/apps/db.rb +2 -7
- data/lib/brut/cli/apps/scaffold.rb +413 -7
- data/lib/brut/cli/apps/test.rb +14 -1
- data/lib/brut/cli/command.rb +141 -6
- data/lib/brut/cli/error.rb +30 -3
- data/lib/brut/cli/execution_results.rb +44 -6
- data/lib/brut/cli/executor.rb +21 -1
- data/lib/brut/cli/options.rb +29 -2
- data/lib/brut/cli/output.rb +47 -0
- data/lib/brut/cli.rb +26 -0
- data/lib/brut/factory_bot.rb +2 -0
- data/lib/brut/framework/app.rb +38 -5
- data/lib/brut/framework/config.rb +97 -54
- data/lib/brut/framework/container.rb +97 -33
- data/lib/brut/framework/errors/abstract_method.rb +3 -7
- data/lib/brut/framework/errors/missing_parameter.rb +12 -0
- data/lib/brut/framework/errors/no_class_for_path.rb +25 -0
- data/lib/brut/framework/errors/not_found.rb +12 -2
- data/lib/brut/framework/errors/not_implemented.rb +14 -0
- data/lib/brut/framework/errors.rb +18 -0
- data/lib/brut/framework/fussy_type_enforcement.rb +17 -12
- data/lib/brut/framework/mcp.rb +106 -49
- data/lib/brut/framework/project_environment.rb +9 -0
- data/lib/brut/framework.rb +1 -0
- data/lib/brut/front_end/asset_metadata.rb +7 -1
- data/lib/brut/front_end/component.rb +129 -38
- data/lib/brut/front_end/components/constraint_violations.rb +57 -0
- data/lib/brut/front_end/components/form_tag.rb +23 -32
- data/lib/brut/front_end/components/i18n_translations.rb +34 -1
- data/lib/brut/front_end/components/input.rb +3 -0
- data/lib/brut/front_end/components/inputs/csrf_token.rb +2 -0
- data/lib/brut/front_end/components/inputs/radio_button.rb +41 -0
- data/lib/brut/front_end/components/inputs/select.rb +26 -2
- data/lib/brut/front_end/components/inputs/text_field.rb +27 -5
- data/lib/brut/front_end/components/inputs/textarea.rb +24 -4
- data/lib/brut/front_end/components/locale_detection.rb +8 -1
- data/lib/brut/front_end/components/page_identifier.rb +2 -0
- data/lib/brut/front_end/components/time.rb +95 -0
- data/lib/brut/front_end/components/traceparent.rb +22 -0
- data/lib/brut/front_end/download.rb +11 -0
- data/lib/brut/front_end/flash.rb +32 -0
- data/lib/brut/front_end/form.rb +109 -106
- data/lib/brut/front_end/forms/constraint_violation.rb +16 -11
- data/lib/brut/front_end/forms/input.rb +30 -42
- data/lib/brut/front_end/forms/input_declarations.rb +90 -0
- data/lib/brut/front_end/forms/input_definition.rb +45 -30
- data/lib/brut/front_end/forms/radio_button_group_input.rb +46 -0
- data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +29 -0
- data/lib/brut/front_end/forms/select_input.rb +47 -0
- data/lib/brut/front_end/forms/select_input_definition.rb +27 -0
- data/lib/brut/front_end/forms/validity_state.rb +23 -9
- data/lib/brut/front_end/handler.rb +27 -8
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +4 -2
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +99 -0
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -4
- data/lib/brut/front_end/handlers/missing_handler.rb +9 -0
- data/lib/brut/front_end/handling_results.rb +14 -4
- data/lib/brut/front_end/http_method.rb +13 -1
- data/lib/brut/front_end/http_status.rb +10 -0
- data/lib/brut/front_end/layouts/_internal.html.erb +68 -0
- data/lib/brut/front_end/middleware.rb +5 -0
- data/lib/brut/front_end/middlewares/annotate_brut_owned_paths.rb +15 -0
- data/lib/brut/front_end/middlewares/favicon.rb +16 -0
- data/lib/brut/front_end/middlewares/open_telemetry_span.rb +12 -0
- data/lib/brut/front_end/middlewares/reload_app.rb +27 -9
- data/lib/brut/front_end/page.rb +50 -11
- data/lib/brut/front_end/pages/_missing_page.html.erb +17 -0
- data/lib/brut/front_end/pages/missing_page.rb +36 -0
- data/lib/brut/front_end/request_context.rb +117 -8
- data/lib/brut/front_end/route_hook.rb +43 -1
- data/lib/brut/front_end/route_hooks/age_flash.rb +3 -2
- data/lib/brut/front_end/route_hooks/csp_no_inline_scripts.rb +8 -0
- data/lib/brut/front_end/route_hooks/csp_no_inline_styles_or_scripts.rb +18 -10
- data/lib/brut/front_end/route_hooks/locale_detection.rb +11 -5
- data/lib/brut/front_end/route_hooks/setup_request_context.rb +7 -4
- data/lib/brut/front_end/routing.rb +138 -31
- data/lib/brut/front_end/session.rb +86 -7
- data/lib/brut/front_end/template.rb +17 -2
- data/lib/brut/front_end/templates/block_filter.rb +4 -3
- data/lib/brut/front_end/templates/erb_parser.rb +1 -1
- data/lib/brut/front_end/templates/escapable_filter.rb +2 -0
- data/lib/brut/front_end/templates/html_safe_string.rb +30 -2
- data/lib/brut/front_end/templates/locator.rb +60 -0
- data/lib/brut/i18n/base_methods.rb +4 -0
- data/lib/brut/i18n.rb +1 -0
- data/lib/brut/instrumentation/logger_span_exporter.rb +75 -0
- data/lib/brut/instrumentation/open_telemetry.rb +107 -0
- data/lib/brut/instrumentation.rb +4 -6
- data/lib/brut/junk_drawer.rb +54 -4
- data/lib/brut/sinatra_helpers.rb +42 -38
- data/lib/brut/spec_support/clock_support.rb +6 -0
- data/lib/brut/spec_support/component_support.rb +53 -26
- data/lib/brut/spec_support/e2e_test_server.rb +82 -0
- data/lib/brut/spec_support/enhanced_node.rb +45 -0
- data/lib/brut/spec_support/general_support.rb +14 -3
- data/lib/brut/spec_support/handler_support.rb +2 -0
- data/lib/brut/spec_support/matcher.rb +6 -3
- data/lib/brut/spec_support/matchers/be_page_for.rb +3 -2
- data/lib/brut/spec_support/matchers/have_constraint_violation.rb +22 -9
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +9 -2
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +24 -0
- data/lib/brut/spec_support/matchers/have_link_to.rb +14 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +30 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +4 -11
- data/lib/brut/spec_support/matchers/have_returned_http_status.rb +16 -8
- data/lib/brut/spec_support/rspec_setup.rb +182 -0
- data/lib/brut/spec_support.rb +8 -3
- data/lib/brut/version.rb +2 -1
- data/lib/brut.rb +28 -5
- data/lib/sequel/extensions/brut_instrumentation.rb +5 -27
- data/lib/sequel/extensions/brut_migrations.rb +18 -8
- data/lib/sequel/plugins/created_at.rb +2 -0
- data/lib/sequel/plugins/external_id.rb +39 -1
- data/lib/sequel/plugins/find_bang.rb +4 -1
- metadata +140 -13
- data/lib/brut/back_end/action.rb +0 -3
- data/lib/brut/back_end/result.rb +0 -46
- data/lib/brut/front_end/components/timestamp.rb +0 -33
- data/lib/brut/instrumentation/basic.rb +0 -66
- data/lib/brut/instrumentation/event.rb +0 -19
- data/lib/brut/instrumentation/http_event.rb +0 -5
- data/lib/brut/instrumentation/subscriber.rb +0 -41
@@ -8,13 +8,13 @@ class Brut::CLI::Apps::BuildAssets < Brut::CLI::App
|
|
8
8
|
requires_project_env
|
9
9
|
default_command :all
|
10
10
|
configure_only!
|
11
|
+
opts.on("--[no-]clean", "If set, any old files from previous runs are deleted. If omitted, is false in production and true otherwise")
|
11
12
|
|
12
13
|
class All < Brut::CLI::Command
|
13
14
|
description "Build all assets"
|
14
|
-
opts.on("--[no-]clean","If set the metadata file used to map the files to their hashed values is deleted before assets are built")
|
15
15
|
|
16
16
|
def execute
|
17
|
-
if
|
17
|
+
if global_options.clean?(default: global_options.env != "production")
|
18
18
|
asset_metadata_file = Brut.container.asset_metadata_file
|
19
19
|
out.puts "Removing #{asset_metadata_file}"
|
20
20
|
FileUtils.rm_f(asset_metadata_file)
|
@@ -40,7 +40,6 @@ This is to ensure that any images your code references will end up in the public
|
|
40
40
|
|
41
41
|
class CSS < Brut::CLI::Command
|
42
42
|
description "Builds a single CSS file suitable for sending to the browser"
|
43
|
-
opts.on("--clean","If set, any .css files hanging around from a prevous build are deleted. Not recommended in production environments")
|
44
43
|
|
45
44
|
detailed_description %{
|
46
45
|
This produces a hashed file in every environment, in order to keep environments consistent and reduce differences. If your CSS file references images, fonts, or other assets via url() or other CSS functions, those files will be hashed and copied into the output directory where CSS is served.
|
@@ -54,7 +53,7 @@ This is to ensure that any images your code references will end up in the public
|
|
54
53
|
esbuild_metafile = Brut.container.tmp_dir / "build-css-meta.json"
|
55
54
|
asset_metadata_file = Brut.container.asset_metadata_file
|
56
55
|
|
57
|
-
if
|
56
|
+
if global_options.clean?(default: global_options.env != "production")
|
58
57
|
out.puts "Cleaning old CSS files from #{Brut.container.css_bundle_output_dir}"
|
59
58
|
Dir[Brut.container.css_bundle_output_dir / "*.*"].each do |file|
|
60
59
|
if File.file?(file)
|
@@ -80,7 +79,6 @@ This is to ensure that any images your code references will end up in the public
|
|
80
79
|
end
|
81
80
|
class JS < Brut::CLI::Command
|
82
81
|
description "Builds and bundles JavaScript destined for the browser"
|
83
|
-
opts.on("--clean","If set, any .js files hanging around from a prevous build are deleted. Not recommended in production environments")
|
84
82
|
opts.on("--output-file=FILE","Bundle to create that will be sent to the browser, relative to the JS public folder. Default is app.js")
|
85
83
|
opts.on("--source-file=FILE","Entry point used to create the bundle, relative to the source JS folder. Default is index.js")
|
86
84
|
|
@@ -91,7 +89,7 @@ This is to ensure that any images your code references will end up in the public
|
|
91
89
|
asset_metadata_file = Brut.container.asset_metadata_file
|
92
90
|
|
93
91
|
name_with_hash_regexp = /app\/public\/(?<path>.+)\/(?<name>.+)\-(?<hash>.+)\.js/
|
94
|
-
if
|
92
|
+
if global_options.clean?(default: global_options.env != "production")
|
95
93
|
out.puts "Cleaning old JS files from #{Brut.container.js_bundle_output_dir}"
|
96
94
|
Dir[Brut.container.js_bundle_output_dir / "*.*"].each do |file|
|
97
95
|
if File.file?(file)
|
data/lib/brut/cli/apps/db.rb
CHANGED
@@ -32,7 +32,7 @@ class Brut::CLI::Apps::DB < Brut::CLI::App
|
|
32
32
|
Dir["#{seeds_dir}/*.rb"].each do |file|
|
33
33
|
require file
|
34
34
|
end
|
35
|
-
seed_data = Brut::
|
35
|
+
seed_data = Brut::BackEnd::SeedData.new
|
36
36
|
seed_data.setup!
|
37
37
|
seed_data.load_seeds!
|
38
38
|
0
|
@@ -43,7 +43,6 @@ class Brut::CLI::Apps::DB < Brut::CLI::App
|
|
43
43
|
|
44
44
|
class Rebuild < Brut::CLI::Command
|
45
45
|
description "Drop, re-create, and run migrations, effecitvely rebuilding the entire database"
|
46
|
-
opts.on("--[no-]seeds","Load seed data after applying migrations")
|
47
46
|
|
48
47
|
requires_project_env default: "development"
|
49
48
|
|
@@ -63,11 +62,7 @@ class Brut::CLI::Apps::DB < Brut::CLI::App
|
|
63
62
|
end
|
64
63
|
|
65
64
|
def execute
|
66
|
-
|
67
|
-
if result.ok? && options.seeds?
|
68
|
-
result = delegate_to_command(Seed)
|
69
|
-
end
|
70
|
-
result
|
65
|
+
delegate_to_commands(Drop, Create, Migrate)
|
71
66
|
end
|
72
67
|
end
|
73
68
|
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require "prism"
|
1
2
|
require "brut/cli"
|
2
3
|
|
3
4
|
class Brut::CLI::Apps::Scaffold < Brut::CLI::App
|
@@ -5,12 +6,13 @@ class Brut::CLI::Apps::Scaffold < Brut::CLI::App
|
|
5
6
|
opts.on("--overwrite", "If set, any files that exists already will be overwritten by new scaffolds")
|
6
7
|
opts.on("--dry-run", "If set, no files are changed. You will see output of what would happen without this flag")
|
7
8
|
|
9
|
+
|
8
10
|
def before_execute
|
9
11
|
ENV["RACK_ENV"] = "development"
|
10
12
|
end
|
11
13
|
|
12
14
|
class Test < Brut::CLI::Command
|
13
|
-
description "Create a test
|
15
|
+
description "Create the shell of a unit test based on an existing source file"
|
14
16
|
args "source_file_paths..."
|
15
17
|
def execute
|
16
18
|
if args.empty?
|
@@ -67,7 +69,7 @@ end}
|
|
67
69
|
}
|
68
70
|
|
69
71
|
if global_options.dry_run?
|
70
|
-
puts code
|
72
|
+
out.puts code
|
71
73
|
else
|
72
74
|
FileUtils.mkdir_p destination.dirname
|
73
75
|
File.open(destination,"w") do |file|
|
@@ -100,17 +102,97 @@ end}
|
|
100
102
|
end
|
101
103
|
classes
|
102
104
|
end
|
105
|
+
end
|
106
|
+
|
107
|
+
class E2ETest < Brut::CLI::Command
|
108
|
+
description "Create the shell of an end-to-end test"
|
109
|
+
args "test_name"
|
110
|
+
def self.command_name = "test:e2e"
|
111
|
+
|
112
|
+
opts.on("--path PATH","Path within the e2e tests to create the file")
|
113
|
+
def execute
|
114
|
+
if args.empty?
|
115
|
+
err.puts "'#{self.class.command_name}' requires a name"
|
116
|
+
return 1
|
117
|
+
end
|
118
|
+
test_name = args.join(" ").gsub(/\"/,"'")
|
119
|
+
test_file_name = args.join("_").gsub(/\W/,"_").gsub(/__+/,"_").downcase + ".spec.rb"
|
120
|
+
test_file_dir = Brut.container.e2e_specs_dir
|
121
|
+
if !options.path.nil?
|
122
|
+
test_file_dir = test_file_dir / options.path
|
123
|
+
end
|
124
|
+
|
125
|
+
path_to_test_file = test_file_dir / test_file_name
|
126
|
+
|
127
|
+
verb = "Created"
|
128
|
+
dry_run_verb = "create"
|
129
|
+
|
130
|
+
if path_to_test_file.exist?
|
131
|
+
if global_options.overwrite?
|
132
|
+
verb = "Overwrote"
|
133
|
+
dry_run_verb = "overwrite"
|
134
|
+
else
|
135
|
+
err.puts "#{path_to_test_file.relative_path_from(Brut.container.project_root)} exists. Use --overwrite to replace it"
|
136
|
+
return 1
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
code = %{require "spec_helper"
|
142
|
+
|
143
|
+
RSpec.describe "#{test_name}" do
|
144
|
+
it "should have tests" do
|
145
|
+
page.goto("/")
|
146
|
+
expect(page).to be_page_for(page_class_here)
|
147
|
+
end
|
148
|
+
end}
|
149
|
+
if global_options.dry_run?
|
150
|
+
out.puts "Will #{dry_run_verb} #{path_to_test_file.relative_path_from(Brut.container.project_root)} with this code:"
|
151
|
+
out.puts_no_prefix
|
152
|
+
out.puts_no_prefix code
|
153
|
+
else
|
154
|
+
FileUtils.mkdir_p test_file_dir
|
155
|
+
File.open(path_to_test_file,"w") do |file|
|
156
|
+
file.puts code
|
157
|
+
end
|
158
|
+
out.puts "#{verb} #{path_to_test_file.relative_path_from(Brut.container.project_root)}"
|
159
|
+
end
|
160
|
+
0
|
161
|
+
end
|
103
162
|
|
163
|
+
private
|
164
|
+
|
165
|
+
def find_classes(ast,current_modules = [])
|
166
|
+
classes = []
|
167
|
+
if ast.nil?
|
168
|
+
return classes
|
169
|
+
end
|
170
|
+
new_module = nil
|
171
|
+
if ast.kind_of?(Prism::ClassNode)
|
172
|
+
classes << [ current_modules, ast ]
|
173
|
+
new_module = ast
|
174
|
+
elsif ast.kind_of?(Prism::ModuleNode)
|
175
|
+
new_module = ast
|
176
|
+
end
|
177
|
+
ast.child_nodes.each do |child|
|
178
|
+
new_current_modules = current_modules + [ new_module ]
|
179
|
+
result = find_classes(child, new_current_modules.compact)
|
180
|
+
classes = classes + result
|
181
|
+
end
|
182
|
+
classes
|
183
|
+
end
|
104
184
|
end
|
185
|
+
|
105
186
|
class Component < Brut::CLI::Command
|
106
187
|
description "Create a new component, template, and associated test"
|
188
|
+
detailed_description "New components go in the `components/` folder of your app, however using --page will create a 'page private' component. To do that, the component name must be an inner class of an existing page, for example HomePage::Welcome. This component goes in a sub-folder inside the `pages/` area of your app"
|
107
189
|
opts.on("--page","If set, this component is for a specific page and won't go with the other components")
|
108
190
|
args "ComponentName"
|
109
191
|
def execute
|
110
192
|
if args.length != 1
|
111
193
|
raise "component requires exactly one argument, got #{args.length}"
|
112
194
|
end
|
113
|
-
class_name = RichString.new(args[0])
|
195
|
+
class_name = RichString.new(args[0]).capitalize(:first_only)
|
114
196
|
if class_name.to_s !~ /Component$/
|
115
197
|
class_name = RichString.new(class_name.to_s + "Component")
|
116
198
|
end
|
@@ -153,9 +235,9 @@ end}
|
|
153
235
|
end
|
154
236
|
|
155
237
|
if global_options.dry_run?
|
156
|
-
puts "FileUtils.mkdir_p #{source_path.dirname}"
|
157
|
-
puts "FileUtils.mkdir_p #{html_source_path.dirname}"
|
158
|
-
puts "FileUtils.mkdir_p #{spec_path.dirname}"
|
238
|
+
out.puts "FileUtils.mkdir_p #{source_path.dirname}"
|
239
|
+
out.puts "FileUtils.mkdir_p #{html_source_path.dirname}"
|
240
|
+
out.puts "FileUtils.mkdir_p #{spec_path.dirname}"
|
159
241
|
else
|
160
242
|
FileUtils.mkdir_p source_path.dirname
|
161
243
|
FileUtils.mkdir_p html_source_path.dirname
|
@@ -186,6 +268,293 @@ end}
|
|
186
268
|
0
|
187
269
|
end
|
188
270
|
end
|
271
|
+
class Page < Brut::CLI::Command
|
272
|
+
class Route < Brut::FrontEnd::Routing::PageRoute
|
273
|
+
def initialize(path_template)
|
274
|
+
path_template = "/#{path_template}".gsub(/\/\//,"/")
|
275
|
+
super(path_template)
|
276
|
+
end
|
277
|
+
def locate_handler_class(suffix,preposition, on_missing: :raise)
|
278
|
+
begin
|
279
|
+
super(suffix,preposition,on_missing: :raise).name.split(/::/)
|
280
|
+
rescue Brut::Framework::Errors::NoClassForPath => ex
|
281
|
+
class_name_path = ex.class_name_path
|
282
|
+
ex.class_name_path
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
description "Create a new page, template, and associated test"
|
287
|
+
args "page_route"
|
288
|
+
def execute
|
289
|
+
if args.length != 1
|
290
|
+
raise "page requires exactly one argument, got #{args.length}"
|
291
|
+
end
|
292
|
+
route = Route.new(args[0])
|
293
|
+
|
294
|
+
page_class_name = RichString.from_string(route.handler_class.join("::"))
|
295
|
+
page_relative_path = page_class_name.underscorized
|
296
|
+
|
297
|
+
pages_src_dir = Brut.container.pages_src_dir
|
298
|
+
pages_specs_dir = Brut.container.pages_specs_dir
|
299
|
+
i18n_locales_dir = Brut.container.i18n_locales_dir
|
300
|
+
|
301
|
+
page_source_path = Pathname( (pages_src_dir / page_relative_path).to_s + ".rb" )
|
302
|
+
template_source_path = Pathname( (pages_src_dir / page_relative_path).to_s + ".html.erb" )
|
303
|
+
page_spec_path = Pathname( (pages_specs_dir / page_relative_path).to_s + ".spec.rb" )
|
304
|
+
app_path = Pathname( Brut.container.app_src_dir / "app.rb" )
|
305
|
+
app_translations = Pathname( i18n_locales_dir / "en" / "2_app.rb")
|
306
|
+
|
307
|
+
exists = [
|
308
|
+
page_source_path,
|
309
|
+
template_source_path,
|
310
|
+
page_spec_path,
|
311
|
+
].select(&:exist?)
|
312
|
+
|
313
|
+
if exists.any? && !global_options.overwrite?
|
314
|
+
exists.each do |path|
|
315
|
+
err.puts "'#{path.relative_path_from(Brut.container.project_root)}' exists already"
|
316
|
+
end
|
317
|
+
err.puts "Re-run with --overwrite to overwrite these files"
|
318
|
+
return 1
|
319
|
+
end
|
320
|
+
|
321
|
+
FileUtils.mkdir_p page_source_path.dirname, noop: global_options.dry_run?
|
322
|
+
FileUtils.mkdir_p template_source_path.dirname, noop: global_options.dry_run?
|
323
|
+
FileUtils.mkdir_p page_spec_path.dirname, noop: global_options.dry_run?
|
324
|
+
|
325
|
+
route_code = "page \"#{route.path_template}\""
|
326
|
+
|
327
|
+
initializer_params = route.path_params
|
328
|
+
initializer_params_code = if initializer_params.empty?
|
329
|
+
""
|
330
|
+
else
|
331
|
+
"(" + initializer_params.map { "#{it}:" }.join(", ") + ")"
|
332
|
+
end
|
333
|
+
|
334
|
+
page_class_code = %{class #{page_class_name} < AppPage
|
335
|
+
def initialize#{initializer_params_code} # add needed arguments here
|
336
|
+
end
|
337
|
+
end}
|
338
|
+
template_code = %{<h1>#{page_class_name} is ready!</h1>}
|
339
|
+
page_spec_code = %{require "spec_helper"
|
340
|
+
|
341
|
+
RSpec.describe #{page_class_name} do
|
342
|
+
it "should have tests" do
|
343
|
+
expect(true).to eq(false)
|
344
|
+
end
|
345
|
+
end}
|
346
|
+
|
347
|
+
title = RichString.new(page_class_name).underscorized.humanized.to_s.capitalize
|
348
|
+
translations_code = " \"#{page_class_name}\": {\n title: \"#{title}\",\n \},"
|
349
|
+
|
350
|
+
if global_options.dry_run?
|
351
|
+
out.puts app_path.relative_path_from(Brut.container.project_root)
|
352
|
+
out.puts "will contain:\n\n#{route_code}\n\n"
|
353
|
+
out.puts page_source_path.relative_path_from(Brut.container.project_root)
|
354
|
+
out.puts "will contain:\n\n#{page_class_code}\n\n"
|
355
|
+
out.puts template_source_path.relative_path_from(Brut.container.project_root)
|
356
|
+
out.puts "will contain:\n\n#{template_code}\n\n"
|
357
|
+
out.puts page_spec_path.relative_path_from(Brut.container.project_root)
|
358
|
+
out.puts "will contain:\n\n#{page_spec_code}\n\n"
|
359
|
+
out.puts app_translations.relative_path_from(Brut.container.project_root)
|
360
|
+
out.puts "will contain:\n\n#{translations_code}\n\n"
|
361
|
+
else
|
362
|
+
|
363
|
+
File.open(page_source_path,"w") { it.puts page_class_code }
|
364
|
+
File.open(template_source_path,"w") { it.puts template_code }
|
365
|
+
File.open(page_spec_path,"w") { it.puts page_spec_code }
|
366
|
+
|
367
|
+
existing_translations = File.read(app_translations).split(/\n/)
|
368
|
+
inserted_translation = false
|
369
|
+
File.open(app_translations,"w") do |file|
|
370
|
+
existing_translations.each do |line|
|
371
|
+
if line =~ /^ pages:\s*{/
|
372
|
+
file.puts line
|
373
|
+
file.puts translations_code
|
374
|
+
inserted_translation = true
|
375
|
+
else
|
376
|
+
file.puts line
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
if !inserted_translation
|
381
|
+
err.puts "WARNING: Could not find a place to insert the translation for this page's title"
|
382
|
+
err.puts " The page may not render properly the first time you load it"
|
383
|
+
end
|
384
|
+
|
385
|
+
routes_editor = RoutesEditor.new(app_path:,out:)
|
386
|
+
routes_editor.add_route!(route_code:)
|
387
|
+
|
388
|
+
if !routes_editor.found_routes?
|
389
|
+
out.puts "Could not find routes declaration in #{app_path.relative_path_from(Brut.container.project_root)}"
|
390
|
+
out.puts "Please add this to wherever you have defined your routes:\n\n#{route_code}\n\n"
|
391
|
+
elsif routes_editor.routes_existed?
|
392
|
+
out.puts "Routes declaration in #{app_path.relative_path_from(Brut.container.project_root)} contained the route defition already"
|
393
|
+
out.puts "Please make sure everything is correct. Here is the defintion that was not inserted:\n\n#{route_code}"
|
394
|
+
end
|
395
|
+
end
|
396
|
+
out.puts "Page source is in #{page_source_path.relative_path_from(Brut.container.project_root)}"
|
397
|
+
out.puts "Page HTML template is in #{template_source_path.relative_path_from(Brut.container.project_root)}"
|
398
|
+
out.puts "Page test is in #{page_spec_path.relative_path_from(Brut.container.project_root)}"
|
399
|
+
out.puts "Added title to #{app_translations.relative_path_from(Brut.container.project_root)}"
|
400
|
+
out.puts "Added route to #{app_path.relative_path_from(Brut.container.project_root)}"
|
401
|
+
0
|
402
|
+
end
|
403
|
+
end
|
404
|
+
class Action < Brut::CLI::Command
|
405
|
+
class Route < Brut::FrontEnd::Routing::FormRoute
|
406
|
+
def initialize(path_template)
|
407
|
+
path_template = "/#{path_template}".gsub(/\/\//,"/")
|
408
|
+
super(path_template)
|
409
|
+
end
|
410
|
+
def locate_handler_class(suffix,preposition, on_missing: :raise)
|
411
|
+
begin
|
412
|
+
super(suffix,preposition,on_missing: :raise).name.split(/::/)
|
413
|
+
rescue Brut::Framework::Errors::NoClassForPath => ex
|
414
|
+
class_name_path = ex.class_name_path
|
415
|
+
ex.class_name_path
|
416
|
+
end
|
417
|
+
end
|
418
|
+
end
|
419
|
+
description "Create a handler for an action"
|
420
|
+
args "action_route"
|
421
|
+
opts.on "--http-method=METHOD", "If present, the action will be a path available on the given route and this HTTP method. If omitted, this will create an action available via POST"
|
422
|
+
def execute(form: false)
|
423
|
+
if args.length != 1
|
424
|
+
raise "#{self.class.command_name} requires exactly one argument, got #{args.length}"
|
425
|
+
end
|
426
|
+
route = Route.new(args[0])
|
427
|
+
|
428
|
+
form_class_name = RichString.from_string(route.form_class.join("::"))
|
429
|
+
handler_class_name = RichString.from_string(route.handler_class.join("::"))
|
430
|
+
|
431
|
+
relative_path = form_class_name.underscorized
|
432
|
+
handler_relative_path = handler_class_name.underscorized
|
433
|
+
|
434
|
+
forms_src_dir = Brut.container.forms_src_dir
|
435
|
+
handlers_src_dir = Brut.container.handlers_src_dir
|
436
|
+
handlers_specs_dir = Brut.container.handlers_specs_dir
|
437
|
+
|
438
|
+
form_source_path = Pathname( (forms_src_dir / relative_path).to_s + ".rb" )
|
439
|
+
handler_source_path = Pathname( (handlers_src_dir / handler_relative_path).to_s + ".rb" )
|
440
|
+
handler_spec_path = Pathname( (handlers_specs_dir / handler_relative_path).to_s + ".spec.rb" )
|
441
|
+
app_path = Pathname( Brut.container.app_src_dir / "app.rb" )
|
442
|
+
|
443
|
+
paths_to_check = [
|
444
|
+
handler_source_path,
|
445
|
+
handler_spec_path,
|
446
|
+
]
|
447
|
+
if form
|
448
|
+
paths_to_check << form_source_path
|
449
|
+
end
|
450
|
+
|
451
|
+
exists = paths_to_check.select(&:exist?)
|
452
|
+
|
453
|
+
if exists.any? && !global_options.overwrite?
|
454
|
+
exists.each do |path|
|
455
|
+
err.puts "'#{path.relative_path_from(Brut.container.project_root)}' exists already"
|
456
|
+
end
|
457
|
+
err.puts "Re-run with global option --overwrite to overwrite these files"
|
458
|
+
return 1
|
459
|
+
end
|
460
|
+
|
461
|
+
if form
|
462
|
+
FileUtils.mkdir_p form_source_path.dirname, noop: global_options.dry_run?
|
463
|
+
end
|
464
|
+
FileUtils.mkdir_p handler_source_path.dirname, noop: global_options.dry_run?
|
465
|
+
FileUtils.mkdir_p handler_spec_path.dirname, noop: global_options.dry_run?
|
466
|
+
|
467
|
+
form_code = %{class #{form_class_name} < AppForm
|
468
|
+
input :some_field, minlength: 3
|
469
|
+
end}
|
470
|
+
handle_method_code = if form
|
471
|
+
'raise "You need to implement your Handler\#{form.class.input_definitions.length < 2 ? " and likely your Form as well" : ""}"'
|
472
|
+
else
|
473
|
+
raise "You need to implement your Handler"
|
474
|
+
end
|
475
|
+
handler_code = begin
|
476
|
+
handle_params = []
|
477
|
+
if form
|
478
|
+
handle_params << :form
|
479
|
+
end
|
480
|
+
handle_params += route.path_params
|
481
|
+
handle_params_code = handle_params.map { "#{it}:" }.join(", ")
|
482
|
+
%{class #{handler_class_name} < AppHandler
|
483
|
+
def handle(#{handle_params_code}) # add other args here as needed
|
484
|
+
#{handle_method_code}
|
485
|
+
end
|
486
|
+
end}
|
487
|
+
end
|
488
|
+
|
489
|
+
spec_code = %{require "spec_helper"
|
490
|
+
|
491
|
+
RSpec.describe #{handler_class_name} do
|
492
|
+
subject(:handler) { described_class.new }
|
493
|
+
describe "#handle!" do
|
494
|
+
it "needs tests" do
|
495
|
+
expect(true).to eq(false)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
end}
|
499
|
+
|
500
|
+
route_code = if form
|
501
|
+
"form \"#{route.path_template}\""
|
502
|
+
elsif options.http_method.nil?
|
503
|
+
"action \"#{route.path_template}\""
|
504
|
+
else
|
505
|
+
"path \"#{route.path_template}\", method: :#{options.http_method.downcase}"
|
506
|
+
end
|
507
|
+
|
508
|
+
if global_options.dry_run?
|
509
|
+
out.puts app_path.relative_path_from(Brut.container.project_root)
|
510
|
+
out.puts "will contain:\n\n#{route_code}\n\n"
|
511
|
+
if form
|
512
|
+
out.puts form_source_path.relative_path_from(Brut.container.project_root)
|
513
|
+
out.puts "will contain:\n\n#{form_code}\n\n"
|
514
|
+
end
|
515
|
+
out.puts handler_source_path.relative_path_from(Brut.container.project_root)
|
516
|
+
out.puts "will contain:\n\n#{handler_code}\n\n"
|
517
|
+
out.puts handler_spec_path.relative_path_from(Brut.container.project_root)
|
518
|
+
out.puts "will contain:\n\n#{spec_code}\n\n"
|
519
|
+
else
|
520
|
+
class_name_length = [ form_class_name.length, handler_class_name.length, "Spec".length ].max
|
521
|
+
printf_string = "%-#{class_name_length}s in %s\n"
|
522
|
+
out.puts "\n\n"
|
523
|
+
if form
|
524
|
+
out.printf printf_string,form_class_name,form_source_path.relative_path_from(Brut.container.project_root)
|
525
|
+
end
|
526
|
+
out.printf printf_string,handler_class_name, handler_source_path.relative_path_from(Brut.container.project_root)
|
527
|
+
out.printf printf_string,"Spec", handler_spec_path.relative_path_from(Brut.container.project_root)
|
528
|
+
|
529
|
+
routes_editor = RoutesEditor.new(app_path:,out:)
|
530
|
+
routes_editor.add_route!(route_code:)
|
531
|
+
|
532
|
+
if form
|
533
|
+
File.open(form_source_path,"w") { it.puts form_code }
|
534
|
+
end
|
535
|
+
File.open(handler_source_path,"w") { it.puts handler_code }
|
536
|
+
File.open(handler_spec_path,"w") { it.puts spec_code }
|
537
|
+
if !routes_editor.found_routes?
|
538
|
+
out.puts "Could not find routes declaration in #{app_path.relative_path_from(Brut.container.project_root)}"
|
539
|
+
out.puts "Please add this to wherever you have defined your routes:\n\n#{route_code}\n\n"
|
540
|
+
elsif routes_editor.routes_existed?
|
541
|
+
out.puts "Routes declaration in #{app_path.relative_path_from(Brut.container.project_root)} contained the route defition already"
|
542
|
+
out.puts "Please make sure everything is correct. Here is the defintion that was not inserted:\n\n#{route_code}"
|
543
|
+
end
|
544
|
+
end
|
545
|
+
0
|
546
|
+
end
|
547
|
+
end
|
548
|
+
|
549
|
+
class Form < Action
|
550
|
+
description "Create a form and handler"
|
551
|
+
args "form_route"
|
552
|
+
|
553
|
+
def execute
|
554
|
+
super(form:true)
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
189
558
|
class CustomElementTest < Brut::CLI::Command
|
190
559
|
description "Create a test for a custom element in your app"
|
191
560
|
args "path_to_js_files..."
|
@@ -214,7 +583,7 @@ end}
|
|
214
583
|
|
215
584
|
if existing_files.any? && !global_options.overwrite?
|
216
585
|
relative_paths = existing_files.map { |_,pathname| pathname.relative_path_from(Brut.container.project_root) }
|
217
|
-
err.puts "Some files to be generated already exist. Set --overwrite to overwrite them:"
|
586
|
+
err.puts "Some files to be generated already exist. Set global option --overwrite to overwrite them:"
|
218
587
|
relative_paths.each do |file|
|
219
588
|
err.puts file
|
220
589
|
end
|
@@ -253,4 +622,41 @@ describe("#{description}", () => {
|
|
253
622
|
0
|
254
623
|
end
|
255
624
|
end
|
625
|
+
|
626
|
+
class RoutesEditor
|
627
|
+
def initialize(app_path:,out:)
|
628
|
+
@app_path = app_path
|
629
|
+
@out = out
|
630
|
+
@found_routes = false
|
631
|
+
@routes_existed = false
|
632
|
+
end
|
633
|
+
|
634
|
+
def found_routes? = @found_routes
|
635
|
+
def routes_existed? = @routes_existed
|
636
|
+
|
637
|
+
def add_route!(route_code:)
|
638
|
+
app_contents = File.read(@app_path).split(/\n/)
|
639
|
+
File.open(@app_path,"w") do |file|
|
640
|
+
in_routes = false
|
641
|
+
app_contents.each do |line|
|
642
|
+
if line =~ /^ routes do\s*$/
|
643
|
+
in_routes = true
|
644
|
+
end
|
645
|
+
if in_routes && line.include?(route_code)
|
646
|
+
@routes_existed = true
|
647
|
+
end
|
648
|
+
if in_routes && line =~ /^ end\s*$/
|
649
|
+
if !@routes_existed
|
650
|
+
@out.puts "Inserted route into #{@app_path.relative_path_from(Brut.container.project_root)}"
|
651
|
+
file.puts " #{route_code}"
|
652
|
+
end
|
653
|
+
@found_routes = true
|
654
|
+
in_routes = false
|
655
|
+
end
|
656
|
+
file.puts line
|
657
|
+
end
|
658
|
+
end
|
659
|
+
end
|
660
|
+
end
|
256
661
|
end
|
662
|
+
|
data/lib/brut/cli/apps/test.rb
CHANGED
@@ -15,6 +15,7 @@ class Brut::CLI::Apps::Test < Brut::CLI::App
|
|
15
15
|
opts.on("--[no-]rebuild-after", "If true, test database is rebuilt after tests are run (default false)")
|
16
16
|
opts.on("--seed SEED", "Set the random seed to allow duplicating a test run")
|
17
17
|
args "specs_to_run..."
|
18
|
+
env_var("LOGGER_LEVEL_FOR_TESTS",purpose: "Can be set to debug, info, warn, error, or fatal to control logging during tests. Defaults to 'warn' to avoid verbose test output")
|
18
19
|
|
19
20
|
def rspec_command
|
20
21
|
parts = [
|
@@ -64,6 +65,10 @@ class Brut::CLI::Apps::Test < Brut::CLI::App
|
|
64
65
|
opts.on("--[no-]rebuild-after", "If true, test database is rebuilt after tests are run (default true)")
|
65
66
|
opts.on("--seed SEED", "Set the random seed to allow duplicating a test run")
|
66
67
|
args "specs_to_run..."
|
68
|
+
env_var("E2E_RECORD_VIDEOS",purpose: "If set to 'true', videos of each test run are saved in `./tmp/e2e-videos`")
|
69
|
+
env_var("E2E_SLOW_MO",purpose: "If set to, will attempt to slow operations down by this many milliseconds")
|
70
|
+
env_var("E2E_TIMEOUT_MS",purpose: "ms to wait for any browser activity before failing the test. And here you didn't think you'd get away without using sleep in browse-based tests?")
|
71
|
+
env_var("LOGGER_LEVEL_FOR_TESTS",purpose: "Can be set to debug, info, warn, error, or fatal to control logging during tests. Defaults to 'warn' to avoid verbose test output")
|
67
72
|
|
68
73
|
def rspec_cli_args = "--tag e2e"
|
69
74
|
def rebuild_by_default? = true
|
@@ -76,7 +81,15 @@ class Brut::CLI::Apps::Test < Brut::CLI::App
|
|
76
81
|
if options.build_assets?
|
77
82
|
system!({ "RACK_ENV" => "test" }, "bin/build-assets")
|
78
83
|
end
|
79
|
-
|
84
|
+
begin
|
85
|
+
system!({ "NODE_DISABLE_COLORS" => "1" },"npx mocha #{Brut.container.js_specs_dir} --no-color --extension 'spec.js' --recursive")
|
86
|
+
rescue Brut::CLI::SystemExecError => ex
|
87
|
+
if ex.exit_status == 1
|
88
|
+
out.puts "mocha exited 1 - assuming this is because there are no test files and that this is intentional"
|
89
|
+
else
|
90
|
+
raise ex
|
91
|
+
end
|
92
|
+
end
|
80
93
|
0
|
81
94
|
end
|
82
95
|
end
|