Class: Falsework::Mould

Inherits:
Object
  • Object
show all
Defined in:
lib/falsework/mould.rb

Overview

The directory with template may have files beginning with # char which will be ignored in #project_seed (a method that creates a shiny new project form a template).

If you need to run through erb not only the contents of a file in a template but it name itself, then use the following convention:

%%VARIABLE%%

which is equivalent of erb’s: <%= VARIABLE %>. See ‘ruby-cli’ template directory for examples.

In the template files you may use any Mould instance variables. The most usefull are:

@classy

An original project name, for example, ‘Foobar Pro’

@project

A project name in lowercase, suitable for a name of an executable, for example, ‘Foobar Pro’ would be ‘foobar_pro’.

@camelcase

A ‘normalized’ project name, for use in source code, for example, ‘foobar pro’ would be ‘FoobarPro’.

@user

Github user name.

@email

User email.

@gecos

A full user name.

Constant Summary collapse

GITCONFIG =

Where @user, @email & @gecos comes from.

'~/.gitconfig'
TEMPLATE_DEFAULT =

The template used if user didn’t select one.

'ruby-cli'
TEMPLATE_CONFIG =

A file name with configurations for the inject commands.

'#config.yaml'
IGNORE_FILES =

A list of files to ignore in any template.

['.gitignore']
NOTE =

Note file name

'.' + Meta::NAME

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(project, template, user = nil, email = nil, gecos = nil) ⇒ Mould

project

A name of the future project; may include all crap with spaces.

template

A name of the template for the project.

user

Github username; if nil we are extracting it from the ~/.gitconfig.

email

Github email

gecos

A full author name from ~/.gitconfig.



78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/falsework/mould.rb', line 78

def initialize(project, template, user = nil, email = nil, gecos = nil)
  @project = Mould.name_project project
  fail MouldError, "invalid project name '#{project}'" unless Mould.name_valid?(@project)
  @camelcase = Mould.name_camelcase project
  @classy = Mould.name_classy project
  
  @batch = false
  @template = template || TEMPLATE_DEFAULT
  @dir_t = Mould.templates[@template] || fail(MouldError, "template '#{@template}' not found")

  # default config
  @conf = {
    'exe' => [{
                'src' => nil,
                'dest' => 'bin/%s',
                'mode_int' => 0744
              }],
    'doc' => [{
                'src' => nil,
                'dest' => 'doc/%s.rdoc',
                'mode_int' => nil
              }],
    'test' => [{
                 'src' => nil,
                 'dir' => 'test/test_%s.rb',
                 'mode_int' => nil
               }],
    'version' => '1.0.0'
  }
  configParse
  
  gc = Git.global_config rescue gc = {}
  @user = user || gc['github.user']
  @email = email || ENV['GIT_AUTHOR_EMAIL'] || ENV['GIT_COMMITTER_EMAIL'] || gc['user.email']
  @gecos = gecos || ENV['GIT_AUTHOR_NAME'] || ENV['GIT_COMMITTER_NAME']  || gc['user.name']

  [['github.user', @user],
   ['user.email', @email],
   ['user.name', @gecos]].each {|i|
    fail MouldError, "missing #{i.first} in #{GITCONFIG}" if i.last.to_s == ''
  }
end

Class Attribute Details

.template_dirsObject (readonly)

Returns the value of attribute template_dirs.



56
57
58
# File 'lib/falsework/mould.rb', line 56

def template_dirs
  @template_dirs
end

Instance Attribute Details

#confObject (readonly)

template configuration



71
72
73
# File 'lib/falsework/mould.rb', line 71

def conf
  @conf
end

#projectObject (readonly)

A directory of a new generated project.



69
70
71
# File 'lib/falsework/mould.rb', line 69

def project
  @project
end

Class Method Details

.extract(from, bng, to) ⇒ Object

Extract file @from into @to.

bng

A binding for eval.



342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/falsework/mould.rb', line 342

def self.extract from, bng, to
  t = ERB.new File.read(from.to_s)
  t.filename = from.to_s # to report errors relative to this file
  begin
    output = t.result bng
    md5_system = Digest::MD5.hexdigest(output)
  rescue Exception
    fail MouldError, "bogus template file '#{from}': #{$!}"
  end

  unless File.exists?(to)
    # write a skeleton
    begin
      FileUtils.mkdir_p File.dirname(to)
      File.open(to, 'w+') { |fp| fp.puts output }
      # transfer the exec bit to the generated result
      File.chmod(0744, to) if !defined?(FakeFS) && File.stat(from.to_s).executable?
    rescue
      fail MouldError, "cannot generate: #{$!}"
    end
  else
    # warn a careless user
    CliUtils.warnx "'#{to}' already exists in modified state" if md5_system != Digest::MD5.file(to).hexdigest
  end
