blueprints 0.8.2 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.gitignore +4 -1
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +82 -18
  4. data/LICENSE +1 -1
  5. data/README.rdoc +38 -8
  6. data/Rakefile +11 -35
  7. data/blueprints.gemspec +29 -123
  8. data/features/support/env.rb +7 -10
  9. data/lib/blueprints.rb +68 -12
  10. data/lib/blueprints/blueprint.rb +39 -19
  11. data/lib/blueprints/buildable.rb +92 -74
  12. data/lib/blueprints/configuration.rb +18 -4
  13. data/lib/blueprints/context.rb +148 -5
  14. data/lib/blueprints/database_cleaner_fix.rb +9 -0
  15. data/lib/blueprints/dependency.rb +32 -21
  16. data/lib/blueprints/eval_context.rb +51 -0
  17. data/lib/blueprints/extensions.rb +115 -0
  18. data/lib/blueprints/extensions/rspec.rb +3 -1
  19. data/lib/blueprints/helper.rb +52 -20
  20. data/lib/blueprints/namespace.rb +31 -12
  21. data/lib/blueprints/root_namespace.rb +24 -25
  22. data/lib/blueprints/version.rb +3 -0
  23. data/spec/{active_record/blueprint.rb → blueprint.rb} +14 -17
  24. data/spec/{active_record/blueprints_spec.rb → blueprints_spec.rb} +40 -59
  25. data/spec/spec_helper.rb +34 -0
  26. data/spec/support/active_record/database.yml.example +7 -0
  27. data/spec/support/active_record/initializer.rb +15 -0
  28. data/spec/{active_record/fixtures → support/active_record}/schema.rb +0 -0
  29. data/spec/support/dm-core/initializer.rb +31 -0
  30. data/spec/support/mongo_mapper/database.yml.example +2 -0
  31. data/spec/support/mongo_mapper/initializer.rb +20 -0
  32. data/spec/support/mongoid/database.yml.example +2 -0
  33. data/spec/support/mongoid/initializer.rb +23 -0
  34. data/spec/support/none/initializer.rb +63 -0
  35. data/spec/unit/active_record_spec.rb +1 -6
  36. data/spec/unit/blueprint_spec.rb +91 -20
  37. data/spec/unit/blueprints_spec.rb +44 -0
  38. data/spec/unit/buildable_spec.rb +37 -6
  39. data/spec/unit/configuration_spec.rb +11 -0
  40. data/spec/unit/context_spec.rb +100 -0
  41. data/spec/unit/dependency_spec.rb +24 -19
  42. data/spec/unit/eval_context_spec.rb +56 -0
  43. data/spec/unit/fixtures.rb +61 -0
  44. data/spec/unit/namespace_spec.rb +59 -11
  45. data/spec/unit/spec_helper.rb +8 -16
  46. data/test/blueprints_test.rb +40 -59
  47. data/test/test_helper.rb +6 -16
  48. data/test_all.sh +45 -0
  49. metadata +178 -61
  50. data/VERSION +0 -1
  51. data/lib/blueprints/core_ext.rb +0 -69
  52. data/lib/blueprints/file_context.rb +0 -37
  53. data/spec/active_record/fixtures/database.yml.example +0 -8
  54. data/spec/active_record/fixtures/fruit.rb +0 -3
  55. data/spec/active_record/fixtures/tree.rb +0 -4
  56. data/spec/active_record/spec_helper.rb +0 -37
  57. data/spec/no_db/blueprint.rb +0 -9
  58. data/spec/no_db/blueprints_spec.rb +0 -45
  59. data/spec/no_db/fixtures/fruit.rb +0 -15
  60. data/spec/no_db/spec_helper.rb +0 -14
  61. data/spec/test_all.sh +0 -39
@@ -1,16 +1,26 @@
1
1
  module Blueprints
2
+ # Contains configuration of blueprints. Instance of this is yielded in Blueprints.enable block.
3
+ # @example Configuring through Blueprints.enable block
4
+ # Blueprints.enable do |config|
5
+ # config.prebuild = :user, :profile
6
+ # end
7
+ # @example Configuring directly
8
+ # Blueprints.config.transactions = false
2
9
  class Configuration
