blueprints 0.3.4 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  .idea
2
2
  *.gem
3
+ html
3
4
  spec/active_record/fixtures/database.yml
4
5
  debug.log
data/Rakefile CHANGED
@@ -9,7 +9,13 @@ begin
9
9
  gemspec.homepage = "http://github.com/sinsiliux/blueprints"
10
10
  gemspec.authors = ["Andrius Chamentauskas"]
11
11
  end
12
- Jeweler::GemcutterTasks.new
12
+ Jeweler::GemcutterTasks.new
13
13
  rescue LoadError
14
14
  puts "Jeweler not available. Install it with: sudo gem install jeweler"
15
15
  end
16
+
17
+ require 'rake/rdoctask'
18
+ Rake::RDocTask.new do |rd|
19
+ rd.main = "README.rdoc"
20
+ rd.rdoc_files.include("README.rdoc", "lib/**/*.rb")
21
+ end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.4
1
+ 0.4.0
data/blueprints.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{blueprints}
8
- s.version = "0.3.4"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Andrius Chamentauskas"]
12
- s.date = %q{2009-12-10}
12
+ s.date = %q{2009-12-25}
13
13
  s.description = %q{Another replacement for factories and fixtures. The library that lazy typists will love}
14
14
  s.email = %q{sinsiliux@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
27
27
  "install.rb",
28
28
  "lib/blueprints.rb",
29
29
  "lib/blueprints/buildable.rb",
30
+ "lib/blueprints/context.rb",
30
31
  "lib/blueprints/database_backends/abstract.rb",
31
32
  "lib/blueprints/database_backends/active_record.rb",
32
33
  "lib/blueprints/database_backends/none.rb",
data/lib/blueprints.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'activesupport'
2
2
  files = %w{
3
- buildable namespace root_namespace plan file_context helper errors
3
+ context buildable namespace root_namespace plan file_context helper errors
4
4
  database_backends/abstract database_backends/active_record database_backends/none
5
5
  }
6
6
  files << if defined? Spec or $0 =~ /script.spec$/
@@ -18,24 +18,37 @@ module Blueprints
18
18
  end
19
19
  end.flatten
20
20
 
21
+ # Returns a list of supported ORMs. For now it supports ActiveRecord and None.
21
22
  def self.supported_orms
22
23
  (DatabaseBackends.constants - ['Abstract']).collect {|class_name| class_name.underscore.to_sym }
23
24
  end
24
25
 
26
+ # Returns root of project blueprints is used in. Automatically detected for rails and merb. Can be overwritten by using
27
+ # <tt>:root</tt> options when loading blueprints. If root can't be determined, returns nil which means that current
28
+ # directory is asumed as root.
25
29
  def self.framework_root
26
30
  @@framework_root ||= RAILS_ROOT rescue Rails.root rescue Merb.root rescue nil
27
31
  end
28
32
 
33
+ # Setups variables from global context and starts transaction. Should be called before every test case.
29
34
  def self.setup(current_context)
30
35
  Namespace.root.setup
31
36
  Namespace.root.copy_ivars(current_context)
32
37
  @@orm.start_transaction
33
38
  end
34
39
 
40
+ # Rollbacks transaction returning everything to state before test. Should be called after every test case.
35
41
  def self.teardown
36
42
  @@orm.rollback_transaction
37
43
  end
38
44
 
45
+ # Sets up configuration, clears database, runs scenarios that have to be prebuilt. Should be run before all test cases and before <tt>setup</tt>.
46
+ # Accepts following options:
47
+ # * <tt>:delete_policy</tt> - allows changing how tables in database should be cleared. By default simply uses delete statement. Supports :delete and :truncate options.
48
+ # * <tt>:filename</tt> - Allows passing custom filename pattern in case blueprints are held in place other than spec/blueprint, test/blueprint, blueprint.
49
+ # * <tt>:prebuild</tt> - Allows passing scenarios that should be prebuilt and available in all tests. Works similarly to fixtures.
50
+ # * <tt>:root</tt> - Allows passing custom root folder to use in case of non rails and non merb project.
51
+ # * <tt>:orm</tt> - Allows specifying what orm should be used. Default to <tt>:active_record</tt>, also allows <tt>:none</tt>
39
52
  def self.load(options = {})
40
53
  options.assert_valid_keys(:delete_policy, :filename, :prebuild, :root, :orm)
41
54
  options.symbolize_keys!
@@ -52,6 +65,14 @@ module Blueprints
52
65
  Namespace.root.prebuild(options[:prebuild])
53
66
  end
54
67
 
68
+ # Clears all tables in database. Also accepts a list of tables in case not all tables should be cleared.
69
+ def self.delete_tables(*tables)
70
+ @@orm.delete_tables(@@delete_policy, *tables)
71
+ end
72
+
73
+ protected
74
+
75
+ # Loads blueprints file and creates blueprints from data it contains. Is run by setup method
55
76
  def self.load_scenarios_files(*patterns)
56
77
  patterns.flatten!
57
78
  patterns.collect! {|pattern| File.join(framework_root, pattern)} if framework_root
@@ -65,8 +86,4 @@ module Blueprints
65
86
 
66
87
  raise "Plans file not found! Put plans in #{patterns.join(' or ')} or pass custom filename pattern with :filename option"
67
88
  end
68
-
69
- def self.delete_tables(*tables)
70
- @@orm.delete_tables(@@delete_policy, *tables)
71
- end
72
89
  end
@@ -10,10 +10,12 @@ module Blueprints
10
10
  Namespace.root.add_child(self) if Namespace.root
11
11
  end
12
12
 
13
+ # Defines blueprint dependencies. Used internally, but can be used externally too.
13
14
  def depends_on(*scenarios)
14
15
  @parents = (@parents || []) + scenarios.map{|s| s.to_sym}
15
16
  end
16
17
 
18
+ # Builds dependencies of blueprint and then blueprint itself.
17
19
  def build
18
20
  namespace = self
19
21
  namespace.build_parent_plans while namespace = namespace.namespace
@@ -51,4 +53,4 @@ module Blueprints
51
53
  end
52
54
  end
53
55
  end
54
- end
56
+ end
@@ -0,0 +1,6 @@
1
+ module Blueprints
2
+ # Class that blueprint blocks are evaluated against. Allows you to access options that were passed to build method.
3
+ class Context
4
+ attr_accessor :options
5
+ end
6
+ end
@@ -1,14 +1,18 @@
1
1
  module Blueprints
2
2
  module DatabaseBackends
3
3
  class Abstract
4
+ # Method to start transaction. Needs to be implemented in child class.
4
5
  def start_transaction
5
6
  raise NotImplementedError
6
7
  end
7
8
 
9
+ # Method to revert transaction. Needs to be implemented in child class.
8
10
  def revert_transaction
9
11
  raise NotImplementedError
10
12
  end
11
13
 
14
+ # Method to clear tables. Should accept delete policy and list of tables to delete. If list of tables is empty, should
15
+ # delete all tables. Needs to be implemented in child class.
12
16
  def delete_tables(delete_policy, *args)
13
17
  raise NotImplementedError
14
18
  end
@@ -3,21 +3,26 @@ module Blueprints
3
3
  class ActiveRecord
4
4
  DELETE_POLICIES = {:delete => "DELETE FROM %s", :truncate => "TRUNCATE %s"}
5
5
 
6
+ # Extends active record with blueprint method
6
7
  def initialize
7
- ::ActiveRecord::Base.extend(ActiveRecordExtensions)
8
+ ::ActiveRecord::Base.send(:include, ActiveRecordExtensions::Instance)
9
+ ::ActiveRecord::Base.extend(ActiveRecordExtensions::Class)
8
10
  end
9
11
 
12
+ # Starts new transaction and marks it as unjoinable so that test case could use transactions too.
10
13
  def start_transaction
11
14
  ::ActiveRecord::Base.connection.increment_open_transactions
12
15
  ::ActiveRecord::Base.connection.transaction_joinable = false
13
16
  ::ActiveRecord::Base.connection.begin_db_transaction
14
17
  end
15
18
 
19
+ # Rollbacks transaction
16
20
  def rollback_transaction
17
21
  ::ActiveRecord::Base.connection.rollback_db_transaction
18
22
  ::ActiveRecord::Base.connection.decrement_open_transactions
19
23
  end
20
24
 
25
+ # Clears all tables using delete policy specified. Also accepts list of tables to delete.
21
26
  def delete_tables(delete_policy, *args)
22
27
  delete_policy ||= :delete
23
28
  raise ArgumentError, "Unknown delete policy #{delete_policy}" unless DELETE_POLICIES.keys.include?(delete_policy)
@@ -25,30 +30,52 @@ module Blueprints
25
30
  args.each { |t| ::ActiveRecord::Base.connection.delete(DELETE_POLICIES[delete_policy] % t) }
26
31
  end
27
32
 
33
+ # Returns all tables without skipped ones.
28
34
  def tables
29
35
  ::ActiveRecord::Base.connection.tables - skip_tables
30
36
  end
31
37
 
38
+ # Returns tables that should never be cleared (those that contain migrations information).
32
39
  def skip_tables
33
40
  %w( schema_info schema_migrations )
34
41
  end
35
42
 
43
+ # Extensions for active record class
36
44
  module ActiveRecordExtensions
37
- def blueprint(*args)
38
- options = args.extract_options!
39
- if args.present?
40
- klass = self
41
- Blueprints::Plan.new(*args) do
42
- klass.blueprint options
43
- end
44
- else
45
- returning(self.new) do |object|
46
- options.each do |attr, value|
47
- value = Blueprints::Namespace.root.context.instance_variable_get(value) if value.is_a? Symbol and value.to_s =~ /^@.+$/
48
- object.send("#{attr}=", value)
45
+ module Class
46
+ # Two forms of this method can be used. First one is typically used inside blueprint block. Essentially it does
47
+ # same as <tt>create!</tt>, except it does bypass attr_protected and attr_accessible. It accepts only a hash or attributes,
48
+ # same as <tt>create!</tt> does.
49
+ # blueprint :post => :user do
50
+ # @user.posts.blueprint(:title => 'first post', :text => 'My first post')
51
+ # end
52
+ # The second form is used when you want to define new blueprint. It takes first argument as name of blueprint
53
+ # and second one as hash of attributes. As you cannot use instance variables outside of blueprint block, you need
54
+ # to prefix them with colon. So the example above could be rewritten like this:
55
+ # Post.blueprint(:post, :title => 'first post', :text => 'My first post', :user => :@user).depends_on(:user)
56
+ # or like this:
57
+ # Post.blueprint({:post => :user}, :title => 'first post', :text => 'My first post', :user => :@user)
58
+ def blueprint(*args)
59
+ attributes = args.extract_options!
60
+ if args.present?
61
+ klass = self
62
+ Blueprints::Plan.new(*args) do
63
+ klass.blueprint attributes.merge(options)
49
64
  end
50
- object.save!
65
+ else
66
+ returning(self.new) { |object| object.blueprint(attributes) }
67
+ end
68
+ end
69
+ end
70
+
71
+ module Instance
72
+ # Updates attributes of object and calls save!. Bypasses attr_protected ant attr_accessible.
73
+ def blueprint(attributes)
74
+ attributes.each do |attr, value|
75
+ value = Blueprints::Namespace.root.context.instance_variable_get(value) if value.is_a? Symbol and value.to_s =~ /^@.+$/
76
+ send("#{attr}=", value)
51
77
  end
78
+ save!
52
79
  end
53
80
  end
54
81
  end
@@ -1,12 +1,16 @@
1
1
  module Blueprints
2
2
  module DatabaseBackends
3
+ # Database backend when no orm is actually used. Can be adapted to work with any kind of data.
3
4
  class None
5
+ # Dummy method
4
6
  def start_transaction
5
7
  end
6
8
 
9
+ # Dummy method
7
10
  def revert_transaction
8
11
  end
9
12
 
13
+ # Dummy method
10
14
  def delete_tables(delete_policy, *args)
11
15
  end
12
16
  end
@@ -1,4 +1,5 @@
1
1
  module Blueprints
2
+ # Is raised when blueprint or namespace is not found.
2
3
  class PlanNotFoundError < NameError
3
4
  def initialize(*args)
4
5
  @plans = args
@@ -8,4 +9,4 @@ module Blueprints
8
9
  "Plan/namespace not found '#{@plans.join(',')}'"
9
10
  end
10
11
  end
11
- end
12
+ end
@@ -1,6 +1,9 @@
1
- module Spec
2
- module Runner
1
+ module Spec #:nodoc:
2
+ module Runner #:nodoc:
3
3
  class Configuration
4
+ # Enables blueprints in rspec. Is automatically added if <tt>Spec</tt> is defined at loading time or <tt>script/spec</tt>
5
+ # is used. You might need to require it manually in certain case (eg. running specs from metrics).
6
+ # Accepts options hash. For supported options please check Blueprints.load.
4
7
  def enable_blueprints(options = {})
5
8
  Blueprints.load(options)
6
9
 
@@ -14,4 +17,4 @@ module Spec
14
17
  end
15
18
  end
16
19
  end
17
- end
20
+ end
@@ -1,12 +1,16 @@
1
- module Test
2
- module Unit
1
+ module Test #:nodoc:
2
+ module Unit #:nodoc:
3
3
  class TestCase
4
+ # Runs tests with blueprints support
4
5
  def run_with_blueprints(result, &progress_block)
5
6
  Blueprints.setup(self)
6
7
  run_without_blueprints(result, &progress_block)
7
8
  Blueprints.teardown
8
9
  end
9
10
 
11
+ # Enables blueprints in test/unit. Is automatically added if <tt>Spec</tt> is not defined at loading time.
12
+ # You might need to require it manually in certain case (eg. using both rspec and test/unit).
13
+ # Accepts options hash. For supported options please check Blueprints.load.
10
14
  def self.enable_blueprints(options = {})
11
15
  include Blueprints::Helper
12
16
  Blueprints.load(options)
@@ -14,4 +18,4 @@ module Test
14
18
  end
15
19
  end
16
20
  end
17
- end
21
+ end
@@ -1,9 +1,12 @@
1
1
  module Blueprints
2
+ # Module that blueprints file is executed against. Defined <tt>blueprint</tt> and <tt>namespace</tt> methods.
2
3
  module FileContext
4
+ # Creates a new plan by name and block passed
3
5
  def self.blueprint(plan, &block)
4
6
  Plan.new(plan, &block)
5
7
  end
6
8
 
9
+ # Creates new namespace by name, and evaluates block against it.
7
10
  def self.namespace(name)
8
11
  old_namespace = Namespace.root
9
12
  namespace = Namespace.new(name)
@@ -13,4 +16,4 @@ module Blueprints
13
16
  Namespace.root = old_namespace
14
17
  end
15
18
  end
16
- end
19
+ end
@@ -1,5 +1,13 @@
1
1
  module Blueprints
2
+ # A helper module that should be included in test framework. Adds methods <tt>build</tt> and <tt>demolish</tt>
2
3
  module Helper
4
+ # Builds one or more blueprints by their names. You can pass names as symbols or strings. You can also pass additional
5
+ # options hash which will be available by calling <tt>options</tt> in blueprint block. Returns result of blueprint block.
6
+ # # build :apple and orange blueprints
7
+ # build :apple, :orange
8
+ #
9
+ # # build :apple scenario with additional options
10
+ # build :apple, :color => 'red'
3
11
  def build_plan(*names)
4
12
  result = Namespace.root.build(*names).last
5
13
  Namespace.root.copy_ivars(self)
@@ -8,6 +16,10 @@ module Blueprints
8
16
 
9
17
  alias :build :build_plan
10
18
 
19
+ # Clears all tables in database. You can pass table names to clear only those tables. You can also pass <tt>:undo</tt> option
20
+ # to remove scenarios from built scenarios cache.
21
+ #
22
+ # TODO: add sample usage
11
23
  def demolish(*args)
12
24
  options = args.extract_options!
13
25
  Blueprints.delete_tables(*args)
@@ -23,4 +35,4 @@ module Blueprints
23
35
  end
24
36
  end
25
37
  end
26
- end
38
+ end
@@ -1,4 +1,6 @@
1
1
  module Blueprints
2
+ # Namespace class, inherits from <tt>Buildable</tt>. Allows adding and finding child blueprints/namespaces and building
3
+ # all it's children.
2
4
  class Namespace < Buildable
3
5
  cattr_accessor :root
4
6
  delegate :empty?, :size, :to => :@children
@@ -28,4 +30,4 @@ module Blueprints
28
30
  Namespace.root.add_variable(path, @children.collect {|p| p.last.build }.uniq)
29
31
  end
30
32
  end
31
- end
33
+ end
@@ -1,14 +1,17 @@
1
1
  module Blueprints
2
+ # Class for actual blueprints. Allows building itself by executing block passed against current context.
2
3
  class Plan < Buildable
4
+ # Initializes blueprint by name and block
3
5
  def initialize(name, &block)
4
6
  super(name)
5
7
  @block = block
6
8
  end
7
9
 
10
+ # Builds plan and adds it to executed plan hash. Setups instance variable with same name as plan if it is not defined yet.
8
11
  def build_plan
9
12
  surface_errors do
10
13
  if @block
11
- @result = Namespace.root.context.module_eval(&@block)
14
+ @result = Namespace.root.context.instance_eval(&@block)
12
15
  Namespace.root.add_variable(path, @result)
13
16
  end
14
17
  end unless Namespace.root.executed_plans.include?(path)
@@ -25,4 +28,4 @@ module Blueprints
25
28
  raise error
26
29
  end
27
30
  end
28
- end
31
+ end
@@ -1,40 +1,50 @@
1
1
  module Blueprints
2
+ # Defines a root namespace that is used when no other namespace is. Apart from functionality in namespace it also allows
3
+ # building blueprints/namespaces by name. Is also used for copying instance variables between blueprints/contexts/global
4
+ # context.
2
5
  class RootNamespace < Namespace
3
6
  attr_reader :context, :plans
4
7
  attr_accessor :executed_plans
5
-
8
+
6
9
  def initialize
7
10
  @executed_plans = Set.new
8
11
  @global_executed_plans = Set.new
9
- @global_context = Module.new
10
12
 
11
13
  super ''
12
14
  end
13
15
 
16
+ # Loads all instance variables from global context to current one.
14
17
  def setup
15
- @context = YAML.load(@global_context)
18
+ @context = Blueprints::Context.new
19
+ YAML.load(@global_variables).each { |name, value| @context.instance_variable_set(name, value) }
16
20
  @executed_plans = @global_executed_plans.clone
17
21
  end
18
22
 
23
+ # Copies all instance variables from current context to another one.
19
24
  def copy_ivars(to)
20
25
  @context.instance_variables.each do |iv|
21
26
  to.instance_variable_set(iv, @context.instance_variable_get(iv))
22
27
  end
23
28
  end
24
29
 
30
+ # Sets up global context and executes prebuilt blueprints against it.
25
31
  def prebuild(plans)
26
- @context = @global_context
32
+ @context = Blueprints::Context.new
27
33
  @global_scenarios = build(*plans) if plans
28
34
  @global_executed_plans = @executed_plans
29
- @global_context = YAML.dump(@global_context)
35
+
36
+ @global_variables = YAML.dump(@context.instance_variables.each_with_object({}) {|iv, hash| hash[iv] = @context.instance_variable_get(iv) })
30
37
  end
31
38
 
39
+ # Builds blueprints that are passed against current context.
32
40
  def build(*names)
41
+ @context.options = names.extract_options!
33
42
  names.map {|name| self[name].build}
34
43
  end
35
44
 
45
+ # Sets instance variable in current context to passed value.
36
46
  def add_variable(name, value)
37
- name = "@#{name}" unless name.to_s[0, 1] == "@"
47
+ name = "@#{name}" unless name.to_s[0, 1] == "@"
38
48
  @context.instance_variable_set(name, value) unless @context.instance_variable_get(name)
39
49
  end
40
50
 
@@ -50,4 +50,8 @@ namespace :pitted => :pine do
50
50
  namespace :red => :orange do
51
51
  Fruit.blueprint(:apple, :species => 'pitted red apple')
52
52
  end
53
- end
53
+ end
54
+
55
+ blueprint :apple_with_params do
56
+ Fruit.create! options.merge(:species => 'apple')
57
+ end
@@ -7,7 +7,7 @@ describe Blueprints do
7
7
  end
8
8
 
9
9
  it "should support required ORMS" do
10
- Blueprints.supported_orms.should == [:active_record, :none]
10
+ Blueprints.supported_orms.should =~ [:active_record, :none]
11
11
  end
12
12
  end
13
13
 
@@ -221,7 +221,7 @@ describe Blueprints do
221
221
  Blueprints::Namespace.root.expects(:empty?).returns(true)
222
222
  lambda {
223
223
  Blueprints.load(:orm => :unknown)
224
- }.should raise_error(ArgumentError, "Unsupported ORM unknown. Blueprints supports only active_record, none")
224
+ }.should raise_error(ArgumentError, "Unsupported ORM unknown. Blueprints supports only #{Blueprints.supported_orms.join(', ')}")
225
225
  end
