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']
@@template_dirs =

The possible dirs for templates. The first one is system-wide.

[CliUtils::DIR_LIB_SRC + 'templates',
Pathname.new(Dir.home) + ".#{Meta::NAME}" + 'templates']

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.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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
# File 'lib/falsework/mould.rb', line 62

def initialize(project, template, user = nil, email = nil, gecos = nil)
  @project = Mould.name_project project
  raise "invalid project name '#{project}'" if !Mould.name_valid? @project
  @camelcase = Mould.name_camelcase project
  @classy = Mould.name_classy project
  
  @verbose = false
  @batch = false
  @template = template
  @dir_t = Mould.templates[@template || TEMPLATE_DEFAULT] || CliUtils.errx(1, "no such template: #{template}")

  # 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
           }]
  }
  Mould.config_parse(@dir_t + '/' + TEMPLATE_CONFIG, [], @conf)
  
  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|
    CliUtils.errx(1, "missing #{i.first} in #{GITCONFIG}") if i.last.to_s == ''
  }
end

Instance Attribute Details

#batchObject

-b CLO.



53
54
55
# File 'lib/falsework/mould.rb', line 53

def batch
  @batch
end

#projectObject (readonly)

A directory of a new generated project.



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

def project
  @project
end

#verboseObject

A verbose level for -v CLO.



51
52
53
# File 'lib/falsework/mould.rb', line 51

def verbose
  @verbose
end

Class Method Details

.config_parse(file, rvars, hash) ⇒ Object

Parse a config. Return false on error.

file

A file to parse.

rvars

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

hash

a hash to merge results with



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/falsework/mould.rb', line 233

def self.config_parse(file, rvars, hash)
  r = true
  
  if File.readable?(file)
    begin
      myconf = YAML.load_file(file)
    rescue
      CliUtils.warnx "cannot parse #{file}: #{$!}"
      return false
    end
    rvars.each { |i|
      CliUtils.warnx "missing or nil '#{i}' in #{file}" if ! myconf.key?(i.to_sym) || ! myconf[i.to_sym]
      r = false
    }
    
    hash.merge!(myconf) if r && hash
  else
    r = false
  end
  
  r
end

.extract(from, binding, to) ⇒ Object

Extract file @from into @to.

binding

A binding for eval.



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
# File 'lib/falsework/mould.rb', line 319

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

  if ! File.exists?(to)
    # write a skeleton
    begin
      File.open(to, 'w+') { |fp| fp.puts output }
      # transfer the exec bit to the generated result
      File.chmod(0744, to) if File.stat(from).executable?
    rescue
      CliUtils.errx(1, "cannot generate: #{$!}")
    end
  elsif
    # warn a careless user
    if md5_system != Digest::MD5.file(to).hexdigest
      CliUtils.errx(1, "'#{to}' already exists")
    end
  end
end

.get_filename(t, binding) ⇒ Object

Resolve @t from possible %%VARIABLE%% scheme.



347
348
349
350
351
352
353
# File 'lib/falsework/mould.rb', line 347

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

.name_camelcase(raw) ⇒ Object

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



161
162
163
164
165
166
# File 'lib/falsework/mould.rb', line 161

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’



143
144
145
# File 'lib/falsework/mould.rb', line 143

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’.



149
150
151
152
153
154
155
156
157
# File 'lib/falsework/mould.rb', line 149

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)


136
137
138
139
# File 'lib/falsework/mould.rb', line 136

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

.template_dirs_add(dirs) ⇒ Object

Modifies an internal list of available template directories



106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/falsework/mould.rb', line 106

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.



170
171
172
173
174
175
176
177
178
# File 'lib/falsework/mould.rb', line 170

