Skip to content

Commit

Permalink
Added ability to set and use different strategies when building bluep…
Browse files Browse the repository at this point in the history
…rints.
  • Loading branch information
andriusch committed Feb 13, 2011
1 parent f486894 commit 32ef854
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 62 deletions.
59 changes: 35 additions & 24 deletions lib/blueprints/blueprint.rb
Expand Up @@ -11,28 +11,13 @@ def initialize(name, context, &block)
super(name, context)

ivname = variable_name
@block = block
@demolish_block = Proc.new { instance_variable_get(ivname).destroy }
@update_block = Proc.new { instance_variable_get(ivname).blueprint(options) }
@strategies = {}
@strategies[:default] = block
@strategies[:demolish] = Proc.new { instance_variable_get(ivname).destroy }
@strategies[:update] = Proc.new { instance_variable_get(ivname).blueprint(options) }
@uses = 0
end

# Builds blueprint and adds it to executed blueprint array. Setups instance variable with same name as blueprint if it is not defined yet.
# Marks blueprint as used.
# @param eval_context (see Buildable#build)
# @param build_once (see Buildable#build)
# @param options (see Buildable#build)
def build_self(eval_context, build_once, options)
@uses += 1 unless built?
surface_errors do
if built? and build_once
eval_block(eval_context, options, &@update_block) if options.present?
elsif @block
result(eval_context) { eval_block(eval_context, options, &@block) }
end
end
end

# Changes blueprint block to build another blueprint by passing additional options to it. Usually used to dry up
# blueprints that are often built with some options.
# @example Extending blueprints
Expand All @@ -41,8 +26,7 @@ def build_self(eval_context, build_once, options)
# @param [Symbol, String] parent Name of parent blueprint.
# @param [Hash] options Options to be passed when building parent.
def extends(parent, options = {})
attributes(options)
@block = Proc.new { build parent => attributes }
attributes(options).blueprint(:default) { build parent => attributes }
end

# Changes backtrace to include what blueprint was being built.
Expand All @@ -60,9 +44,9 @@ def backtrace(trace)
# @raise [Blueprints::DemolishError] If blueprint has not been built yet.
def demolish(eval_context = nil, &block)
if block
@demolish_block = block
blueprint(:demolish, &block)
elsif eval_context and built?
eval_context.instance_eval(&@demolish_block)
eval_context.instance_eval(&@strategies[:demolish])
undo!
else
raise DemolishError, @name
Expand All @@ -71,7 +55,16 @@ def demolish(eval_context = nil, &block)

# Allows customizing what happens when blueprint is already built and it's being built again.
def update(&block)
@update_block = block
blueprint(:update, &block)
end

# Defines strategy for this blueprint. Blueprint can later be built using this strategy by passing :strategy option
# to Buildable#build method.
# @param [#to_sym] name Name of strategy.
# @return [Blueprints::Blueprint] self.
def blueprint(name, &block)
@strategies[name.to_sym] = block
self
end

# Returns normalized attributes for this blueprint. Normalized means that all dependencies are replaced by real
Expand All @@ -85,6 +78,24 @@ def normalized_attributes(eval_context, options = {})

private

# Builds blueprint and adds it to executed blueprint array. Setups instance variable with same name as blueprint if it is not defined yet.
# Marks blueprint as used.
# @param eval_context (see Buildable#build)
# @param options (see Buildable#build)
# @option :rebuild (see Buildable#build)
def build_self(eval_context, options)
@uses += 1 unless built?
opts = options[:options] || {}
strategy = (options[:strategy] || :default).to_sym
surface_errors do
if built? and not options[:rebuild]
eval_block(eval_context, opts, &@strategies[:update]) if opts.present?
elsif @strategies[strategy]
result(eval_context) { eval_block(eval_context, opts, &@strategies[strategy]) }
end
end
end

def eval_block(eval_context, options, &block)
with_method(eval_context, :options, options = normalize_hash(eval_context, options)) do
with_method(eval_context, :attributes, normalized_attributes(eval_context, options)) do
Expand Down
14 changes: 8 additions & 6 deletions lib/blueprints/buildable.rb
Expand Up @@ -44,18 +44,20 @@ def attributes(value = nil)
end
end

# Builds dependencies of blueprint and then blueprint itself.
# Builds dependencies of buildable and then buildable itself.
# @param [Blueprints::EvalContext] eval_context Context to build buildable object in.
# @param [true, false] build_once Used if buildable is already built. If true then old one is updated else buildable is built again.
# @param [Hash] options List of options to be accessible in the body of a blueprint.
def build(eval_context, build_once = true, options = {})
return result(eval_context) if @building or (built? and build_once and options.blank?)
# @param [Hash] options List of options to build this buildable with.
# @option options [Hash] :options ({}) List of options to be accessible in the body of a blueprint.
# @option options [true, false] :rebuild (false) If true this buildable is treated as not built yet and is rebuilt even if it was built before.
# @option options [Symbol] :strategy (:default) Strategy to use when building.
def build(eval_context, options = {})
return result(eval_context) if @building or (built? and not options[:rebuild] and options[:options].blank?)
@building = true

