diff options
author | danielsdeleo <dan@opscode.com> | 2013-05-26 15:45:54 -0700 |
---|---|---|
committer | danielsdeleo <dan@opscode.com> | 2013-05-29 11:32:22 -0700 |
commit | e7d836f1bc6478a05419abb242197eee98c803b1 (patch) | |
tree | d451cc0e4a5d3fabece658bc3bb1d25816e433cc | |
parent | fa4c2ec1560da0c295482072df60fc7cb36c2504 (diff) | |
download | chef-e7d836f1bc6478a05419abb242197eee98c803b1.tar.gz |
Add helper method/module support to template resource
Adds helper methods to a template via the following syntax:
```
template "name" do
# singular method definition:
helper(:method_name) { method_body }
# inline module definition
helpers do
def method_name
method_body
end
end
# external module inclusion
helpers(MyHelperModule)
end
```
In each of the above cases, variables defined by Chef (e.g., `@node`)
or the user (via `:variables => {}`) can be accessed as normal.
Could use the following improvements:
- Template partials are not supported
- Input validation on Resource::Template should be improved
- Needs Unit tests for Template provider/implementation classes
-rw-r--r-- | lib/chef/provider/template/content.rb | 37 | ||||
-rw-r--r-- | lib/chef/resource/template.rb | 22 | ||||
-rw-r--r-- | spec/data/cookbooks/openldap/templates/default/helper_test.erb | 1 | ||||
-rw-r--r-- | spec/functional/resource/template_spec.rb | 97 | ||||
-rw-r--r-- | spec/unit/cookbook/syntax_check_spec.rb | 3 | ||||
-rw-r--r-- | spec/unit/provider/template/content_spec.rb | 10 | ||||
-rw-r--r-- | spec/unit/resource/template_spec.rb | 43 |
7 files changed, 208 insertions, 5 deletions
diff --git a/lib/chef/provider/template/content.rb b/lib/chef/provider/template/content.rb index 756db4642c..5629ee9a19 100644 --- a/lib/chef/provider/template/content.rb +++ b/lib/chef/provider/template/content.rb @@ -22,6 +22,37 @@ require 'chef/file_content_management/content_base' class Chef class Provider class Template + + # TODO: extract to file + # TODO: integrate into mixin/template (make it work with partials) + # TODO: docs + class TemplateContext < Erubis::Context + + def _define_helpers(helper_methods) + # TODO (ruby 1.8 hack) + # This is most elegantly done with Object#define_singleton_method, + # however ruby 1.8.7 does not support that, so we create a module and + # include it. This should be revised when 1.8 support is not needed. + helper_mod = Module.new do + helper_methods.each do |method_name, method_body| + define_method(method_name, &method_body) + end + end + extend(helper_mod) + end + + def _define_helpers_from_blocks(blocks) + blocks.each do |module_body| + helper_mod = Module.new(&module_body) + extend(helper_mod) + end + end + + def _extend_modules(module_names) + module_names.each { |mod| extend(mod) } + end + end + class Content < Chef::FileContentManagement::ContentBase include Chef::Mixin::Template @@ -35,10 +66,12 @@ class Chef private def file_for_provider - context = {} - context.merge!(@new_resource.variables) + context = TemplateContext.new(@new_resource.variables) context[:node] = @run_context.node context[:template_finder] = template_finder + context._define_helpers(@new_resource.inline_helper_blocks) + context._define_helpers_from_blocks(@new_resource.inline_helper_modules) + context._extend_modules(@new_resource.helper_modules) file = nil render_template(IO.read(template_location), context) { |t| file = t } file diff --git a/lib/chef/resource/template.rb b/lib/chef/resource/template.rb index af51b64700..0e468ed401 100644 --- a/lib/chef/resource/template.rb +++ b/lib/chef/resource/template.rb @@ -29,6 +29,10 @@ class Chef provides :template, :on_platforms => :all + attr_reader :inline_helper_blocks + attr_reader :inline_helper_modules + attr_reader :helper_modules + def initialize(name, run_context=nil) super @resource_name = :template @@ -38,6 +42,9 @@ class Chef @local = false @variables = Hash.new @provider = Chef::Provider::Template + @inline_helper_blocks = {} + @inline_helper_modules = [] + @helper_modules = [] end def source(file=nil) @@ -71,6 +78,21 @@ class Chef :kind_of => [ TrueClass, FalseClass ] ) end + + def helper(method_name, &block) + # TODO: method_name must be symbol or coerce. + # TODO: block is not optional. + @inline_helper_blocks[method_name] = block + end + + def helpers(module_name=nil,&block) + if block_given? + @inline_helper_modules << block + else + @helper_modules << module_name + end + end + end end end diff --git a/spec/data/cookbooks/openldap/templates/default/helper_test.erb b/spec/data/cookbooks/openldap/templates/default/helper_test.erb new file mode 100644 index 0000000000..92e6fe0427 --- /dev/null +++ b/spec/data/cookbooks/openldap/templates/default/helper_test.erb @@ -0,0 +1 @@ +<%= helper_method %> diff --git a/spec/functional/resource/template_spec.rb b/spec/functional/resource/template_spec.rb index ae568a496a..4d8c6c8738 100644 --- a/spec/functional/resource/template_spec.rb +++ b/spec/functional/resource/template_spec.rb @@ -71,4 +71,101 @@ describe Chef::Resource::Template do IO.read(path).should == expected_content end end + + describe "when the template resource defines helper methods" do + + include_context "diff disabled" + + let!(:resource) do + r = create_resource + r.source "helper_test.erb" + r + end + + let(:expected_content) { "value from helper method\n" } + + shared_examples "a template with helpers" do + it "generates expected content by calling helper methods" do + resource.run_action(:create) + IO.read(path).should == expected_content + end + end + + context "using single helper syntax" do + before do + resource.helper(:helper_method) { "value from helper method" } + end + + it_behaves_like "a template with helpers" + end + + context "using single helper syntax referencing @node" do + before do + node.set[:helper_test_attr] = "value from helper method" + resource.helper(:helper_method) { "#{@node[:helper_test_attr]}" } + end + + it_behaves_like "a template with helpers" + end + + context "using an inline block to define helpers" do + before do + resource.helpers do + def helper_method + "value from helper method" + end + end + end + + it_behaves_like "a template with helpers" + end + + context "using an inline block referencing @node" do + before do + node.set[:helper_test_attr] = "value from helper method" + + resource.helpers do + def helper_method + @node[:helper_test_attr] + end + end + end + + it_behaves_like "a template with helpers" + + end + + context "using a module from a library" do + + module ExampleModule + def helper_method + "value from helper method" + end + end + + before do + resource.helpers(ExampleModule) + end + + it_behaves_like "a template with helpers" + + end + context "using a module from a library referencing @node" do + + module ExampleModuleReferencingATNode + def helper_method + @node[:helper_test_attr] + end + end + + before do + node.set[:helper_test_attr] = "value from helper method" + + resource.helpers(ExampleModuleReferencingATNode) + end + + it_behaves_like "a template with helpers" + + end + end end diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb index 948b3c5a38..cea5c89e87 100644 --- a/spec/unit/cookbook/syntax_check_spec.rb +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -34,8 +34,7 @@ describe Chef::Cookbook::SyntaxCheck do @recipes = %w{default.rb gigantor.rb one.rb}.map { |f| File.join(cookbook_path, 'recipes', f) } @ruby_files = @attr_files + @defn_files + @recipes - @template_files = %w{openldap_stuff.conf.erb openldap_variable_stuff.conf.erb test.erb}.map { |f| File.join(cookbook_path, 'templates', 'default', f)} - + @template_files = %w{helper_test.erb openldap_stuff.conf.erb openldap_variable_stuff.conf.erb test.erb}.map { |f| File.join(cookbook_path, 'templates', 'default', f)} end it "creates a syntax checker given the cookbook name when Chef::Config.cookbook_path is set" do diff --git a/spec/unit/provider/template/content_spec.rb b/spec/unit/provider/template/content_spec.rb index 946549238c..4061d99d7b 100644 --- a/spec/unit/provider/template/content_spec.rb +++ b/spec/unit/provider/template/content_spec.rb @@ -21,7 +21,15 @@ require 'spec_helper' describe Chef::Provider::Template::Content do let(:new_resource) do - mock("Chef::Resource::Template (new)", :cookbook_name => 'openldap', :source => 'openldap_stuff.conf.erb', :local => false, :cookbook => nil, :variables => {}) + mock("Chef::Resource::Template (new)", + :cookbook_name => 'openldap', + :source => 'openldap_stuff.conf.erb', + :local => false, + :cookbook => nil, + :variables => {}, + :inline_helper_blocks => {}, + :inline_helper_modules => [], + :helper_modules => []) end let(:rendered_file_location) { Dir.tmpdir + '/openldap_stuff.conf' } diff --git a/spec/unit/resource/template_spec.rb b/spec/unit/resource/template_spec.rb index fc16d6c21d..35c684c9ff 100644 --- a/spec/unit/resource/template_spec.rb +++ b/spec/unit/resource/template_spec.rb @@ -105,4 +105,47 @@ describe Chef::Resource::Template do @resource.identity.should == "/tmp/foo.txt" end end + + describe "defining helper methods" do + + it "collects helper method bodies as blocks" do + @resource.helper(:example_1) { "example_1" } + @resource.helper(:example_2) { "example_2" } + @resource.inline_helper_blocks[:example_1].call.should == "example_1" + @resource.inline_helper_blocks[:example_2].call.should == "example_2" + end + + it "raises an error when attempting to define a helper method without a method body" do + pending + @resource.helper(:example) # should raise_error() + end + + it "collects helper module bodies as blocks" do + @resource.helpers do + def example_1 + "example_1" + end + end + module_body = @resource.inline_helper_modules.first + module_body.should be_a(Proc) + test_mod = Module.new(&module_body) + test_context = Object.new + test_context.extend(test_mod) + test_context.example_1.should == "example_1" + end + + module ExampleHelpers + end + + it "collects helper modules" do + @resource.helpers(ExampleHelpers) + @resource.helper_modules.should include(ExampleHelpers) + end + + it "raises an error if a non-module is given as a helper module" do + pending + end + + end + end |