app_archetype 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
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,75 @@
1
+ require 'ostruct'
2
+ require 'json'
3
+
4
+ module AppArchetype
5
+ module Template
6
+ # Manages a collection of variables
7
+ class VariableManager
8
+ def initialize(vars)
9
+ vars ||= []
10
+ @data = []
11
+
12
+ vars.each do |name, spec|
13
+ @data << AppArchetype::Template::Variable.new(name, spec)
14
+ end
15
+ end
16
+
17
+ ##
18
+ # Returns all variables managed by the variable manager.
19
+ #
20
+ # @return [Array]
21
+ #
22
+ def all
23
+ @data
24
+ end
25
+
26
+ ##
27
+ # Retrieves a variable by name from the variable manager.
28
+ #
29
+ # @param [String] name
30
+ #
31
+ # @return [AppArchetype::Template::Variable]
32
+ #
33
+ def get(name)
34
+ @data.detect { |var| var.name == name }
35
+ end
36
+
37
+ ##
38
+ # Creates a hash representation of variables.
39
+ #
40
+ # The variable name is the key, and the currrent value
41
+ # is the value.
42
+ #
43
+ # @return [Hash]
44
+ #
45
+ def to_h
46
+ var_hash = {}
47
+
48
+ @data.each do |var|
49
+ var_hash[var.name] = var.value
50
+ end
51
+
52
+ var_hash
53
+ end
54
+
55
+ ##
56
+ # Method missing retrieves variable from manager and
57
+ # returns the value to the caller if it is found.
58
+ #
59
+ # When a call is made to an undefined variable, a
60
+ # MethodMissing error will be raised.
61
+ #
62
+ # @param [Symbol] method
63
+ # @param [Array] args
64
+ #
65
+ # @return [Object]
66
+ #
67
+ def method_missing(method, *args)
68
+ var = get(method.to_s)
69
+ return var.value if var
70
+
71
+ super
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,113 @@
1
+ module AppArchetype
2
+ # Manager looks after a set of archetypes
3
+ class TemplateManager
4
+ ##
5
+ # Default filter is a lambda that returns true for all manifests
6
+ #
7
+ DEFAULT_QUERY = ->(_manifest) { return true }
8
+
9
+ attr_reader :manifests
10
+
11
+ ##
12
+ # Creates a manager
13
+ #
14
+ # @param [String] template_dir
15
+ #
16
+ def initialize(template_dir)
17
+ @template_dir = template_dir
18
+ @manifests = []
19
+ end
20
+
21
+ ##
22
+ # Loads and parses each manifest within the template directory into
23
+ # memory. Any invalid manifests are ignored and a message is printed
24
+ # to STDOUT indicating which manifest is not valid.
25
+ #
26
+ def load
27
+ Dir.glob(
28
+ File.join(@template_dir, '**', 'manifest.json*')
29
+ ).each do |manifest|
30
+ begin
31
+ @manifests << AppArchetype::Template::Manifest.new_from_file(manifest)
32
+ rescue StandardError
33
+ puts "WARN: `#{manifest}` is invalid, skipping"
34
+ next
35
+ end
36
+ end
37
+ end
38
+
39
+ ##
40
+ # Filter executes a query function in a select call against each manifest
41
+ # in the manager's collection.
42
+ #
43
+ # The given query function should be something that will evaluate to true
44
+ # when the manifest matches - this will hence filter the manifest set to
45
+ # the filtered set.
46
+ #
47
+ # @example
48
+ # manager = AppArchetype::TemplateManager.new('/path/to/templates')
49
+ # query = -> (manifest) { manifest.name = "fudge" }
50
+ #
51
+ # fudge_templates = manager.filter(query)
52
+ #
53
+ # @param [Lambda] query
54
+ #
55
+ # @return [Array]
56
+ def filter(query = DEFAULT_QUERY)
57
+ @manifests.select do |template|
58
+ query.call(template)
59
+ end
60
+ end
61
+
62
+ ##
63
+ # Searches for manifests matching given name and returns it to caller.
64
+ #
65
+ # @example:
66
+ # manager = AppArchetype::TemplateManager.new('/path/to/templates')
67
+ # fudge_manifest = manager.find('fudge')
68
+ #
69
+ # @param [String] name
70
+ #
71
+ # @return [Array]
72
+ #
73
+ def search_by_name(name)
74
+ name_query = lambda do |template|
75
+ template.name.include?(name)
76
+ end
77
+
78
+ filter(name_query)
79
+ end
80
+
81
+ ##
82
+ # Finds a specific manifest by name and returns it to the caller.
83
+ #
84
+ # It is possible that a more than one manifest is found when searching
85
+ # by name. If this happens while ignore_dupe is set to false, then a
86
+ # Runtime error is raised. If ignore_dupe is set to false then the first
87
+ # matching manifest is returned.
88
+ #
89
+ # @example:
90
+ # manager = AppArchetype::TemplateManager.new('/path/to/templates')
91
+ # fudge_manifest = manager.find('fudge')
92
+ #
93
+ # @param [String] name
94
+ # @param [Boolean] ignore_dupe
95
+ #
96
+ # @return [AppArchetype::Template::Manifest]
97
+ #
98
+ def find_by_name(name, ignore_dupe: false)
99
+ name_query = lambda do |template|
100
+ template.name == name
101
+ end
102
+
103
+ results = filter(name_query)
104
+
105
+ if results.count > 1 && ignore_dupe == false
106
+ raise 'more than one manifest matching the'\
107
+ ' given name were found'
108
+ end
109
+
110
+ results.first
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,6 @@
1
+ module AppArchetype
2
+ ##
3
+ # AppArchetype version
4
+ #
5
+ VERSION = '1.2.3'.freeze
6
+ end
@@ -0,0 +1,67 @@
1
+ require 'app_archetype/template/helpers'
2
+
3
+ # Archetype extensions for String
4
+ class String
5
+ ##
6
+ # Converts string to snake case
7
+ #
8
+ # @return [String]
9
+ #
10
+ def snake_case
11
+ helper.snake_case(self)
12
+ end
13
+
14
+ ##
15
+ # Converts string to dash case
16
+ #
17
+ # @return [String]
18
+ #
19
+ def dash_case
20
+ helper.dash_case(self)
21
+ end
22
+
23
+ ##
24
+ # Converts a string to camel case
25
+ #
26
+ # @return [String]
27
+ #
28
+ def camel_case
29
+ helper.camel_case(self)
30
+ end
31
+
32
+ ##
33
+ # Attempts to pluralize a word
34
+ #
35
+ # @return [String]
36
+ #
37
+ def pluralize
38
+ helper.pluralize(self)
39
+ end
40
+
41
+ ##
42
+ # Attempts to singluarize a word
43
+ #
44
+ # @return [String]
45
+ #
46
+ def singularize
47
+ helper.singularize(self)
48
+ end
49
+
50
+ ##
51
+ # Adds a random string of specified length at the end
52
+ #
53
+ # @return [String]
54
+ #
55
+ def randomize(size = 5)
56
+ helper.randomize(self, size.to_s)
57
+ end
58
+
59
+ private
60
+
61
+ # Instance helper methods
62
+ def helper
63
+ Object
64
+ .new
65
+ .extend(AppArchetype::Template::Helpers)
66
+ end
67
+ end
@@ -0,0 +1,99 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe AppArchetype::CLI::Presenters do
4
+ describe '.manifest_list' do
5
+ let(:manifest) do
6
+ double(
7
+ AppArchetype::Template::Manifest,
8
+ name: 'test_manifest',
9
+ version: '1.0.0'
10
+ )
11
+ end
12
+ let(:manifest_list_row) { ['test_manifest', '1.0.0'] }
13
+
14
+ let(:presenter) { double(CliFormat::Presenter) }
15
+ let(:manifests) { [manifest, manifest] }
16
+
17
+ before do
18
+ allow(presenter).to receive(:show)
19
+ allow(subject).to receive(:table).and_return(presenter)
20
+
21
+ described_class.manifest_list(manifests)
22
+ end
23
+
24
+ it 'builds table presenter' do
25
+ expect(subject).to have_received(:table).with(
26
+ header: AppArchetype::CLI::Presenters::RESULT_HEADER,
27
+ data: [manifest_list_row, manifest_list_row]
28
+ )
29
+ end
30
+
31
+ it 'shows table' do
32
+ expect(presenter).to have_received(:show)
33
+ end
34
+ end
35
+
36
+ describe '.variable_list' do
37
+ let(:variable) do
38
+ double(
39
+ AppArchetype::Template::Variable,
40
+ name: 'foo',
41
+ description: 'a foo',
42
+ default: 'yolo',
43
+ value: 'bar'
44
+ )
45
+ end
46
+ let(:variable_row) { ['foo', 'a foo', 'yolo'] }
47
+
48
+ let(:presenter) { double(CliFormat::Presenter) }
49
+ let(:variables) { [variable, variable] }
50
+
51
+ before do
52
+ allow(presenter).to receive(:show)
53
+ allow(subject).to receive(:table).and_return(presenter)
54
+
55
+ described_class.variable_list(variables)
56
+ end
57
+
58
+ it 'builds table presenter' do
59
+ expect(subject).to have_received(:table).with(
60
+ header: AppArchetype::CLI::Presenters::VARIABLE_HEADER,
61
+ data: [variable_row, variable_row]
62
+ )
63
+ end
64
+
65
+ it 'shows table' do
66
+ expect(presenter).to have_received(:show)
67
+ end
68
+ end
69
+
70
+ describe '.validation_results' do
71
+ let(:results) do
72
+ [
73
+ 'something went wrong',
74
+ 'something went wrong'
75
+ ]
76
+ end
77
+
78
+ let(:result_row) { ['something went wrong'] }
79
+ let(:presenter) { double(CliFormat::Presenter) }
80
+
81
+ before do
82
+ allow(presenter).to receive(:show)
83
+ allow(subject).to receive(:table).and_return(presenter)
84
+
85
+ described_class.validation_result(results)
86
+ end
87
+
88
+ it 'builds table presenter' do
89
+ expect(subject).to have_received(:table).with(
90
+ header: AppArchetype::CLI::Presenters::VALIDATION_HEADER,
91
+ data: [result_row, result_row]
92
+ )
93
+ end
94
+
95
+ it 'shows table' do
96
+ expect(presenter).to have_received(:show)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,292 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe AppArchetype::CLI::Prompts do
4
+ let(:var_name) { 'foo' }
5
+ let(:var_type) { 'string' }
6
+ let(:var_desc) { 'a foo' }
7
+ let(:var_default) { 'bar' }
8
+ let(:var_value) { nil }
9
+
10
+ let(:variable) do
11
+ double(
12
+ AppArchetype::Template::Variable,
13
+ name: var_name,
14
+ description: var_desc,
15
+ type: var_type,
16
+ default: var_default,
17
+ value: var_value
18
+ )
19
+ end
20
+
21
+ describe '::VAR_PROMPT_MESSAGE' do
22
+ before do
23
+ @prompt = described_class::VAR_PROMPT_MESSAGE.call(variable)
24
+ end
25
+
26
+ it 'displays prompt' do
27
+ expect(
28
+ @prompt.include?("Enter value for `#{var_name}` variable")
29
+ ).to be true
30
+ end
31
+
32
+ it 'displays variable description' do
33
+ expect(@prompt.include?("DESCRIPTION: #{var_desc}")).to be true
34
+ end
35
+
36
+ it 'displays variable type' do
37
+ expect(@prompt.include?("TYPE: #{var_type}")).to be true
38
+ end
39
+
40
+ it 'displays variable default' do
41
+ expect(@prompt.include?("DEFAULT: #{var_default}")).to be true
42
+ end
43
+ end
44
+
45
+ describe '.prompt' do
46
+ it 'returns a prompt' do
47
+ expect(described_class.prompt).to be_a(HighLine)
48
+ end
49
+ end
50
+
51
+ describe '.yes?' do
52
+ let(:prompt) { double(HighLine) }
53
+ let(:message) { 'Would you like to do stuff?' }
54
+ let(:response) { 'Y' }
55
+
56
+ before do
57
+ allow(subject).to receive(:prompt).and_return(prompt)
58
+ allow(prompt).to receive(:ask).and_yield(response)
59
+
60
+ @resp = subject.yes?(message)
61
+ end
62
+
63
+ it 'asks question' do
64
+ expect(prompt).to have_received(:ask).with("#{message} [Y/n]", String)
65
+ end
66
+
67
+ it 'returns true when user responds Y' do
68
+ expect(@resp).to be true
69
+ end
70
+
71
+ context 'when user inputs anything other than Y' do
72
+ let(:response) { 'Nope' }
73
+
74
+ it 'returns false' do
75
+ expect(@resp).to be false
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '.ask' do
81
+ let(:prompt) { double(HighLine) }
82
+
83
+ let(:message) { 'Give me some info' }
84
+ let(:validator) { String }
85
+ let(:default) { 'Default' }
86
+
87
+ let(:response) { 'Sure here it is' }
88
+
89
+ before do
90
+ allow(subject).to receive(:prompt).and_return(prompt)
91
+ allow(prompt).to receive(:ask).and_return(response)
92
+
93
+ @resp = subject.ask(message, validator: validator, default: default)
94
+ end
95
+
96
+ it 'asks question with validator' do
97
+ expect(prompt).to have_received(:ask).with(message, validator)
98
+ end
99
+
100
+ it 'returns user response as string' do
101
+ expect(@resp).to eq response
102
+ end
103
+
104
+ context 'when default is specifed and empty response' do
105
+ let(:response) { '' }
106
+
107
+ it 'returns default' do
108
+ expect(@resp).to eq default
109
+ end
110
+ end
111
+
112
+ context 'when empty and no default specified' do
113
+ let(:default) { nil }
114
+ let(:response) { '' }
115
+
116
+ it 'returns empty string' do
117
+ expect(@resp).to eq ''
118
+ end
119
+ end
120
+
121
+ context 'when asking for an integer' do
122
+ let(:response) { 1 }
123
+ let(:validator) { Integer }
124
+
125
+ it 'returns user response as integer' do
126
+ expect(@resp).to eq 1
127
+ end
128
+ end
129
+ end
130
+
131
+ describe '.delete_template' do
132
+ let(:prompt) { double(HighLine) }
133
+
134
+ let(:manifest_name) { 'test_manifest' }
135
+ let(:manifest) do
136
+ double(AppArchetype::Template::Manifest, name: manifest_name)
137
+ end
138
+
139
+ let(:choice) { false }
140
+
141
+ before do
142
+ allow(subject).to receive(:prompt).and_return(prompt)
143
+ allow(subject).to receive(:yes?).and_return(choice)
144
+
145
+ @result = described_class.delete_template(manifest)
146
+ end
147
+
148
+ it 'asks if it is okay to delete template' do
149
+ expect(subject).to have_received(:yes?)
150
+ .with('Are you sure you want to delete `test_manifest`?')
151
+ end
152
+
153
+ it 'returns user choice' do
154
+ expect(@result).to eq choice
155
+ end
156
+ end
157
+
158
+ describe '.variable_prompt_for' do
159
+ let(:has_value) { false }
160
+
161
+ before do
162
+ allow(variable).to receive(:value?).and_return(has_value)
163
+
164
+ allow(described_class).to receive(:boolean_variable_prompt)
165
+ allow(described_class).to receive(:integer_variable_prompt)
166
+ allow(described_class).to receive(:string_variable_prompt)
167
+ end
168
+
169
+ context 'when variable value is set' do
170
+ let(:has_value) { true }
171
+ let(:var_value) { 'some-value' }
172
+
173
+ it 'returns value' do
174
+ expect(
175
+ described_class.variable_prompt_for(variable)
176
+ ).to eq var_value
177
+ end
178
+ end
179
+
180
+ context 'when variable type is a boolean' do
181
+ let(:var_type) { 'boolean' }
182
+
183
+ before { described_class.variable_prompt_for(variable) }
184
+
185
+ it 'calls boolean variable prompt' do
186
+ expect(described_class)
187
+ .to have_received(:boolean_variable_prompt)
188
+ .with(variable)
189
+ end
190
+ end
191
+
192
+ context 'when variable type is an integer' do
193
+ let(:var_type) { 'integer' }
194
+
195
+ before { described_class.variable_prompt_for(variable) }
196
+
197
+ it 'calls integer variable prompt' do
198
+ expect(described_class)
199
+ .to have_received(:integer_variable_prompt)
200
+ .with(variable)
201
+ end
202
+ end
203
+
204
+ context 'when variable type is a string' do
205
+ let(:var_type) { 'string' }
206
+
207
+ before { described_class.variable_prompt_for(variable) }
208
+
209
+ it 'calls string variable prompt' do
210
+ expect(described_class)
211
+ .to have_received(:string_variable_prompt)
212
+ .with(variable)
213
+ end
214
+ end
215
+
216
+ context 'treats variables as strings by default' do
217
+ let(:var_type) { nil }
218
+
219
+ before { described_class.variable_prompt_for(variable) }
220
+
221
+ it 'calls string variable prompt' do
222
+ expect(described_class)
223
+ .to have_received(:string_variable_prompt)
224
+ .with(variable)
225
+ end
226
+ end
227
+ end
228
+
229
+ describe '.boolean_variable_prompt' do
230
+ let(:choice) { false }
231
+
232
+ before do
233
+ allow(subject).to receive(:yes?).and_return(choice)
234
+ @result = described_class.boolean_variable_prompt(variable)
235
+ end
236
+
237
+ it 'asks for boolean input' do
238
+ expect(subject).to have_received(:yes?)
239
+ .with(
240
+ AppArchetype::CLI::Prompts::VAR_PROMPT_MESSAGE.call(variable)
241
+ )
242
+ end
243
+
244
+ it 'returns user choice' do
245
+ expect(@result).to eq choice
246
+ end
247
+ end
248
+
249
+ describe '.integer_variable_prompt' do
250
+ let(:choice) { 1 }
251
+
252
+ before do
253
+ allow(subject).to receive(:ask).and_return(choice)
254
+ @result = described_class.integer_variable_prompt(variable)
255
+ end
256
+
257
+ it 'asks for boolean input' do
258
+ expect(subject).to have_received(:ask)
259
+ .with(
260
+ AppArchetype::CLI::Prompts::VAR_PROMPT_MESSAGE.call(variable),
261
+ default: variable.default,
262
+ validator: Integer
263
+ )
264
+ end
265
+
266
+ it 'returns user choice' do
267
+ expect(@result).to eq choice
268
+ end
269
+ end
270
+
271
+ describe '.string_variable_prompt' do
272
+ let(:choice) { 'some string' }
273
+
274
+ before do
275
+ allow(subject).to receive(:ask).and_return(choice)
276
+
277
+ @result = described_class.string_variable_prompt(variable)
278
+ end
279
+
280
+ it 'asks for boolean input' do
281
+ expect(subject).to have_received(:ask)
282
+ .with(
283
+ AppArchetype::CLI::Prompts::VAR_PROMPT_MESSAGE.call(variable),
284
+ default: variable.default
285
+ )
286
+ end
287
+
288
+ it 'returns user choice' do
289
+ expect(@result).to eq choice
290
+ end
291
+ end
292
+ end