end

.name_camelcase(raw) ⇒ Object

Return a ‘normalized’ project name, for use in source code; for example, ‘foobar pro’ would be ‘FoobarPro’.



177
178
179
180
181
182
# File 'lib/falsework/mould.rb', line 177

def self.name_camelcase(raw)
  raw || (return '')
  raw.strip.split(/[^a-zA-Z0-9]+/).map{|idx|
    idx[0].upcase + idx[1..-1]
  }.join
end

.name_classy(t) ⇒ Object

Return cleaned version of an original project name, for example, ‘Foobar Pro’



159
160
161
# File 'lib/falsework/mould.rb', line 159

def self.name_classy(t)
  t ? t.gsub(/\s+/, ' ').strip : ''
end

.name_project(raw) ⇒ Object

Return a project name in lowercase, suitable for a name of an executable; for example, ‘Foobar Pro’ would be ‘foobar_pro’.



165
166
167
168
169
170
171
172
173
# File 'lib/falsework/mould.rb', line 165

def self.name_project(raw)
  raw || (return '')

  r = raw.gsub(/[^a-zA-Z0-9_]+/, '_').downcase
  r.sub!(/^_/, '');
  r.sub!(/_$/, '');

  r
end

.name_valid?(t) ⇒ Boolean

Return false if @t is invalid.

Returns:

  • (Boolean)


152
153
154
155
# File 'lib/falsework/mould.rb', line 152

def self.name_valid?(t)
  return false if !t || t[0] =~ /\d/
  t =~ /^[a-zA-Z0-9_]+$/ ? true : false
end

.resolve_filename(t, bng) ⇒ Object

Resolve t from possible %%VARIABLE%% scheme.



369
370
371
372
373
374
375
# File 'lib/falsework/mould.rb', line 369

