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,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