3
- SUPPORTED_ORMS = [nil, :active_record]
4
10
  # Allows passing custom filename pattern in case blueprints are held in place other than spec/blueprint, test/blueprint, blueprint.
5
11
  attr_reader :filename
6
12
  # Allows passing scenarios that should be prebuilt and available in all tests. Works similarly to fixtures.
7
13
  attr_accessor :prebuild
8
- # Allows passing custom root folder to use in case of non rails project. Defaults to RAILS_ROOT or current folder if RAILS_ROOT is not defined.
14
+ # Allows passing custom root folder to use in case of non rails project. Defaults to Rails.root or current folder if Rails is not defined.
9
15
  attr_reader :root
10
16
  # By default blueprints runs each test in it's own transaction. This may sometimes be not desirable so this options allows to turn this off.
11
17
  attr_accessor :transactions
18
+ # Default attributes are used when blueprints has no name specified.
19
+ attr_reader :default_attributes
12
20
 
13
- # Sets default attributes for all attributes
21
+ # Initializes new Configuration object with default attributes.
22
+ # By defaults filename patterns are: blueprint.rb and blueprint/*.rb in spec, test and root directories.
23
+ # Also by default prebuildable blueprints list is empty, transactions are enabled and root is set to Rails.root or current directory.
14
24
  def initialize
15
25
  self.filename = [nil, "spec", "test"].map do |dir|
16
26
  ["blueprint"].map do |file|
@@ -18,10 +28,10 @@ module Blueprints
18
28
  ["#{path}.rb", File.join(path, "*.rb")]
19
29
  end
20
30
  end.flatten
21
- @orm = :active_record
22
31
  @prebuild = []
23
32
  @transactions = true
24
33
  @root = defined?(Rails) ? Rails.root : Pathname.pwd
34
+ @default_attributes = [:name]
25
35
  end
26
36
 
27
37
  def filename=(value)
@@ -31,5 +41,9 @@ module Blueprints
31
41
  def root=(value)
32
42
  @root = Pathname.new(value)
33
43
  end
44
+
45
+ def default_attributes=(value)
46
+ @default_attributes = Array(value)
47
+ end
34
48
  end
35
49
  end
@@ -1,11 +1,154 @@
1
1
  module Blueprints
2
- # Class that blueprint blocks are evaluated against. Allows you to access options that were passed to build method.
2
+ # Class that blueprint files are evaluated against. Has methods for setting and retrieving attributes and dependencies.
3
+ # Allows defining new blueprints and namespaces.
3
4
  class Context
4
- attr_accessor :options, :attributes
5
+ @@chain = []
5
6
 