def self.resolve_filename t, bng
  t || (return '')
  
  re = /%%([^%]+)%%/
  t = ERB.new(t.gsub(re, '<%= \+ %>')).result(bng) if t =~ re
  t.sub(/\.#erb$/, '')
end

.template_dirs_add(dirs) ⇒ Object

Modifies an internal list of available template directories



122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/falsework/mould.rb', line 122

def self.template_dirs_add(dirs)
  return unless defined? dirs.each

  dirs.each {|idx|
    fail "#{idx} is not a Pathname" unless idx.instance_of?(Pathname)
    
    if ! File.directory?(idx)
      CliUtils.warnx "invalid additional template directory: #{idx}"
    else
      @template_dirs << idx
    end
  }
end

.templatesObject

Return a hash => dir with current possible template names and corresponding directories.



186
187
188
189
190
191
192
193
194
# File 'lib/falsework/mould.rb', line 186

def self.templates
  r = {}
  @template_dirs.each {|i|
    Dir.glob(i + '*').each {|j|
      r[File.basename(j)] = Pathname.new(j) if File.directory?(j)
    }
  }
  r
end

.traverse(start, &block) ⇒ Object

Walk through a directory tree, executing a block for each file or directory. Ignores ., .. and files starting with _#_ character.

start

The directory to start with.



325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/falsework/mould.rb', line 325

def self.traverse(start, &block)
  l = Dir.glob(start + '/*', File::FNM_DOTMATCH).delete_if {|i|
    i.match(/\/?\.\.?$/) || i.match(/^#|\/#/)
  }
  # stop if directory is empty (contains only . and ..)
  return if l.size == 0
  
  l.sort.each {|i|
    yield i
    # recursion!
    self.traverse(i) {|j| block.call j} if File.directory?(i)
  }
end

.uuidgen_fakeObject

Hyper-fast generator of something like uuid suitable for code identifiers. Return a string.



138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/falsework/mould.rb', line 138

def self.uuidgen_fake
  loop {
    r = ('%s_%s_%s_%s_%s' % [
                             SecureRandom.hex(4),
                             SecureRandom.hex(2),
                             SecureRandom.hex(2),
                             SecureRandom.hex(2),
                             SecureRandom.hex(6),
                            ]).upcase
    return r if r[0] !~ /\d/
  }
end

Instance Method Details

#add(mode, target) ⇒ Object

Add a file from the template.

mode

Is either ‘exe’, ‘doc’ or ‘test’.

target

A test/doc/exe file to create.

Return a list of a created files.

Useful variables in the template:

target
target_camelcase
target_classy
uuid


284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/falsework/mould.rb', line 284

def add(mode, target)
  target_orig = target
  target = Mould.name_project target_orig
  fail MouldError, "invalid target name '#{target_orig}'" if !Mould.name_valid? target
  target_camelcase = Mould.name_camelcase target_orig
  target_classy = Mould.name_classy target_orig
  uuid = Mould.uuidgen_fake
  
  created = []

  unless @conf[mode] && @conf[mode][0]['src']
    if @conf[:file]
      CliUtils.warnx "hash '#{mode}' is empty in #{@conf[:file]}"
    else
      CliUtils.warnx "template '#{@template}' doesn't have '#{TEMPLATE_CONFIG}' file"
    end
    return []
  end

  @conf[mode].each {|idx|
    to = idx['dest'] % target

    begin
      # 'binding' to include local vars
      Mould.extract @dir_t + idx['src'], binding, to
      File.chmod(idx['mode_int'], to) if idx['mode_int']
    rescue
      CliUtils.warnx "failed to create '#{to}' (check your #config.yaml): #{$!}"
    else
      created << to
    end
  }

  created
end

#configParse(rvars = []) ⇒ Object

Parse a config. Return false on error.

rvars

A list of variable names which must be in the config.



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/falsework/mould.rb', line 247

def configParse rvars = []
  r = false

  file = @dir_t + TEMPLATE_CONFIG
  if File.readable?(file)
    begin
      myconf = YAML.load_file file
      myconf[:file] = file
      r = true
    rescue
      CliUtils.warnx "cannot parse #{file}: #{$!}"
      return false
    end
    rvars.each { |i|
      CliUtils.warnx "missing or nil '#{i}' in #{file}" if ! myconf.key?(i) || ! myconf[i]
      return false
    }

    @conf.merge!(myconf) if r
  end
  
  r
end

#getBindingObject



377
378
379
# File 'lib/falsework/mould.rb', line 377

def getBinding
  binding
end

#noteCreate(after_upgrade = false) ⇒ Object

Write a YAML file into a created project directory. This file is required for Upgrader.



383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/falsework/mould.rb', line 383

def noteCreate after_upgrade = false
  h = {
    'project' => {
      'classy' => @classy,
      'upgraded' => DateTime.now.iso8601
    },
    'template' => {
      'version' => @conf['version'],
      'name' => @template
    }
  }

  file = @project + '/' + NOTE
  file = NOTE if after_upgrade # in 'upgrade' mode we are in project dir
  File.open(file, 'w+') {|fp|
    CliUtils.veputs 1, "N: #{File.basename(file)}"
    fp.puts "# DO NOT DELETE THIS FILE"
    fp.puts "# unless you don't want to upgrade scaffolds in the future."
    fp.puts h.to_yaml
  }
end

#project_seedObject

Generate a new project in @project directory from @template.

Return false if nothing was extracted.



199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# File 'lib/falsework/mould.rb', line 199

def project_seed
  uuid = Mould.uuidgen_fake # useful variable for the template
  
  # check for existing project
  fail MouldError, "directory '#{@project}' is not empty" if Dir.glob(@project + '/*').size > 0

  Dir.mkdir @project unless File.directory?(@project)
  CliUtils.veputs 1, "Project path: #{File.expand_path(@project)}"

  r = false
  CliUtils.veputs 1, "Template: #{@dir_t}"
  symlinks = []
  Dir.chdir(@project) {
    Mould.traverse(@dir_t.to_s) {|idx|
      file = idx.sub(/^#{@dir_t}\//, '')
      next if IGNORE_FILES.index {|i| file.match(/#{i}$/) }

      if File.symlink?(idx)
        # we'll process them later on
#            is_dir = File.directory?(@dir_t + '/' + File.readlink(idx))
        symlinks << [Mould.resolve_filename(File.readlink(idx), getBinding),
                     Mould.resolve_filename(file, getBinding)]
      elsif File.directory?(idx)
        CliUtils.veputs 1, "D: #{file}"
        Dir.mkdir Mould.resolve_filename(file, getBinding)
      else
        CliUtils.veputs 1, "N: #{file}"
        to = Mould.resolve_filename file, getBinding
        Mould.extract idx, binding, to # 'binding' to include local uuid
      end
      r = true
    }

    # create saved symlinks
    symlinks.each {|idx|
      src = idx[0]
      dest = idx[1]
      CliUtils.veputs 1, "L: #{dest} => #{src}"
      File.symlink src, dest
    }
  }
  
  r
end