bricks 0.0.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.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +20 -0
- data/README.md +221 -0
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/bricks.gemspec +67 -0
- data/lib/bricks/adapters/active_record.rb +43 -0
- data/lib/bricks/builder.rb +150 -0
- data/lib/bricks/builder_set.rb +40 -0
- data/lib/bricks/dsl.rb +23 -0
- data/lib/bricks.rb +43 -0
- data/rails/init.rb +5 -0
- data/spec/bricks/adapters/active_record_spec.rb +13 -0
- data/spec/bricks/builder_spec.rb +41 -0
- data/spec/bricks_spec.rb +189 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/active_record.rb +52 -0
- metadata +146 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "rspec", "~> 2.0"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.6.2"
|
12
|
+
gem "rcov", ">= 0"
|
13
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2011 Mojo Tech
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
Bricks
|
2
|
+
======
|
3
|
+
|
4
|
+
Bricks is a hybrid Object Builder/Factory implementation. It aims to be a more flexible alternative to the existing Object Factory solutions while retaining as much simplicity as possible.
|
5
|
+
|
6
|
+
Usage
|
7
|
+
-----
|
8
|
+
|
9
|
+
Let's assume you have the following class:
|
10
|
+
|
11
|
+
# Only ActiveRecord objects are supported right now.
|
12
|
+
|
13
|
+
# == Schema Information
|
14
|
+
#
|
15
|
+
# Table name: articles
|
16
|
+
#
|
17
|
+
# id :integer(4) not null, primary key
|
18
|
+
# title :string(255)
|
19
|
+
# author :string(255)
|
20
|
+
# formatted_title :string(510)
|
21
|
+
# publication_id :integer(4)
|
22
|
+
#
|
23
|
+
class Article < ActiveRecord::Base
|
24
|
+
belongs_to :publication
|
25
|
+
has_many :readers
|
26
|
+
end
|
27
|
+
|
28
|
+
# == Schema Information
|
29
|
+
#
|
30
|
+
# Table name: publications
|
31
|
+
#
|
32
|
+
# id :integer(4) not null, primary key
|
33
|
+
# name :string(255)
|
34
|
+
# type :string(255)
|
35
|
+
#
|
36
|
+
class Publication < ActiveRecord::Base
|
37
|
+
end
|
38
|
+
|
39
|
+
class Newspaper < Publication
|
40
|
+
end
|
41
|
+
|
42
|
+
# == Schema Information
|
43
|
+
#
|
44
|
+
# Table name: publications
|
45
|
+
#
|
46
|
+
# id :integer(4) not null, primary key
|
47
|
+
# name :string(255)
|
48
|
+
# birth_date :date
|
49
|
+
#
|
50
|
+
class Reader < ActiveRecord::Base
|
51
|
+
end
|
52
|
+
|
53
|
+
At its simplest, you can start using Bricks without declaring any builder (*note:* it gets less verbose).
|
54
|
+
|
55
|
+
article_builder = build(Article)
|
56
|
+
|
57
|
+
This will give you a builder for the Article class, which you can then use to build an Article
|
58
|
+
|
59
|
+
article_builder.
|
60
|
+
title("Why I hate Guybrush Threepwood").
|
61
|
+
author("Ghost Pirate LeChuck")
|
62
|
+
|
63
|
+
Contrary to the original pattern, builders are stateful (i.e., you don't get a new builder every time you call a method on the current builder).
|
64
|
+
|
65
|
+
You can get the underlying instance by calling _#generate_.
|
66
|
+
|
67
|
+
article = article_builder.generate
|
68
|
+
|
69
|
+
This will initialize an Article with the attributes you passed the builder. If, instead of initializing, you'd prefer the record to be created right away, use _#create_ instead.
|
70
|
+
|
71
|
+
If you don't really care about the builder and just want the underlying instance you can instead use.
|
72
|
+
|
73
|
+
article = build(Article).
|
74
|
+
title("Why I hate Guybrush Threepwood").
|
75
|
+
author!("Ghost Pirate LeChuck") # Note the "!"
|
76
|
+
|
77
|
+
When you want to use the default builder, without customizing it any further, you can tack the "!" at the end of the builder method:
|
78
|
+
|
79
|
+
build!(Article)
|
80
|
+
create!(Article)
|
81
|
+
|
82
|
+
### Building builders
|
83
|
+
|
84
|
+
Of course, using builders like described above isn't of much use. Let's create a builder for _Article_:
|
85
|
+
|
86
|
+
Bricks do
|
87
|
+
builder Article do
|
88
|
+
title "Why I hate Guybrush Threepwood"
|
89
|
+
author "Ghost Pirate LeChuck"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
You can then use it as you'd expect:
|
94
|
+
|
95
|
+
# initializes an Article with default attributes set, and saves it
|
96
|
+
article = create!(Article)
|
97
|
+
|
98
|
+
### Deferred initialization
|
99
|
+
|
100
|
+
builder Article do
|
101
|
+
# ...
|
102
|
+
|
103
|
+
formatted_title { "The formatted title at #{Date.now}." }
|
104
|
+
end
|
105
|
+
|
106
|
+
You can get at the underlying instance from deferred blocks:
|
107
|
+
|
108
|
+
builder Article do
|
109
|
+
# ...
|
110
|
+
|
111
|
+
formatted_title { |obj| obj.title + " by " + obj.author }
|
112
|
+
end
|
113
|
+
|
114
|
+
### Associations
|
115
|
+
|
116
|
+
Bricks supports setting association records.
|
117
|
+
|
118
|
+
#### Many-to-one (belongs to)
|
119
|
+
|
120
|
+
Bricks do
|
121
|
+
builder Publication do
|
122
|
+
name "The Caribbean Times"
|
123
|
+
end
|
124
|
+
|
125
|
+
builder Article do
|
126
|
+
# ...
|
127
|
+
|
128
|
+
publication # instantiate a publication with the default attributes set
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
You can also customize the association builder instance:
|
133
|
+
|
134
|
+
builder Article do
|
135
|
+
# ...
|
136
|
+
publication.name("The Caribeeaneer")
|
137
|
+
end
|
138
|
+
|
139
|
+
If you prepend a "~" to the association declaration, the record will be initialized/created *only* if a record with the given attributes doesn't exist yet:
|
140
|
+
|
141
|
+
builder Article do
|
142
|
+
# ...
|
143
|
+
~publication # will search for a record with name "The Caribbean Times"
|
144
|
+
end
|
145
|
+
|
146
|
+
#### One-to-many, Many-to-many (has many, has and belongs to many)
|
147
|
+
|
148
|
+
Bricks do
|
149
|
+
builder Article do
|
150
|
+
# ...
|
151
|
+
|
152
|
+
# readers association will have 3 records
|
153
|
+
%w(Tom Dick Harry).each { |r| readers.name(r) }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
Each call to the *-to-many association name will add a new builder, which you can then further customize:
|
158
|
+
|
159
|
+
readers.name("Tom").birth_date(30.years.ago)
|
160
|
+
|
161
|
+
(Note that you don't use "!" here. That's only when building the records in your tests.)
|
162
|
+
|
163
|
+
### Builder Inheritance
|
164
|
+
|
165
|
+
Given the builder:
|
166
|
+
|
167
|
+
builder Publication do
|
168
|
+
name "The Caribbean Times"
|
169
|
+
end
|
170
|
+
|
171
|
+
you can do something like:
|
172
|
+
|
173
|
+
np = build!(Newspaper)
|
174
|
+
np.name # => "The Caribbean Times"
|
175
|
+
|
176
|
+
### Traits
|
177
|
+
|
178
|
+
The real power of the Builder pattern comes from the use of traits. Instead of declaring name factories in a single-inheritance model, you instead declare traits, which you can then mix and match:
|
179
|
+
|
180
|
+
builder Article
|
181
|
+
# ...
|
182
|
+
|
183
|
+
trait :alternative_publication do |name|
|
184
|
+
publication.name(name)
|
185
|
+
end
|
186
|
+
|
187
|
+
trait :by_elaine do
|
188
|
+
title "Why I love Guybrush Threepwood"
|
189
|
+
author "Elaine Marley-Threepwood"
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
Use it like this:
|
194
|
+
|
195
|
+
build(Article).alternative_publication("The Caribeaneer").by_elaine
|
196
|
+
|
197
|
+
Note that if you want to override a *-to-many association inside a trait, you need to clear it first:
|
198
|
+
|
199
|
+
builder Article
|
200
|
+
# ...
|
201
|
+
|
202
|
+
# this will reset the readers association
|
203
|
+
trait :new_readers do
|
204
|
+
readers.clear
|
205
|
+
|
206
|
+
%(Charlotte Emily Anne).each { |r| readers.name(r) }
|
207
|
+
end
|
208
|
+
|
209
|
+
# this will add to the readers association
|
210
|
+
trait :more_readers do
|
211
|
+
readers.name("Groucho")
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
For an executable version of this documentation, please see spec/bricks_spec.rb.
|
216
|
+
|
217
|
+
Copyright
|
218
|
+
---------
|
219
|
+
|
220
|
+
Copyright (c) 2011 Mojo Tech. See LICENSE.txt for further details.
|
221
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "bricks"
|
18
|
+
gem.homepage = "http://github.com/mojotech/bricks"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Hybrid object builder/factory.}
|
21
|
+
gem.email = "david@mojotech.com"
|
22
|
+
gem.author = "David Leal"
|
23
|
+
# dependencies defined in Gemfile
|
24
|
+
end
|
25
|
+
Jeweler::RubygemsDotOrgTasks.new
|
26
|
+
|
27
|
+
require 'rspec/core'
|
28
|
+
require 'rspec/core/rake_task'
|
29
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
30
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
34
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
35
|
+
spec.rcov = true
|
36
|
+
end
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
require 'rake/rdoctask'
|
41
|
+
Rake::RDocTask.new do |rdoc|
|
42
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
43
|
+
|
44
|
+
rdoc.rdoc_dir = 'rdoc'
|
45
|
+
rdoc.title = "bricks #{version}"
|
46
|
+
rdoc.rdoc_files.include('README*')
|
47
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
48
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/bricks.gemspec
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{bricks}
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["David Leal"]
|
12
|
+
s.date = %q{2011-06-16}
|
13
|
+
s.email = %q{david@mojotech.com}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"LICENSE.txt",
|
16
|
+
"README.md"
|
17
|
+
]
|
18
|
+
s.files = [
|
19
|
+
".document",
|
20
|
+
".rspec",
|
21
|
+
"Gemfile",
|
22
|
+
"LICENSE.txt",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"bricks.gemspec",
|
27
|
+
"lib/bricks.rb",
|
28
|
+
"lib/bricks/adapters/active_record.rb",
|
29
|
+
"lib/bricks/builder.rb",
|
30
|
+
"lib/bricks/builder_set.rb",
|
31
|
+
"lib/bricks/dsl.rb",
|
32
|
+
"rails/init.rb",
|
33
|
+
"spec/bricks/adapters/active_record_spec.rb",
|
34
|
+
"spec/bricks/builder_spec.rb",
|
35
|
+
"spec/bricks_spec.rb",
|
36
|
+
"spec/spec_helper.rb",
|
37
|
+
"spec/support/active_record.rb"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/mojotech/bricks}
|
40
|
+
s.licenses = ["MIT"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.rubygems_version = %q{1.3.7}
|
43
|
+
s.summary = %q{Hybrid object builder/factory.}
|
44
|
+
|
45
|
+
if s.respond_to? :specification_version then
|
46
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
47
|
+
s.specification_version = 3
|
48
|
+
|
49
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
50
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.0"])
|
51
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
|
52
|
+
s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
|
53
|
+
s.add_development_dependency(%q<rcov>, [">= 0"])
|
54
|
+
else
|
55
|
+
s.add_dependency(%q<rspec>, ["~> 2.0"])
|
56
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
57
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
|
58
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
59
|
+
end
|
60
|
+
else
|
61
|
+
s.add_dependency(%q<rspec>, ["~> 2.0"])
|
62
|
+
s.add_dependency(%q<bundler>, ["~> 1.0.0"])
|
63
|
+
s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
|
64
|
+
s.add_dependency(%q<rcov>, [">= 0"])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'bricks'
|
2
|
+
require 'active_record'
|
3
|
+
|
4
|
+
module Bricks
|
5
|
+
module Adapters
|
6
|
+
class ActiveRecord
|
7
|
+
class Association
|
8
|
+
attr_reader :type
|
9
|
+
|
10
|
+
def initialize(klass, kind)
|
11
|
+
@class = klass
|
12
|
+
@type = case kind
|
13
|
+
when :belongs_to; :one
|
14
|
+
when :has_many, :has_and_belongs_to_many; :many
|
15
|
+
else "Unknown AR association type: #{kind}."
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def klass
|
20
|
+
@class
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def association?(klass, name, type = nil)
|
25
|
+
association(klass, name, type)
|
26
|
+
end
|
27
|
+
|
28
|
+
def association(klass, name, type = nil)
|
29
|
+
if ar = klass.reflect_on_association(name.to_sym)
|
30
|
+
a = Association.new(ar.klass, ar.macro)
|
31
|
+
|
32
|
+
a if type.nil? || a.type == type
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def find(klass, obj)
|
37
|
+
klass.find(:first, :conditions => obj.attributes)
|
38
|
+
end
|
39
|
+
|
40
|
+
Bricks::Builder.adapter = self.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'bricks/dsl'
|
2
|
+
require 'bricks/builder_set'
|
3
|
+
|
4
|
+
module Bricks
|
5
|
+
class Builder
|
6
|
+
include Bricks::DSL
|
7
|
+
|
8
|
+
def self.adapter
|
9
|
+
@@adapter
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.adapter=(adapter)
|
13
|
+
@@adapter = adapter
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.instances
|
17
|
+
@@instances ||= {}
|
18
|
+
end
|
19
|
+
|
20
|
+
def ~@()
|
21
|
+
@search = true
|
22
|
+
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def derive(klass = @class, save = @save)
|
27
|
+
Builder.new(klass, @attrs, @traits, save)
|
28
|
+
end
|
29
|
+
|
30
|
+
def dup_as_builder
|
31
|
+
derive(@class, false)
|
32
|
+
end
|
33
|
+
|
34
|
+
def dup_as_creator
|
35
|
+
derive(@class, true)
|
36
|
+
end
|
37
|
+
|
38
|
+
def initialize(klass, attrs = nil, traits = nil, save = false, &block)
|
39
|
+
@class = klass
|
40
|
+
@attrs = attrs ? deep_copy(attrs) : []
|
41
|
+
@traits = traits ? Module.new { include traits } : Module.new
|
42
|
+
@save = save
|
43
|
+
|
44
|
+
extend @traits
|
45
|
+
|
46
|
+
instance_eval &block if block_given?
|
47
|
+
end
|
48
|
+
|
49
|
+
def generate
|
50
|
+
obj = initialize_object
|
51
|
+
|
52
|
+
obj = adapter.find(@class, obj) || obj if @search
|
53
|
+
save_object(obj) if @save
|
54
|
+
|
55
|
+
obj
|
56
|
+
end
|
57
|
+
|
58
|
+
def trait(name, &block)
|
59
|
+
@traits.class_eval do
|
60
|
+
define_method "__#{name}", &block
|
61
|
+
|
62
|
+
define_method name do |*args|
|
63
|
+
send "__#{name}", *args
|
64
|
+
|
65
|
+
self
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def method_missing(name, *args, &block)
|
71
|
+
attr = (return_object = name.to_s =~ /!$/) ? name.to_s.chop : name
|
72
|
+
|
73
|
+
result = if respond_to?(attr)
|
74
|
+
send(attr, *args)
|
75
|
+
elsif settable?(attr)
|
76
|
+
set attr, *args, &block
|
77
|
+
else
|
78
|
+
raise Bricks::NoAttributeOrTrait, "Can't find `#{name}'."
|
79
|
+
end
|
80
|
+
|
81
|
+
if return_object
|
82
|
+
generate
|
83
|
+
else
|
84
|
+
result
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def subject
|
91
|
+
Builder.instances[@class] ||= @class.new
|
92
|
+
end
|
93
|
+
|
94
|
+
def adapter
|
95
|
+
Builder.adapter
|
96
|
+
end
|
97
|
+
|
98
|
+
def deep_copy(attrs)
|
99
|
+
attrs.inject([]) { |a, (k, v)|
|
100
|
+
a.tap { a << [k, Builder === v ? v.derive : v] }
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def save_object(obj)
|
105
|
+
obj.save!
|
106
|
+
end
|
107
|
+
|
108
|
+
def initialize_object
|
109
|
+
obj = @class.new
|
110
|
+
|
111
|
+
@attrs.each { |(k, v)|
|
112
|
+
val = case v
|
113
|
+
when Proc
|
114
|
+
v.call *[obj].take([v.arity, 0].max)
|
115
|
+
when Builder, BuilderSet
|
116
|
+
v.generate
|
117
|
+
else
|
118
|
+
v
|
119
|
+
end
|
120
|
+
|
121
|
+
obj.send "#{k}=", val
|
122
|
+
}
|
123
|
+
|
124
|
+
obj
|
125
|
+
end
|
126
|
+
|
127
|
+
def settable?(name)
|
128
|
+
subject.respond_to?("#{name}=")
|
129
|
+
end
|
130
|
+
|
131
|
+
def set(name, val = nil, &block)
|
132
|
+
raise Bricks::BadSyntax, "Block and value given" if val && block_given?
|
133
|
+
|
134
|
+
pair = @attrs.assoc(name) || (@attrs << [name, nil]).last
|
135
|
+
|
136
|
+
if block_given?
|
137
|
+
pair[-1] = block
|
138
|
+
elsif val
|
139
|
+
pair[-1] = val
|
140
|
+
elsif adapter.association?(@class, name, :one)
|
141
|
+
pair[-1] = create(adapter.association(@class, name).klass)
|
142
|
+
elsif adapter.association?(@class, name, :many)
|
143
|
+
pair[-1] ||= BuilderSet.new(adapter.association(@class, name).klass)
|
144
|
+
else
|
145
|
+
raise Bricks::BadSyntax,
|
146
|
+
"No value or block given and not an association: #{name}."
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'bricks/dsl'
|
2
|
+
|
3
|
+
module Bricks
|
4
|
+
class BuilderSet
|
5
|
+
include Bricks::DSL
|
6
|
+
|
7
|
+
def build(klass)
|
8
|
+
(@builders << super).last
|
9
|
+
end
|
10
|
+
|
11
|
+
def build!(klass)
|
12
|
+
(@builders << super).last
|
13
|
+
end
|
14
|
+
|
15
|
+
def clear
|
16
|
+
@builders.clear
|
17
|
+
end
|
18
|
+
|
19
|
+
def create(klass)
|
20
|
+
(@builders << super).last
|
21
|
+
end
|
22
|
+
|
23
|
+
def create!(klass)
|
24
|
+
(@builders << super).last
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(klass)
|
28
|
+
@class = klass
|
29
|
+
@builders = []
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(name, *args)
|
33
|
+
build(@class).send(name, *args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate
|
37
|
+
@builders.map { |b| b.generate }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/bricks/dsl.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
module Bricks
|
2
|
+
module DSL
|
3
|
+
def build(klass)
|
4
|
+
builder(klass).dup_as_builder
|
5
|
+
end
|
6
|
+
|
7
|
+
def build!(klass)
|
8
|
+
build(klass).generate
|
9
|
+
end
|
10
|
+
|
11
|
+
def create(klass)
|
12
|
+
builder(klass).dup_as_creator
|
13
|
+
end
|
14
|
+
|
15
|
+
def create!(klass)
|
16
|
+
create(klass).generate
|
17
|
+
end
|
18
|
+
|
19
|
+
def builder(klass)
|
20
|
+
Bricks.builders[klass]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/bricks.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'bricks/builder'
|
2
|
+
require 'bricks/dsl'
|
3
|
+
|
4
|
+
module Bricks
|
5
|
+
class << self
|
6
|
+
attr_writer :builders
|
7
|
+
|
8
|
+
def builders
|
9
|
+
@builders ||= BuilderHashSet.new
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class NoAttributeOrTrait < StandardError; end
|
14
|
+
class BadSyntax < StandardError; end
|
15
|
+
|
16
|
+
class BuilderHashSet
|
17
|
+
def initialize(&block)
|
18
|
+
@builders = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](key)
|
22
|
+
if @builders[key]
|
23
|
+
@builders[key]
|
24
|
+
elsif Class === key
|
25
|
+
@builders[key] = if builder = @builders[key.superclass]
|
26
|
+
builder.derive(key)
|
27
|
+
else
|
28
|
+
builder(key)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def builder(klass, &block)
|
34
|
+
@builders[klass] = Bricks::Builder.new(klass, &block)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def Bricks(&block)
|
40
|
+
Bricks::builders = Bricks::BuilderHashSet.new
|
41
|
+
|
42
|
+
Bricks::builders.instance_eval(&block)
|
43
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
|
2
|
+
|
3
|
+
describe Bricks::Adapters::ActiveRecord do
|
4
|
+
subject { Bricks::Adapters::ActiveRecord.new }
|
5
|
+
|
6
|
+
it "gracefully handles a missing association" do
|
7
|
+
subject.association?(Reader, :birth_date, :one).should be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
it "gracefully handles a missing association of the given type" do
|
11
|
+
subject.association?(Article, :newspaper, :many).should be_nil
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Bricks::Builder do
|
4
|
+
before :all do
|
5
|
+
Bricks::Builder.adapter = Class.new {
|
6
|
+
def association(*args)
|
7
|
+
nil
|
8
|
+
end
|
9
|
+
|
10
|
+
alias_method :association?, :association
|
11
|
+
}.new
|
12
|
+
|
13
|
+
class Person
|
14
|
+
attr_accessor :name
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "fails if the model is missing the given attribute" do
|
19
|
+
lambda {
|
20
|
+
Bricks::Builder.new(Person).birth_date(Date.new(1978, 5, 3))
|
21
|
+
}.should raise_error(Bricks::NoAttributeOrTrait)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "forbids passing a block and an initial value" do
|
25
|
+
lambda {
|
26
|
+
Bricks::Builder.new(Person).name("Jack") { "heh" }
|
27
|
+
}.should raise_error(Bricks::BadSyntax)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "forbids passing no value or block to a non-association attribute" do
|
31
|
+
lambda {
|
32
|
+
Bricks::Builder.new(Person).name
|
33
|
+
}.should raise_error(Bricks::BadSyntax)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "always generates a new object" do
|
37
|
+
b = Bricks::Builder.new(Person)
|
38
|
+
|
39
|
+
b.generate.object_id.should_not == b.generate.object_id
|
40
|
+
end
|
41
|
+
end
|
data/spec/bricks_spec.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Bricks do
|
4
|
+
include Bricks::DSL
|
5
|
+
|
6
|
+
before :all do
|
7
|
+
Bricks::Builder.adapter = Bricks::Adapters::ActiveRecord.new
|
8
|
+
|
9
|
+
Bricks do
|
10
|
+
builder PrintMedium do
|
11
|
+
start_date Date.new(1900, 1, 1)
|
12
|
+
end
|
13
|
+
|
14
|
+
builder Newspaper do
|
15
|
+
name "The Daily Planet"
|
16
|
+
|
17
|
+
trait :daily_bugle do
|
18
|
+
name "The Daily Bugle"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
builder Article do
|
23
|
+
author 'Jack Jupiter'
|
24
|
+
title 'a title'
|
25
|
+
body 'the body'
|
26
|
+
formatted_title { |obj| obj.title + " by " + obj.author }
|
27
|
+
deferred { Time.now }
|
28
|
+
newspaper
|
29
|
+
|
30
|
+
%w(Socrates Plato Aristotle).each { |n| readers.name(n) }
|
31
|
+
|
32
|
+
trait :in_english do
|
33
|
+
language "English"
|
34
|
+
end
|
35
|
+
|
36
|
+
trait :by_jove do
|
37
|
+
author "Jack Jupiter"
|
38
|
+
end
|
39
|
+
|
40
|
+
trait :maybe_bugle do
|
41
|
+
~newspaper.daily_bugle
|
42
|
+
end
|
43
|
+
|
44
|
+
trait :on_the_bugle do
|
45
|
+
newspaper.daily_bugle
|
46
|
+
end
|
47
|
+
|
48
|
+
trait :with_alternative_readers do
|
49
|
+
readers.clear
|
50
|
+
|
51
|
+
%w(Tom Dick Harry).each { |n| readers.name(n) }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
after do
|
58
|
+
Reader.delete_all
|
59
|
+
Article.delete_all
|
60
|
+
PrintMedium.delete_all
|
61
|
+
Newspaper.delete_all
|
62
|
+
end
|
63
|
+
|
64
|
+
it "#build returns the constructor" do
|
65
|
+
build(Article).should be_kind_of(Bricks::Builder)
|
66
|
+
end
|
67
|
+
|
68
|
+
it "#create returns the constructor" do
|
69
|
+
create(Article).should be_kind_of(Bricks::Builder)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "initializes a model" do
|
73
|
+
a = build!(Article)
|
74
|
+
|
75
|
+
a.should be_instance_of(Article)
|
76
|
+
a.should be_new_record
|
77
|
+
end
|
78
|
+
|
79
|
+
it "creates a model" do
|
80
|
+
a = create!(Article)
|
81
|
+
|
82
|
+
a.should be_instance_of(Article)
|
83
|
+
a.should be_saved
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "with simple fields" do
|
87
|
+
it "initializes model fields" do
|
88
|
+
a = build!(Article)
|
89
|
+
|
90
|
+
a.title.should == 'a title'
|
91
|
+
a.body.should == 'the body'
|
92
|
+
end
|
93
|
+
|
94
|
+
it "defers field initialization" do
|
95
|
+
time = Time.now
|
96
|
+
a = build!(Article)
|
97
|
+
|
98
|
+
a.deferred.should > time
|
99
|
+
end
|
100
|
+
|
101
|
+
it "uses the object being built in deferred initialization" do
|
102
|
+
build!(Article).formatted_title.should == "a title by Jack Jupiter"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
describe "with traits" do
|
107
|
+
it "returns the builder after calling the trait" do
|
108
|
+
build(Article).in_english.should be_kind_of(Bricks::Builder)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "#build returns the object if the trait is called with a bang" do
|
112
|
+
a = build(Article).in_english!
|
113
|
+
|
114
|
+
a.should be_kind_of(Article)
|
115
|
+
a.should be_new_record
|
116
|
+
end
|
117
|
+
|
118
|
+
it "#create creates the object if the trait is called with a bang" do
|
119
|
+
a = create(Article).in_english!
|
120
|
+
|
121
|
+
a.should be_kind_of(Article)
|
122
|
+
a.should be_saved
|
123
|
+
end
|
124
|
+
|
125
|
+
it "initializes the model fields" do
|
126
|
+
build(Article).in_english!.language.should == "English"
|
127
|
+
end
|
128
|
+
|
129
|
+
it "combines multiple traits" do
|
130
|
+
a = build(Article).in_english.by_jove!
|
131
|
+
|
132
|
+
a.language.should == "English"
|
133
|
+
a.author.should == "Jack Jupiter"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe "with a many-to-one association" do
|
138
|
+
it "initializes an association with the default values" do
|
139
|
+
build!(Article).newspaper.name.should == 'The Daily Planet'
|
140
|
+
end
|
141
|
+
|
142
|
+
it "overrides the association" do
|
143
|
+
build(Article).on_the_bugle!.newspaper.name.
|
144
|
+
should == 'The Daily Bugle'
|
145
|
+
end
|
146
|
+
|
147
|
+
it "possibly looks for an existing record" do
|
148
|
+
n = create(Newspaper).daily_bugle!
|
149
|
+
a = create(Article).maybe_bugle!
|
150
|
+
|
151
|
+
a.newspaper.should == n
|
152
|
+
end
|
153
|
+
|
154
|
+
it "possibly looks for an existing record (and finds none)" do
|
155
|
+
a = create(Article).maybe_bugle!
|
156
|
+
|
157
|
+
a.newspaper.should_not be_new_record
|
158
|
+
a.newspaper.name.should == "The Daily Bugle"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
describe "with a one-to-many association" do
|
163
|
+
it "initializes an association with the default values" do
|
164
|
+
build!(Article).readers.map { |r|
|
165
|
+
r.name
|
166
|
+
}.should == %w(Socrates Plato Aristotle)
|
167
|
+
end
|
168
|
+
|
169
|
+
it "overrides the association" do
|
170
|
+
build(Article).with_alternative_readers!.readers.map { |r|
|
171
|
+
r.name
|
172
|
+
}.should == %w(Tom Dick Harry)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "builder inheritance" do
|
177
|
+
it "uses the parent's builder if the model has none" do
|
178
|
+
mag = build!(Magazine)
|
179
|
+
|
180
|
+
mag.should be_a(Magazine)
|
181
|
+
mag.start_date.should == Date.new(1900, 1, 1)
|
182
|
+
end
|
183
|
+
|
184
|
+
it "creates a builder for models that don't have one" do
|
185
|
+
build!(Reader).should be_a(Reader)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'bricks'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute_accessors'
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
3
|
+
require 'benchmark'
|
4
|
+
require 'bricks/adapters/active_record'
|
5
|
+
|
6
|
+
ActiveRecord::Base.establish_connection(
|
7
|
+
:adapter => "sqlite3",
|
8
|
+
:database => ":memory:"
|
9
|
+
)
|
10
|
+
|
11
|
+
ActiveRecord::Schema.define(:version => 20110608204150) do
|
12
|
+
create_table "articles", :force => true do |t|
|
13
|
+
t.string "author"
|
14
|
+
t.string "body"
|
15
|
+
t.datetime "deferred"
|
16
|
+
t.string "formatted_title"
|
17
|
+
t.string "language"
|
18
|
+
t.integer "newspaper_id"
|
19
|
+
t.string "title"
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table "newspapers", :force => true do |t|
|
23
|
+
t.string "name"
|
24
|
+
end
|
25
|
+
|
26
|
+
create_table "print_media", :force => true do |t|
|
27
|
+
t.date "start_date"
|
28
|
+
t.string "type"
|
29
|
+
end
|
30
|
+
|
31
|
+
create_table "readers", :force => true do |t|
|
32
|
+
t.integer "article_id"
|
33
|
+
t.string "name"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Article < ActiveRecord::Base
|
38
|
+
belongs_to :newspaper
|
39
|
+
has_many :readers
|
40
|
+
|
41
|
+
def saved?
|
42
|
+
! new_record?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class PrintMedium < ActiveRecord::Base; end
|
47
|
+
|
48
|
+
class Magazine < PrintMedium; end
|
49
|
+
|
50
|
+
class Newspaper < ActiveRecord::Base; end
|
51
|
+
|
52
|
+
class Reader < ActiveRecord::Base; end
|
metadata
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bricks
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 31
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 0.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- David Leal
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-06-16 00:00:00 +01:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 2
|
31
|
+
- 0
|
32
|
+
version: "2.0"
|
33
|
+
name: rspec
|
34
|
+
requirement: *id001
|
35
|
+
type: :development
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 23
|
44
|
+
segments:
|
45
|
+
- 1
|
46
|
+
- 0
|
47
|
+
- 0
|
48
|
+
version: 1.0.0
|
49
|
+
name: bundler
|
50
|
+
requirement: *id002
|
51
|
+
type: :development
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
prerelease: false
|
54
|
+
version_requirements: &id003 !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ~>
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
hash: 11
|
60
|
+
segments:
|
61
|
+
- 1
|
62
|
+
- 6
|
63
|
+
- 2
|
64
|
+
version: 1.6.2
|
65
|
+
name: jeweler
|
66
|
+
requirement: *id003
|
67
|
+
type: :development
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
name: rcov
|
80
|
+
requirement: *id004
|
81
|
+
type: :development
|
82
|
+
description:
|
83
|
+
email: david@mojotech.com
|
84
|
+
executables: []
|
85
|
+
|
86
|
+
extensions: []
|
87
|
+
|
88
|
+
extra_rdoc_files:
|
89
|
+
- LICENSE.txt
|
90
|
+
- README.md
|
91
|
+
files:
|
92
|
+
- .document
|
93
|
+
- .rspec
|
94
|
+
- Gemfile
|
95
|
+
- LICENSE.txt
|
96
|
+
- README.md
|
97
|
+
- Rakefile
|
98
|
+
- VERSION
|
99
|
+
- bricks.gemspec
|
100
|
+
- lib/bricks.rb
|
101
|
+
- lib/bricks/adapters/active_record.rb
|
102
|
+
- lib/bricks/builder.rb
|
103
|
+
- lib/bricks/builder_set.rb
|
104
|
+
- lib/bricks/dsl.rb
|
105
|
+
- rails/init.rb
|
106
|
+
- spec/bricks/adapters/active_record_spec.rb
|
107
|
+
- spec/bricks/builder_spec.rb
|
108
|
+
- spec/bricks_spec.rb
|
109
|
+
- spec/spec_helper.rb
|
110
|
+
- spec/support/active_record.rb
|
111
|
+
has_rdoc: true
|
112
|
+
homepage: http://github.com/mojotech/bricks
|
113
|
+
licenses:
|
114
|
+
- MIT
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
hash: 3
|
126
|
+
segments:
|
127
|
+
- 0
|
128
|
+
version: "0"
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
130
|
+
none: false
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
hash: 3
|
135
|
+
segments:
|
136
|
+
- 0
|
137
|
+
version: "0"
|
138
|
+
requirements: []
|
139
|
+
|
140
|
+
rubyforge_project:
|
141
|
+
rubygems_version: 1.3.7
|
142
|
+
signing_key:
|
143
|
+
specification_version: 3
|
144
|
+
summary: Hybrid object builder/factory.
|
145
|
+
test_files: []
|
146
|
+
|