6
- # Method that allows building one blueprint inside of another. Simply delegates to root namespace.
7
- def build(*names)
8
- Namespace.root.build(*names)
7
+ attr_reader :dependencies, :file
8
+
9
+ # Initializes new context with passed parent, attributes, dependencies, file and namespace.
10
+ # Attributes and dependencies are automatically merged with parents' attributes and dependencies.
11
+ # File and namespace are automatically set to parent counterparts unless they are explicitly changed.
12
+ # @param [Hash] options Options for new context.
13
+ # @option options [Hash] :attributes ({}) List of attributes, merged with parent attributes.
14
+ # @option options [Array<String, Symbol>] :dependencies ([]) List of dependencies, merged with parent dependencies.
15
+ # @option options [Pathname] :file File this context is evaluated in. Should be passed for top level contexts only.
16
+ # @option options [Blueprints::Namespace] :namespace Namespace that new blueprints and namespaces should be children of.
17
+ # @option options [Blueprints::Context] :parent Parent context that is used to retrieve unchanged values.
18
+ def initialize(options = {})
19
+ options.assert_valid_keys(:dependencies, :attributes, :file, :parent, :namespace)
20
+ @dependencies = (options[:dependencies] || []).collect(&:to_sym)
21
+ @attributes = options[:attributes] || {}
22
+ @file = options[:file]
23
+ @namespace = options[:namespace]
24
+
25
+ if parent = options[:parent]
26
+ @attributes.reverse_merge!(parent.attributes)
27
+ @dependencies = (parent.dependencies + @dependencies).uniq
28
+ @file ||= parent.file
29
+ @namespace ||= parent.namespace
30
+ end
31
+ end
32
+
33
+ # Checks if two contexts are equal by comparing attributes, dependencies, namespace and file
34
+ # @param [Blueprints::Context] context Context to compare this one to.
35
+ # @return [true, false] Whether contexts are equal or not.
36
+ def ==(context)
37
+ @dependencies == context.dependencies and @attributes == context.attributes and @file == context.file and @namespace == context.namespace
38
+ end
39
+
40
+ # Defines a new blueprint by name and block passed.
41
+ # @example Define blueprint.
42
+ # blueprint :user do
43
+ # User.blueprint :name => 'User'
44
+ # end
45
+ # @param name (see Buildable#initialize)
46
+ # @return [Blueprints::Blueprint] Newly defined blueprint.
47
+ def blueprint(name = nil, &block)
48
+ Blueprint.new(name, self, &block)
49
+ end
50
+
51
+ # @overload namespace(name, &block)
52
+ # Defines new namespace by name, and evaluates block against it.
53
+ # @example Define namespace and blueprint in it.
54
+ # namespace :banned do
55
+ # blueprint :user do
56
+ # User.blueprint :name => 'User'
57
+ # end
58
+ # end
59
+ # @param [String, Symbol] name Name of namespace.
60
+ # @return [Blueprints::Namespace] Newly defined namespace.
61
+ # @overload namespace
62
+ # Returns namespace for this context.
63
+ # @return [Blueprints::Namespace] Namespace for this context.
64
+ def namespace(name = nil, &block)
65
+ if name
66
+ Namespace.new(name, self).tap do |namespace|
67
+ with_context(:namespace => namespace, &block)
68
+ end
69
+ else
70
+ @namespace
71
+ end
72
+ end
73
+
74
+ # @overload attributes(new_attributes, &block)
75
+ # Yields and returns child context that has new attributes set.
76
+ # @example Define blueprint with attributes
77
+ # attributes(:name => 'User').blueprint(:user) do
78
+ # User.blueprint attributes
79
+ # end
80
+ # @example Define multiple blueprints with same attributes
81
+ # attributes(:name => 'User') do
82
+ # blueprint(:user1) do
83
+ # User.blueprint attributes
84
+ # end
85
+ #
86
+ # blueprint(:user2) do
87
+ # User.blueprint attributes
88
+ # end
89
+ # end
90
+ # @param [Hash] new_attributes Attributes for child context.
91
+ # @return [Blueprints::Context] Child context
92
+ # @overload attributes
93
+ # Returns attributes of context.
94
+ # @return [Hash] Attributes of context.
95
+ def attributes(new_attributes = nil, &block)
96
+ if new_attributes
97
+ with_context(:attributes => new_attributes, &block)
98
+ else
99
+ @attributes
100
+ end
101
+ end
102
+
103
+ # Yields and returns child context that has dependencies set.
104
+ # @example Define blueprint with dependencies
105
+ # depends_on(:user, :admin).blueprint(:user_and_admin)
106
+ # @example Define multiple blueprints with same dependencies.
107
+ # depends_on :user, :admin do
108
+ # blueprint :user_and_admin
109
+ # blueprint :admin_and_user
110
+ # end
111
+ # @param [Array<Symbol, String>] new_dependencies Dependencies for child context.
112
+ # @return [Blueprints::Context] Child context
113
+ def depends_on(*new_dependencies, &block)
114
+ with_context(:dependencies => new_dependencies, &block)
115
+ end
116
+
117
+ # Yields and returns child context that has new options set.
118
+ # @param options (see Context#initialize)
119
+ # @return [Blueprints::Context] Child context
120
+ def with_context(options, &block)
121
+ Context.eval_within_context(options.merge(:parent => self), &block)
122
+ end
123
+
124
+ # Initializes new Blueprints::Dependency object.
125
+ # @overload d(name, options = {})
126
+ # @param name (see Dependency#initialize)
127
+ # @param options (see Dependency#initialize)
128
+ # @overload d(name, instance_variable_name, options = {})
129
+ # @param name (see Dependency#initialize)
130
+ # @param instance_variable_name (see Dependency#initialize)
131
+ # @param options (see Dependency#initialize)
132
+ def d(*args)
133
+ Dependency.new(*args)
134
+ end
135
+
136
+ # Return current context.
137
+ # @return [Blueprints::Context] Current context.
138
+ def self.current
139
+ @@chain.last
140
+ end
141
+
142
+ # Creates child context and sets it as current. Evaluates block and file within child context if any are passed.
143
+ # @param [Hash] new_options Options for child context.
144
+ def self.eval_within_context(new_options, &block)
145
+ @@chain << context = new(new_options)
146
+
147
+ file = new_options[:file]
148
+ context.instance_eval(File.read(file), file) if file
149
+ context.instance_eval(&block) if block
150
+
151
+ @@chain.pop
9
152
  end
