app_archetype 1.2.3

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.doxie.json +25 -0
  3. data/.github/workflows/build.yml +25 -0
  4. data/.gitignore +22 -0
  5. data/.rubocop.yml +35 -0
  6. data/.ruby-version +1 -0
  7. data/CONTRIBUTING.md +51 -0
  8. data/Gemfile +3 -0
  9. data/Gemfile.lock +172 -0
  10. data/LICENSE +21 -0
  11. data/README.md +138 -0
  12. data/Rakefile +19 -0
  13. data/app_archetype.gemspec +39 -0
  14. data/bin/archetype +20 -0
  15. data/lib/app_archetype.rb +14 -0
  16. data/lib/app_archetype/cli.rb +204 -0
  17. data/lib/app_archetype/cli/presenters.rb +106 -0
  18. data/lib/app_archetype/cli/prompts.rb +152 -0
  19. data/lib/app_archetype/generators.rb +95 -0
  20. data/lib/app_archetype/logger.rb +69 -0
  21. data/lib/app_archetype/renderer.rb +116 -0
  22. data/lib/app_archetype/template.rb +12 -0
  23. data/lib/app_archetype/template/helpers.rb +216 -0
  24. data/lib/app_archetype/template/manifest.rb +193 -0
  25. data/lib/app_archetype/template/plan.rb +172 -0
  26. data/lib/app_archetype/template/source.rb +39 -0
  27. data/lib/app_archetype/template/variable.rb +237 -0
  28. data/lib/app_archetype/template/variable_manager.rb +75 -0
  29. data/lib/app_archetype/template_manager.rb +113 -0
  30. data/lib/app_archetype/version.rb +6 -0
  31. data/lib/core_ext/string.rb +67 -0
  32. data/spec/app_archetype/cli/presenters_spec.rb +99 -0
  33. data/spec/app_archetype/cli/prompts_spec.rb +292 -0
  34. data/spec/app_archetype/cli_spec.rb +132 -0
  35. data/spec/app_archetype/generators_spec.rb +119 -0
  36. data/spec/app_archetype/logger_spec.rb +86 -0
  37. data/spec/app_archetype/renderer_spec.rb +291 -0
  38. data/spec/app_archetype/template/helpers_spec.rb +251 -0
  39. data/spec/app_archetype/template/manifest_spec.rb +245 -0
  40. data/spec/app_archetype/template/plan_spec.rb +191 -0
  41. data/spec/app_archetype/template/source_spec.rb +60 -0
  42. data/spec/app_archetype/template/variable_manager_spec.rb +103 -0
  43. data/spec/app_archetype/template/variable_spec.rb +245 -0
  44. data/spec/app_archetype/template_manager_spec.rb +221 -0
  45. data/spec/core_ext/string_spec.rb +143 -0
  46. data/spec/spec_helper.rb +29 -0
  47. metadata +370 -0
