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