ardm-sweatshop 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +35 -0
- data/.travis.yml +11 -0
- data/Gemfile +52 -0
- data/LICENSE +20 -0
- data/README.rdoc +259 -0
- data/Rakefile +4 -0
- data/ardm-sweatshop.gemspec +25 -0
- data/lib/ardm-sweatshop.rb +1 -0
- data/lib/dm-sweatshop.rb +6 -0
- data/lib/dm-sweatshop/model.rb +107 -0
- data/lib/dm-sweatshop/support/class_attributes.rb +46 -0
- data/lib/dm-sweatshop/sweatshop.rb +145 -0
- data/lib/dm-sweatshop/unique.rb +89 -0
- data/lib/dm-sweatshop/version.rb +5 -0
- data/spec/dm-sweatshop/model_spec.rb +209 -0
- data/spec/dm-sweatshop/sweatshop_spec.rb +166 -0
- data/spec/dm-sweatshop/unique_spec.rb +84 -0
- data/spec/rcov.opts +6 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +17 -0
- data/tasks/spec.rake +38 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +134 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module DataMapper
|
2
|
+
class Sweatshop
|
3
|
+
module ClassAttributes
|
4
|
+
def self.reader(klass, *attributes)
|
5
|
+
attributes.each do |attribute|
|
6
|
+
klass.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
7
|
+
unless defined? @@#{attribute}
|
8
|
+
@@#{attribute} = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.#{attribute}
|
12
|
+
@@#{attribute}
|
13
|
+
end
|
14
|
+
|
15
|
+
def #{attribute}
|
16
|
+
@@#{attribute}
|
17
|
+
end
|
18
|
+
RUBY
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.writer(klass, *attributes)
|
23
|
+
attributes.each do |attribute|
|
24
|
+
klass.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
25
|
+
unless defined? @@#{attribute}
|
26
|
+
@@#{attribute} = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.#{attribute}=(obj)
|
30
|
+
@@#{attribute} = obj
|
31
|
+
end
|
32
|
+
|
33
|
+
def #{attribute}=(obj)
|
34
|
+
@@#{attribute} = obj
|
35
|
+
end
|
36
|
+
RUBY
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.accessor(klass, *attributes)
|
41
|
+
self.reader(klass, *attributes)
|
42
|
+
self.writer(klass, *attributes)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
module DataMapper
|
2
|
+
class Sweatshop
|
3
|
+
# Raise when requested attributes hash or instance are not
|
4
|
+
# found in model and record maps, respectively.
|
5
|
+
#
|
6
|
+
# This usually happens when you forget to use +make+ or
|
7
|
+
# +generate+ method before trying to +pick+ an object.
|
8
|
+
class NoFixtureExist < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_accessor :model_map
|
13
|
+
attr_accessor :record_map
|
14
|
+
end
|
15
|
+
|
16
|
+
# Models map stores named Procs for a class.
|
17
|
+
# Each Proc must return a Hash of attributes.
|
18
|
+
self.model_map = Hash.new {|ha,ka| ha[ka] = Hash.new {|hb,kb| hb[kb] = []}}
|
19
|
+
# Records map stores named instances of a class.
|
20
|
+
# Those instances may or may not be new records.
|
21
|
+
self.record_map = Hash.new {|ha,ka| ha[ka] = Hash.new {|hb,kb| hb[kb] = []}}
|
22
|
+
|
23
|
+
# Adds a Proc to model map. Proc must return a Hash of attributes.
|
24
|
+
#
|
25
|
+
# @param klass [Class, DataMapper::Resource]
|
26
|
+
# @param name [Symbol]
|
27
|
+
# @param instance [DataMapper::Resource]
|
28
|
+
#
|
29
|
+
# @api private
|
30
|
+
#
|
31
|
+
# @return [Array] model map
|
32
|
+
def self.add(klass, name, &proc)
|
33
|
+
self.model_map[klass][name.to_sym] << proc
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adds an instance to records map.
|
37
|
+
#
|
38
|
+
# @param klass [Class, DataMapper::Resource]
|
39
|
+
# @param name [Symbol]
|
40
|
+
# @param instance [DataMapper::Resource]
|
41
|
+
#
|
42
|
+
# @api private
|
43
|
+
#
|
44
|
+
# @return [DataMapper::Resource] added instance
|
45
|
+
def self.record(klass, name, instance)
|
46
|
+
self.record_map[klass][name.to_sym] << instance
|
47
|
+
instance
|
48
|
+
end
|
49
|
+
|
50
|
+
# Same as create but calls Model#create! and does save
|
51
|
+
# invalid models
|
52
|
+
#
|
53
|
+
# @param klass [Class, DataMapper::Resource]
|
54
|
+
# @param name [Symbol]
|
55
|
+
# @param attributes [Hash]
|
56
|
+
#
|
57
|
+
# @api private
|
58
|
+
#
|
59
|
+
# @return [DataMapper::Resource] added instance
|
60
|
+
def self.create!(klass, name, attributes = {})
|
61
|
+
record(klass, name, klass.create!(attributes(klass, name).merge(attributes)))
|
62
|
+
end
|
63
|
+
|
64
|
+
# Creates an instance from given hash of attributes, saves it
|
65
|
+
# and adds it to the record map.
|
66
|
+
#
|
67
|
+
# @param klass [Class, DataMapper::Resource]
|
68
|
+
# @param name [Symbol]
|
69
|
+
# @param attributes [Hash]
|
70
|
+
#
|
71
|
+
# @api private
|
72
|
+
#
|
73
|
+
# @return [DataMapper::Resource] added instance
|
74
|
+
def self.create(klass, name, attributes = {})
|
75
|
+
record(klass, name, klass.create(attributes(klass, name).merge(attributes)))
|
76
|
+
end
|
77
|
+
|
78
|
+
# Creates an instance from given hash of attributes
|
79
|
+
# and adds it to records map without saving.
|
80
|
+
#
|
81
|
+
# @param klass [Class, DataMapper::Resource]
|
82
|
+
# @param name [Symbol]
|
83
|
+
# @param attributes [Hash]
|
84
|
+
#
|
85
|
+
# @api private
|
86
|
+
#
|
87
|
+
# @return [DataMapper::Resource] added instance
|
88
|
+
def self.make(klass, name, attributes = {})
|
89
|
+
record(klass, name, klass.new(attributes(klass, name).merge(attributes)))
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns a pre existing instance of a model from the record map
|
93
|
+
#
|
94
|
+
# @param klass [Class, DataMapper::Resource]
|
95
|
+
# @param name [Symbol]
|
96
|
+
#
|
97
|
+
# @return [DataMapper::Resource] existing instance of a model from the record map
|
98
|
+
# @raise DataMapper::Sweatshop::NoFixtureExist when requested fixture does not exist in the record map
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
def self.pick(klass, name)
|
102
|
+
self.record_map[klass][name.to_sym].pick || raise(NoFixtureExist, "no #{name} context fixtures have been generated for the #{klass} class")
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns a Hash of attributes from the model map
|
106
|
+
#
|
107
|
+
# @param klass [Class, DataMapper::Resource]
|
108
|
+
# @param name [Symbol]
|
109
|
+
#
|
110
|
+
# @return [Hash] existing instance of a model from the model map
|
111
|
+
# @raise NoFixtureExist when requested fixture does not exist in the model map
|
112
|
+
#
|
113
|
+
# @api private
|
114
|
+
def self.attributes(klass, name)
|
115
|
+
proc = model_map[klass][name.to_sym].pick
|
116
|
+
|
117
|
+
if proc
|
118
|
+
expand_callable_values(proc.call)
|
119
|
+
elsif klass.superclass.is_a?(DataMapper::Model)
|
120
|
+
attributes(klass.superclass, name)
|
121
|
+
else
|
122
|
+
raise NoFixtureExist, "#{name} fixture was not found for class #{klass}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Returns a Hash with callable values evaluated.
|
127
|
+
#
|
128
|
+
# @param hash [Hash]
|
129
|
+
#
|
130
|
+
# @return [Hash] existing instance of a model from the model map
|
131
|
+
#
|
132
|
+
# @api private
|
133
|
+
def self.expand_callable_values(hash)
|
134
|
+
expanded = {}
|
135
|
+
hash.each do |key, value|
|
136
|
+
if value.respond_to?(:call)
|
137
|
+
expanded[key] = value.call
|
138
|
+
else
|
139
|
+
expanded[key] = value
|
140
|
+
end
|
141
|
+
end
|
142
|
+
expanded
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'dm-sweatshop/support/class_attributes'
|
2
|
+
|
3
|
+
module DataMapper
|
4
|
+
class Sweatshop
|
5
|
+
module Unique
|
6
|
+
# Yields a value to the block. The value is unique for each invocation
|
7
|
+
# with the same block. Alternatively, you may provide an explicit key to
|
8
|
+
# identify the block.
|
9
|
+
#
|
10
|
+
# If a block with no parameter is supplied, unique keeps track of previous
|
11
|
+
# invocations, and will continue yielding until a unique value is generated.
|
12
|
+
# If a unique value is not generated after @UniqueWorker::MAX_TRIES@, an exception
|
13
|
+
# is raised.
|
14
|
+
#
|
15
|
+
# ParseTree is required unless an explicit key is provided
|
16
|
+
#
|
17
|
+
# (1..3).collect { unique {|x| x }} # => [0, 1, 2]
|
18
|
+
# (1..3).collect { unique {|x| x + 1 }} # => [1, 2, 3]
|
19
|
+
# (1..3).collect { unique {|x| x }} # => [3, 4, 5] # Continued on from above
|
20
|
+
# (1..3).collect { unique(:a) {|x| x }} # => [0, 1, 2] # Explicit key overrides block identity
|
21
|
+
#
|
22
|
+
# a = [1, 1, 1, 2, 2, 3]
|
23
|
+
# (1..3).collect { unique { a.shift }} # => [1, 2, 3]
|
24
|
+
# (1..3).collect { unique { 1 }} # raises TooManyTriesException
|
25
|
+
#
|
26
|
+
# return <Object> the return value of the block
|
27
|
+
def unique(key = nil, &block)
|
28
|
+
if block.arity < 1
|
29
|
+
UniqueWorker.unique_map ||= {}
|
30
|
+
|
31
|
+
key ||= UniqueWorker.key_for(&block)
|
32
|
+
set = UniqueWorker.unique_map[key] || Set.new
|
33
|
+
result = block[]
|
34
|
+
tries = 0
|
35
|
+
while set.include?(result)
|
36
|
+
result = block[]
|
37
|
+
tries += 1
|
38
|
+
|
39
|
+
raise TooManyTriesException.new("Could not generate unique value after #{tries} attempts") if tries >= UniqueWorker::MAX_TRIES
|
40
|
+
end
|
41
|
+
set << result
|
42
|
+
UniqueWorker.unique_map[key] = set
|
43
|
+
else
|
44
|
+
UniqueWorker.count_map ||= Hash.new() { 0 }
|
45
|
+
|
46
|
+
key ||= UniqueWorker.key_for(&block)
|
47
|
+
result = block[UniqueWorker.count_map[key]]
|
48
|
+
UniqueWorker.count_map[key] += 1
|
49
|
+
end
|
50
|
+
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
class TooManyTriesException < RuntimeError; end;
|
55
|
+
end
|
56
|
+
extend(Unique)
|
57
|
+
|
58
|
+
class UniqueWorker
|
59
|
+
MAX_TRIES = 10
|
60
|
+
|
61
|
+
unless defined?(JRUBY_VERSION) || RUBY_VERSION >= '1.9'
|
62
|
+
begin
|
63
|
+
gem 'ParseTree', '~>3.0.3'
|
64
|
+
require 'parse_tree'
|
65
|
+
rescue LoadError
|
66
|
+
puts 'DataMapper::Sweatshop::Unique - ParseTree could not be loaded, anonymous uniques will not be allowed'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
ClassAttributes.accessor(self, :count_map)
|
71
|
+
ClassAttributes.accessor(self, :unique_map)
|
72
|
+
ClassAttributes.accessor(self, :parser)
|
73
|
+
|
74
|
+
# Use the sexp representation of the block as a unique key for the block
|
75
|
+
# If you copy and paste a block, it will still have the same key
|
76
|
+
#
|
77
|
+
# return <Object> the unique key for the block
|
78
|
+
def self.key_for(&block)
|
79
|
+
raise "You need to install ParseTree to use anonymous an anonymous unique (gem install ParseTree). In the mean time, explicitly declare a key: unique(:my_key) { ... }" unless Object::const_defined?("ParseTree")
|
80
|
+
|
81
|
+
klass = Class.new
|
82
|
+
name = "tmp"
|
83
|
+
klass.send(:define_method, name, &block)
|
84
|
+
self.parser ||= ParseTree.new(false)
|
85
|
+
self.parser.parse_tree_for_method(klass, name).last
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe DataMapper::Model do
|
4
|
+
|
5
|
+
class Widget
|
6
|
+
include DataMapper::Resource
|
7
|
+
property :id, Serial
|
8
|
+
property :type, Discriminator
|
9
|
+
property :name, String
|
10
|
+
property :price, Integer
|
11
|
+
|
12
|
+
belongs_to :order, :required => false
|
13
|
+
validates_presence_of :price
|
14
|
+
end
|
15
|
+
|
16
|
+
class Wonket < Widget
|
17
|
+
property :size, String
|
18
|
+
end
|
19
|
+
|
20
|
+
class Order
|
21
|
+
include DataMapper::Resource
|
22
|
+
|
23
|
+
property :id, Serial
|
24
|
+
|
25
|
+
has n, :widgets
|
26
|
+
end
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
DataMapper.auto_migrate!
|
30
|
+
DataMapper::Sweatshop.model_map.clear
|
31
|
+
DataMapper::Sweatshop.record_map.clear
|
32
|
+
end
|
33
|
+
|
34
|
+
supported_by :all do
|
35
|
+
|
36
|
+
describe ".default_fauxture_name" do
|
37
|
+
it "is :default" do
|
38
|
+
Order.default_fauxture_name.should == :default
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe ".fixture" do
|
43
|
+
describe "without fauxture name" do
|
44
|
+
before :each do
|
45
|
+
Widget.fixture {{
|
46
|
+
:name => /\w+/.gen.capitalize,
|
47
|
+
:price => /\d{4,5}/.gen.to_i
|
48
|
+
}}
|
49
|
+
|
50
|
+
@default = DataMapper::Sweatshop.model_map[Widget][:default]
|
51
|
+
end
|
52
|
+
|
53
|
+
it "add a fixture proc for the model with name :default" do
|
54
|
+
@default.should_not be_empty
|
55
|
+
@default.first.should be_kind_of(Proc)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should allow handle complex named fixtures" do
|
60
|
+
Wonket.fix {{
|
61
|
+
:name => /\w+ Wonket/.gen.capitalize,
|
62
|
+
:price => /\d{2,3}99/.gen.to_i,
|
63
|
+
:size => %w[small medium large xl].pick
|
64
|
+
}}
|
65
|
+
|
66
|
+
Order.fix {{
|
67
|
+
:widgets => (1..5).of { Widget.gen }
|
68
|
+
}}
|
69
|
+
|
70
|
+
Order.fix(:wonket_order) {{
|
71
|
+
:widgets => (5..10).of { Wonket.gen }
|
72
|
+
}}
|
73
|
+
|
74
|
+
wonket_order = Order.gen(:wonket_order)
|
75
|
+
wonket_order.widgets.should_not be_empty
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should allow for STI fixtures" do
|
79
|
+
Widget.fix {{
|
80
|
+
:name => /\w+/.gen.capitalize,
|
81
|
+
:price => /\d{4,5}/.gen.to_i
|
82
|
+
}}
|
83
|
+
|
84
|
+
Order.fix {{
|
85
|
+
:widgets => (1..5).of { Wonket.gen }
|
86
|
+
}}
|
87
|
+
|
88
|
+
Order.gen.widgets.should_not be_empty
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
describe ".make" do
|
93
|
+
before :each do
|
94
|
+
Widget.fix(:red) {{
|
95
|
+
:name => "red",
|
96
|
+
:price => 20
|
97
|
+
}}
|
98
|
+
|
99
|
+
@widget = Widget.make(:red)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "creates an object from named attributes hash" do
|
103
|
+
@widget.name.should == "red"
|
104
|
+
@widget.price.should == 20
|
105
|
+
end
|
106
|
+
|
107
|
+
it "returns a new object" do
|
108
|
+
@widget.should be_new
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe ".generate" do
|
113
|
+
before :each do
|
114
|
+
Widget.fix(:red) {{
|
115
|
+
:name => "red",
|
116
|
+
:price => 20
|
117
|
+
}}
|
118
|
+
|
119
|
+
@widget = Widget.gen(:red)
|
120
|
+
|
121
|
+
Widget.fix(:blue) {{
|
122
|
+
:name => "blue"
|
123
|
+
}}
|
124
|
+
end
|
125
|
+
|
126
|
+
it "creates an object from named attributes hash" do
|
127
|
+
@widget.name.should == "red"
|
128
|
+
@widget.price.should == 20
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns a saved object" do
|
132
|
+
@widget.should be_saved
|
133
|
+
end
|
134
|
+
|
135
|
+
it "does not save invalid model" do
|
136
|
+
blue_widget = Widget.gen(:blue)
|
137
|
+
blue_widget.should be_new
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe ".generate!" do
|
142
|
+
it "saves a model even if it is invalid" do
|
143
|
+
Widget.fix(:blue) {{
|
144
|
+
:name => "blue"
|
145
|
+
}}
|
146
|
+
|
147
|
+
blue_widget = Widget.gen!(:blue)
|
148
|
+
blue_widget.should be_saved
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe ".pick" do
|
153
|
+
before :each do
|
154
|
+
Widget.fix(:red) {{
|
155
|
+
:name => "rosso",
|
156
|
+
:price => 20
|
157
|
+
}}
|
158
|
+
|
159
|
+
Widget.fix(:yellow) {{
|
160
|
+
:name => "giallo",
|
161
|
+
:price => 30
|
162
|
+
}}
|
163
|
+
|
164
|
+
Widget.fix(:blue) {{
|
165
|
+
:name => Proc.new { "b" + "lu" },
|
166
|
+
:price => 40
|
167
|
+
}}
|
168
|
+
|
169
|
+
@red = Widget.gen(:red)
|
170
|
+
@yellow = Widget.gen(:yellow)
|
171
|
+
@blue = Widget.gen(:blue)
|
172
|
+
end
|
173
|
+
|
174
|
+
it "returns a pre existing object with named attributes hash" do
|
175
|
+
@red.name.should == "rosso"
|
176
|
+
@red.price.should == 20
|
177
|
+
|
178
|
+
@yellow.name.should == "giallo"
|
179
|
+
@yellow.price.should == 30
|
180
|
+
end
|
181
|
+
|
182
|
+
it "expands callable values of attributes hash" do
|
183
|
+
@blue.name.should == "blu"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
describe ".generate_attributes" do
|
188
|
+
before :each do
|
189
|
+
Widget.fix(:red) {{
|
190
|
+
:name => "red",
|
191
|
+
:price => 20
|
192
|
+
}}
|
193
|
+
|
194
|
+
@hash = Widget.generate_attributes(:red)
|
195
|
+
end
|
196
|
+
|
197
|
+
it "returns a Hash" do
|
198
|
+
@hash.should be_an_instance_of(Hash)
|
199
|
+
end
|
200
|
+
|
201
|
+
it "returns stored attributes hash by name" do
|
202
|
+
@hash[:name].should == "red"
|
203
|
+
@hash[:price].should == 20
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|