@@ -0,0 +1,132 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe AppArchetype::CLI do
4
+ subject { described_class.new }
5
+
6
+ describe '.manager' do
7
+ let(:manager) { double }
8
+ let(:template_dir) { 'path/to/templates' }
9
+
10
+ before do
11
+ allow(described_class).to receive(:template_dir).and_return(template_dir)
12
+ allow(AppArchetype::TemplateManager).to receive(:new).and_return(manager)
13
+
14
+ allow(manager).to receive(:load)
15
+
16
+ @manager = described_class.manager
17
+ end
18
+
19
+ it 'creates a manager' do
20
+ expect(AppArchetype::TemplateManager)
21
+ .to have_received(:new)
22
+ .with(template_dir)
23
+
24
+ expect(manager).to have_received(:load)
25
+ expect(@manager).to eq manager
26
+ end
27
+ end
28
+
29
+ describe '.template_dir' do
30
+ let(:env_template_dir) { 'path/to/templates' }
31
+ let(:exist) { true }
32
+
33
+ before do
34
+ allow(ENV).to receive(:[])
35
+ .with('ARCHETYPE_TEMPLATE_DIR')
36
+ .and_return(env_template_dir)
37
+
38
+ allow(File).to receive(:exist?).and_return(exist)
39
+ end
40
+
41
+ it 'returns template dir' do
42
+ expect(described_class.template_dir).to eq env_template_dir
43
+ end
44
+
45
+ context 'when ARCHETYPE_TEMPLATE_DIR environment variable not set' do
46
+ let(:env_template_dir) { nil }
47
+
48
+ it 'raises environment not set error' do
49
+ expect { described_class.template_dir }.to raise_error(
50
+ RuntimeError,
51
+ 'ARCHETYPE_TEMPLATE_DIR environment variable not set'
52
+ )
53
+ end
54
+ end
55
+
56
+ context 'when templates do not exist' do
57
+ let(:exist) { false }
58
+
59
+ it 'raises environment not set error' do
60
+ expect { described_class.template_dir }.to raise_error(
61
+ RuntimeError,
62
+ "ARCHETYPE_TEMPLATE_DIR #{env_template_dir} does not exist"
63
+ )
64
+ end
65
+ end
66
+ end
67
+
68
+ describe '.editor' do
69
+ let(:env_editor) { 'ivm' }
70
+ let(:exit_status) { 0 }
71
+
72
+ before do
73
+ allow(ENV).to receive(:[])
74
+ .with('ARCHETYPE_EDITOR')
75
+ .and_return(env_editor)
76
+
77
+ allow(described_class).to receive(:`)
78
+ allow($?).to receive(:exitstatus).and_return(exit_status)
79
+ end
80
+
81
+ it 'returns editor' do
82
+ expect(described_class.editor).to eq env_editor
83
+ end
84
+
85
+ context 'when ARCHETYPE_EDITOR environment variable not set' do
86
+ let(:env_editor) { nil }
87
+
88
+ it 'raises environment not set error' do
89
+ expect { described_class.editor }.to raise_error(
90
+ RuntimeError,
91
+ 'ARCHETYPE_EDITOR environment variable not set'
92
+ )
93
+ end
94
+ end
95
+
96
+ context 'when editor check does not pass' do
97
+ let(:exit_status) { 1 }
98
+
99
+ before do
100
+ allow(AppArchetype::CLI).to receive(:print_warning)
101
+ described_class.editor
102
+ end
103
+
104
+ it 'logs a warning' do
105
+ expect(AppArchetype::CLI)
106
+ .to have_received(:print_warning)
107
+ .with(
108
+ "WARN: Configured editor #{env_editor} is not installed correctly "\
109
+ 'please check your configuration'
110
+ )
111
+ end
112
+ end
113
+ end
114
+
115
+ describe '.exit_on_failure?' do
116
+ it 'returns true' do
117
+ expect(described_class.exit_on_failure?).to be true
118
+ end
119
+ end
120
+
121
+ describe '#version' do
122
+ before do
123
+ allow(subject).to receive(:print_message)
124
+ subject.version
125
+ end
126
+
127
+ it 'prints current version number' do
128
+ expect(subject).to have_received(:print_message)
129
+ .with(AppArchetype::VERSION)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe AppArchetype::Generators do
4
+ describe '::TEMPLATE_MANIFEST' do
5
+ let(:name) { 'project' }
6
+
7
+ before do
8
+ @manifest = OpenStruct.new(
9
+ described_class::TEMPLATE_MANIFEST.call(name)
10
+ )
11
+ end
12
+
13
+ it 'has expected name' do
14
+ expect(@manifest.name).to eq name
15
+ end
16
+
17
+ it 'has expected version' do
18
+ expect(@manifest.version).to eq '1.0.0'
19
+ end
20
+
21
+ it 'has app archetype metadata' do
22
+ expect(@manifest.metadata['app_archetype']).not_to be nil
23
+ end
24
+
25
+ it 'renders app version into archetype metadata' do
26
+ expect(@manifest.metadata['app_archetype']['version'])
27
+ .to eq AppArchetype::VERSION
28
+ end
29
+
30
+ it 'has default variables' do
31
+ expect(@manifest.variables).to eq described_class::DEFAULT_VARS
32
+ end
33
+
34
+ it 'renders a manifest that is compliant with the manifest schema' do
35
+ expect(
36
+ JSON::Validator.fully_validate(
37
+ AppArchetype::Template::Manifest::SCHEMA,
38
+ @manifest.to_h.to_json,
39
+ strict: true
40
+ ).empty?
41
+ ).to be true
42
+ end
43
+ end
44
+
45
+ describe '::TEMPLATE_README' do
46
+ let(:name) { 'project' }
47
+
48
+ before do
49
+ @readme = described_class::TEMPLATE_README.call(name)
50
+ end
51
+
52
+ it 'renders name into heading' do
53
+ expect(@readme.include?("# #{name} Template")).to be true
54
+ end
55
+
56
+ it 'renders name into generation example' do
57
+ expect(@readme.include?("mkdir my_#{name}")).to be true
58
+ expect(@readme.include?("cd $HOME/Code/my_#{name}")).to be true
59
+ expect(@readme.include?("archetype render #{name}")).to be true
60
+ end
61
+ end
62
+
63
+ describe '.render_empty_template' do
64
+ let(:manifest_file) { double }
65
+ let(:manifest_path) { File.join(templates_path, 'manifest.json') }
66
+
67
+ let(:readme_file) { double }
68
+ let(:readme_path) { File.join(templates_path, 'README.md') }
69
+
70
+ let(:name) { 'new_template' }
71
+ let(:templates_path) { 'path/to/templates' }
72
+
73
+ let(:manifest) { 'manifest' }
74
+ let(:readme) { 'readme' }
75
+
76
+ before do
77
+ allow(File).to receive(:open)
78
+ .with(manifest_path, 'w')
79
+ .and_yield(manifest_file)
80
+ allow(manifest_file).to receive(:write)
81
+
82
+ allow(File).to receive(:open)
83
+ .with(readme_path, 'w')
84
+ .and_yield(readme_file)
85
+ allow(readme_file).to receive(:write)
86
+
87
+ allow(FileUtils).to receive(:mkdir_p)
88
+
89
+ allow(described_class::TEMPLATE_MANIFEST)
90
+ .to receive(:call).and_return(manifest)
91
+
92
+ allow(described_class::TEMPLATE_README)
93
+ .to receive(:call).and_return(readme)
94
+
95
+ described_class.render_empty_template(
96
+ name,
97
+ templates_path
98
+ )
99
+ end
100
+
101
+ it 'makes template dir' do
102
+ expect(FileUtils)
103
+ .to have_received(:mkdir_p)
104
+ .with(File.join(templates_path, name))
105
+ end
106
+
107
+ it 'renders blank manifest' do
108
+ expect(manifest_file)
109
+ .to have_received(:write)
110
+ .with("\"#{manifest}\"")
111
+ end
112
+
113
+ it 'renders readme' do
114
+ expect(readme_file)
115
+ .to have_received(:write)
116
+ .with(readme)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,86 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe AppArchetype::Logger do
4
+ let(:logger) { double(Logger) }
5
+ let(:err_logger) { double(Logger) }
6
+ let(:message) { 'All work and no play makes Jack a dull boy' }
7
+
8
+ subject { Object.new.extend(described_class) }
9
+
10
+ before do
11
+ allow(subject).to receive(:exit).and_return(double)
12
+ end
13
+
14
+ describe '.logger' do
15
+ let(:out) { double(STDOUT) }
16
+ before do
17
+ allow(Logger).to receive(:new).and_return(logger)
18
+ allow(logger).to receive(:formatter=)
19
+
20
+ subject.logger(out)
21
+ end
22
+
23
+ it 'creates a new logger to STDOUT' do
24
+ expect(Logger).to have_received(:new).with(out)
25
+ end
26
+ end
27
+
28
+ describe '.print_message' do
29
+ let(:logger) { double(Logger) }
30
+
31
+ before do
32
+ allow(subject).to receive(:logger).and_return(logger)
33
+ allow(logger).to receive(:info)
34
+ subject.print_message(message)
35
+ end
36
+
37
+ it 'prints message to stdout' do
38
+ expect(logger).to have_received(:info).with(message)
39
+ end
40
+ end
41
+
42
+ describe '.print_warning' do
43
+ let(:logger) { double(Logger) }
44
+
45
+ before do
46
+ allow(subject).to receive(:logger).and_return(logger)
47
+ allow(logger).to receive(:warn)
48
+ subject.print_warning(message)
49
+ end
50
+
51
+ it 'prints message to stdout' do
52
+ expect(logger).to have_received(:warn).with(message)
53
+ end
54
+ end
55
+
56
+ describe '.print_error' do
57
+ let(:logger) { double(Logger) }
58
+
59
+ before do
60
+ allow(subject).to receive(:logger).and_return(logger)
61
+ allow(logger).to receive(:error)
62
+ subject.print_error(message)
63
+ end
64
+
65
+ it 'prints message to stderr' do
66
+ expect(logger).to have_received(:error).with(message)
67
+ end
68
+ end
69
+
70
+ describe '.print_message_and_exit' do
71
+ let(:exit_code) { 2 }
72
+
73
+ before do
74
+ allow(subject).to receive(:print_message)
75
+ subject.print_message_and_exit(message, exit_code)
76
+ end
77
+
78
+ it 'prints message' do
79
+ expect(subject).to have_received(:print_message).with(message)
80
+ end
81
+
82
+ it 'exits with set status' do
83
+ expect(subject).to have_received(:exit).with(exit_code)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,291 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe AppArchetype::Renderer do
4
+ let(:logger) { double(Logger) }
5
+ let(:template) { AppArchetype::Template::Source.new('path/to/template') }
6
+ let(:destination) { 'path/to/destination' }
7
+ let(:variables) do
8
+ OpenStruct.new(
9
+ foo: 'bar'
10
+ )
11
+ end
12
+ let(:plan) do
13
+ AppArchetype::Template::Plan.new(
14
+ template,
15
+ variables,
16
+ destination_path: destination
17
+ )
18
+ end
19
+ let(:overwrite) { false }
20
+
21
+ before do
22
+ allow(subject).to receive(:logger).and_return(logger)
23
+ allow(logger).to receive(:info)
24
+ end
25
+
26
+ subject { described_class.new(plan, overwrite) }
27
+
28
+ describe '#render' do
29
+ let(:file) do
30
+ AppArchetype::Template::OutputFile.new(
31
+ 'path/to/source/file',
32
+ 'path/to/destination/file'
33
+ )
34
+ end
35
+
36
+ let(:erb_template) do
37
+ AppArchetype::Template::OutputFile.new(
38
+ 'path/to/template.erb',
39
+ 'path/to/destination/template'
40
+ )
41
+ end
42
+
43
+ let(:hbs_template) do
44
+ AppArchetype::Template::OutputFile.new(
45
+ 'path/to/template.hbs',
46
+ 'path/to/destination/template'
47
+ )
48
+ end
49
+
50
+ let(:directory) do
51
+ AppArchetype::Template::OutputFile.new(
52
+ 'path/to/source/dir',
53
+ 'path/to/destination/dir'
54
+ )
55
+ end
56
+
57
+ let(:file_double) { double(File) }
58
+
59
+ before do
60
+ allow(subject).to receive(:write_dir)
61
+ allow(subject).to receive(:render_erb_file)
62
+ allow(subject).to receive(:render_hbs_file)
63
+ allow(subject).to receive(:copy_file)
64
+
65
+ allow(File).to receive(:new).and_return(file_double)
66
+
67
+ allow(file).to receive(:source_file?).and_return(true)
68
+ allow(erb_template).to receive(:source_erb?).and_return(true)
69
+ allow(hbs_template).to receive(:source_hbs?).and_return(true)
70
+ allow(directory).to receive(:source_directory?).and_return(true)
71
+
72
+ plan.instance_variable_set(
73
+ :@files,
74
+ [file, erb_template, hbs_template, directory]
75
+ )
76
+
77
+ subject.render
78
+ end
79
+
80
+ it 'creates destination directory' do
81
+ expect(subject).to have_received(:write_dir).with(file_double)
82
+ end
83
+
84
+ it 'creates directory' do
85
+ expect(subject).to have_received(:write_dir).with(directory)
86
+ end
87
+
88
+ it 'renders erb template' do
89
+ expect(subject).to have_received(:render_erb_file).with(erb_template)
90
+ end
91
+
92
+ it 'renders hbs template' do
93
+ expect(subject).to have_received(:render_hbs_file).with(hbs_template)
94
+ end
95
+
96
+ it 'copies file' do
97
+ expect(subject).to have_received(:copy_file).with(file)
98
+ end
99
+
100
+ context 'when no method error is raised' do
101
+ before do
102
+ allow(subject).to receive(:render_erb_file).and_raise(NoMethodError.new)
103
+
104
+ plan.instance_variable_set(
105
+ :@files,
106
+ [erb_template]
107
+ )
108
+ end
109
+
110
+ it 'raises missing variable error' do
111
+ expect { subject.render }.to raise_error(
112
+ RuntimeError,
113
+ 'error rendering path/to/destination/template cannot '\
114
+ 'find variable `` in template'
115
+ )
116
+ end
117
+ end
118
+
119
+ context 'when no template is invalid' do
120
+ before do
121
+ allow(subject).to receive(:render_erb_file).and_raise(SyntaxError.new)
122
+
123
+ plan.instance_variable_set(
124
+ :@files,
125
+ [erb_template]
126
+ )
127
+ end
128
+
129
+ it 'raises missing variable error' do
130
+ expect { subject.render }.to raise_error(
131
+ RuntimeError,
132
+ 'error parsing path/to/destination/template template is invalid'
133
+ )
134
+ end
135
+ end
136
+ end
137
+
138
+ describe '#write_dir' do
139
+ let(:source_path) { 'path/to/template/dir' }
140
+ let(:dest_path) { 'path/to/destination/dir' }
141
+ let(:dir) { AppArchetype::Template::OutputFile.new(source_path, dest_path) }
142
+ let(:exists) { false }
143
+
144
+ before do
145
+ allow(FileUtils).to receive(:mkdir_p)
146
+ end
147
+
148
+ it 'creates directory' do
149
+ subject.write_dir(dir)
150
+ expect(FileUtils).to have_received(:mkdir_p).with(dest_path)
151
+ end
152
+ end
153
+
154
+ describe '#render_erb_file' do
155
+ let(:source_path) { 'path/to/template/file' }
156
+ let(:dest_path) { 'path/to/destination/file' }
157
+
158
+ let(:file) do
159
+ AppArchetype::Template::OutputFile.new(source_path, dest_path)
160
+ end
161
+
162
+ let(:input) do
163
+ <<~INPUT
164
+ this is the content of the <%= foo %> file
165
+ INPUT
166
+ end
167
+
168
+ let(:expected_output) do
169
+ <<~OUTPUT
170
+ this is the content of the bar file
171
+ OUTPUT
172
+ end
173
+
174
+ let(:write_double) { double }
175
+
176
+ before do
177
+ allow(File).to receive(:read).and_return(input)
178
+ allow(File).to receive(:open).and_yield(write_double)
179
+ allow(write_double).to receive(:write)
180
+
181
+ subject.render_erb_file(file)
182
+ end
183
+
184
+ it 'reads source' do
185
+ expect(File).to have_received(:read).with(source_path)
186
+ end
187
+
188
+ it 'writes rendered template' do
189
+ expect(write_double).to have_received(:write).with(expected_output)
190
+ end
191
+
192
+ context 'when a variable is missing' do
193
+ let(:vars) { OpenStruct.new }
194
+ let(:var_value) { '' }
195
+
196
+ it 'renders template with blank' do
197
+ expect(write_double).to have_received(:write).with(expected_output)
198
+ end
199
+ end
200
+ end
201
+
202
+ describe '#render_hbs_file' do
203
+ let(:source_path) { 'path/to/template/file' }
204
+ let(:dest_path) { 'path/to/destination/file' }
205
+
206
+ let(:file) do
207
+ AppArchetype::Template::OutputFile.new(source_path, dest_path)
208
+ end
209
+
210
+ let(:input) do
211
+ <<~INPUT
212
+ this is the content of the {{ foo }} file
213
+ INPUT
214
+ end
215
+
216
+ let(:expected_output) do
217
+ <<~OUTPUT
218
+ this is the content of the bar file
219
+ OUTPUT
220
+ end
221
+
222
+ let(:write_double) { double }
223
+
224
+ before do
225
+ allow(File).to receive(:read).and_return(input)
226
+ allow(File).to receive(:open).and_yield(write_double)
227
+ allow(write_double).to receive(:write)
228
+
229
+ subject.render_hbs_file(file)
230
+ end
231
+
232
+ it 'reads source' do
233
+ expect(File).to have_received(:read).with(source_path)
234
+ end
235
+
236
+ it 'writes rendered template' do
237
+ expect(write_double).to have_received(:write).with(expected_output)
238
+ end
239
+
240
+ context 'when a variable is missing' do
241
+ let(:vars) { OpenStruct.new }
242
+ let(:var_value) { '' }
243
+
244
+ it 'renders template with blank' do
245
+ expect(write_double).to have_received(:write).with(expected_output)
246
+ end
247
+ end
248
+ end
249
+
250
+ describe '#copy_file' do
251
+ let(:source_path) { 'path/to/template/file' }
252
+ let(:dest_path) { 'path/to/destination/file' }
253
+
254
+ let(:file) do
255
+ AppArchetype::Template::OutputFile.new(source_path, dest_path)
256
+ end
257
+
258
+ let(:exists) { false }
259
+
260
+ before do
261
+ allow(FileUtils).to receive(:cp)
262
+ allow(file).to receive(:exist?).and_return(exists)
263
+ end
264
+
265
+ it 'copies file' do
266
+ subject.copy_file(file)
267
+ expect(FileUtils).to have_received(:cp).with(source_path, dest_path)
268
+ end
269
+
270
+ context 'when file already exists and overwrite not allowed' do
271
+ let(:exists) { true }
272
+
273
+ it 'raises error' do
274
+ expect do
275
+ subject.copy_file(file)
276
+ end.to raise_error('cannot overwrite file')
277
+ end
278
+ end
279
+
280
+ context 'when file already exists and overwrite is allowed' do
281
+ let(:exists) { true }
282
+ let(:overwrite) { true }
283
+
284
+ it 'raises error' do
285
+ expect do
286
+ subject.copy_file(file)
287
+ end.not_to raise_error
288
+ end
289
+ end
290
+ end
291
+ end