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.
Files changed (43) hide show
  1. data/README.markdown +10 -0
  2. data/lib/superdupe/active_resource_extensions.rb +161 -0
  3. data/lib/superdupe/attribute_template.rb +71 -0
  4. data/lib/superdupe/cucumber_hooks.rb +16 -0
  5. data/lib/superdupe/custom_mocks.rb +116 -0
  6. data/lib/superdupe/database.rb +69 -0
  7. data/lib/superdupe/dupe.rb +534 -0
  8. data/lib/superdupe/hash_pruner.rb +37 -0
  9. data/lib/superdupe/log.rb +38 -0
  10. data/lib/superdupe/mock.rb +127 -0
  11. data/lib/superdupe/model.rb +59 -0
  12. data/lib/superdupe/network.rb +56 -0
  13. data/lib/superdupe/record.rb +42 -0
  14. data/lib/superdupe/rest_validation.rb +16 -0
  15. data/lib/superdupe/schema.rb +52 -0
  16. data/lib/superdupe/sequence.rb +20 -0
  17. data/lib/superdupe/singular_plural_detection.rb +9 -0
  18. data/lib/superdupe/string.rb +7 -0
  19. data/lib/superdupe/symbol.rb +3 -0
  20. data/lib/superdupe.rb +20 -0
  21. data/rails_generators/dupe/dupe_generator.rb +20 -0
  22. data/rails_generators/dupe/templates/custom_mocks.rb +4 -0
  23. data/rails_generators/dupe/templates/definitions.rb +9 -0
  24. data/rails_generators/dupe/templates/load_dupe.rb +9 -0
  25. data/spec/lib_specs/active_resource_extensions_spec.rb +141 -0
  26. data/spec/lib_specs/attribute_template_spec.rb +173 -0
  27. data/spec/lib_specs/database_spec.rb +163 -0
  28. data/spec/lib_specs/dupe_spec.rb +522 -0
  29. data/spec/lib_specs/hash_pruner_spec.rb +73 -0
  30. data/spec/lib_specs/log_spec.rb +78 -0
  31. data/spec/lib_specs/logged_request_spec.rb +22 -0
  32. data/spec/lib_specs/mock_definitions_spec.rb +58 -0
  33. data/spec/lib_specs/mock_spec.rb +194 -0
  34. data/spec/lib_specs/model_spec.rb +95 -0
  35. data/spec/lib_specs/network_spec.rb +130 -0
  36. data/spec/lib_specs/record_spec.rb +70 -0
  37. data/spec/lib_specs/rest_validation_spec.rb +17 -0
  38. data/spec/lib_specs/schema_spec.rb +109 -0
  39. data/spec/lib_specs/sequence_spec.rb +39 -0
  40. data/spec/lib_specs/string_spec.rb +31 -0
  41. data/spec/lib_specs/symbol_spec.rb +17 -0
  42. data/spec/spec_helper.rb +8 -0
  43. 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