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,279 @@
1
+ require "sequel"
2
+ require "uri"
3
+ require "date"
4
+ require "brut/cli"
5
+
6
+ class Brut::CLI::Apps::DB < Brut::CLI::App
7
+ description "Manage your database in development, test, and production"
8
+
9
+ class Seed < Brut::CLI::Command
10
+ description "Load seed data into the database"
11
+ requires_project_env default: "development"
12
+
13
+ def handle_bootstrap_exception(ex)
14
+ case ex
15
+ when Sequel::DatabaseConnectionError
16
+ err.puts "Database needs to be created"
17
+ stop_execution
18
+ when Sequel::DatabaseError
19
+ if ex.cause.kind_of?(PG::UndefinedTable)
20
+ err.puts "Migrations need to be run"
21
+ stop_execution
22
+ else
23
+ super
24
+ end
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def execute
31
+ seeds_dir = Brut.container.db_seeds_dir
32
+ Dir["#{seeds_dir}/*.rb"].each do |file|
33
+ require file
34
+ end
35
+ seed_data = Brut::Backend::SeedData.new
36
+ seed_data.setup!
37
+ seed_data.load_seeds!
38
+ 0
39
+ rescue Sequel::UniqueConstraintViolation => ex
40
+ out.puts "Seed data may have already been loaded: #{ex}"
41
+ end
42
+ end
43
+
44
+ class Rebuild < Brut::CLI::Command
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
+
48
+ requires_project_env default: "development"
49
+
50
+ def handle_bootstrap_exception(ex)
51
+ case ex
52
+ when Sequel::DatabaseConnectionError
53
+ continue_execution
54
+ when Sequel::DatabaseError
55
+ if ex.cause.kind_of?(PG::UndefinedTable)
56
+ continue_execution
57
+ else
58
+ super
59
+ end
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ def execute
66
+ result = delegate_to_commands(Drop, Create, Migrate)
67
+ if result.ok? && options.seeds?
68
+ result = delegate_to_command(Seed)
69
+ end
70
+ result
71
+ end
72
+ end
73
+
74
+ class Create < Brut::CLI::Command
75
+ description "Create the database if it does not exist"
76
+ requires_project_env default: "development"
77
+
78
+ def handle_bootstrap_exception(ex)
79
+ case ex
80
+ when Sequel::DatabaseConnectionError
81
+ uri_no_database = URI(Brut.container.database_url.to_s)
82
+ database_name = uri_no_database.path.gsub(/^\//,"")
83
+ uri_no_database.path = ""
84
+ begin
85
+ connection = Sequel.connect(uri_no_database.to_s)
86
+ out.puts "#{database_name} does not exit. Creating..."
87
+ connection.run("CREATE DATABASE \"#{database_name}\"")
88
+ connection.disconnect
89
+ rescue => ex
90
+ err.puts ex.message
91
+ end
92
+ stop_execution
93
+ when Sequel::DatabaseError
94
+ if ex.cause.kind_of?(PG::UndefinedTable)
95
+ out.puts "Migrations need to be run"
96
+ continue_execution
97
+ else
98
+ super
99
+ end
100
+ else
101
+ super
102
+ end
103
+ end
104
+ def execute
105
+ connection = Sequel.connect(Brut.container.database_url)
106
+ out.puts "Database already exists"
107
+ connection.disconnect
108
+ 0
109
+ rescue => ex
110
+ handle_bootstrap_exception(ex)
111
+ end
112
+ end
113
+
114
+ class Drop < Brut::CLI::Command
115
+ description "Drop the database if it exists"
116
+ requires_project_env default: "development"
117
+
118
+ def handle_bootstrap_exception(ex)
119
+ case ex
120
+ when Sequel::DatabaseConnectionError
121
+ out.puts "Database does not exist"
122
+ stop_execution
123
+ when Sequel::DatabaseError
124
+ if ex.cause.kind_of?(PG::UndefinedTable)
125
+ continue_execution
126
+ else
127
+ super
128
+ end
129
+ else
130
+ super
131
+ end
132
+ end
133
+
134
+ def execute
135
+ uri_no_database = URI(Brut.container.database_url.to_s)
136
+ database_name = uri_no_database.path.gsub(/^\//,"")
137
+ uri_no_database.path = ""
138
+ out.puts "Database exists. Dropping..."
139
+ begin
140
+ Brut.container.sequel_db_handle.disconnect
141
+ rescue Sequel::DatabaseConnectionError
142
+ end
143
+ connection = Sequel.connect(uri_no_database.to_s)
144
+ connection.run("DROP DATABASE IF EXISTS \"#{database_name}\"")
145
+ connection.disconnect
146
+ 0
147
+ rescue => ex
148
+ handle_bootstrap_exception(ex)
149
+ end
150
+ end
151
+
152
+ class Migrate < Brut::CLI::Command
153
+ description "Apply any outstanding migrations to the database"
154
+ requires_project_env default: "development"
155
+
156
+ def handle_bootstrap_exception(ex)
157
+ case ex
158
+ when Sequel::DatabaseConnectionError
159
+ err.puts "Database does not exist. Create it first"
160
+ stop_execution
161
+ when Sequel::DatabaseError
162
+ if ex.cause.kind_of?(PG::UndefinedTable)
163
+ # ignoring - we are running migrations which will address this
164
+ continue_execution
165
+ else
166
+ super
167
+ end
168
+ else
169
+ super
170
+ end
171
+ end
172
+
173
+ def execute
174
+ Sequel.extension :migration
175
+ Brut.container.sequel_db_handle.extension :brut_migrations
176
+ migrations_dir = Brut.container.migrations_dir
177
+ if !migrations_dir.exist?
178
+ err.puts "#{migrations_dir} doesn't exist"
179
+ return
180
+ end
181
+ Brut.container.sequel_db_handle.extension :pg_array
182
+
183
+ logger = Logger.new(STDOUT)
184
+ logger.level = ENV["LOG_LEVEL"]
185
+ indent = ""
186
+ logger.formatter = proc { |severity,time,progname,message|
187
+ formatted = "#{indent} - #{message}\n"
188
+ if message =~ /^Begin applying/
189
+ indent = " "
190
+ elsif message =~ /^Finished applying/
191
+ indent = ""
192
+ formatted = "#{indent} - #{message}\n"
193
+ end
194
+ formatted
195
+ }
196
+ Brut.container.sequel_db_handle.logger = logger
197
+ Sequel::Migrator.run(Brut.container.sequel_db_handle,migrations_dir)
198
+ out.puts "Migrations applied"
199
+ end
200
+ end
201
+
202
+ class NewMigration < Brut::CLI::Command
203
+ description "Create a new migration file"
204
+ args "migration_name"
205
+
206
+ def before_execute
207
+ ENV["RACK_ENV"] = "development"
208
+ end
209
+
210
+ def execute
211
+ if @args.length == 0
212
+ return abort_execution("You must provide a name for the migration")
213
+ end
214
+ migrations_dir = Brut.container.migrations_dir
215
+ name = @args.join(" ").gsub(/[^\w\d\-]/,"-")
216
+ date = DateTime.now.strftime("%Y%m%d%H%M%S")
217
+ file_name = migrations_dir / "#{date}_#{name}.rb"
218
+ File.open(file_name,"w") do |file|
219
+ file.puts "Sequel.migration do"
220
+ file.puts " up do"
221
+ file.puts " end"
222
+ file.puts "end"
223
+ end
224
+ relative_path = file_name.relative_path_from(Brut.container.project_root)
225
+ out.puts "Migration created:\n #{relative_path}"
226
+ end
227
+ end
228
+
229
+ class Status < Brut::CLI::Command
230
+ description "Check the status of the database and migrations"
231
+ requires_project_env default: "development"
232
+
233
+ def handle_bootstrap_exception(ex)
234
+ case ex
235
+ when Sequel::DatabaseConnectionError
236
+ uri_no_database = URI(Brut.container.database_url.to_s)
237
+ database_name = uri_no_database.path.gsub(/^\//,"")
238
+ uri_no_database.path = ""
239
+ begin
240
+ connection = Sequel.connect(uri_no_database.to_s)
241
+ out.puts "Database Server is Up"
242
+ out.puts "Database #{database_name} does not exist"
243
+ rescue => ex
244
+ err.puts ex.message
245
+ end
246
+ stop_execution
247
+ when Sequel::DatabaseError
248
+ if ex.cause.kind_of?(PG::UndefinedTable)
249
+ err.puts "Migrations need to be run"
250
+ continue_execution
251
+ else
252
+ super
253
+ end
254
+ end
255
+ end
256
+
257
+ def execute
258
+ database_name = URI(Brut.container.database_url).path
259
+ connection = Brut.container.sequel_db_handle
260
+ out.puts "Database Server is Up"
261
+ out.puts "Database #{database_name} exists"
262
+ migrations_run = if connection.table_exists?("schema_migrations")
263
+ connection["select filename from schema_migrations order by filename"].all.map { |_| _[:filename] }
264
+ else
265
+ []
266
+ end
267
+ migration_files = Dir[Brut.container.migrations_dir / "*.rb"].map { |file|
268
+ filename = Pathname(file).basename.to_s
269
+ }
270
+ max_length = migration_files.map(&:length).max
271
+ printf_string = "%-#{max_length}s - %s\n"
272
+ migration_files.each do |filename|
273
+ applied = migrations_run.include?(filename)
274
+ printf(printf_string,filename,applied ? "✅ APPLIED" : "❌ NOT APPLIED")
275
+ end
276
+ 0
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,256 @@
1
+ require "brut/cli"
2
+
3
+ class Brut::CLI::Apps::Scaffold < Brut::CLI::App
4
+ description "Create scaffolds of various files to help develop more quckly"
5
+ opts.on("--overwrite", "If set, any files that exists already will be overwritten by new scaffolds")
6
+ opts.on("--dry-run", "If set, no files are changed. You will see output of what would happen without this flag")
7
+
8
+ def before_execute
9
+ ENV["RACK_ENV"] = "development"
10
+ end
11
+
12
+ class Test < Brut::CLI::Command
13
+ description "Create a test for a given file in the app"
14
+ args "source_file_paths..."
15
+ def execute
16
+ if args.empty?
17
+ err.puts "'test' requires one or more files to scaffold a test for"
18
+ return 1
19
+ end
20
+ files_to_test_files = args.map { |arg|
21
+ Pathname(arg).expand_path
22
+ }.map { |pathname|
23
+ relative = pathname.relative_path_from(Brut.container.app_src_dir)
24
+ test_file = Brut.container.app_specs_dir / relative.dirname / "#{relative.basename(relative.extname)}.spec.rb"
25
+ [ pathname, test_file ]
26
+ }.to_h
27
+
28
+ non_existent_sources = files_to_test_files.keys.select { |pathname| !pathname.exist? }
29
+ existent_destinations = files_to_test_files.values.select { |pathname| pathname.exist? }
30
+
31
+ if non_existent_sources.any?
32
+ relative_paths = non_existent_sources.map { |pathname| pathname.relative_path_from(Brut.container.project_root) }
33
+ err.puts "Not all input files exist:"
34
+ relative_paths.each do |file|
35
+ err.puts file
36
+ end
37
+ return 1
38
+ end
39
+
40
+ if existent_destinations.any? && !global_options.overwrite?
41
+ relative_paths = existent_destinations.map { |pathname| pathname.relative_path_from(Brut.container.project_root) }
42
+ err.puts "Some files to be generated already exist. Set --overwrite to overwrite them:"
43
+ relative_paths.each do |file|
44
+ err.puts file
45
+ end
46
+ return 1
47
+ end
48
+
49
+ files_to_test_files.each do |source,destination|
50
+ result = Prism.parse_file(source.to_s)
51
+ if !result
52
+ raise "For some reason Prism did not parse #{source.to_s}"
53
+ end
54
+ classes = find_classes(result.value).map { |(module_nodes,class_node)|
55
+ (module_nodes.map(&:constant_path).map(&:full_name).map(&:to_s) + [class_node.constant_path.full_name.to_s]).compact.join("::")
56
+ }
57
+
58
+
59
+ out.puts "#{destination} will contain tests for:\n#{classes.join("\n")}\n\n"
60
+
61
+ code = ["require \"spec_helper\"\n"] + classes.map { |class_name|
62
+ %{RSpec.describe #{class_name} do
63
+ it "should have tests" do
64
+ expect(false).to eq(true)
65
+ end
66
+ end}
67
+ }
68
+
69
+ if global_options.dry_run?
70
+ puts code
71
+ else
72
+ FileUtils.mkdir_p destination.dirname
73
+ File.open(destination,"w") do |file|
74
+ file.puts code
75
+ end
76
+ end
77
+ end
78
+
79
+ 0
80
+ end
81
+
82
+ private
83
+
84
+ def find_classes(ast,current_modules = [])
85
+ classes = []
86
+ if ast.nil?
87
+ return classes
88
+ end
89
+ new_module = nil
90
+ if ast.kind_of?(Prism::ClassNode)
91
+ classes << [ current_modules, ast ]
92
+ new_module = ast
93
+ elsif ast.kind_of?(Prism::ModuleNode)
94
+ new_module = ast
95
+ end
96
+ ast.child_nodes.each do |child|
97
+ new_current_modules = current_modules + [ new_module ]
98
+ result = find_classes(child, new_current_modules.compact)
99
+ classes = classes + result
100
+ end
101
+ classes
102
+ end
103
+
104
+ end
105
+ class Component < Brut::CLI::Command
106
+ description "Create a new component, template, and associated test"
107
+ opts.on("--page","If set, this component is for a specific page and won't go with the other components")
108
+ args "ComponentName"
109
+ def execute
110
+ if args.length != 1
111
+ raise "component requires exactly one argument, got #{args.length}"
112
+ end
113
+ class_name = RichString.new(args[0])
114
+ if class_name.to_s !~ /Component$/
115
+ class_name = RichString.new(class_name.to_s + "Component")
116
+ end
117
+
118
+ relative_path = class_name.underscorized
119
+
120
+ components_src_dir = Brut.container.components_src_dir
121
+ components_specs_dir = Brut.container.components_specs_dir
122
+
123
+ if options.page?
124
+ components_src_dir = Brut.container.pages_src_dir
125
+ components_specs_dir = Brut.container.pages_specs_dir
126
+ if class_name.to_s !~ /::/
127
+ raise "component #{class_name} cannot be a page component - it must be an inner class of an existing page"
128
+ else
129
+ existing_page = RichString.new(class_name.to_s.split(/::/)[0..-2].join("::")).underscorized.to_s + ".rb"
130
+
131
+ if !(components_src_dir / existing_page).exist?
132
+ raise "#{class_name} was set as a page component, however we cannot find the page it belongs in. File #{existing_page} does not exist and should contain that page"
133
+ end
134
+ end
135
+ end
136
+
137
+ source_path = Pathname( (components_src_dir / relative_path).to_s + ".rb" )
138
+ html_source_path = Pathname( (components_src_dir / relative_path).to_s + ".html.erb" )
139
+ spec_path = Pathname( (components_specs_dir / relative_path).to_s + ".spec.rb" )
140
+
141
+ exists = [
142
+ source_path,
143
+ html_source_path,
144
+ spec_path,
145
+ ].select(&:exist?)
146
+
147
+ if exists.any? && !global_options.overwrite?
148
+ exists.each do |path|
149
+ err.puts "'#{path.relative_path_from(Brut.container.project_root)}' exists already"
150
+ end
151
+ err.puts "Re-run with --overwrite to overwrite these files"
152
+ return 1
153
+ end
154
+
155
+ 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}"
159
+ else
160
+ FileUtils.mkdir_p source_path.dirname
161
+ FileUtils.mkdir_p html_source_path.dirname
162
+ FileUtils.mkdir_p spec_path.dirname
163
+
164
+ File.open(source_path,"w") do |file|
165
+ file.puts %{class #{class_name} < AppComponent
166
+ def initialize
167
+ end
168
+ end}
169
+ end
170
+ File.open(html_source_path,"w") do |file|
171
+ file.puts "<h1>#{class_name} is ready!</h1>"
172
+ end
173
+ File.open(spec_path,"w") do |file|
174
+ file.puts %{require "spec_helper"
175
+
176
+ RSpec.describe #{class_name} do
177
+ it "should have tests" do
178
+ expect(true).to eq(false)
179
+ end
180
+ end}
181
+ end
182
+ end
183
+ out.puts "Component source is in #{source_path.relative_path_from(Brut.container.project_root)}"
184
+ out.puts "Component HTML template is in #{html_source_path.relative_path_from(Brut.container.project_root)}"
185
+ out.puts "Component test is in #{spec_path.relative_path_from(Brut.container.project_root)}"
186
+ 0
187
+ end
188
+ end
189
+ class CustomElementTest < Brut::CLI::Command
190
+ description "Create a test for a custom element in your app"
191
+ args "path_to_js_files..."
192
+ def execute
193
+ if args.empty?
194
+ err.puts "'custom-element-test' requires one or more files to scaffold a test for"
195
+ return 1
196
+ end
197
+
198
+ if args.any? { |file| Pathname(file).extname != ".js" }
199
+ err.puts "'custom-element-test' must be given only .js files"
200
+ return 1
201
+ end
202
+
203
+ files_to_create = args.map { |arg|
204
+ path = Pathname(arg).expand_path
205
+ relative_path = path.relative_path_from(Brut.container.js_src_dir)
206
+ relative_path_as_spec = relative_path.dirname / (relative_path.basename(relative_path.extname).to_s + ".spec.js")
207
+ spec_path = Brut.container.js_specs_dir / relative_path_as_spec
208
+ [ path, spec_path ]
209
+ }
210
+
211
+ existing_files = files_to_create.select { |_,spec|
212
+ spec.exist?
213
+ }
214
+
215
+ if existing_files.any? && !global_options.overwrite?
216
+ 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:"
218
+ relative_paths.each do |file|
219
+ err.puts file
220
+ end
221
+ return 1
222
+ end
223
+
224
+ files_to_create.each do |source_file, spec_file|
225
+ source_class = source_file.basename(source_file.extname)
226
+ tag_name = File.read(source_file).split(/\n/).map { |line|
227
+ if line =~ /static\s+tagName\s*=\s*\"([^"]+)\"/
228
+ "<#{$1}>"
229
+ else
230
+ nil
231
+ end
232
+ }.compact.first
233
+ description = tag_name || source_class
234
+ code = %{import { withHTML } from "brut-js/testing/index.js"
235
+
236
+ describe("#{description}", () => {
237
+ withHTML(`
238
+ #{ tag_name ? "#{tag_name}" : "<!-- HTML here -->" }
239
+ #{ tag_name ? "#{tag_name.gsub(/^</,'</')}" : "" }
240
+ `).test("description here", ({document,window,assert}) => {
241
+ assert.fail("test goes here")
242
+ })
243
+ })}
244
+ if global_options.dry_run?
245
+ out.puts "Would generate this code:\n\n#{code}"
246
+ else
247
+ File.open(spec_file, "w") do |file|
248
+ file.puts code
249
+ end
250
+ end
251
+ end
252
+
253
+ 0
254
+ end
255
+ end
256
+ end