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