ardm-sweatshop 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,5 @@
1
+ module DataMapper
2
+ class Sweatshop
3
+ VERSION = '1.2.0'
4
+ end
5
+ 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