directory_template 1.0.0

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.
@@ -0,0 +1,306 @@
1
+ # encoding: utf-8
2
+
3
+
4
+
5
+ require 'fileutils'
6
+ require 'directory_template/process_data'
7
+ require 'directory_template/processor'
8
+ require 'directory_template/version'
9
+
10
+
11
+
12
+ # @version 1.0.0
13
+ # @author Stefan Rusterholz <stefan.rusterholz@gmail.com>
14
+ #
15
+ # DirectoryTemplate
16
+ # Create directory structures from template directory structures or template data.
17
+ #
18
+ # Preregistered processors
19
+ # * :stop: Stops the preprocessing chain, it's advised to add that to all files for
20
+ # future-proofness.
21
+ class DirectoryTemplate
22
+
23
+ # All registered processors
24
+ Processors = []
25
+
26
+ # You can register custom processors for templates. They're triggered based on the
27
+ # pattern.
28
+ # A processor can change the ProcessData struct passed to it, which will be reflected
29
+ # when creating the file or directory
30
+ #
31
+ # @param [DirectoryTemplate::Processor] processor
32
+ # The processor to register
33
+ #
34
+ def self.register(processor)
35
+ Processors << processor
36
+ end
37
+ Processor.register_all
38
+
39
+ # The standard path processor, just replaces `%{sprintf_style_variables}` with their
40
+ # values.
41
+ StandardPathProcessor = Processor::Format
42
+
43
+ # The default options used by DirectoryTemplate
44
+ DefaultOptions = {
45
+ :verbose => false,
46
+ :silent => false,
47
+ :out => $stdout,
48
+ :processors => Processors,
49
+ :path_processor => StandardPathProcessor,
50
+ :source => '(unknown)',
51
+ :meta => {},
52
+ }
53
+
54
+ # Create a DirectoryTemplate from an existing directory structure.
55
+ def self.directory(template_path, options={})
56
+ data = Dir.chdir(template_path) {
57
+ paths = Dir['**/{*,.*}']
58
+ paths -= paths.grep(/(?:^|\/)\.\.?$/)
59
+ directories, files = paths.sort.partition { |path| File.directory?(path) }
60
+ filemap = Hash[files.map { |path| [path, File.read(path)] }]
61
+
62
+ {:directories => directories, :files => filemap}
63
+ }
64
+
65
+ new(data, options.merge(:source => template_path))
66
+ end
67
+
68
+ # @private
69
+ # Converts a recursive hash into a suitable data structure for DirectoryTemplate::new
70
+ def self.convert_recursive_structure(current, stack=[], dirs=[], files={})
71
+ current.each do |segment, content|
72
+ new_stack = stack+[segment]
73
+ path = new_stack.join('/')
74
+ case content
75
+ when String,nil
76
+ files[path] = content || ''
77
+ when Hash
78
+ dirs << path
79
+ convert_recursive_structure(content, new_stack, dirs, files)
80
+ else
81
+ raise "Invalid structure"
82
+ end
83
+ end
84
+
85
+ return dirs, files
86
+ end
87
+
88
+ # Create a DirectoryTemplate from a nested hash structure.
89
+ # The hash should just be a recursive hash of strings. Use an empty hash to indicate
90
+ # an empty directory. Leaf-strings are considered to be the content of a file. Use nil
91
+ # to indicate an empty file.
92
+ def self.from_hash(hash, options=nil)
93
+ dirs, files = convert_recursive_structure(hash)
94
+ data = {:directories => dirs, :files => files}
95
+
96
+ new(data, options)
97
+ end
98
+
99
+ # Create a DirectoryTemplate from a YAML file.
100
+ # The yaml should just be a recursive hash of strings. Use an empty hash to indicate
101
+ # an empty directory. Leaf-strings are considered to be the content of a file. Use nil
102
+ # to indicate an empty file.
103
+ def self.yaml_file(path, options={})
104
+ from_hash(YAML.load_file(path), options.merge(:source => template_path))
105
+ end
106
+
107
+ # Meta information can be used by processors. There's no requirements on them, except
108
+ # that the toplevel container is a hash.
109
+ attr_reader :meta
110
+
111
+ # @return [Array] All directories of the template
112
+ attr_reader :directories
113
+
114
+ # @return [Hash<String,String>] All files of the template and their content.
115
+ attr_reader :files
116
+
117
+ # @return [Array<DirectoryTemplate::Processor>] The content processors used by this template.
118
+ attr_reader :processors
119
+
120
+ # @return [#call] The path processor used by this template.
121
+ attr_reader :path_processor
122
+
123
+ # @return [IO, #puts] The object on which info and debug messages are printed
124
+ attr_reader :out
125
+
126
+ # @private
127
+ # @return [Boolean] Whether the current run is a dry-run or not.
128
+ attr_reader :dry_run
129
+
130
+ # When true, will not even output info messages
131
+ attr_accessor :silent
132
+
133
+ # When true, will additionally output debug messages
134
+ attr_accessor :verbose
135
+
136
+ # Create a new DirectoryTemplate
137
+ #
138
+ # @param [Hash] data
139
+ # A hash with the two keys :directories and :files, where :directories contains an
140
+ # Array of all directory names, and :files contains a Hash of all file names and their
141
+ # unprocessed content.
142
+ #
143
+ # @param [Hash, nil] options
144
+ # An options hash, @see DirectoryTemplate::DefaultOptions for a list of all available
145
+ # options.
146
+ #
147
+ # @see DirectoryTemplate::directory
148
+ # To create a DirectoryTemplate from an existing directory structure.
149
+ # @see DirectoryTemplate::from_yaml
150
+ # To create a DirectoryTemplate from a description in a YAML file.
151
+ def initialize(data, options=nil)
152
+ options = options ? DefaultOptions.merge(options) : DefaultOptions.dup
153
+ @directories = data[:directories] || []
154
+ @files = data[:files] || []
155
+ @source = options.delete(:source)
156
+ @meta = options.delete(:meta)
157
+ @verbose = options.delete(:verbose)
158
+ @silent = options.delete(:silent)
159
+ @out = options.delete(:out)
160
+ @processors = options.delete(:processors)
161
+ @path_processor = options.delete(:path_processor)
162
+ @dry_run = false
163
+ raise ArgumentError, "Unknown options: #{options.keys.join(', ')}" unless options.empty?
164
+ end
165
+
166
+ # Same as #materialize, but doesn't actually do anything, except print the debug and
167
+ # info messages. It additionally prints an info message, containing the file content
168
+ # of files that would be created.
169
+ def dry_run(in_path='.', env={}, &on_collision)
170
+ @dry_run = true
171
+ materialize(in_path, env, &on_collision)
172
+ ensure
173
+ @dry_run = false
174
+ end
175
+
176
+ # Creates all the directories and files from the template in the given path.
177
+ #
178
+ # @param [String] in_path
179
+ # The directory within which to generate the structure.
180
+ #
181
+ # @param [Hash] env
182
+ # A hash with various information used to generate the structure. Most important one
183
+ # being the :variables value.
184
+ # @option env [Hash] :variables
185
+ # A hash with variables available to both, path and content processing
186
+ # @option env [Hash] :path_variables
187
+ # A hash with variables available to path processing
188
+ # @option env [Hash] :content_variables
189
+ # A hash with variables available to content processing
190
+ #
191
+ # @see #dry_run For a way to see what would happen with materialize
192
+ def materialize(in_path='.', env={}, &on_collision)
193
+ in_path = File.expand_path(in_path)
194
+ create_directory(in_path) { "Creating root '#{in_path}'" }
195
+
196
+ Dir.chdir(in_path) do
197
+ if @directories.empty? then
198
+ info { "No directories to create" }
199
+ else
200
+ info { "Creating directories" }
201
+ @directories.each do |source_dir_path|
202
+ target_dir_path = process_path(source_dir_path, env)
203
+ create_directory(target_dir_path) { " #{target_dir_path}" }
204
+ end
205
+ end
206
+
207
+ if @files.empty? then
208
+ info { "No files to create" }
209
+ else
210
+ info { "Creating files" }
211
+ @files.each do |source_file_path, content|
212
+ target_file_path = process_path(source_file_path, env)
213
+ data = process_content(target_file_path, content, env)
214
+ if File.exist?(data.path) then
215
+ if block_given? && yield(data) then
216
+ create_file(data.path, data.content) { " #{data.path} (overwrite)" }
217
+ else
218
+ info { " #{data.path} (exists already)" }
219
+ end
220
+ else
221
+ create_file(data.path, data.content) { " #{data.path} (new)" }
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ self
228
+ end
229
+
230
+ # @private
231
+ # Preprocesses the given path
232
+ def process_path(path, env)
233
+ @path_processor.call(ProcessData.new(self, path, nil, env)).path
234
+ end
235
+
236
+ # @private
237
+ # Preprocesses the given content
238
+ def process_content(path, content, env)
239
+ data = ProcessData.new(self, path, content, env)
240
+ catch(:stop_processing) {
241
+ #p :process_content => path, :available => @processors.size, :processors => processors_for(data).tap { |x| x && x.size }
242
+ while processor = processor_for(data)
243
+ debug { " -> Applying #{processor.name}" }
244
+ processor.call(data)
245
+ end
246
+ }
247
+
248
+ data
249
+ end
250
+
251
+ # @private
252
+ # Create the given directory and emit an info message (unless in dry_run mode).
253
+ #
254
+ # @note The mode param is currently unused.
255
+ def create_directory(path, mode=0755, &message)
256
+ unless File.exist?(path) then
257
+ info(&message)
258
+ FileUtils.mkdir_p(path) unless @dry_run
259
+ end
260
+ end
261
+
262
+ # @private
263
+ # Create the given file and emit an info message (unless in dry_run mode).
264
+ #
265
+ # @note The mode param is currently unused.
266
+ def create_file(path, content="", mode=0644, &message)
267
+ info(&message)
268
+ if @dry_run then
269
+ info { " Content:\n#{content.gsub(/^/, ' ')}" }
270
+ else
271
+ File.open(path, 'wb:binary') do |fh|
272
+ fh.write(content)
273
+ end
274
+ end
275
+ end
276
+
277
+ # @private
278
+ # @param [ProcessData] data
279
+ # The data which the processor should apply to.
280
+ #
281
+ # @return [Processor, nil]
282
+ # Returns the processor or nil
283
+ def processor_for(data)
284
+ @processors.find { |processor| processor === data }
285
+ end
286
+
287
+ # @private
288
+ # Emit an info string (the return value of the block). Will not be emitted if
289
+ # DirectoryTemplate#silent is true
290
+ def info
291
+ @out.puts yield unless @silent
292
+ end
293
+
294
+ # @private
295
+ # Emit a debug string (the return value of the block). Will only be emitted if
296
+ # DirectoryTemplate#debug is true
297
+ def debug
298
+ @out.puts yield if @verbose
299
+ end
300
+
301
+ # @private
302
+ # See Object#inspect
303
+ def inspect
304
+ sprintf "#<%s:0x%x source=%p>", self.class, object_id<<1, @source
305
+ end
306
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: directory_template
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Stefan Rusterholz
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-03 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: ! 'Create directories from templates.
15
+
16
+ Existing directory structures, yaml files and ruby datastructures can all serve
17
+ as
18
+
19
+ sources of a template.
20
+
21
+ Can preprocess pathnames and content.
22
+
23
+ Path- and ContentProcessors are exchangeable.'
24
+ email: stefan.rusterholz@gmail.com
25
+ executables: []
26
+ extensions: []
27
+ extra_rdoc_files: []
28
+ files:
29
+ - lib/directory_template/blank_slate.rb
30
+ - lib/directory_template/erb_template.rb
31
+ - lib/directory_template/process_data.rb
32
+ - lib/directory_template/processor/erb.rb
33
+ - lib/directory_template/processor/format.rb
34
+ - lib/directory_template/processor/markdown.rb
35
+ - lib/directory_template/processor/stop.rb
36
+ - lib/directory_template/processor.rb
37
+ - lib/directory_template/version.rb
38
+ - lib/directory_template.rb
39
+ - examples/dir_gem_template/%{gem_name}/%{gem_name}.gemspec.stop.erb
40
+ - examples/dir_gem_template/%{gem_name}/lib/%{require_name}/version.rb.stop.erb
41
+ - examples/dir_gem_template/%{gem_name}/lib/%{require_name}.rb.stop.erb
42
+ - examples/dir_gem_template/%{gem_name}/Rakefile.stop
43
+ - examples/dir_gem_template/%{gem_name}/README.markdown.stop.erb
44
+ - examples/dir_gem_template/%{gem_name}/README_DIRECTORIES.stop
45
+ - examples/yaml_gem_template.yaml
46
+ - documentation/ContentProcessors.markdown
47
+ - documentation/PathProcessors.markdown
48
+ - directory_template.gemspec
49
+ - Rakefile
50
+ - README.markdown
51
+ homepage: https://github.com/apeiros/directory_template
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>'
67
+ - !ruby/object:Gem::Version
68
+ version: 1.3.1
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 1.8.24
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Create directories from templates, optionally preprocessing paths and contents.
75
+ test_files: []
76
+ has_rdoc: