brut 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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