blueprints 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)