app_archetype 1.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.doxie.json +25 -0
- data/.github/workflows/build.yml +25 -0
- data/.gitignore +22 -0
- data/.rubocop.yml +35 -0
- data/.ruby-version +1 -0
- data/CONTRIBUTING.md +51 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +172 -0
- data/LICENSE +21 -0
- data/README.md +138 -0
- data/Rakefile +19 -0
- data/app_archetype.gemspec +39 -0
- data/bin/archetype +20 -0
- data/lib/app_archetype.rb +14 -0
- data/lib/app_archetype/cli.rb +204 -0
- data/lib/app_archetype/cli/presenters.rb +106 -0
- data/lib/app_archetype/cli/prompts.rb +152 -0
- data/lib/app_archetype/generators.rb +95 -0
- data/lib/app_archetype/logger.rb +69 -0
- data/lib/app_archetype/renderer.rb +116 -0
- data/lib/app_archetype/template.rb +12 -0
- data/lib/app_archetype/template/helpers.rb +216 -0
- data/lib/app_archetype/template/manifest.rb +193 -0
- data/lib/app_archetype/template/plan.rb +172 -0
- data/lib/app_archetype/template/source.rb +39 -0
- data/lib/app_archetype/template/variable.rb +237 -0
- data/lib/app_archetype/template/variable_manager.rb +75 -0
- data/lib/app_archetype/template_manager.rb +113 -0
- data/lib/app_archetype/version.rb +6 -0
- data/lib/core_ext/string.rb +67 -0
- data/spec/app_archetype/cli/presenters_spec.rb +99 -0
- data/spec/app_archetype/cli/prompts_spec.rb +292 -0
- data/spec/app_archetype/cli_spec.rb +132 -0
- data/spec/app_archetype/generators_spec.rb +119 -0
- data/spec/app_archetype/logger_spec.rb +86 -0
- data/spec/app_archetype/renderer_spec.rb +291 -0
- data/spec/app_archetype/template/helpers_spec.rb +251 -0
- data/spec/app_archetype/template/manifest_spec.rb +245 -0
- data/spec/app_archetype/template/plan_spec.rb +191 -0
- data/spec/app_archetype/template/source_spec.rb +60 -0
- data/spec/app_archetype/template/variable_manager_spec.rb +103 -0
- data/spec/app_archetype/template/variable_spec.rb +245 -0
- data/spec/app_archetype/template_manager_spec.rb +221 -0
- data/spec/core_ext/string_spec.rb +143 -0
- data/spec/spec_helper.rb +29 -0
- 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
|