10
153
  end
11
154
  end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class Mysql2Adapter < AbstractAdapter
4
+ def truncate_table(table_name)
5
+ execute("TRUNCATE TABLE #{quote_table_name(table_name)};")
6
+ end
7
+ end
8
+ end
9
+ end if defined?(ActiveRecord)
@@ -1,35 +1,46 @@
1
- # Class for defining blueprint dependencies. Accepts up to 3 params:
2
- # * name - pass the name of blueprint to build when trying to access value of this dependency.
3
- # * iv_name (optional) - pass the name of instance variable to use for value. Defaults to same name as blueprint name.
4
- # * options (optional) - pass options that are then passed to blueprint when building.
5
- # Examples:
6
- # Blueprints::Dependency.new(:blueprint).value # Builds blueprint 'blueprint' and returns value of @blueprint instance variable
7
- # Blueprints::Dependency.new(:blueprint, value).value # Builds blueprint 'blueprint' and returns value of @value instance variable
8
- # Blueprints::Dependency.new(:blueprint, :option => true).value # Builds blueprint 'blueprint' with options and returns value of @value instance variable
9
- #
10
- # Blueprints::Dependency objects also catch all missing methods. They are later replayed on instance variable when getting value. Example:
11
- # d = Blueprints::Dependency.new(:blueprint).name.size
12
- # d.value # => 4 when @blueprint.name == 'John'
1
+ # Class for defining blueprint dependencies.
13
2
  class Blueprints::Dependency
14
3
  instance_methods.each { |m| undef_method m if m =~ /^(to_|id$)/ }
15
4
 
16
- # Initializes new copy of Blueprints::Dependency with name, iv_name and options.
5
+ # Initializes new Blueprints::Dependency object.
6
+ # @example Build blueprint 'blueprint' and returns value of @blueprint instance variable.
7
+ # Blueprints::Dependency.new(:blueprint)
8
+ # @example Build blueprint 'blueprint' and returns value of @value instance variable.
9
+ # Blueprints::Dependency.new(:blueprint, value)
10
+ # @example Build blueprint 'blueprint' with options and returns value of @value instance variable.
11
+ # Blueprints::Dependency.new(:blueprint, :option => true)
12
+ # @example Register called methods
13
+ # d = Blueprints::Dependency.new(:blueprint).name.size
14
+ # @overload d(name, options = {})
15
+ # Use result of blueprint/namespace +name+ and pass options when building.
16
+ # @param [Symbol, String] name Name of blueprint/namespace.
17
+ # @param [Hash] options Options to pass when building blueprint/namespace.
18
+ # @overload d(name, instance_variable_name, options = {})
19
+ # Build blueprint/namespace with options and use differently names instance variable as result.
20
+ # @param [Symbol, String] name Name of blueprint/namespace.
21
+ # @param [Symbol, String] instance_variable_name Name of instance variable to use as a result.
22
+ # @param [Hash] options Options to pass when building blueprint/namespace.
17
23
  def initialize(name, *args)
