ardm-sweatshop 1.2.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.
@@ -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