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,193 @@
1
+ require 'json-schema'
2
+ require 'ostruct'
3
+ require 'jsonnet'
4
+
5
+ module AppArchetype
6
+ module Template
7
+ # Manifest is a description of an archetype
8
+ class Manifest
9
+ ##
10
+ # Minimum supported archetype version
11
+ #
12
+ MIN_ARCHETYPE_VERSION = '1.0.0'.freeze
13
+
14
+ ##
15
+ # Manifest JSON schema
16
+ #
17
+ SCHEMA = {
18
+ type: 'object',
19
+ required: %w[name version metadata variables],
20
+
21
+ properties: {
22
+ name: {
23
+ type: 'string'
24
+ },
25
+ version: {
26
+ type: 'string'
27
+ },
28
+ metadata: {
29
+ type: 'object',
30
+ required: %w[app_archetype],
31
+
32
+ properties: {
33
+ app_archetype: {
34
+ type: 'object',
35
+ required: %w[version]
36
+ }
37
+ }
38
+ },
39
+ variables: {
40
+ type: 'object'
41
+ }
42
+ }
43
+ }.freeze
44
+
45
+ class <<self
46
+ ##
47
+ # Creates a [AppArchetype::Template] from a manifest json so long as the
48
+ # manifest is compatible with this version of AppArchetype.
49
+ #
50
+ # @param [String] file_path
51
+ #
52
+ def new_from_file(file_path)
53
+ manifest = Jsonnet.evaluate(
54
+ File.read(file_path)
55
+ )
56
+
57
+ if incompatible?(manifest)
58
+ raise 'provided manifest is invalid or incompatible with '\
59
+ 'this version of app archetype'
60
+ end
61
+
62
+ new(
63
+ file_path,
64
+ manifest
65
+ )
66
+ end
67
+
68
+ ##
69
+ # Incompatible returns true if the current manifest is not compatible
70
+ # with this version of AppArchetype.
71
+ #
72
+ # A manifest is not compatible if it was created with a version greater
73
+ # than this the installed version.
74
+ #
75
+ # @param [Hash] manifest
76
+ #
77
+ # @return [Boolean]
78
+ #
79
+ def incompatible?(manifest)
80
+ manifest_version = manifest['metadata']['app_archetype']['version']
81
+ return true if manifest_version < MIN_ARCHETYPE_VERSION
82
+ return true if manifest_version > AppArchetype::VERSION
83
+ rescue NoMethodError
84
+ true
85
+ end
86
+ end
87
+
88
+ attr_reader :path, :data, :variables
89
+
90
+ ##
91
+ # Creates a manifest and memoizes the manifest data hash as a Hashe::Map
92
+ #
93
+ # On initialize the manifest variables are retrieved and memoized for use
94
+ # in rendering the templates.
95
+ #
96
+ # @param [String] path
97
+ # @param [Hash] data
98
+ #
99
+ def initialize(path, data)
100
+ @path = path
101
+ @data = OpenStruct.new(data)
102
+ @variables = AppArchetype::Template::VariableManager
103
+ .new(@data.variables)
104
+ end
105
+
106
+ ##
107
+ # Manifest name getter
108
+ #
109
+ # @return [String]
110
+ #
111
+ def name
112
+ @data.name
113
+ end
114
+
115
+ ##
116
+ # Manifest version getter
117
+ #
118
+ # @return [String]
119
+ #
120
+ def version
121
+ @data.version
122
+ end
123
+
124
+ ##
125
+ # Manifest metadata getter
126
+ #
127
+ # @return [String]
128
+ #
129
+ def metadata
130
+ @data.metadata
131
+ end
132
+
133
+ ##
134
+ # Parent path of the manifest (working directory)
135
+ #
136
+ # @return [String]
137
+ def parent_path
138
+ File.dirname(@path)
139
+ end
140
+
141
+ ##
142
+ # Template files path
143
+ #
144
+ # @return [String]
145
+ #
146
+ def template_path
147
+ File.join(parent_path, 'template')
148
+ end
149
+
150
+ ##
151
+ # Loads the template that is adjacent to the manifest.json or
152
+ # manifest.jsonnet file.
153
+ #
154
+ # If the template cannot be found, a RuntimeError explaining that
155
+ # the template cannot
156
+ # be found is raised.
157
+ #
158
+ # Loaded template is memoized for the current session.
159
+ #
160
+ # @return [AppArchetype::Template::Source]
161
+ def template
162
+ unless File.exist?(template_path)
163
+ raise "cannot find template for manifest #{name}"
164
+ end
165
+
166
+ @template ||= AppArchetype::Template::Source.new(template_path)
167
+ @template
168
+ end
169
+
170
+ ##
171
+ # Runs a schema validation on the given manifest to determine whether
172
+ # the schema is valid. Returns an array of validation messages.
173
+ #
174
+ # @return [Array]
175
+ def validate
176
+ JSON::Validator.fully_validate(
177
+ SCHEMA,
178
+ @data.to_h.to_json,
179
+ strict: true
180
+ )
181
+ end
182
+
183
+ ##
184
+ # Returns true if manifest is valid
185
+ #
186
+ # @return [Boolean]
187
+ #
188
+ def valid?
189
+ validate.empty?
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,172 @@
1
+ require 'ruby-handlebars'
2
+ require_relative '../../core_ext/string'
3
+
4
+ module AppArchetype
5
+ module Template
6
+ # Plan builds an in memory representation of template output
7
+ class Plan
8
+ attr_reader :template, :destination_path, :files, :variables
9
+
10
+ ##
11
+ # Creates a new plan from given source and variables.
12
+ #
13
+ # @param [AppArchetype::Template::Source] template
14
+ # @param [AppArchetype::Template::VariableManager] variables
15
+ # @param [String] destination_path
16
+ # @param [Boolean] overwrite
17
+ #
18
+ def initialize(
19
+ template,
20
+ variables,
21
+ destination_path: nil,
22
+ overwrite: false
23
+ )
24
+ @template = template
25
+ @destination_path = destination_path
26
+ @files = []
27
+ @variables = variables
28
+ @overwrite = overwrite
29
+ end
30
+
31
+ ##
32
+ # Devise builds an in memory representation of what needs to be done to
33
+ # render the template.
34
+ #
35
+ # When the destination path does not exist - a RuntimeError is raised -
36
+ # however at this stage we should always have a destination path to
37
+ # render to.
38
+ #
39
+ def devise
40
+ raise 'destination path does not exist' unless destination_exist?
41
+
42
+ @template.files.each do |file|
43
+ @files << OutputFile.new(
44
+ file,
45
+ render_dest_file_path(file)
46
+ )
47
+ end
48
+ end
49
+
50
+ ##
51
+ # Execute will render the plan to disk
52
+ #
53
+ def execute
54
+ renderer = Renderer.new(
55
+ self,
56
+ @overwrite
57
+ )
58
+
59
+ renderer.render
60
+ end
61
+
62
+ ##
63
+ # Check for whether the destination exists
64
+ #
65
+ # @return [Boolean]
66
+ def destination_exist?
67
+ return false unless @destination_path
68
+
69
+ File.exist?(
70
+ File.dirname(@destination_path)
71
+ )
72
+ end
73
+
74
+ ##
75
+ # Determines what the destination file path is going to be by taking
76
+ # the source path, subbing the template path and then joining it
77
+ # with the specified destination path.
78
+ #
79
+ # Calls render path to handle any handlebars moustaches included within
80
+ # the file name.
81
+ #
82
+ # @param [String] source_path
83
+ #
84
+ # @return [String]
85
+ def render_dest_file_path(source_path)
86
+ rel_path = render_path(
87
+ source_path.gsub(@template.path, '')
88
+ )
89
+
90
+ File.join(@destination_path, rel_path)
91
+ end
92
+
93
+ ##
94
+ # Renders template variables into any moustaches included in the filename
95
+ #
96
+ # This permits us to have variable file names as well as variable file
97
+ # content.
98
+ #
99
+ # @param [String] path
100
+ #
101
+ # @return [String]
102
+ #
103
+ def render_path(path)
104
+ hbs = Handlebars::Handlebars.new
105
+ hbs.compile(path).call(@variables.to_h)
106
+ end
107
+ end
108
+
109
+ # OutputFile represents a plan action, in other words holds a reference
110
+ # to a source file, and what the output is likely to be
111
+ class OutputFile
112
+ attr_reader :source_file_path, :path
113
+
114
+ ##
115
+ # Creates an output file
116
+ #
117
+ # @param [String] source_file_path
118
+ # @param [String] path
119
+ #
120
+ def initialize(source_file_path, path)
121
+ @source_file_path = source_file_path
122
+ @path = path
123
+ end
124
+
125
+ ##
126
+ # Evaluates whether the source file is a directory
127
+ #
128
+ # @return [Boolean]
129
+ #
130
+ def source_directory?
131
+ File.directory?(@source_file_path)
132
+ end
133
+
134
+ ##
135
+ # Evaluates whether the source file is a erb template
136
+ #
137
+ # @return [Boolean]
138
+ #
139
+ def source_erb?
140
+ File.extname(@source_file_path) == '.erb'
141
+ end
142
+
143
+ ##
144
+ # Evaluates whether the source file is a handlebars template
145
+ #
146
+ # @return [Boolean]
147
+ #
148
+ def source_hbs?
149
+ File.extname(@source_file_path) == '.hbs'
150
+ end
151
+
152
+ ##
153
+ # Evaluates whether the source file is a file as opposed to
154
+ # being a directory.
155
+ #
156
+ # @return [Boolean]
157
+ #
158
+ def source_file?
159
+ File.file?(@source_file_path)
160
+ end
161
+
162
+ ##
163
+ # Evaluates whether the source file actually exists
164
+ #
165
+ # @return [Boolean]
166
+ #
167
+ def exist?
168
+ File.exist?(@path)
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,39 @@
1
+ module AppArchetype
2
+ module Template
3
+ # Source is an in memory representation of a template source
4
+ class Source
5
+ attr_reader :path, :files
6
+
7
+ ##
8
+ # Creates a templatte source from path and initializes file array.
9
+ #
10
+ # @param [String] path
11
+ #
12
+ def initialize(path)
13
+ @path = path
14
+ @files = []
15
+ end
16
+
17
+ ##
18
+ # Loads template files into memory. Will raise a RuntimeError if
19
+ # by the time we're loading the source no longer exists.
20
+ #
21
+ def load
22
+ raise 'template source does not exist' unless exist?
23
+
24
+ Dir.glob(File.join(@path, '**', '*')).each do |file|
25
+ @files << file
26
+ end
27
+ end
28
+
29
+ ##
30
+ # Evaluates whether template source still exists.
31
+ #
32
+ # @return [Boolean]
33
+ #
34
+ def exist?
35
+ File.exist?(@path)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,237 @@
1
+ require 'ostruct'
2
+ require 'json'
3
+
4
+ module AppArchetype
5
+ module Template
6
+ # Variable is a class representing a single variable
7
+ class Variable
8
+ ##
9
+ # Default variable type (String)
10
+ #
11
+ DEFAULT_TYPE = 'string'.freeze
12
+
13
+ ##
14
+ # Default value map
15
+ #
16
+ DEFAULT_VALUES = {
17
+ 'string' => '',
18
+ 'boolean' => false,
19
+ 'integer' => 0
20
+ }.freeze
21
+
22
+ ##
23
+ # String validation function. Ensures given input
24
+ # is indeed a string.
25
+ #
26
+ # @param [Object] input
27
+ #
28
+ # @return [Boolean]
29
+ #
30
+ STRING_VALIDATOR = lambda do |input|
31
+ input.is_a?(String)
32
+ end
33
+
34
+ ##
35
+ # Boolean validation function. Ensures given input
36
+ # is a boolean.
37
+ #
38
+ # @param [Object] input
39
+ #
40
+ # @return [Boolean]
41
+ #
42
+ BOOLEAN_VALIDATOR = lambda do |input|
43
+ [true, false, 'true', 'false'].include?(input)
44
+ end
45
+
46
+ ##
47
+ # Integer validation function. Ensures given input is
48
+ # an integer.
49
+ #
50
+ # @param [Object] input
51
+ #
52
+ # @return [Boolean]
53
+ #
54
+ INTEGER_VALIDATOR = lambda do |input|
55
+ input != '0' && input.to_i != 0
56
+ end
57
+
58
+ ##
59
+ # Maps type to validation function
60
+ #
61
+ VALIDATORS = {
62
+ 'string' => STRING_VALIDATOR,
63
+ 'boolean' => BOOLEAN_VALIDATOR,
64
+ 'integer' => INTEGER_VALIDATOR
65
+ }.freeze
66
+
67
+ ##
68
+ # Default validation function (string validator)
69
+ #
70
+ DEFAULT_VALIDATOR = STRING_VALIDATOR
71
+
72
+ attr_reader :name
73
+
74
+ def initialize(name, spec)
75
+ @name = name
76
+ @spec = OpenStruct.new(spec)
77
+ @value = @spec.value
78
+ end
79
+
80
+ ##
81
+ # Sets value of variable so long as it's valid.
82
+ #
83
+ # A runtime error will be raised if the valdiation
84
+ # fails for the given value.
85
+ #
86
+ # Has a side effect of setting @value instance variable
87
+ #
88
+ # @param [Object] value
89
+ def set!(value)
90
+ raise 'invalid value' unless valid?(value)
91
+
92
+ @value = value
93
+ end
94
+
95
+ ##
96
+ # Returns default value for the variable.
97
+ #
98
+ # In the event the manifest does not specify a default
99
+ # one will be picked from the DEFAULT_VALUES map based on
100
+ # the variable's type.
101
+ #
102
+ # @return [Object]
103
+ #
104
+ def default
105
+ return DEFAULT_VALUES[type] unless @spec.default
106
+
107
+ @spec.default
108
+ end
109
+
110
+ ##
111
+ # Returns variable description.
112
+ #
113
+ # In the event the manifest does not specify a description
114
+ # an empty string will be returned.
115
+ #
116
+ # @return [String]
117
+ #
118
+ def description
119
+ return '' unless @spec.description
120
+
121
+ @spec.description
122
+ end
123
+
124
+ ##
125
+ # Returns variable type.
126
+ #
127
+ # In the event the manifest does not specify a type, the
128
+ # default type of String will be returned.
129
+ #
130
+ # @return [String]
131
+ #
132
+ def type
133
+ return DEFAULT_TYPE unless @spec.type
134
+
135
+ @spec.type
136
+ end
137
+
138
+ ##
139
+ # Returns variable value.
140
+ #
141
+ # If the value has not been set (i.e. overridden) then the
142
+ # default value will be returned.
143
+ #
144
+ # Values set beginning with `#` are passed into the helpers
145
+ # class and evaluated as functions. That permits the manifest
146
+ # to use string helpers as values from the manifest.
147
+ #
148
+ # Function calls must be in the format `#method_name,arg1,arg2`
149
+ # for example to call the join function `#join,.,biggerconcept,com`
150
+ # will result in `biggerconcept.com` becoming the value.
151
+ #
152
+ # @return [String]
153
+ #
154
+ def value
155
+ return default if @value.nil?
156
+ return call_helper if method?
157
+
158
+ @value
159
+ end
160
+
161
+ ##
162
+ # Returns true if value has been set
163
+ #
164
+ # @return [Boolean]
165
+ def value?
166
+ !@value.nil?
167
+ end
168
+
169
+ ##
170
+ # Retrieves the appropriate validator function basedd on the
171
+ # specified type.
172
+ #
173
+ # If a type is not set then a string validator function is
174
+ # returned by default
175
+ #
176
+ # @return [Proc]
177
+ #
178
+ def validator
179
+ validator = VALIDATORS[@spec.type]
180
+ validator ||= DEFAULT_VALIDATOR
181
+
182
+ validator
183
+ end
184
+
185
+ ##
186
+ # Returns true if the value input is valid.
187
+ #
188
+ # @param [String] input
189
+ #
190
+ # @return [Boolean]
191
+ #
192
+ def valid?(input)
193
+ validator.call(input)
194
+ end
195
+
196
+ private
197
+
198
+ # Returns an object which extends helpers module.
199
+ #
200
+ # This is used for calling helper functions
201
+ def helpers
202
+ Object
203
+ .new
204
+ .extend(AppArchetype::Template::Helpers)
205
+ end
206
+
207
+ # Returns true if variable value begins with `#` this
208
+ # indicates the variable value has been set to a function
209
+ def method?
210
+ return false unless @value.is_a?(String)
211
+
212
+ @value[0, 1] == '#'
213
+ end
214
+
215
+ # Calls a helper function to generate a value
216
+ def call_helper
217
+ method = deserialize_method(@value)
218
+ helpers.send(method.name, *method.args)
219
+ end
220
+
221
+ # Creates a struct representing a method call to be made
222
+ # to resolve a variable value
223
+ def deserialize_method(method)
224
+ method = method.delete('#')
225
+ parts = method.split(',')
226
+ name = parts.shift
227
+
228
+ args = parts || []
229
+
230
+ OpenStruct.new(
231
+ name: name.to_sym,
232
+ args: args
233
+ )
234
+ end
235
+ end
236
+ end
237
+ end