superdupe 0.4.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/README.markdown +10 -0
- data/lib/superdupe/active_resource_extensions.rb +161 -0
- data/lib/superdupe/attribute_template.rb +71 -0
- data/lib/superdupe/cucumber_hooks.rb +16 -0
- data/lib/superdupe/custom_mocks.rb +116 -0
- data/lib/superdupe/database.rb +69 -0
- data/lib/superdupe/dupe.rb +534 -0
- data/lib/superdupe/hash_pruner.rb +37 -0
- data/lib/superdupe/log.rb +38 -0
- data/lib/superdupe/mock.rb +127 -0
- data/lib/superdupe/model.rb +59 -0
- data/lib/superdupe/network.rb +56 -0
- data/lib/superdupe/record.rb +42 -0
- data/lib/superdupe/rest_validation.rb +16 -0
- data/lib/superdupe/schema.rb +52 -0
- data/lib/superdupe/sequence.rb +20 -0
- data/lib/superdupe/singular_plural_detection.rb +9 -0
- data/lib/superdupe/string.rb +7 -0
- data/lib/superdupe/symbol.rb +3 -0
- data/lib/superdupe.rb +20 -0
- data/rails_generators/dupe/dupe_generator.rb +20 -0
- data/rails_generators/dupe/templates/custom_mocks.rb +4 -0
- data/rails_generators/dupe/templates/definitions.rb +9 -0
- data/rails_generators/dupe/templates/load_dupe.rb +9 -0
- data/spec/lib_specs/active_resource_extensions_spec.rb +141 -0
- data/spec/lib_specs/attribute_template_spec.rb +173 -0
- data/spec/lib_specs/database_spec.rb +163 -0
- data/spec/lib_specs/dupe_spec.rb +522 -0
- data/spec/lib_specs/hash_pruner_spec.rb +73 -0
- data/spec/lib_specs/log_spec.rb +78 -0
- data/spec/lib_specs/logged_request_spec.rb +22 -0
- data/spec/lib_specs/mock_definitions_spec.rb +58 -0
- data/spec/lib_specs/mock_spec.rb +194 -0
- data/spec/lib_specs/model_spec.rb +95 -0
- data/spec/lib_specs/network_spec.rb +130 -0
- data/spec/lib_specs/record_spec.rb +70 -0
- data/spec/lib_specs/rest_validation_spec.rb +17 -0
- data/spec/lib_specs/schema_spec.rb +109 -0
- data/spec/lib_specs/sequence_spec.rb +39 -0
- data/spec/lib_specs/string_spec.rb +31 -0
- data/spec/lib_specs/symbol_spec.rb +17 -0
- data/spec/spec_helper.rb +8 -0
- metadata +142 -0
@@ -0,0 +1,534 @@
|
|
1
|
+
# Author:: Matt Parker (mailto:moonmaster9000@gmail.com)
|
2
|
+
# License:: Distributes under the same terms as Ruby
|
3
|
+
|
4
|
+
class Dupe
|
5
|
+
class << self
|
6
|
+
|
7
|
+
attr_reader :models #:nodoc:
|
8
|
+
attr_reader :sequences #:nodoc:
|
9
|
+
attr_reader :database #:nodoc:
|
10
|
+
|
11
|
+
# set this to "true" if you want Dupe to spit out mocked requests
|
12
|
+
# after each of your cucumber scenario's run
|
13
|
+
attr_accessor :debug
|
14
|
+
|
15
|
+
# Suppose we're creating a 'book' resource. Perhaps our app assumes every book has a title, so let's define a book resource
|
16
|
+
# that specifies just that:
|
17
|
+
#
|
18
|
+
# irb# Dupe.define :book do |attrs|
|
19
|
+
# --# attrs.title 'Untitled'
|
20
|
+
# --# attrs.author
|
21
|
+
# --# end
|
22
|
+
# ==> #<Dupe::Model:0x17b2694 ...>
|
23
|
+
#
|
24
|
+
# Basically, this reads like "A book resource has a title attribute with a default value of 'Untitled'. It also has an author attribute." Thus, if we create a book and we don't specify a "title" attribute, it should create a "title" for us, as well as provide a nil "author" attribute.
|
25
|
+
#
|
26
|
+
# irb# b = Dupe.create :book
|
27
|
+
# ==> <#Duped::Book author=nil title="Untitled" id=1>
|
28
|
+
#
|
29
|
+
#
|
30
|
+
# If we provide our own title, it should allow us to override the default value:
|
31
|
+
#
|
32
|
+
# irb# b = Dupe.create :book, :title => 'Monkeys!'
|
33
|
+
# ==> <#Duped::Book author=nil title="Monkeys!" id=2>
|
34
|
+
#
|
35
|
+
# === Attributes with procs as default values
|
36
|
+
#
|
37
|
+
# Sometimes it might be convenient to procedurally define the default value for an attribute:
|
38
|
+
#
|
39
|
+
# irb# Dupe.define :book do |attrs|
|
40
|
+
# --# attrs.title 'Untitled'
|
41
|
+
# --# attrs.author
|
42
|
+
# --# attrs.isbn do
|
43
|
+
# --# rand(1000000)
|
44
|
+
# --# end
|
45
|
+
# --# end
|
46
|
+
#
|
47
|
+
# Now, every time we create a book, it will get assigned a random ISBN number:
|
48
|
+
#
|
49
|
+
# irb# b = Dupe.create :book
|
50
|
+
# ==> <#Duped::Book author=nil title="Untitled" id=1 isbn=895825>
|
51
|
+
#
|
52
|
+
# irb# b = Dupe.create :book
|
53
|
+
# ==> <#Duped::Book author=nil title="Untitled" id=2 isbn=606472>
|
54
|
+
#
|
55
|
+
# Another common use of this feature is for associations. Lets suppose we'd like to make sure that a book always has a genre, but a genre should be its own resource. We can accomplish that by taking advantage of Dupe's "find_or_create" method:
|
56
|
+
#
|
57
|
+
# irb# Dupe.define :book do |attrs|
|
58
|
+
# --# attrs.title 'Untitled'
|
59
|
+
# --# attrs.author
|
60
|
+
# --# attrs.isbn do
|
61
|
+
# --# rand(1000000)
|
62
|
+
# --# end
|
63
|
+
# --# attrs.genre do
|
64
|
+
# --# Dupe.find_or_create :genre
|
65
|
+
# --# end
|
66
|
+
# --# end
|
67
|
+
#
|
68
|
+
# Now when we create books, Dupe will associate them with an existing genre (the first one it finds), or if none yet exist, it will create one.
|
69
|
+
#
|
70
|
+
# First, let's confirm that no genres currently exist:
|
71
|
+
#
|
72
|
+
# irb# Dupe.find :genre
|
73
|
+
# Dupe::Database::TableDoesNotExistError: The table ':genre' does not exist.
|
74
|
+
# from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/database.rb:30:in `select'
|
75
|
+
# from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/dupe.rb:295:in `find'
|
76
|
+
# from (irb):135
|
77
|
+
#
|
78
|
+
# Next, let's create a book:
|
79
|
+
#
|
80
|
+
# irb# b = Dupe.create :book
|
81
|
+
# ==> <#Duped::Book genre=<#Duped::Genre id=1> author=nil title="Untitled" id=1 isbn=62572>
|
82
|
+
#
|
83
|
+
# Notice that it create a genre. If we tried to do another Dupe.find for the genre:
|
84
|
+
#
|
85
|
+
# irb# Dupe.find :genre
|
86
|
+
# ==> <#Duped::Genre id=1>
|
87
|
+
#
|
88
|
+
# Now, if create another book, it will associate with the genre that was just created:
|
89
|
+
#
|
90
|
+
# irb# b = Dupe.create :book
|
91
|
+
# ==> <#Duped::Book genre=<#Duped::Genre id=1> author=nil title="Untitled" id=2 isbn=729317>
|
92
|
+
#
|
93
|
+
#
|
94
|
+
#
|
95
|
+
# === Attributes with transformers
|
96
|
+
#
|
97
|
+
# Occasionally, you may find it useful to have attribute values transformed upon creation.
|
98
|
+
#
|
99
|
+
# For example, suppose we want to create books with publish dates. In our cucumber scenario's, we may prefer to simply specify a date like '2009-12-29', and have that automatically transformed into an ruby Date object.
|
100
|
+
#
|
101
|
+
# irb# Dupe.define :book do |attrs|
|
102
|
+
# --# attrs.title 'Untitled'
|
103
|
+
# --# attrs.author
|
104
|
+
# --# attrs.isbn do
|
105
|
+
# --# rand(1000000)
|
106
|
+
# --# end
|
107
|
+
# --# attrs.publish_date do |publish_date|
|
108
|
+
# --# Date.parse(publish_date)
|
109
|
+
# --# end
|
110
|
+
# --# end
|
111
|
+
#
|
112
|
+
# Now, let's create a book:
|
113
|
+
#
|
114
|
+
# irb# b = Dupe.create :book, :publish_date => '2009-12-29'
|
115
|
+
# ==> <#Duped::Book author=nil title="Untitled" publish_date=Tue, 29 Dec 2009 id=1 isbn=826291>
|
116
|
+
#
|
117
|
+
# irb# b.publish_date
|
118
|
+
# ==> Tue, 29 Dec 2009
|
119
|
+
#
|
120
|
+
# irb# b.publish_date.class
|
121
|
+
# ==> Date
|
122
|
+
#
|
123
|
+
#
|
124
|
+
#
|
125
|
+
# === Uniquify attributes
|
126
|
+
#
|
127
|
+
# If you'd just like to make sure that some attributes get a unique value, then you can use the uniquify
|
128
|
+
# method:
|
129
|
+
#
|
130
|
+
# irb# Dupe.define :book do |attrs|
|
131
|
+
# --# attrs.uniquify :title, :genre, :author
|
132
|
+
# --# end
|
133
|
+
#
|
134
|
+
# Now, Dupe will do its best to assign unique values to the :title, :genre, and :author attributes on
|
135
|
+
# any records it creates:
|
136
|
+
#
|
137
|
+
# irb# b = Dupe.create :book
|
138
|
+
# ==> <#Duped::Book author="book 1 author" title="book 1 title" genre="book 1 genre" id=1>
|
139
|
+
#
|
140
|
+
# irb# b2 = Dupe.create :book, :title => 'Rooby'
|
141
|
+
# ==> <#Duped::Book author="book 2 author" title="Rooby" genre="book 2 genre" id=2>
|
142
|
+
#
|
143
|
+
#
|
144
|
+
#
|
145
|
+
# === Callbacks
|
146
|
+
#
|
147
|
+
# Suppose we'd like to make sure that our books get a unique label. We can accomplish that with an after_create callback:
|
148
|
+
#
|
149
|
+
# irb# Dupe.define :book do |attrs|
|
150
|
+
# --# attrs.title 'Untitled'
|
151
|
+
# --# attrs.author
|
152
|
+
# --# attrs.isbn do
|
153
|
+
# --# rand(1000000)
|
154
|
+
# --# end
|
155
|
+
# --# attrs.publish_date do |publish_date|
|
156
|
+
# --# Date.parse(publish_date)
|
157
|
+
# --# end
|
158
|
+
# --# attrs.after_create do |book|
|
159
|
+
# --# book.label = book.title.downcase.gsub(/\ +/, '-') + "--#{book.id}"
|
160
|
+
# --# end
|
161
|
+
# --# end
|
162
|
+
#
|
163
|
+
# irb# b = Dupe.create :book, :title => 'Rooby on Rails'
|
164
|
+
# ==> <#Duped::Book author=nil label="rooby-on-rails--1" title="Rooby on Rails" publish_date=nil id=1 isbn=842518>
|
165
|
+
#
|
166
|
+
def define(*args, &block) # yield: define
|
167
|
+
model_name, model_object = create_model_if_definition_parameters_are_valid(args, block)
|
168
|
+
model_object.tap do |m|
|
169
|
+
models[model_name] = m
|
170
|
+
database.create_table model_name
|
171
|
+
mocks = %{
|
172
|
+
network.define_service_mock(
|
173
|
+
:get,
|
174
|
+
%r{^#{model_name.to_s.titleize.constantize.prefix rescue '/'}#{model_name.to_s.pluralize}\\.xml$},
|
175
|
+
proc { Dupe.find(:#{model_name.to_s.pluralize}) }
|
176
|
+
)
|
177
|
+
network.define_service_mock(
|
178
|
+
:get,
|
179
|
+
%r{^#{model_name.to_s.titleize.constantize.prefix rescue '/'}#{model_name.to_s.pluralize}/(\\d+)\\.xml$},
|
180
|
+
proc {|id| Dupe.find(:#{model_name}) {|resource| resource.id == id.to_i}}
|
181
|
+
)
|
182
|
+
network.define_service_mock(
|
183
|
+
:post,
|
184
|
+
%r{^#{model_name.to_s.titleize.constantize.prefix rescue '/'}#{model_name.to_s.pluralize}\\.xml$},
|
185
|
+
proc { |post_body| Dupe.create(:#{model_name.to_s}, post_body) }
|
186
|
+
)
|
187
|
+
network.define_service_mock(
|
188
|
+
:put,
|
189
|
+
%r{^#{model_name.to_s.titleize.constantize.prefix rescue '/'}#{model_name.to_s.pluralize}/(\\d+)\\.xml$},
|
190
|
+
proc { |id, put_data| Dupe.find(:#{model_name.to_s}) {|resource| resource.id == id.to_i}.merge!(put_data) }
|
191
|
+
)
|
192
|
+
network.define_service_mock(
|
193
|
+
:delete,
|
194
|
+
%r{^#{model_name.to_s.titleize.constantize.prefix rescue '/'}#{model_name.to_s.pluralize}/(\\d+)\\.xml$},
|
195
|
+
proc { |id| Dupe.delete(:#{model_name.to_s}) {|resource| resource.id == id.to_i} }
|
196
|
+
)
|
197
|
+
}
|
198
|
+
eval(mocks)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
# This method will cause Dupe to mock resources for the record(s) provided.
|
203
|
+
# The "records" value may be either a hash or an array of hashes.
|
204
|
+
# For example, suppose you'd like to mock a single author:
|
205
|
+
#
|
206
|
+
# author = Dupe.create :author, :name => 'Arthur C. Clarke'
|
207
|
+
# ==> <#Duped::Author name="Arthur C. Clarke" id=1>
|
208
|
+
#
|
209
|
+
# This will translate into the following two mocked resource calls:
|
210
|
+
#
|
211
|
+
# # GET /authors.xml
|
212
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
213
|
+
# <authors>
|
214
|
+
# <author>
|
215
|
+
# <id type="integer">1</id>
|
216
|
+
# <name>Arthur C. Clarke</name>
|
217
|
+
# </author>
|
218
|
+
# </authors>
|
219
|
+
#
|
220
|
+
# # GET /authors/1.xml
|
221
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
222
|
+
# <author>
|
223
|
+
# <id type="integer">1</id>
|
224
|
+
# <name>Arthur C. Clarke</name>
|
225
|
+
# </author>
|
226
|
+
#
|
227
|
+
# However, suppose you wanted to mock two or more authors.
|
228
|
+
#
|
229
|
+
# authors = Dupe.create :author, [{:name => 'Arthur C. Clarke'}, {:name => 'Robert Heinlein'}]
|
230
|
+
# ==> [<#Duped::Author name="Arthur C. Clarke" id=1>, <#Duped::Author name="Robert Heinlein" id=2>]
|
231
|
+
#
|
232
|
+
# This will translate into the following three mocked resource calls:
|
233
|
+
#
|
234
|
+
# # GET /authors.xml
|
235
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
236
|
+
# <authors>
|
237
|
+
# <author>
|
238
|
+
# <id type="integer">1</id>
|
239
|
+
# <name>Arthur C. Clarke</name>
|
240
|
+
# </author>
|
241
|
+
# <author>
|
242
|
+
# <id type="integer">2</id>
|
243
|
+
# <name>Robert Heinlein</name>
|
244
|
+
# </author>
|
245
|
+
# </authors>
|
246
|
+
#
|
247
|
+
# # GET /authors/1.xml
|
248
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
249
|
+
# <author>
|
250
|
+
# <id type="integer">1</id>
|
251
|
+
# <name>Arthur C. Clarke</name>
|
252
|
+
# </author>
|
253
|
+
#
|
254
|
+
# # GET /authors/2.xml
|
255
|
+
# <?xml version="1.0" encoding="UTF-8"?>
|
256
|
+
# <author>
|
257
|
+
# <id type="integer">2</id>
|
258
|
+
# <name>Robert Heinlein</name>
|
259
|
+
# </author>
|
260
|
+
def create(model_name, records={})
|
261
|
+
model_name = model_name.to_s.singularize.to_sym
|
262
|
+
define model_name unless model_exists(model_name)
|
263
|
+
records = records.kind_of?(Array) ? records.map {|r| r.symbolize_keys} : records.symbolize_keys!
|
264
|
+
create_and_insert records, :into => model_name
|
265
|
+
end
|
266
|
+
|
267
|
+
# You can use this method to quickly stub out a large number of resources. For example:
|
268
|
+
#
|
269
|
+
# Dupe.stub 20, :authors
|
270
|
+
#
|
271
|
+
#
|
272
|
+
# Assuming you had an :author resource definition like:
|
273
|
+
#
|
274
|
+
# Dupe.define :author {|author| author.name('default')}
|
275
|
+
#
|
276
|
+
#
|
277
|
+
# then stub would have generated 20 author records like:
|
278
|
+
#
|
279
|
+
# <#Duped::Author name="default" id=1>
|
280
|
+
# ....
|
281
|
+
# <#Duped::Author name="default" id=1>
|
282
|
+
#
|
283
|
+
# and it would also have mocked find(id) and find(:all) responses for these records (along with any other custom mocks you've
|
284
|
+
# setup via Dupe.define_mocks). (Had you not defined an author resource, then the stub would have generated 20 author records
|
285
|
+
# where the only attribute is the id).
|
286
|
+
#
|
287
|
+
# Of course, it's more likely that you wanted to dupe 20 <em>different</em> authors. You can accomplish this by simply doing:
|
288
|
+
#
|
289
|
+
# Dupe.stub 20, :authors, :like => {:name => proc {|n| "author #{n}"}}
|
290
|
+
#
|
291
|
+
# which would generate 20 author records like:
|
292
|
+
#
|
293
|
+
# <#Duped::Author name="author 1" id=1>
|
294
|
+
# ....
|
295
|
+
# <#Duped::Author name="author 20" id=20>
|
296
|
+
#
|
297
|
+
# Naturally, stub will consult the Dupe.define definitions for anything it's attempting to stub
|
298
|
+
# and will honor those definitions (default values, transformations, callbacks) as you would expect.
|
299
|
+
def stub(count, model_name, options={})
|
300
|
+
start_at = options[:starting_with] || 1
|
301
|
+
record_template = options[:like] || {}
|
302
|
+
records = []
|
303
|
+
(start_at..(start_at + count - 1)).each do |i|
|
304
|
+
records <<
|
305
|
+
record_template.map do |k,v|
|
306
|
+
{ k => (v.kind_of?(Proc) ? v.call(i) : v) }
|
307
|
+
end.inject({}) {|h, v| h.merge(v)}
|
308
|
+
end
|
309
|
+
create model_name, records
|
310
|
+
end
|
311
|
+
|
312
|
+
# Dupe has a built-in querying system for finding resources you create.
|
313
|
+
#
|
314
|
+
# irb# a = Dupe.create :author, :name => 'Monkey'
|
315
|
+
# ==> <#Duped::Author name="Monkey" id=1>
|
316
|
+
#
|
317
|
+
# irb# b = Dupe.create :book, :title => 'Bananas', :author => a
|
318
|
+
# ==> <#Duped::Book author=<#Duped::Author name="Monkey" id=1> title="Bananas" id=1>
|
319
|
+
#
|
320
|
+
# irb# Dupe.find(:author) {|a| a.name == 'Monkey'}
|
321
|
+
# ==> <#Duped::Author name="Monkey" id=1>
|
322
|
+
#
|
323
|
+
# irb# Dupe.find(:book) {|b| b.author.name == 'Monkey'}
|
324
|
+
# ==> <#Duped::Book author=<#Duped::Author name="Monkey" id=1> title="Bananas" id=1>
|
325
|
+
#
|
326
|
+
# irb# Dupe.find(:author) {|a| a.id == 1}
|
327
|
+
# ==> <#Duped::Author name="Monkey" id=1>
|
328
|
+
#
|
329
|
+
# irb# Dupe.find(:author) {|a| a.id == 2}
|
330
|
+
# ==> nil
|
331
|
+
#
|
332
|
+
# In all cases, notice that we provided the singular form of a model name to Dupe.find.
|
333
|
+
# This ensures that we either get back either a single resource (if the query was successful), or _nil_.
|
334
|
+
#
|
335
|
+
# If we'd like to find several resources, we can use the plural form of the model name. For example:
|
336
|
+
#
|
337
|
+
# irb# a = Dupe.create :author, :name => 'Monkey', :published => true
|
338
|
+
# ==> <#Duped::Author published=true name="Monkey" id=1>
|
339
|
+
#
|
340
|
+
# irb# b = Dupe.create :book, :title => 'Bananas', :author => a
|
341
|
+
# ==> <#Duped::Book author=<#Duped::Author published=true name="Monkey" id=1> title="Bananas" id=1>
|
342
|
+
#
|
343
|
+
# irb# Dupe.create :author, :name => 'Tiger', :published => false
|
344
|
+
# ==> <#Duped::Author published=false name="Tiger" id=2>
|
345
|
+
#
|
346
|
+
# irb# Dupe.find(:authors)
|
347
|
+
# ==> [<#Duped::Author published=true name="Monkey" id=1>, <#Duped::Author published=false name="Tiger" id=2>]
|
348
|
+
#
|
349
|
+
# irb# Dupe.find(:authors) {|a| a.published == true}
|
350
|
+
# ==> [<#Duped::Author published=true name="Monkey" id=1>]
|
351
|
+
#
|
352
|
+
# irb# Dupe.find(:books)
|
353
|
+
# ==> [<#Duped::Book author=<#Duped::Author published=true name="Monkey" id=1> title="Bananas" id=1>]
|
354
|
+
#
|
355
|
+
# irb# Dupe.find(:books) {|b| b.author.published == false}
|
356
|
+
# ==> []
|
357
|
+
#
|
358
|
+
# Notice that by using the plural form of the model name, we ensure that we receive back an array -
|
359
|
+
# even in the case that the query did not find any results (it simply returns an empty array).
|
360
|
+
def find(model_name, &block) # yield: record
|
361
|
+
results = database.select model_name.to_s.singularize.to_sym, block
|
362
|
+
model_name.plural? ? results : results.first
|
363
|
+
end
|
364
|
+
|
365
|
+
# This method will create a resource with the given specifications if one doesn't already exist.
|
366
|
+
#
|
367
|
+
# irb# Dupe.find :genre
|
368
|
+
# Dupe::Database::TableDoesNotExistError: The table ':genre' does not exist.
|
369
|
+
# from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/database.rb:30:in `select'
|
370
|
+
# from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/dupe.rb:295:in `find'
|
371
|
+
# from (irb):40
|
372
|
+
#
|
373
|
+
# irb# Dupe.find_or_create :genre
|
374
|
+
# ==> <#Duped::Genre id=1>
|
375
|
+
#
|
376
|
+
# irb# Dupe.find_or_create :genre
|
377
|
+
# ==> <#Duped::Genre id=1>
|
378
|
+
#
|
379
|
+
# You can also pass conditions to find_or_create as a hash:
|
380
|
+
#
|
381
|
+
# irb# Dupe.find_or_create :genre, :name => 'Science Fiction', :label => 'sci-fi'
|
382
|
+
# ==> <#Duped::Genre label="sci-fi" name="Science Fiction" id=2>
|
383
|
+
#
|
384
|
+
# irb# Dupe.find_or_create :genre, :name => 'Science Fiction', :label => 'sci-fi'
|
385
|
+
# ==> <#Duped::Genre label="sci-fi" name="Science Fiction" id=2>
|
386
|
+
def find_or_create(model_name, attributes={})
|
387
|
+
results = nil
|
388
|
+
if model_exists(model_name)
|
389
|
+
results = eval("find(:#{model_name}) #{build_conditions(attributes)}")
|
390
|
+
end
|
391
|
+
|
392
|
+
if !results
|
393
|
+
if model_name.singular?
|
394
|
+
create model_name, attributes
|
395
|
+
else
|
396
|
+
stub((rand(5)+1), model_name, :like => attributes)
|
397
|
+
end
|
398
|
+
elsif results.kind_of?(Array) && results.empty?
|
399
|
+
stub((rand(5)+1), model_name, :like => attributes)
|
400
|
+
else
|
401
|
+
results
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def delete(resource, &conditions)
|
406
|
+
database.delete resource, conditions
|
407
|
+
end
|
408
|
+
|
409
|
+
def sequence(name, &block)
|
410
|
+
sequences[name.to_sym] = Sequence.new 1, block
|
411
|
+
end
|
412
|
+
|
413
|
+
def next(name)
|
414
|
+
raise ArgumentError, "Unknown sequence \":#{name}\"" unless sequences.has_key?(name)
|
415
|
+
sequences[name].next
|
416
|
+
end
|
417
|
+
|
418
|
+
def models #:nodoc:
|
419
|
+
@models ||= {}
|
420
|
+
end
|
421
|
+
|
422
|
+
def network #:nodoc:
|
423
|
+
@network ||= Dupe::Network.new
|
424
|
+
end
|
425
|
+
|
426
|
+
def database #:nodoc:
|
427
|
+
@database ||= Dupe::Database.new
|
428
|
+
end
|
429
|
+
|
430
|
+
def sequences #:nodoc:
|
431
|
+
@sequences ||= {}
|
432
|
+
end
|
433
|
+
|
434
|
+
# clears out all model definitions, sequences, and database records / tables.
|
435
|
+
def reset
|
436
|
+
reset_models
|
437
|
+
reset_database
|
438
|
+
reset_network
|
439
|
+
reset_sequences
|
440
|
+
end
|
441
|
+
|
442
|
+
def reset_sequences
|
443
|
+
@sequences = {}
|
444
|
+
end
|
445
|
+
|
446
|
+
def reset_models
|
447
|
+
@models = {}
|
448
|
+
end
|
449
|
+
|
450
|
+
def reset_database
|
451
|
+
@database = Dupe::Database.new
|
452
|
+
end
|
453
|
+
|
454
|
+
def reset_network
|
455
|
+
@network = Dupe::Network.new
|
456
|
+
end
|
457
|
+
|
458
|
+
# set to true if you want to see mocked results spit out after each cucumber scenario
|
459
|
+
def debug
|
460
|
+
@debug ||= false
|
461
|
+
end
|
462
|
+
|
463
|
+
|
464
|
+
|
465
|
+
private
|
466
|
+
def build_conditions(conditions)
|
467
|
+
return '' if conditions.empty?
|
468
|
+
select =
|
469
|
+
"{|record| " +
|
470
|
+
conditions.map do |k,v|
|
471
|
+
"record.#{k} == #{v.kind_of?(String) ? "\"#{v}\"" : v}"
|
472
|
+
end.join(" && ") + " }"
|
473
|
+
end
|
474
|
+
|
475
|
+
def model_exists(model_name)
|
476
|
+
models[model_name.to_s.singularize.to_sym]
|
477
|
+
end
|
478
|
+
|
479
|
+
def create_model(model_name)
|
480
|
+
models[model_name] = Dupe::Model.new(model_name) unless models[model_name]
|
481
|
+
end
|
482
|
+
|
483
|
+
def create_and_insert(records, into)
|
484
|
+
raise(
|
485
|
+
ArgumentError,
|
486
|
+
"You must pass a hash containing :into => :model_name " +
|
487
|
+
"as the second parameter to create_and_insert."
|
488
|
+
) if !into || !into.kind_of?(Hash) || !into[:into]
|
489
|
+
|
490
|
+
# do we have several records to create, and are they each a hash?
|
491
|
+
if records.kind_of?(Array) and
|
492
|
+
records.inject(true) {|bool, r| bool and r.kind_of?(Hash)}
|
493
|
+
[].tap do |results|
|
494
|
+
records.each do |record|
|
495
|
+
results << models[into[:into]].create(record).tap {|r| database.insert r}
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
# do we only have one record to create, and is it a hash?
|
500
|
+
elsif records.kind_of?(Hash)
|
501
|
+
models[into[:into]].create(records).tap {|r| database.insert r}
|
502
|
+
|
503
|
+
else
|
504
|
+
raise ArgumentError, "You must call Dupe.create with either a hash or an array of hashes."
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
def create_model_if_definition_parameters_are_valid(args, definition)
|
509
|
+
if args.length == 1 and
|
510
|
+
args.first.kind_of?(Symbol) and
|
511
|
+
definition == nil
|
512
|
+
|
513
|
+
return args.first, Dupe::Model.new(args.first)
|
514
|
+
|
515
|
+
elsif args.length == 1 and
|
516
|
+
args.first.kind_of?(Symbol) and
|
517
|
+
definition.kind_of?(Proc) and
|
518
|
+
definition.arity == 1
|
519
|
+
|
520
|
+
model_name = args.first
|
521
|
+
return model_name, Dupe::Model.new(model_name).tap {|m| m.define definition}
|
522
|
+
|
523
|
+
else
|
524
|
+
raise ArgumentError.new(
|
525
|
+
"Unknown Dupe.define parameter format. Please consult the API for information on how to use Dupe.define"
|
526
|
+
)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
class Dupe
|
533
|
+
class UnprocessableEntity < StandardError; end
|
534
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class HashPruner
|
2
|
+
class << self
|
3
|
+
def prune(hash)
|
4
|
+
HashPruner.new.prune hash
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize()
|
9
|
+
@visited = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def prune(item)
|
13
|
+
if item.kind_of? Array
|
14
|
+
item.map {|i| prune(i)}.reject {|v| v==nil}
|
15
|
+
elsif item.kind_of? Hash
|
16
|
+
if @visited[item.object_id]
|
17
|
+
item.dup.delete_if {|k,v| v.kind_of?(Hash) || v.kind_of?(Array)}
|
18
|
+
else
|
19
|
+
@visited[item.object_id] = true
|
20
|
+
new_hash = {}
|
21
|
+
item.each do |k,v|
|
22
|
+
new_hash[k] = prune(v)
|
23
|
+
end
|
24
|
+
@visited.delete item.object_id
|
25
|
+
new_hash
|
26
|
+
end
|
27
|
+
else
|
28
|
+
item
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Hash
|
34
|
+
def to_xml_safe(options={})
|
35
|
+
HashPruner.prune(self).to_xml(options)
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Dupe
|
2
|
+
class Network #:nodoc:
|
3
|
+
class Log #:nodoc:
|
4
|
+
include RestValidation #:nodoc:
|
5
|
+
attr_reader :requests #:nodoc:
|
6
|
+
|
7
|
+
class Request #:nodoc:
|
8
|
+
attr_reader :verb, :path, :response_body
|
9
|
+
|
10
|
+
def initialize(verb, path, response_body)
|
11
|
+
@verb, @path, @response_body = verb, path, response_body
|
12
|
+
end
|
13
|
+
|
14
|
+
def pretty_print
|
15
|
+
"Request: #{@verb.to_s.upcase} #{@path}\n" +
|
16
|
+
"Response:\n" + @response_body.indent
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize #:nodoc:
|
21
|
+
@requests = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def add_request(verb, path, response_body='') #:nodoc:
|
25
|
+
validate_request_type verb
|
26
|
+
@requests << Request.new(verb, path, response_body)
|
27
|
+
end
|
28
|
+
|
29
|
+
def pretty_print
|
30
|
+
"Logged Requests:\n" + requests.map {|r| r.pretty_print.indent }.join("\n\n") + "\n\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset #:nodoc:
|
34
|
+
@requests = []
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|