18
- @name = name
19
- @options = args.extract_options!
20
- @iv_name = args.first || @name
24
+ @name = name
25
+ @options = args.extract_options!
26
+ @iv_name = (args.first || @name).to_s.gsub('.', '_')
21
27
  @registry = []
22
28
  end
23
29
 
24
- # Builds blueprint (if necessary) and returns the value of instance variable.
25
- def blueprint_value
26
- Blueprints::RootNamespace.root.build @name => @options
27
- @registry.inject(Blueprints::RootNamespace.root.context.instance_variable_get(:"@#{@iv_name}")) do |value, (method, args, block)|
28
- value.send(method, *args, &block)
30
+ # Returns block that builds blueprint (if necessary) takes instance variable for this dependency and calls all methods from method registry.
31
+ # @return [Proc] Proc that can be called to return value for this dependency.
32
+ def to_proc
33
+ name, options, registry, variable_name = @name, @options, @registry, @iv_name
34
+ Proc.new do
35
+ build name => options
36
+ registry.inject(instance_variable_get(:"@#{variable_name}")) do |value, (method, args, block)|
37
+ value.send(method, *args, &block)
38
+ end
29
39
  end
30
40
  end
31
41
 
32
42
  # Catches all missing methods to later replay when asking for value.
43
+ # @return [Blueprints::Dependency] self
33
44
  def method_missing(method, *args, &block)
34
45
  @registry << [method, args, block]
35
46
  self