def self.templates
  r = {}
  @@template_dirs.each {|i|
    Dir.glob(i + '*').each {|j|
      r[File.basename(j)] = 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.



302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/falsework/mould.rb', line 302

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.



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

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 an executable or a test 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


269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/falsework/mould.rb', line 269

def add(mode, target)
  target_orig = target
  target = Mould.name_project target_orig
  raise "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 = []

  return [] unless @conf[mode.to_sym][0][:src]

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

    begin
      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

#project_seedObject

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

Return false if nothing was extracted.



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
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
# File 'lib/falsework/mould.rb', line 183

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

  Dir.mkdir @project unless File.directory?(@project)
  puts "Project path: #{File.expand_path(@project)}" if @verbose

  r = false
  puts "Template: #{@dir_t}" if @verbose
  symlinks = []
  Dir.chdir(@project) {
    Mould.traverse(@dir_t) {|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.get_filename(File.readlink(idx), binding),
                     Mould.get_filename(file, binding)]
      elsif File.directory?(idx)
        puts "D: #{file}"  if @verbose
        Dir.mkdir Mould.get_filename(file, binding)
      else
        puts "N: #{file}" if @verbose
        to = Mould.get_filename(file, binding)
        Mould.extract(idx, binding, to)
      end
      r = true
    }

    # create saved symlinks
    symlinks.each {|idx|
      src = idx[0]
      dest = idx[1]
      puts "L: #{dest} => #{src}" if @verbose
      File.symlink(src, dest)
    }
  }
  
  r
end

#upgradable_filesObject

Search for all files in the template directory the line

/^..? :erb:/

in first n lines. If the line is found, the file is considered a candidate for an upgrade. Return a hash target:template



362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/falsework/mould.rb', line 362

def upgradable_files()
  line_max = 4
  r = {}
  Mould.traverse(@dir_t) {|i|
    next if File.directory?(i)
    next if File.symlink?(i) # hm...

    File.open(i) {|fp|
      n = 0
      while n < line_max && line = fp.gets
        if line =~ /^..? :erb:/
          t = i.sub(/#{@dir_t}\//, '')
          r[Mould.get_filename(t, binding)] = i
          break
        end
        n += 1
      end
    }
  }
  
  r
end

#upgradeObject

We can upgrade only those files, which were explicitly marked by ‘:erb’ sign a the top the file. They are collected by upgradable_files() method.

The upgrade can happened only if one following conditions is met:

  1. there is no such files (all or some of them) in the project at all;

  2. the files are from the previous version of falsework.

The situation may combine: you may have some missing and some old files. But if there is at least 1 file from a newer version of falsework then no upgrade is possible–it’s considered a user decision to intentionally have some files from the old versions of falsework.

Neithe we do check for a content of upgradable files nor try to merge old with new. (Why?)



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/falsework/mould.rb', line 404

def upgrade()
  # 0. search for 'new' files in the template
  uf = upgradable_files
  fail "template #{@template} cannot offer you files for the upgrade" if uf.size == 0
 #     pp uf
  
  # 1. analyse 'old' files
  u = {}
  uf.each {|k, v|
    if ! File.readable?(k)
      u[k] = v
    else
      # check for its version
      File.open(k) {|fp|
        is_versioned = false
        while line = fp.gets
          if line =~ /^# Don't remove this: falsework\/(#{Gem::Version::VERSION_PATTERN})\/(.+)\/.+/
            is_versioned = true
            if $3 != (@template || TEMPLATE_DEFAULT)
              fail "file #{k} is from '#{$3}' template"
            end
            if Gem::Version.new(Meta::VERSION) >= Gem::Version.new($1)
#                  puts "#{k}: #{$1}"
              u[k] = v
              break
            else
              fail "file #{k} is from a newer version of #{Meta::NAME}: " + $1
            end
          end
        end

        CliUtils.warnx("#{k}: unversioned") if ! is_versioned
      }
    end
  }
  fail "template #{@template || TEMPLATE_DEFAULT} cannot find files for an upgrade" if u.size == 0

  # 2. ask user for a commitment
  if ! @batch
    puts "Here is a list of files in project #{@project} we can try to upgrade/add:\n\n"
    u.each {|k,v| puts "\t#{k}"}
    printf %{
Does this look fine? Type y/n and press enter. If you choose 'y', those files
will be replaced with newer versions. Your old files will be preserved with
an '.old' extension. So? }
    if STDIN.gets =~ /^y/i
      puts ""
    else
      puts "\nNo? See you later."
      exit 0
    end
  end

  # 3. rename & write new
  count = 1
  total = u.size
  tsl = total.to_s.size*2+1
  u.each {|k, v|
    printf("%#{tsl}s) mv %s %s\n",
           "#{count}/#{total}", k, "#{k}.old") if @verbose
    File.rename(k, "#{k}.old") rescue CliUtils.warnx('renaming failed')
    printf("%#{tsl}s  Extracting %s ...\n", "", File.basename(v)) if @verbose
    FileUtils.mkdir_p(File.dirname(k))
    Mould.extract(v, binding, k)
    count += 1
  }
end