226
226
  end
227
227
 
@@ -246,6 +246,18 @@ describe Blueprints do
246
246
  @acorn.should_not be_nil
247
247
  @acorn.tree.should == @oak
248
248
  end
249
+
250
+ it "should allow updating object using blueprint method" do
251
+ build :oak
252
+ @oak.blueprint(:size => 'updated')
253
+ @oak.reload.size.should == 'updated'
254
+ end
255
+
256
+ it "should automatically merge passed options" do
257
+ build :oak, :size => 'optional'
258
+ @oak.name.should == 'Oak'
259
+ @oak.size.should == 'optional'
260
+ end
249
261
  end
250
262
 
251
263
  describe "with pitted namespace" do
@@ -294,5 +306,19 @@ describe Blueprints do
294
306
  end
295
307
  end
296
308
  end
309
+
310
+ describe 'extra parameters' do
311
+ it "should allow passing extra parameters when building" do
312
+ build :apple_with_params, :average_diameter => 14
313
+ @apple_with_params.average_diameter.should == 14
314
+ @apple_with_params.species.should == 'apple'
315
+ end
316
+
317
+ it "should allow set options to empty hash if no parameters are passed" do
318
+ build :apple_with_params
319
+ @apple_with_params.average_diameter.should == nil
320
+ @apple_with_params.species.should == 'apple'
321
+ end
322
+ end
297
323
  end
298
324
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blueprints
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrius Chamentauskas
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-10 00:00:00 +02:00
12
+ date: 2009-12-25 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -33,6 +33,7 @@ files:
33
33
  - install.rb
34
34
  - lib/blueprints.rb
35
35
  - lib/blueprints/buildable.rb
36
+ - lib/blueprints/context.rb
36
37
  - lib/blueprints/database_backends/abstract.rb
37
38
  - lib/blueprints/database_backends/active_record.rb
38
39
  - lib/blueprints/database_backends/none.rb