@@ -0,0 +1,51 @@
1
+ module Blueprints
2
+ class EvalContext
3
+ # Copy instance variables to another object.
4
+ # @param target Object to copy instance variables to.
5
+ def copy_instance_variables(target)
6
+ instance_variables.each do |iv_name|
7
+ target.instance_variable_set(iv_name, instance_variable_get(iv_name))
8
+ end
9
+ end
10
+
11
+ # Sets options and attributes and evaluates block against self.
12
+ # @param [Blueprints::Context] context Context of buildable object. Used to extract attributes.
13
+ # @param [Hash] options Options hash, merged into attributes.
14
+ def instance_eval(context, options, &block)
15
+ options = normalize_hash(options)
16
+ define_singleton_method(:options) { options }
17
+ attributes = normalize_hash(context.attributes).merge(options)
18
+ define_singleton_method(:attributes) { attributes }
19
+
20
+ super(&block)
21
+ end
22
+
23
+ # Builds blueprints by delegating to root namespace.
24
+ # @param [Array<String, Symbol>] blueprints Names of buildables.
25
+ # @return Result of last buildable.
26
+ def build(*blueprints)
27
+ Namespace.root.build(blueprints)
28
+ end
29
+
30
+ # Normalizes attributes hash by evaluating all Proc and Blueprints::Dependency objects against itself.
31
+ # @param [Hash] hash Attributes hash.
32
+ # @return [Hash] Normalized hash.
33
+ def normalize_hash(hash)
34
+ hash.each_with_object({}) do |(attr, value), normalized|
35
+ normalized[attr] = if value.respond_to?(:to_proc) and not Symbol === value
36
+ instance_exec(&value)
37
+ else
38
+ value
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def define_singleton_method(name, &block)
46
+ singleton_class.class_eval do
47
+ define_method(name, &block)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,115 @@
1
+ module Blueprints::Extensions
2
+ # Here to support ActiveSupport 2.x. Will be removed once support for ActiveRecord 2.3 is terminated.
3
+ module Extendable
4
+ def self.included(mod)
5
+ if defined?(ActiveSupport::Concern)
6
+ mod.extend ActiveSupport::Concern
7
+ else
8
+ def mod.included(mod)
9
+ mod.extend Blueprints::Extensions::Blueprintable::ClassMethods
10
+ end
11
+ end
12
+ end
13
+ end
14
+
15
+ # Include this module into your class if you need klass.blueprint and object.blueprint methods.
16
+ module Blueprintable
17
+ include Extendable
18
+
19
+ module ClassMethods
20
+ # @overload blueprint(attributes)
21
+ # Does same as +create!+ method except that it also bypasses attr_protected and attr_accessible. Typically used in blueprint block.
22
+ # @example Create post for user
23
+ # @user.posts.blueprint(:title => 'first post', :text => 'My first post')
24
+ # @param [Hash, Array<Hash>] attributes Attributes used to create objects.
25
+ # @return Created object(s).
26
+ # @overload blueprint(name, attributes = {})
27
+ # Defines new blueprint that creates an object with attributes passed.
28
+ # @example Create blueprint named :post.
29
+ # Post.blueprint(:post, :title => 'first post', :text => 'My first post', :user => d(:user)).depends_on(:board)
30
+ # @param [String, Symbol, Hash] name Name of blueprint.
31
+ # @param [Hash] attributes Attributes hash.
32
+ # @return [Blueprints::Blueprint] Defined blueprint.
33
+ def blueprint(*args)
34
+ if Blueprints::Context.current
35
+ attrs = args.extract_options!
36
+ define_blueprint(args.first, attrs)
37
+ else
38
+ objects = args.collect { |attrs| blueprint_object(attrs) }
39
+ args.size == 1 ? objects.first : objects
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def define_blueprint(name, attrs)
46
+ klass = self
47
+ Blueprints::Context.current.attributes(attrs).blueprint(name) { klass.blueprint attributes }
48
+ end
49
+
50
+ def blueprint_object(attrs)
51
+ new.tap { |object| object.blueprint(attrs) }
52
+ end
53
+ end
54
+
55
+ # Updates attributes of object by calling setter methods. Does same as +update_attributes!+ except it also bypasses attr_protected and attr_accessible.
56
+ # @param [Hash] attributes Attributes that are used to update object.
57
+ def blueprint(attributes)
58
+ attributes.each do |attribute, value|
59
+ blueprint_attribute attribute, value
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def blueprint_attribute(attribute, value)
66
+ send(:"#{attribute}=", value)
67
+ end
68
+ end
69
+
70
+ # Include this instead of Blueprints::Extensions::Blueprintable if record needs to persist, includes Blueprints::Extensions::Blueprintable
71
+ module Saveable
72
+ include Extendable
73
+ include Blueprintable
74
+
75
+ # Overrides object.blueprint to also call save!
76
+ def blueprint(attributes)
77
+ super(attributes)
78
+ save!
79
+ end
80
+ end
81
+
82
+ # Include this instead of Blueprints::Extensions::Saveable if you want non bang save method (eg.using datamapper)
83
+ module SoftSaveable
84
+ include Extendable
85
+ include Blueprintable
86
+
87
+ # Overrides object.blueprint to also call save
88
+ def blueprint(attributes)
89
+ super(attributes)
90
+ save
91
+ end
92
+ end
93
+
94
+ # Include this instead of Blueprints::Extensions::Saveable if you need support for dynamic attributes (eg. using mongodb)
95
+ module DynamicSaveable
96
+ include Extendable
97
+ include Saveable
98
+
99
+ private
100
+
101
+ def blueprint_attribute(attribute, value)
102
+ setter = :"#{attribute}="
103
+ if respond_to?(setter)
104
+ send(setter, value)
105
+ else
106
+ write_attribute(attribute, value)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ ActiveRecord::Base.send(:include, Blueprints::Extensions::Saveable) if defined?(ActiveRecord)
113
+ Mongoid::Document.send(:include, Blueprints::Extensions::DynamicSaveable) if defined?(Mongoid)
114
+ MongoMapper::Document.send(:append_inclusions, Blueprints::Extensions::DynamicSaveable) if defined?(MongoMapper)
115
+ DataMapper::Model.send(:append_inclusions, Blueprints::Extensions::SoftSaveable) if defined?(DataMapper)