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
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'private_gem/tasks'
2
+ require 'bundler/gem_tasks'
3
+ require 'bump/tasks'
4
+ require 'rspec/core/rake_task'
5
+ require 'rubocop/rake_task'
6
+ require 'rubycritic/rake_task'
7
+ require 'yard'
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+ RuboCop::RakeTask.new
11
+
12
+ RubyCritic::RakeTask.new do |task|
13
+ task.paths = FileList['lib/**/*.rb'] - FileList['spec/**/*_spec.rb']
14
+ task.options = '--no-browser --path ./target/reports/critique'
15
+ end
16
+
17
+ YARD::Rake::YardocTask.new
18
+
19
+ task default: %i[spec rubocop rubycritic yard]
@@ -0,0 +1,39 @@
1
+ lib = File.expand_path('lib', __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'app_archetype/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'app_archetype'
7
+ spec.version = AppArchetype::VERSION
8
+ spec.authors = ['Andrew Bigger']
9
+ spec.email = ['andrew.bigger@gmail.com']
10
+ spec.summary = 'Code project template renderer'
11
+ spec.homepage = 'https://github.com/andrewbigger/app_archetype'
12
+ spec.license = 'MIT'
13
+
14
+ spec.files = `git ls-files -z`.split("\x0")
15
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
16
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
17
+ spec.require_paths = ['lib']
18
+
19
+ spec.add_dependency 'cli-format', '~> 0.2'
20
+ spec.add_dependency 'highline', '~> 2.0'
21
+ spec.add_dependency 'json', '~> 2.3'
22
+ spec.add_dependency 'jsonnet', '~> 0.3.0'
23
+ spec.add_dependency 'json-schema', '~> 2.8'
24
+ spec.add_dependency 'logger', '~> 1.4.2'
25
+ spec.add_dependency 'os', '~> 1.1'
26
+ spec.add_dependency 'ostruct', '~> 0.3'
27
+ spec.add_dependency 'ruby-handlebars', '~> 0.4'
28
+ spec.add_dependency 'thor', '~> 1.0'
29
+
30
+ spec.add_development_dependency 'bump', '~> 0.9'
31
+ spec.add_development_dependency 'private_gem', '~> 1.1'
32
+ spec.add_development_dependency 'pry', '~> 0.13'
33
+ spec.add_development_dependency 'rake', '~> 13.0'
34
+ spec.add_development_dependency 'rspec', '~> 3.9'
35
+ spec.add_development_dependency 'rubocop', '~> 0.92'
36
+ spec.add_development_dependency 'rubycritic', '~> 4.5'
37
+ spec.add_development_dependency 'simplecov', '~> 0.19'
38
+ spec.add_development_dependency 'yard', '~> 0.9'
39
+ end
data/bin/archetype ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib_path = File.expand_path('../lib', __dir__)
4
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
5
+
6
+ require 'pry'
7
+ require 'app_archetype/cli'
8
+
9
+ Signal.trap('INT') do
10
+ warn("\n#{caller.join("\n")}: interrupted")
11
+ exit(1)
12
+ end
13
+
14
+ begin
15
+ AppArchetype::CLI.start
16
+ rescue StandardError => e
17
+ puts "ERROR: #{e.message}"
18
+ puts e.backtrace.join("\n")
19
+ exit 1
20
+ end
@@ -0,0 +1,14 @@
1
+ require 'core_ext/string'
2
+
3
+ require 'app_archetype/logger'
4
+ require 'app_archetype/template'
5
+
6
+ require 'app_archetype/template_manager'
7
+ require 'app_archetype/renderer'
8
+ require 'app_archetype/generators'
9
+
10
+ require 'app_archetype/version'
11
+
12
+ # AppArchetype is the namespace for app_archetype
13
+ module AppArchetype
14
+ end
@@ -0,0 +1,204 @@
1
+ require 'logger'
2
+ require 'thor'
3
+ require 'highline'
4
+
5
+ require 'app_archetype'
6
+ require 'app_archetype/cli/presenters'
7
+ require 'app_archetype/cli/prompts'
8
+
9
+ module AppArchetype
10
+ # Command line interface helpers and actions
11
+ class CLI < Thor
12
+ package_name 'Archetype'
13
+
14
+ class <<self
15
+ ##
16
+ # Retrieves template dir from environment and raises error
17
+ # when TEMPLATE_DIR environment variable is not set.
18
+ #
19
+ # @return [String]
20
+ #
21
+ def template_dir
22
+ @template_dir = ENV['ARCHETYPE_TEMPLATE_DIR']
23
+
24
+ unless @template_dir
25
+ raise 'ARCHETYPE_TEMPLATE_DIR environment variable not set'
26
+ end
27
+
28
+ return @template_dir if File.exist?(@template_dir)
29
+
30
+ raise "ARCHETYPE_TEMPLATE_DIR #{@template_dir} does not exist"
31
+ end
32
+
33
+ ##
34
+ # Editor retrieves the chosen editor command to open text files
35
+ # and raises error when ARCHETYPE_EDITOR is not set.
36
+ #
37
+ # If we detect that the which command fails then we warn the user that
38
+ # something appears awry
39
+ #
40
+ # @return [String]
41
+ #
42
+ def editor
43
+ @editor = ENV['ARCHETYPE_EDITOR']
44
+ raise 'ARCHETYPE_EDITOR environment variable not set' unless @editor
45
+
46
+ `which #{@editor}`
47
+ if $?.exitstatus != 0
48
+ CLI.print_warning(
49
+ "WARN: Configured editor #{@editor} is not installed correctly "\
50
+ 'please check your configuration'
51
+ )
52
+ end
53
+
54
+ @editor
55
+ end
56
+
57
+ ##
58
+ # Template manager creates and loads a template manager
59
+ #
60
+ # @return [AppArchetype::TemplateManager]
61
+ #
62
+ def manager
63
+ @manager ||= AppArchetype::TemplateManager.new(template_dir)
64
+ @manager.load
65
+
66
+ @manager
67
+ end
68
+ end
69
+
70
+ include AppArchetype::Logger
71
+
72
+ def self.exit_on_failure?
73
+ true
74
+ end
75
+
76
+ desc 'version', 'Prints archetype gem version'
77
+ def version
78
+ print_message(AppArchetype::VERSION)
79
+ end
80
+ map %w[--version -v] => :version
81
+
82
+ desc 'list', 'Lists known templates in ARCHETYPE_TEMPLATE_DIR'
83
+ def list
84
+ print_message(
85
+ Presenters.manifest_list(
86
+ CLI.manager.manifests
87
+ )
88
+ )
89
+ end
90
+
91
+ desc 'path', 'Prints configured ARCHETYPE_TEMPLATE_DIR'
92
+ def path
93
+ print_message(
94
+ CLI.template_dir
95
+ )
96
+ end
97
+
98
+ desc 'open', 'Opens template manifest'
99
+ def open(name)
100
+ editor = CLI.editor
101
+ manifest = CLI.manager.find_by_name(name)
102
+
103
+ pid = Process.spawn("#{editor} #{manifest.path}")
104
+ Process.waitpid(pid)
105
+ end
106
+
107
+ desc 'new', 'Creates a template in ARCHETYPE_TEMPLATE_DIR'
108
+ def new(rel)
109
+ raise 'template rel not provided' unless rel
110
+
111
+ dest = File.join(CLI.template_dir, rel)
112
+ FileUtils.mkdir_p(dest)
113
+
114
+ name = File.basename(rel)
115
+ AppArchetype::Generators.render_empty_template(name, dest)
116
+
117
+ print_message("Template `#{name}` created at #{dest}")
118
+ end
119
+
120
+ desc 'delete', 'Deletes a template in ARCHETYPE_TEMPLATE_DIR'
121
+ def delete(name)
122
+ manifest = CLI.manager.find_by_name(name)
123
+ raise 'Cannot find template' unless manifest
124
+
125
+ proceed = Prompts.delete_template(manifest)
126
+
127
+ return unless proceed
128
+
129
+ FileUtils.rm_rf(manifest.parent_path)
130
+ print_message("Template `#{manifest.name}` has been removed")
131
+ end
132
+
133
+ desc 'validate', 'Runs a schema validation on given template'
134
+ def validate(name)
135
+ manifest = CLI.manager.find_by_name(name)
136
+ raise 'Cannot find template' unless manifest
137
+
138
+ result = manifest.validate
139
+
140
+ print_message("VALIDATION RESULTS FOR `#{name}`")
141
+ if result.any?
142
+ print_message(
143
+ Presenters.validation_result(result)
144
+ )
145
+
146
+ raise "Manifest `#{name}` is not valid"
147
+ end
148
+
149
+ print_message("Manifest `#{name}` is valid") if result.empty?
150
+ end
151
+
152
+ desc 'variables', 'Prints template variables'
153
+ def variables(search_term)
154
+ result = CLI.manager.find_by_name(search_term)
155
+ return print_message("Manifest `#{search_term}` not found") unless result
156
+
157
+ print_message("VARIABLES FOR `#{search_term}`")
158
+ print_message(
159
+ Presenters.variable_list(result.variables.all)
160
+ )
161
+ end
162
+
163
+ desc 'find', 'Finds a template in collection by name'
164
+ def find(search_term)
165
+ result = CLI.manager.find_by_name(search_term)
166
+ return print_message("Manifest `#{search_term}` not found") unless result
167
+
168
+ print_message("SEARCH RESULTS FOR `#{search_term}`")
169
+ print_message(
170
+ Presenters.manifest_list([result])
171
+ )
172
+ end
173
+
174
+ desc 'render', 'Renders project template'
175
+ method_option(
176
+ :overwrite,
177
+ type: :boolean,
178
+ default: false,
179
+ desc: 'Option to overwrite any existing files'
180
+ )
181
+ def render(manifest_name)
182
+ manifest = CLI.manager.find_by_name(manifest_name)
183
+
184
+ raise "Unable to find manifest `#{manifest_name}`" unless manifest
185
+
186
+ template = manifest.template
187
+ template.load
188
+
189
+ manifest.variables.all.each do |var|
190
+ value = Prompts.variable_prompt_for(var)
191
+ var.set!(value)
192
+ end
193
+
194
+ plan = AppArchetype::Template::Plan.new(
195
+ template,
196
+ manifest.variables,
197
+ destination_path: FileUtils.pwd,
198
+ overwrite: options.overwrite
199
+ )
200
+ plan.devise
201
+ plan.execute
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,106 @@
1
+ require 'cli-format'
2
+
3
+ module AppArchetype
4
+ class CLI < Thor
5
+ # CLI output presenters
6
+ module Presenters
7
+ ##
8
+ # Output table header
9
+ #
10
+ RESULT_HEADER = %w[NAME VERSION].freeze
11
+
12
+ ##
13
+ # Variable table header
14
+ #
15
+ VARIABLE_HEADER = %w[NAME DESCRIPTION DEFAULT].freeze
16
+
17
+ ##
18
+ # Validation result table header
19
+ #
20
+ VALIDATION_HEADER = %w[ERROR].freeze
21
+
22
+ class <<self
23
+ ##
24
+ # Creates a presenter for given data
25
+ #
26
+ # Accepts header row data and has configurable format.
27
+ #
28
+ # Header must be array of string
29
+ #
30
+ # Data is array of arrays where the inner array is a row.
31
+ #
32
+ # Format by default is a table, although can be 'csv'
33
+ # or 'json'.
34
+ #
35
+ # @param header [Array]
36
+ # @param data [Array]
37
+ # @param format [String]
38
+ #
39
+ # @return [CliFormat::Presenter]
40
+ #
41
+ def table(header: [], data: [], format: 'table')
42
+ has_header = header.any?
43
+ opts = { header: has_header, format: format }
44
+
45
+ presenter = CliFormat::Presenter.new(opts)
46
+ presenter.header = header if has_header
47
+
48
+ data.each { |row| presenter.rows << row }
49
+
50
+ presenter
51
+ end
52
+
53
+ ##
54
+ # Builds a table of manifest information
55
+ #
56
+ # @param [Array] manifests
57
+ #
58
+ def manifest_list(manifests)
59
+ table(
60
+ header: RESULT_HEADER,
61
+ data: manifests.map do |manifest|
62
+ [
63
+ manifest.name,
64
+ manifest.version
65
+ ]
66
+ end
67
+ ).show
68
+ end
69
+
70
+ ##
71
+ # Builds a table of variable information
72
+ #
73
+ # @param [Array] variables
74
+ #
75
+ def variable_list(variables)
76
+ table(
77
+ header: VARIABLE_HEADER,
78
+ data: variables.map do |variable|
79
+ [
80
+ variable.name,
81
+ variable.description,
82
+ variable.default
83
+ ]
84
+ end
85
+ ).show
86
+ end
87
+
88
+ ##
89
+ # Builds a table for manifest validation results
90
+ #
91
+ # @param [Array] results
92
+ #
93
+ def validation_result(results)
94
+ table(
95
+ header: VALIDATION_HEADER,
96
+ data: results.map do |result|
97
+ [
98
+ result
99
+ ]
100
+ end
101
+ ).show
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,152 @@
1
+ require 'highline'
2
+
3
+ module AppArchetype
4
+ class CLI < Thor
5
+ # CLI output presenters
6
+ module Prompts
7
+ ##
8
+ # Variable prompt question. Asked when evaluating template
9
+ # variables
10
+ #
11
+ # @param [AppArchetype::Template::Variable] variable
12
+ #
13
+ # @return [Proc]
14
+ #
15
+ VAR_PROMPT_MESSAGE = lambda do |variable|
16
+ "\nEnter value for `#{variable.name}` variable\n\n"\
17
+ "DESCRIPTION: #{variable.description}\n"\
18
+ "TYPE: #{variable.type}\n"\
19
+ "DEFAULT: #{variable.default}"
20
+ end
21
+
22
+ class <<self
23
+ ##
24
+ # Prompt returns a TTY prompt object for asking the user
25
+ # questions.
26
+ #
27
+ # @return [HighLine]
28
+ def prompt
29
+ HighLine.new
30
+ end
31
+
32
+ ##
33
+ # A yes/no prompt for asking the user a yes or no question.
34
+ #
35
+ # @return [Boolean]
36
+ #
37
+ def yes?(message)
38
+ prompt.ask("#{message} [Y/n]", String) { |input| input.strip == 'Y' }
39
+ end
40
+
41
+ ##
42
+ # Prompt for requesting user input.
43
+ #
44
+ # A default can be provided in the event the user does not
45
+ # provide an answer.
46
+ #
47
+ # Validator also performs type conversion by default it is
48
+ # a string
49
+ #
50
+ # @param message [String]
51
+ # @param default [Object]
52
+ # @param validator [Object|Lambda]
53
+ #
54
+ # @return [Object]
55
+ #
56
+ def ask(message, validator: String, default: nil)
57
+ resp = prompt.ask(message, validator)
58
+ return default if !default.nil? && resp.to_s.empty?
59
+
60
+ resp
61
+ end
62
+
63
+ ##
64
+ # Y/N prompt to ensure user is sure they wish to delete
65
+ # the selected template
66
+ #
67
+ # @param [AppArchetype::Template::Manifest] manifest
68
+ #
69
+ # @return [Boolean]
70
+ def delete_template(manifest)
71
+ yes?(
72
+ "Are you sure you want to delete `#{manifest.name}`?"
73
+ )
74
+ end
75
+
76
+ ##
77
+ # Returns a variable prompt based on the type of variable
78
+ # required. Once prompt has been executed, the response is
79
+ # returned to the caller.
80
+ #
81
+ # When the value is set in the manifest, the set value is
82
+ # returned without a prompt.
83
+ #
84
+ # For boolean and integer variables, the relevant prompt
85
+ # function is called.
86
+ #
87
+ # By default the string variable prompt will be used.
88
+ #
89
+ # @param [AppArchetype::Template::Variable] var
90
+ #
91
+ # @return [Object]
92
+ #
93
+ def variable_prompt_for(var)
94
+ return var.value if var.value?
95
+ return boolean_variable_prompt(var) if var.type == 'boolean'
96
+ return integer_variable_prompt(var) if var.type == 'integer'
97
+
98
+ string_variable_prompt(var)
99
+ end
100
+
101
+ ##
102
+ # Prompt for boolean variable. This quizzes the user as to
103
+ # whether they want the variable set or not. The response
104
+ # is returned to the caller.
105
+ #
106
+ # @param [AppArchetype::Template::Variable] variable
107
+ #
108
+ # @return [Boolean]
109
+ #
110
+ def boolean_variable_prompt(variable)
111
+ yes?(
112
+ VAR_PROMPT_MESSAGE.call(variable)
113
+ )
114
+ end
115
+
116
+ ##
117
+ # Prompt for integer. This quizzes the user for their
118
+ # choice and then attempts to convert it to an integer.
119
+ #
120
+ # In the event a non integer value is entered, a
121
+ # RuntimeError is thrown.
122
+ #
123
+ # @param [AppArchetype::Template::Variable] variable
124
+ #
125
+ # @return [Integer]
126
+ #
127
+ def integer_variable_prompt(variable)
128
+ ask(
129
+ VAR_PROMPT_MESSAGE.call(variable),
130
+ default: variable.default,
131
+ validator: Integer
132
+ )
133
+ end
134
+
135
+ ##
136
+ # Prompt for a string. Asks user for input and returns
137
+ # it.
138
+ #
139
+ # @param [AppArchetype::Template::Variable] variable
140
+ #
141
+ # @return [String]
142
+ #
143
+ def string_variable_prompt(variable)
144
+ ask(
145
+ VAR_PROMPT_MESSAGE.call(variable),
146
+ default: variable.default
147
+ )
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end