each_namespace { |namespace| namespace.build_parents(eval_context) }
build_parents(eval_context)

result = build_self(eval_context, build_once, options)
result = build_self(eval_context, options)
Namespace.root.executed_blueprints << self

@building = false
Expand Down
7 changes: 6 additions & 1 deletion lib/blueprints/extensions.rb
Expand Up @@ -44,7 +44,11 @@ def blueprint(*args)

def define_blueprint(name, attrs)
klass = self
Blueprints::Context.current.attributes(attrs).blueprint(name) { klass.blueprint attributes }
Blueprints::Context.current.attributes(attrs).blueprint(name) do
klass.blueprint attributes
end.blueprint(:new) do
klass.new.tap { |object| object.blueprint_without_save(attributes) }
end
end

def blueprint_object(attrs)
Expand All @@ -59,6 +63,7 @@ def blueprint(attributes)
blueprint_attribute attribute, value
end
end
alias blueprint_without_save blueprint

private

Expand Down
20 changes: 14 additions & 6 deletions lib/blueprints/helper.rb
Expand Up @@ -12,25 +12,33 @@ module Helper
# @param [Array<Symbol, String, Hash>] names Names of blueprints/namespaces to build. Pass Hash if you want to pass additional options.
# @return Return value of last blueprint
def build(*names)
Namespace.root.build(names, self, true)
Namespace.root.build(names, self)
end

# Same as #build except that you can use it to build same blueprint several times.
# Same as Blueprints::Helper#build except that you can use it to build same blueprint several times.
# @overload build!(*names)
# @param [Array<Symbol, String, Hash>] names Names of blueprints/namespaces to build. Pass Hash if you want to pass additional options.
# @return Return value of last blueprint
# @param names (see Helper#build)
# @return (see Helper#build)
# @overload build!(count, *names)
# @param [Integer] count Times to build passed blueprint
# @param [Array<Symbol, String, Hash>] names Names of blueprints/namespaces to build. Pass Hash if you want to pass additional options.
# @param names (see Helper#build)
# @return [Array] Array of return values of last blueprint, which is same size as count that you pass
def build!(*names)
if names.first.is_a?(Integer)
(0...names.shift).collect { build! *names }
else
Namespace.root.build(names, self, false)
Namespace.root.build(names, self, :rebuild => true)
end
end

# Same as Blueprints::Helper#build except it also allows you to pass strategy to use (#build always uses default strategy).
# @param [Symbol] strategy Strategy to use when building blueprint/namespace.
# @param names (see Helper#build)
# @return (see Helper#build)
def build_with(strategy, *names)
Namespace.root.build(names, self, :strategy => strategy)
end

# Returns attributes that are used to build blueprint.
# @example Setting and retrieving attributes.
# # In blueprint.rb file
Expand Down
18 changes: 9 additions & 9 deletions lib/blueprints/namespace.rb
Expand Up @@ -36,15 +36,6 @@ def [](path)
end
end

# Builds all children and sets an instance variable named by name of namespace with the results.
# @param eval_context (see Buildable#build)
# @param build_once (see Buildable#build)
# @param options (see Buildable#build)
# @return [Array] Results of all blueprints.
def build_self(eval_context, build_once, options)
result(eval_context) { @children.values.collect { |child| child.build(eval_context, build_once, options) }.uniq }
end

# Demolishes all child blueprints and namespaces.
# @param [Blueprints::EvalContext] eval_context Eval context that this namespace was built in.
def demolish(eval_context)
Expand All @@ -53,6 +44,15 @@ def demolish(eval_context)

protected

# Builds all children and sets an instance variable named by name of namespace with the results.
# @param eval_context (see Buildable#build)
# @param build_once (see Buildable#build)
# @param options (see Buildable#build)
# @return [Array] Results of all blueprints.
def build_self(eval_context, options)
result(eval_context) { @children.values.collect { |child| child.build(eval_context, options) }.uniq }
end

def update_context(options)
@children.each_value { |child| child.update_context(options) }
super
Expand Down
9 changes: 5 additions & 4 deletions lib/blueprints/root_namespace.rb
Expand Up @@ -43,15 +43,16 @@ def prebuild(blueprints)
# Builds blueprints that are passed against current context.
# @param [Array<Symbol, String>] names List of blueprints/namespaces to build.
# @param current_context Object to build blueprints against.
# @param build_once (see Buildable.build)
# @param options (see Buildable#build)
# @option options (see Buildable#build)
# @return Result of last blueprint/namespace.
def build(names, current_context, build_once = true)
def build(names, current_context, options = {})
names = [names] unless names.is_a?(Array)
result = names.inject(nil) do |result, member|
if member.is_a?(Hash)
member.map { |name, options| self[name].build(current_context, build_once, options) }.last
member.map { |name, opts| self[name].build(current_context, options.merge(:options => opts)) }.last
else
self[member].build(current_context, build_once)
self[member].build(current_context, options)
end
end

