templater 0.1.6 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README +13 -7
- data/Rakefile +30 -0
- data/lib/templater.rb +4 -2
- data/lib/templater/actions/action.rb +43 -0
- data/lib/templater/actions/empty_directory.rb +16 -14
- data/lib/templater/actions/file.rb +16 -21
- data/lib/templater/actions/template.rb +13 -22
- data/lib/templater/cli/generator.rb +13 -9
- data/lib/templater/core_ext/string.rb +1 -1
- data/lib/templater/description.rb +61 -0
- data/lib/templater/discovery.rb +16 -9
- data/lib/templater/generator.rb +115 -137
- data/lib/templater/spec/helpers.rb +1 -1
- data/spec/actions/empty_directory_spec.rb +105 -0
- data/spec/actions/file_spec.rb +112 -0
- data/spec/actions/template_spec.rb +141 -0
- data/spec/generator/actions_spec.rb +92 -43
- data/spec/generator/arguments_spec.rb +7 -3
- data/spec/generator/empty_directories_spec.rb +12 -4
- data/spec/generator/files_spec.rb +17 -26
- data/spec/generator/templates_spec.rb +17 -28
- data/spec/spec_helper.rb +24 -1
- metadata +18 -6
- data/lib/templater/proxy.rb +0 -62
- data/spec/empty_directory_spec.rb +0 -97
- data/spec/file_spec.rb +0 -97
- data/spec/template_spec.rb +0 -137
data/README
CHANGED
@@ -11,15 +11,9 @@ Hierarchy is pretty simple: manifold has one or many public and private generato
|
|
11
11
|
by end user. Generators have one or more action that specify what they do, where they take files, how they name resulting
|
12
12
|
files and so forth.
|
13
13
|
|
14
|
-
|
15
14
|
== Idea behind Templater
|
16
15
|
|
17
|
-
Templater
|
18
|
-
There's no magical %variables% in filenames sprinkled across your generator, generator rules are simple, readable, concise
|
19
|
-
and are in one place making it easy to take existing generator as example.
|
20
|
-
|
21
|
-
This is how Templater is different from some other code/components generation frameworks.
|
22
|
-
|
16
|
+
Templater is designed to be flexible and designed to be reflective. Generators created with templater are definitions of what goes where, they provide no interface for the user. This allows you to use templater generators inside your own code, or with the bundled CLI interface, or with your own interface.
|
23
17
|
|
24
18
|
== Example
|
25
19
|
|
@@ -150,6 +144,18 @@ will result in
|
|
150
144
|
|
151
145
|
4
|
152
146
|
<%= 2 + 2 %>
|
147
|
+
|
148
|
+
== Callbacks
|
149
|
+
|
150
|
+
Sometimes it might be desirable to add a callback to your actions, an example might be to chmod a binary file after it is created.
|
151
|
+
|
152
|
+
class MyGenerator < Templater::Generator
|
153
|
+
template :something, 'something.rb', :after => :chmod
|
154
|
+
|
155
|
+
def chmod(action)
|
156
|
+
File.chmod(action.destination, 0750)
|
157
|
+
end
|
158
|
+
end
|
153
159
|
|
154
160
|
== An advanced example
|
155
161
|
|
data/Rakefile
CHANGED
@@ -3,6 +3,7 @@ require 'rake/gempackagetask'
|
|
3
3
|
require 'rubygems/specification'
|
4
4
|
require 'rake/rdoctask'
|
5
5
|
require 'date'
|
6
|
+
require 'spec/rake/spectask'
|
6
7
|
require File.join(File.dirname(__FILE__), 'lib', 'templater')
|
7
8
|
|
8
9
|
PLUGIN = "templater"
|
@@ -34,6 +35,7 @@ spec = Gem::Specification.new do |s|
|
|
34
35
|
|
35
36
|
s.add_dependency "highline", ">= 1.4.0"
|
36
37
|
s.add_dependency "diff-lcs", ">= 1.1.2"
|
38
|
+
s.add_dependency "extlib", ">= 0.9.5"
|
37
39
|
end
|
38
40
|
|
39
41
|
Rake::GemPackageTask.new(spec) do |pkg|
|
@@ -99,3 +101,31 @@ task :rcov do
|
|
99
101
|
`#{command} 2>&1`
|
100
102
|
end
|
101
103
|
end
|
104
|
+
|
105
|
+
file_list = FileList['spec/**/*_spec.rb']
|
106
|
+
|
107
|
+
desc "Run all examples"
|
108
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
109
|
+
t.spec_files = file_list
|
110
|
+
end
|
111
|
+
|
112
|
+
namespace :spec do
|
113
|
+
desc "Run all examples with RCov"
|
114
|
+
Spec::Rake::SpecTask.new('rcov') do |t|
|
115
|
+
t.spec_files = file_list
|
116
|
+
t.rcov = true
|
117
|
+
t.rcov_dir = "doc/coverage"
|
118
|
+
t.rcov_opts = ['--exclude', 'spec']
|
119
|
+
end
|
120
|
+
|
121
|
+
desc "Generate an html report"
|
122
|
+
Spec::Rake::SpecTask.new('report') do |t|
|
123
|
+
t.spec_files = file_list
|
124
|
+
t.spec_opts = ["--format", "html:doc/reports/specs.html"]
|
125
|
+
t.fail_on_error = false
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
desc 'Default: run unit tests.'
|
131
|
+
task :default => 'spec'
|
data/lib/templater.rb
CHANGED
@@ -1,17 +1,19 @@
|
|
1
1
|
path = File.dirname(__FILE__) + '/templater/'
|
2
2
|
|
3
3
|
require 'rubygems'
|
4
|
+
require 'extlib'
|
4
5
|
require 'highline'
|
5
6
|
require "highline/import"
|
6
7
|
require 'diff/lcs'
|
7
8
|
|
8
9
|
require path + 'discovery'
|
9
10
|
require path + 'capture_helpers'
|
11
|
+
require path + 'actions/action'
|
10
12
|
require path + 'actions/template'
|
11
13
|
require path + 'actions/file'
|
12
14
|
require path + 'actions/empty_directory'
|
15
|
+
require path + 'description'
|
13
16
|
require path + 'generator'
|
14
|
-
require path + 'proxy'
|
15
17
|
require path + 'manifold'
|
16
18
|
require path + 'cli/parser'
|
17
19
|
require path + 'cli/manifold'
|
@@ -40,6 +42,6 @@ module Templater
|
|
40
42
|
class MalformattedArgumentError < ArgumentError #:nodoc:
|
41
43
|
end
|
42
44
|
|
43
|
-
VERSION = '0.
|
45
|
+
VERSION = '0.2'
|
44
46
|
|
45
47
|
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Templater
|
2
|
+
module Actions
|
3
|
+
class Action
|
4
|
+
|
5
|
+
attr_accessor :generator, :name, :source, :destination, :options
|
6
|
+
|
7
|
+
def source=(source)
|
8
|
+
unless source.blank?
|
9
|
+
@source = ::File.expand_path(source, generator.source_root)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def destination=(destination)
|
14
|
+
unless destination.blank?
|
15
|
+
@destination = ::File.expand_path(convert_encoded_instructions(destination), generator.destination_root)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns the destination path relative to Dir.pwd. This is useful for prettier output in interfaces
|
20
|
+
# where the destination root is Dir.pwd.
|
21
|
+
#
|
22
|
+
# === Returns
|
23
|
+
# String:: The destination relative to Dir.pwd
|
24
|
+
def relative_destination
|
25
|
+
@destination.relative_path_from(@generator.destination_root)
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def callback(name)
|
31
|
+
@generator.send(@options[name], self) if @options[name]
|
32
|
+
end
|
33
|
+
|
34
|
+
def convert_encoded_instructions(filename)
|
35
|
+
filename.gsub(/%.*?%/) do |string|
|
36
|
+
instruction = string.match(/%(.*?)%/)[1]
|
37
|
+
@generator.respond_to?(instruction) ? @generator.send(instruction) : string
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,20 +1,19 @@
|
|
1
1
|
module Templater
|
2
2
|
module Actions
|
3
|
-
class EmptyDirectory
|
3
|
+
class EmptyDirectory < Action
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
@destination.sub(::Dir.pwd + ::File::SEPARATOR, '')
|
5
|
+
# Builds a new Directory
|
6
|
+
#
|
7
|
+
# === Parameters
|
8
|
+
# generator<Object>:: The generator
|
9
|
+
# name<Symbol>:: The name of this directory
|
10
|
+
# destination<String>:: Full path to the destination of this directory
|
11
|
+
# options<Hash{Symbol=>Symbol}:: Options, including callbacks.
|
12
|
+
def initialize(generator, name, destination, options={})
|
13
|
+
self.generator = generator
|
14
|
+
self.name = name
|
15
|
+
self.destination = destination
|
16
|
+
self.options = options
|
18
17
|
end
|
19
18
|
|
20
19
|
# Returns the contents of the source file as a String
|
@@ -43,13 +42,16 @@ module Templater
|
|
43
42
|
|
44
43
|
# Renders the template and copies it to the destination.
|
45
44
|
def invoke!
|
45
|
+
@generator.send(@options[:before], self) if @options[:before]
|
46
46
|
::FileUtils.mkdir_p(destination)
|
47
|
+
@generator.send(@options[:after], self) if @options[:after]
|
47
48
|
end
|
48
49
|
|
49
50
|
# removes the destination file
|
50
51
|
def revoke!
|
51
52
|
::FileUtils.rm_rf(::File.expand_path(destination))
|
52
53
|
end
|
54
|
+
|
53
55
|
end # EmptyDirectory
|
54
56
|
end # Actions
|
55
57
|
end # Templater
|
@@ -1,28 +1,21 @@
|
|
1
1
|
module Templater
|
2
2
|
module Actions
|
3
|
-
class File
|
3
|
+
class File < Action
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
# Builds a new file, given the name of the file and its source and destination.
|
5
|
+
# Builds a new file.
|
8
6
|
#
|
9
7
|
# === Parameters
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
#
|
22
|
-
# === Returns
|
23
|
-
# String:: The destination relative to Dir.pwd
|
24
|
-
def relative_destination
|
25
|
-
@destination.sub(::Dir.pwd + ::File::SEPARATOR, '')
|
8
|
+
# generator<Object>:: The generator
|
9
|
+
# name<Symbol>:: The name of this file
|
10
|
+
# source<String>:: Full path to the source of this file
|
11
|
+
# destination<String>:: Full path to the destination of this file
|
12
|
+
# options<Hash{Symbol=>Symbol}:: Options, including callbacks.
|
13
|
+
def initialize(generator, name, source, destination, options={})
|
14
|
+
self.generator = generator
|
15
|
+
self.name = name
|
16
|
+
self.source = source
|
17
|
+
self.destination = destination
|
18
|
+
self.options = options
|
26
19
|
end
|
27
20
|
|
28
21
|
# Returns the contents of the source file as a String
|
@@ -51,15 +44,17 @@ module Templater
|
|
51
44
|
|
52
45
|
# Renders the template and copies it to the destination.
|
53
46
|
def invoke!
|
47
|
+
callback(:before)
|
54
48
|
::FileUtils.mkdir_p(::File.dirname(destination))
|
55
49
|
::FileUtils.copy_file(source, destination)
|
50
|
+
callback(:after)
|
56
51
|
end
|
57
52
|
|
58
53
|
# removes the destination file
|
59
54
|
def revoke!
|
60
55
|
::FileUtils.rm(destination, :force => true)
|
61
56
|
end
|
62
|
-
|
57
|
+
|
63
58
|
end
|
64
59
|
end
|
65
60
|
end
|
@@ -1,32 +1,21 @@
|
|
1
1
|
module Templater
|
2
2
|
module Actions
|
3
|
-
class Template
|
3
|
+
class Template < Action
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
# Builds a new template, given the context (e.g. binding) in which the template will be rendered
|
8
|
-
# (usually a generator), the name of the template and its source and destination.
|
5
|
+
# Builds a new template.
|
9
6
|
#
|
10
7
|
# === Parameters
|
11
|
-
#
|
8
|
+
# generator<Object>:: Context for rendering
|
12
9
|
# name<Symbol>:: The name of this template
|
13
10
|
# source<String>:: Full path to the source of this template
|
14
11
|
# destination<String>:: Full path to the destination of this template
|
15
|
-
#
|
16
|
-
def initialize(
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# Returns the destination path relative to Dir.pwd. This is useful for prettier output in interfaces
|
24
|
-
# where the destination root is Dir.pwd.
|
25
|
-
#
|
26
|
-
# === Returns
|
27
|
-
# String:: The destination relative to Dir.pwd
|
28
|
-
def relative_destination
|
29
|
-
@destination.sub(::Dir.pwd + ::File::SEPARATOR, '')
|
12
|
+
# options<Hash{Symbol=>Symbol}:: Options, including callbacks.
|
13
|
+
def initialize(generator, name, source, destination, options={})
|
14
|
+
self.generator = generator
|
15
|
+
self.name = name
|
16
|
+
self.source = source
|
17
|
+
self.destination = destination
|
18
|
+
self.options = options
|
30
19
|
end
|
31
20
|
|
32
21
|
# Renders the template using ERB and returns the result as a String.
|
@@ -34,7 +23,7 @@ module Templater
|
|
34
23
|
# === Returns
|
35
24
|
# String:: The rendered template.
|
36
25
|
def render
|
37
|
-
ERB.new(::File.read(source), nil, '-').result(
|
26
|
+
ERB.new(::File.read(source), nil, '-').result(generator.send(:binding))
|
38
27
|
end
|
39
28
|
|
40
29
|
# Checks if the destination file already exists.
|
@@ -55,8 +44,10 @@ module Templater
|
|
55
44
|
|
56
45
|
# Renders the template and copies it to the destination.
|
57
46
|
def invoke!
|
47
|
+
@generator.send(@options[:before], self) if @options[:before]
|
58
48
|
::FileUtils.mkdir_p(::File.dirname(destination))
|
59
49
|
::File.open(destination, 'w') {|f| f.write render }
|
50
|
+
@generator.send(@options[:after], self) if @options[:after]
|
60
51
|
end
|
61
52
|
|
62
53
|
# removes the destination file
|
@@ -5,8 +5,11 @@ module Templater
|
|
5
5
|
class Generator
|
6
6
|
|
7
7
|
def initialize(generator_name, generator_class, destination_root, name, version)
|
8
|
-
@
|
9
|
-
@
|
8
|
+
@generator_name = generator_name
|
9
|
+
@destination_root = destination_root
|
10
|
+
@generator_class = generator_class
|
11
|
+
@name = name
|
12
|
+
@version = version
|
10
13
|
end
|
11
14
|
|
12
15
|
def version
|
@@ -27,6 +30,7 @@ module Templater
|
|
27
30
|
|
28
31
|
def run(arguments)
|
29
32
|
generator_class = @generator_class # FIXME: closure wizardry, there has got to be a better way than this?
|
33
|
+
|
30
34
|
@options = Templater::CLI::Parser.parse(arguments) do |opts, options|
|
31
35
|
opts.separator "Options specific for this generator:"
|
32
36
|
# the reason this is reversed is so that the 'main' generator will always have the last word
|
@@ -35,14 +39,14 @@ module Templater
|
|
35
39
|
# Loop through this generator's options and add them as valid command line options
|
36
40
|
# so that they show up in help messages and such
|
37
41
|
generator.options.each do |option|
|
38
|
-
name = option
|
39
|
-
if option
|
40
|
-
opts.on("--#{name}", option
|
41
|
-
options[option
|
42
|
+
name = option.name.to_s.gsub('_', '-')
|
43
|
+
if option.options[:as] == :boolean
|
44
|
+
opts.on("--#{name}", option.options[:desc]) do |s|
|
45
|
+
options[option.name] = s
|
42
46
|
end
|
43
47
|
else
|
44
|
-
opts.on("--#{name} OPTION", option
|
45
|
-
options[option
|
48
|
+
opts.on("--#{name} OPTION", option.options[:desc]) do |s|
|
49
|
+
options[option.name] = s.gsub('-', '_').to_sym
|
46
50
|
end
|
47
51
|
end
|
48
52
|
end
|
@@ -72,7 +76,7 @@ module Templater
|
|
72
76
|
end
|
73
77
|
|
74
78
|
def step_through_templates
|
75
|
-
@generator.
|
79
|
+
@generator.all_actions.each do |action|
|
76
80
|
if @options[:delete]
|
77
81
|
action.revoke! unless @options[:pretend]
|
78
82
|
say_status('deleted', action, :red)
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Templater
|
2
|
+
|
3
|
+
class Description
|
4
|
+
attr_accessor :name, :options, :block
|
5
|
+
|
6
|
+
def initialize(name, options={}, &block)
|
7
|
+
@name = name
|
8
|
+
@options = options
|
9
|
+
@block = block
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ActionDescription < Description
|
14
|
+
|
15
|
+
def compile(generator)
|
16
|
+
@block.call(generator)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
class ArgumentDescription < Description
|
22
|
+
|
23
|
+
# Checks if the given argument is valid according to this description
|
24
|
+
#
|
25
|
+
# === Parameters
|
26
|
+
# argument<Object>:: Checks if the given argument is valid.
|
27
|
+
# === Returns
|
28
|
+
# Boolean:: Validity of the argument
|
29
|
+
def valid?(argument)
|
30
|
+
if argument.nil? and options[:required]
|
31
|
+
raise Templater::TooFewArgumentsError
|
32
|
+
elsif not argument.nil?
|
33
|
+
if options[:as] == :hash and not argument.is_a?(Hash)
|
34
|
+
raise Templater::MalformattedArgumentError, "Expected the argument to be a Hash, but was '#{argument.inspect}'"
|
35
|
+
elsif options[:as] == :array and not argument.is_a?(Array)
|
36
|
+
raise Templater::MalformattedArgumentError, "Expected the argument to be an Array, but was '#{argument.inspect}'"
|
37
|
+
end
|
38
|
+
|
39
|
+
invalid = catch :invalid do
|
40
|
+
block.call(argument) if block
|
41
|
+
throw :invalid, :not_invalid
|
42
|
+
end
|
43
|
+
raise Templater::ArgumentError, invalid unless invalid == :not_invalid
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
class InvocationDescription < Description
|
50
|
+
|
51
|
+
def get(generator)
|
52
|
+
klass = generator.class.manifold.generator(name)
|
53
|
+
if klass and block
|
54
|
+
generator.instance_exec(klass, &block)
|
55
|
+
elsif klass
|
56
|
+
klass.new(generator.destination_root, generator.options, *generator.arguments)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|