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,95 @@
1
+ module AppArchetype
2
+ # Generators create empty projects for the app_archetype gem
3
+ module Generators
4
+ # Default variables provided to new projects
5
+ DEFAULT_VARS = {
6
+ 'example_string' => {
7
+ 'type' => 'string',
8
+ 'description' => 'This is an example string variable',
9
+ 'default' => 'default value'
10
+ },
11
+ 'example_random_string' => {
12
+ 'type' => 'string',
13
+ 'description' => 'Example call to helper to generate 25 char string',
14
+ 'value' => '#random_string,25'
15
+ }
16
+ }.freeze
17
+
18
+ # Function that creates a named, empty manifest for new templates
19
+ TEMPLATE_MANIFEST = lambda do |name|
20
+ {
21
+ 'name' => name,
22
+ 'version' => '1.0.0',
23
+ 'metadata' => {
24
+ 'app_archetype' => {
25
+ 'version' => AppArchetype::VERSION
26
+ }
27
+ },
28
+ 'variables' => DEFAULT_VARS
29
+ }
30
+ end
31
+
32
+ # Function that creates a readme for a new blank template
33
+ TEMPLATE_README = lambda do |name|
34
+ <<~MD
35
+ # #{name} Template
36
+
37
+ ## Installation
38
+
39
+ To generate:
40
+
41
+ ```bash
42
+ cd $HOME/Code
43
+ mkdir my_#{name}
44
+ cd $HOME/Code/my_#{name}
45
+
46
+ archetype render #{name}
47
+ ```
48
+ MD
49
+ end
50
+
51
+ class <<self
52
+ ##
53
+ # Render empty template renders a manifest and template folder at
54
+ # the given path.
55
+ #
56
+ # The name param will be rendered into the template manifest at
57
+ # runtime
58
+ #
59
+ # @param [String] name
60
+ # @param [String] path
61
+ #
62
+ def render_empty_template(name, path)
63
+ template_path = File.join(path, name)
64
+ manifest_path = File.join(path, 'manifest.json')
65
+ readme_path = File.join(path, 'README.md')
66
+
67
+ make_template_dir(template_path)
68
+ render_manifest(manifest_path, name)
69
+ render_readme(readme_path, name)
70
+ end
71
+
72
+ private
73
+
74
+ def make_template_dir(path)
75
+ FileUtils.mkdir_p(path)
76
+ end
77
+
78
+ def render_manifest(path, name)
79
+ File.open(path, 'w') do |f|
80
+ f.write(
81
+ TEMPLATE_MANIFEST.call(name).to_json
82
+ )
83
+ end
84
+ end
85
+
86
+ def render_readme(path, name)
87
+ File.open(path, 'w') do |f|
88
+ f.write(
89
+ TEMPLATE_README.call(name)
90
+ )
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,69 @@
1
+ module AppArchetype
2
+ # CLI Logging methods
3
+ module Logger
4
+ ##
5
+ # Creates logger for printing messages
6
+ #
7
+ # Sets the formatter to output only the provided message to the
8
+ # specified IO
9
+ #
10
+ # @param [IO] out - default: STDOUT
11
+ #
12
+ # @return [::Logger]
13
+ #
14
+ def logger(out = STDOUT)
15
+ @logger ||= ::Logger.new(out)
16
+ @logger.formatter = proc do |_sev, _time, _prog, msg|
17
+ "#{msg}\n"
18
+ end
19
+
20
+ @logger
21
+ end
22
+
23
+ ##
24
+ # Prints command line message to STDOUT
25
+ #
26
+ # For use when printing info messages for a user to STDOUT
27
+ #
28
+ # @param [String] message - message to be printed
29
+ #
30
+ def print_message(message)
31
+ logger.info(message)
32
+ end
33
+
34
+ ##
35
+ # Prints warning to STDOUT
36
+ #
37
+ # For use when printing warn messages to STDOUT
38
+ #
39
+ # @param [String] message - message to be printed
40
+ #
41
+ def print_warning(message)
42
+ logger.warn(message)
43
+ end
44
+
45
+ ##
46
+ # Prints error to STDERR
47
+ #
48
+ # For indicating fatal message to user
49
+ #
50
+ # @param [String] message - message to be printed
51
+ #
52
+ def print_error(message)
53
+ logger(STDERR).error(message)
54
+ end
55
+
56
+ ##
57
+ # Prints a message and then exits with given status code
58
+ #
59
+ # This will terminate the program with the given status code
60
+ #
61
+ # @param [String] message - message to be printed
62
+ # @param [Integer] exit_code - exit status (default: 1)
63
+ #
64
+ def print_message_and_exit(message, exit_code = 1)
65
+ print_message(message)
66
+ exit(exit_code)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,116 @@
1
+ require 'fileutils'
2
+ require 'erb'
3
+ require 'ruby-handlebars'
4
+
5
+ module AppArchetype
6
+ # Renderer renders a plan
7
+ class Renderer
8
+ include AppArchetype::Logger
9
+
10
+ ##
11
+ # Creates a renderer instance
12
+ #
13
+ # @param [AppArchetype::Template::Plan] plan
14
+ # @param [Boolean] overwrite
15
+ #
16
+ def initialize(plan, overwrite = false)
17
+ @plan = plan
18
+ @overwrite = overwrite
19
+ end
20
+
21
+ ##
22
+ # Renders plan to disk. The renderer is capable of:
23
+ # - creating directories
24
+ # - Rendering ERB templates with plan variables
25
+ # - Rendering Handlebars templates with plan variables
26
+ # - Copying static files
27
+ #
28
+ # When a template requests a varaible that does not exist within
29
+ # the plan - then the rendering process stops and a RuntimeError
30
+ # is raised
31
+ #
32
+ # Similarly when a template cannot be parsed a Runtime Error will
33
+ # be raised.
34
+ #
35
+ def render
36
+ write_dir(File.new(@plan.destination_path))
37
+
38
+ @last_file = ''
39
+ @plan.files.each do |file|
40
+ @last_file = file
41
+ if file.source_directory?
42
+ write_dir(file)
43
+ elsif file.source_erb?
44
+ render_erb_file(file)
45
+ elsif file.source_hbs?
46
+ render_hbs_file(file)
47
+ elsif file.source_file?
48
+ copy_file(file)
49
+ end
50
+ end
51
+ rescue NoMethodError => e
52
+ raise "error rendering #{@last_file.path} "\
53
+ "cannot find variable `#{e.name}` in template"
54
+ rescue SyntaxError
55
+ raise "error parsing #{@last_file.path} template is invalid"
56
+ end
57
+
58
+ ##
59
+ # Creates a directory at the specified location
60
+ #
61
+ # @param [AppArchetype::Template::OutputFile] file
62
+ #
63
+ def write_dir(file)
64
+ print_message("CREATE dir -> #{file.path}")
65
+
66
+ FileUtils.mkdir_p(file.path)
67
+ end
68
+
69
+ ##
70
+ # Renders erb template to output location
71
+ #
72
+ # @param [AppArchetype::Template::OutputFile] file
73
+ #
74
+ def render_erb_file(file)
75
+ raise 'cannot overwrite file' if file.exist? && !@overwrite
76
+
77
+ print_message("RENDER erb ->: #{file.path}")
78
+ input = File.read(file.source_file_path)
79
+ out = ERB.new(input).result(@plan.variables.instance_eval { binding })
80
+
81
+ File.open(file.path.gsub('.erb', ''), 'w+') { |f| f.write(out) }
82
+ end
83
+
84
+ ##
85
+ # Renders handlebars template to output location
86
+ #
87
+ # @param [AppArchetype::Template::OutputFile] file
88
+ #
89
+ def render_hbs_file(file)
90
+ raise 'cannot overwrite file' if file.exist? && !@overwrite
91
+
92
+ print_message("RENDER hbs ->: #{file.path}")
93
+
94
+ input = File.read(file.source_file_path)
95
+
96
+ hbs = Handlebars::Handlebars.new
97
+ out = hbs.compile(input).call(@plan.variables.to_h)
98
+
99
+ File.open(file.path.gsub('.hbs', ''), 'w+') { |f| f.write(out) }
100
+ end
101
+
102
+ ##
103
+ # Copies source file to planned path only ovewriting if permitted by the
104
+ # renderer.
105
+ #
106
+ # @param [AppArchetype::Template::OutputFile] file
107
+ #
108
+ def copy_file(file)
109
+ raise 'cannot overwrite file' if file.exist? && !@overwrite
110
+
111
+ print_message("COPY file ->: #{file.path}")
112
+
113
+ FileUtils.cp(file.source_file_path, file.path)
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,12 @@
1
+ require 'app_archetype/template/helpers'
2
+ require 'app_archetype/template/source'
3
+ require 'app_archetype/template/variable_manager'
4
+ require 'app_archetype/template/variable'
5
+ require 'app_archetype/template/manifest'
6
+ require 'app_archetype/template/plan'
7
+
8
+ module AppArchetype
9
+ # Template is a namespace for template components
10
+ module Template
11
+ end
12
+ end
@@ -0,0 +1,216 @@
1
+ require 'securerandom'
2
+
3
+ module AppArchetype
4
+ module Template
5
+ # Template rendering helpers
6
+ module Helpers
7
+ # dot provides a convenient way for a noop render at the
8
+ # beginning of dotfiles
9
+ def dot
10
+ ''
11
+ end
12
+
13
+ ##
14
+ # Returns this year as YYYY
15
+ #
16
+ # @return [String]
17
+ #
18
+ def this_year
19
+ Time.now.strftime('%Y')
20
+ end
21
+
22
+ ##
23
+ # Returns timestamp at current time
24
+ #
25
+ # @return [String]
26
+ #
27
+ def timestamp_now
28
+ Time.now.strftime('%Y%m%d%H%M%S%L')
29
+ end
30
+
31
+ ##
32
+ # Returns timestamp at utc current time
33
+ #
34
+ # @return [String]
35
+ #
36
+ def timestamp_utc_now
37
+ Time.now.utc.strftime('%Y%m%d%H%M%S%L')
38
+ end
39
+
40
+ ##
41
+ # Generates a random string at specified length
42
+ #
43
+ # @param [String] length
44
+ def random_string(length = '256')
45
+ length = length.to_i
46
+ key_set = ('a'..'z').to_a + ('A'..'Z').to_a + (0..9).to_a
47
+ (0...length).map { key_set[Random.rand(0..key_set.length)] }.join
48
+ end
49
+
50
+ ##
51
+ # Randomizes a given string by addding a slice of a hex
52
+ # to the end of it at the specified size.
53
+ #
54
+ # The template will pass through a string as arguments for this
55
+ # function, thus it must accept a string as an argument.
56
+ #
57
+ # @param [String] string
58
+ # @param [String] size
59
+ #
60
+ # @return [String]
61
+ def randomize(string, size = '5')
62
+ size = size.to_i
63
+ raise 'size must be an integer' unless size.is_a?(Integer) && size != 0
64
+ raise 'randomize supports up to 32 characters' if size > 32
65
+
66
+ hex = SecureRandom.hex
67
+ suffix = hex[hex.length - size..hex.length]
68
+
69
+ "#{string}_#{suffix}"
70
+ end
71
+
72
+ ##
73
+ # Converts a string to upper case
74
+ #
75
+ # @param [String] string
76
+ #
77
+ # @return [String]
78
+ #
79
+ def upcase(string)
80
+ string.upcase
81
+ end
82
+
83
+ ##
84
+ # Converts a string to lower case
85
+ #
86
+ # @param [String] string
87
+ #
88
+ # @return [String]
89
+ def downcase(string)
90
+ string.downcase
91
+ end
92
+
93
+ ##
94
+ # Joins a string with specified delimiter
95
+ #
96
+ # @param [String] delim
97
+ # @param [Array] strings
98
+ #
99
+ # @return [String]
100
+ def join(delim, *strings)
101
+ strings.join(delim)
102
+ end
103
+
104
+ ##
105
+ # Changes a string into snake case. Useful for
106
+ # converting class names to function or file names.
107
+ #
108
+ # @example
109
+ # str = 'AGreatExample'
110
+ # puts snake_case(str) # => outputs 'a_great_example'
111
+ #
112
+ # @param [String] string
113
+ #
114
+ # @return [String]
115
+ #
116
+ def snake_case(string)
117
+ return string.downcase if string =~ /\A[A-Z]+\z/
118
+
119
+ string
120
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
121
+ .gsub(/([a-z])([A-Z])/, '\1_\2')
122
+ .gsub(/\s/, '_')
123
+ .tr('-', '_')
124
+ .downcase
125
+ end
126
+
127
+ ##
128
+ # Downcase and converts a string into dashcase string
129
+ #
130
+ # @example
131
+ # str = 'AGreatExample'
132
+ # puts = dash_case(str) # => outputs 'a-great-example'
133
+
134
+ def dash_case(string)
135
+ return string.downcase if string =~ /\A[A-Z]+\z/
136
+
137
+ string
138
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1-\2')
139
+ .gsub(/([a-z])([A-Z])/, '\1-\2')
140
+ .tr(' ', '-')
141
+ .tr('_', '-')
142
+ .downcase
143
+ end
144
+
145
+ ##
146
+ # Camelcases a given string
147
+ #
148
+ # Usage:
149
+ # in_string = "an example"
150
+ # out_string = camel_case(in_string) => AnExample
151
+ #
152
+ # @param [String] string
153
+ # @return [String]
154
+ def camel_case(string)
155
+ str = snake_case(string)
156
+ snake_to_camel(str)
157
+ end
158
+
159
+ ##
160
+ # Converts snake case string to camelcase
161
+ #
162
+ # Usage:
163
+ # in_string = "an_example"
164
+ # out_string = snake_to_camel(in_string) => AnExample
165
+ #
166
+ # @param [String] string
167
+ # @return [String]
168
+ def snake_to_camel(string)
169
+ str = snake_case(string)
170
+ str.to_s.split('_').map(&:capitalize).join('')
171
+ end
172
+
173
+ ##
174
+ # Attempts to pluralize a word
175
+ #
176
+ # Usage:
177
+ # in_string = "Thing"
178
+ # out_string = pluralize(in_string) => "Things"
179
+ #
180
+ # @param [String] string
181
+ # @return [String]
182
+ #
183
+ def pluralize(string)
184
+ str = string.to_s
185
+
186
+ if str.match(/([^aeiouy]|qu)y$/i)
187
+ str = str.gsub(/y\Z/, 'ies')
188
+ else
189
+ str << 's'
190
+ end
191
+
192
+ str
193
+ end
194
+
195
+ ##
196
+ # Singularizes plural words
197
+ #
198
+ # Usage:
199
+ # in_string = "Things"
200
+ # out_string = singularize(in_string) => "Thing"
201
+ #
202
+ # @param [String] string
203
+ # @return [String]
204
+ #
205
+ def singularize(string)
206
+ str = string.to_s
207
+
208
+ if str.end_with?('ies')
209
+ str.gsub(/ies\Z/, 'y')
210
+ else
211
+ str.gsub(/s\Z/, '')
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end