Expand Down
8 changes: 8 additions & 0 deletions spec/blueprints_spec.rb
Expand Up @@ -371,4 +371,12 @@
it "should allow inferring blueprint name" do
build(:infered).name.should == 'infered'
end

it "should allow building with :new strategy" do
build_with(:new, :oak)
@oak.should be_instance_of(Tree)
@oak.should be_new_record
@oak.name.should == 'Oak'
@oak.size.should == 'large'
end
end
4 changes: 4 additions & 0 deletions spec/support/none/initializer.rb
Expand Up @@ -44,6 +44,10 @@ def reload
def destroy
self.class.destroy(self)
end

def new_record?
true
end
end

class Fruit < NoneOrm
Expand Down
28 changes: 20 additions & 8 deletions spec/unit/blueprint_spec.rb
Expand Up @@ -22,7 +22,7 @@
it "should not increase build count if blueprint was already built" do
blueprint.build(stage)
lambda {
blueprint.build(stage, false)
blueprint.build(stage, :rebuild => true)
}.should_not change(blueprint, :uses)
end
end
Expand Down Expand Up @@ -51,14 +51,14 @@
it "should reset auto variable" do
blueprint.build(stage)
stage.instance_variable_set(:@blueprint, :false_result)
blueprint.build(stage, false)
blueprint.build(stage, :rebuild => true)
stage.instance_variable_get(:@blueprint).should == mock1
end
end

it "should allow passing options" do
(result = mock).expects(:options=).with(:option => 'value')
blueprint2 { result.options = options }.build(stage, true, :option => 'value')
blueprint2 { result.options = options }.build(stage, :options => {:option => 'value'})
end

it "should include attributes for blueprint" do
Expand All @@ -80,7 +80,7 @@ def stage.attributes
:attributes
end

blueprint2.build(stage, true, :option => 'value')
blueprint2.build(stage, :options => {:option => 'value'})

stage.options.should == :options
stage.attributes.should == :attributes
Expand All @@ -90,7 +90,7 @@ def stage.attributes
blueprint
stage.instance_variable_set(:@value, 2)
blueprint2 { [options, attributes] }.attributes(:attr => Blueprints::Dependency.new(:blueprint))
options, attributes = blueprint2.build(stage, true, :attr2 => lambda { @value + 2 }, :attr3 => :value)
options, attributes = blueprint2.build(stage, :options => {:attr2 => lambda { @value + 2 }, :attr3 => :value})

options.should == {:attr2 => 4, :attr3 => :value}
attributes.should == {:attr => mock1, :attr2 => 4, :attr3 => :value}
Expand All @@ -101,6 +101,18 @@ def stage.attributes
blueprint.attributes(:attr => Blueprints::Dependency.new(:blueprint2))
blueprint.normalized_attributes(stage, :attr2 => 1).should == {:attr => mock1, :attr2 => 1}
end

describe "strategies" do
it "should allow defining different strategies" do
new_result = mock('new_result')
blueprint.blueprint(:new) { new_result }
blueprint.build(stage, :strategy => 'new').should == new_result
end

it "should return blueprint itself" do
blueprint.blueprint(:new) { 1 }.should == blueprint
end
end
end

describe "demolish" do
Expand Down Expand Up @@ -138,17 +150,17 @@ def stage.attributes

it "should allow building blueprint with different parameters" do
@result.expects(:blueprint).with(:option => 'value')
blueprint.build stage, true, :option => 'value'
blueprint.build stage, :options => {:option => 'value'}
end

it "should allow customizing update block" do
blueprint.update { @blueprint.update_attributes(options) }
@result.expects(:update_attributes).with(:option => 'value')
blueprint.build stage, true, :option => 'value'
blueprint.build stage, :options => {:option => 'value'}
end

it "should not update if build_once is false" do
blueprint.build stage, false, :option => 'value'
blueprint.build stage, :options => {:option => 'value'}, :rebuild => true
end
end

Expand Down
8 changes: 4 additions & 4 deletions spec/unit/namespace_spec.rb
Expand Up @@ -50,10 +50,10 @@
result.should =~ [mock1, mock2]
end

it "should pass build once and eval context params" do
namespace_blueprint.expects(:build).with(stage, false, :option => 'value')
namespace_blueprint2.expects(:build).with(stage, false, :option => 'value')
namespace.build(stage, false, :option => 'value')
it "should pass options and eval context params" do
namespace_blueprint.expects(:build).with(stage, :option => 'value')
namespace_blueprint2.expects(:build).with(stage, :option => 'value')
namespace.build(stage, :option => 'value')
end
end

Expand Down
8 changes: 8 additions & 0 deletions test/blueprints_test.rb
Expand Up @@ -371,4 +371,12 @@ class BlueprintsTest < ActiveSupport::TestCase
should "allow inferring blueprint name" do
assert(build(:infered).name == 'infered')
end

should "allow building with :new strategy" do
build_with(:new, :oak)
assert(@oak.instance_of?(Tree))
assert(@oak.new_record?)
assert(@oak.name == 'Oak')
assert(@oak.size == 'large')
end
end

0 comments on commit 32ef854

Please sign in to comment.