dupe 0.3.7 → 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 (44) hide show
  1. data/README.rdoc +390 -147
  2. data/lib/dupe/active_resource_extensions.rb +25 -0
  3. data/lib/dupe/attribute_template.rb +71 -0
  4. data/lib/dupe/cucumber_hooks.rb +15 -7
  5. data/lib/dupe/custom_mocks.rb +102 -0
  6. data/lib/dupe/database.rb +51 -0
  7. data/lib/dupe/dupe.rb +359 -372
  8. data/lib/dupe/log.rb +38 -0
  9. data/lib/dupe/mock.rb +50 -0
  10. data/lib/dupe/model.rb +55 -0
  11. data/lib/dupe/network.rb +38 -0
  12. data/lib/dupe/record.rb +35 -56
  13. data/lib/dupe/rest_validation.rb +16 -0
  14. data/lib/dupe/schema.rb +36 -0
  15. data/lib/dupe/sequence.rb +11 -10
  16. data/lib/dupe/singular_plural_detection.rb +9 -0
  17. data/lib/dupe/string.rb +6 -8
  18. data/lib/dupe/symbol.rb +3 -0
  19. data/lib/dupe.rb +13 -12
  20. data/rails_generators/dupe/templates/custom_mocks.rb +4 -34
  21. data/rails_generators/dupe/templates/dupe_setup.rb +3 -23
  22. data/spec/lib_specs/active_resource_extensions_spec.rb +29 -0
  23. data/spec/lib_specs/attribute_template_spec.rb +173 -0
  24. data/spec/lib_specs/database_spec.rb +133 -0
  25. data/spec/lib_specs/dupe_spec.rb +307 -0
  26. data/spec/lib_specs/log_spec.rb +78 -0
  27. data/spec/lib_specs/logged_request_spec.rb +22 -0
  28. data/spec/lib_specs/mock_definitions_spec.rb +32 -0
  29. data/spec/lib_specs/mock_spec.rb +67 -0
  30. data/spec/lib_specs/model_spec.rb +90 -0
  31. data/spec/lib_specs/network_spec.rb +77 -0
  32. data/spec/lib_specs/record_spec.rb +70 -0
  33. data/spec/lib_specs/rest_validation_spec.rb +17 -0
  34. data/spec/lib_specs/schema_spec.rb +90 -0
  35. data/spec/lib_specs/sequence_spec.rb +26 -0
  36. data/spec/lib_specs/string_spec.rb +31 -0
  37. data/spec/lib_specs/symbol_spec.rb +17 -0
  38. data/spec/spec_helper.rb +2 -5
  39. metadata +29 -7
  40. data/lib/dupe/active_resource.rb +0 -135
  41. data/lib/dupe/attribute.rb +0 -17
  42. data/lib/dupe/configuration.rb +0 -20
  43. data/lib/dupe/mock_service_response.rb +0 -55
  44. data/spec/lib_specs/dupe_record_spec.rb +0 -57
data/lib/dupe/dupe.rb CHANGED
@@ -2,121 +2,175 @@
2
2
  # License:: Distributes under the same terms as Ruby
3
3
 
4
4
  class Dupe
5
- attr_reader :factory_name #:nodoc:
6
- attr_reader :configuration #:nodoc:
7
- attr_reader :attributes #:nodoc:
8
- attr_reader :config #:nodoc:
9
- attr_reader :mocker #:nodoc:
10
- attr_reader :records #:nodoc:
11
-
12
5
  class << self
13
- attr_accessor :factories #:nodoc:
14
- attr_accessor :global_configuration #:nodoc:
15
6
 
16
- # Create data definitions for your resources. This allows you to setup default values for columns
17
- # and even provide data transformations.
18
- #
19
- # For example, suppose you had the following cucumber scenario:
20
- #
21
- # # RAILS_ROOT/features/library/find_book.feature
22
- # Feature: Find a book
23
- # As a reader
24
- # I want to find books in my library
25
- # So that I can read them
26
- #
27
- # Scenario: Browsing books
28
- # Given the following author:
29
- # | name | date_of_birth |
30
- # | Arthur C. Clarke | 1917-12-16 |
31
- #
32
- # And the following book:
33
- # | name | author |
34
- # | 2001: A Space Odyssey | Arthur C. Clarke |
35
- #
36
- # When....
37
- #
38
- #
39
- # We can use Dupe.define to
40
- # * Transform data (e.g., turn the string '1917-12-16' into a Date object)
41
- # * Provide default values for attributes (e.g., give all author's a default biography)
42
- # * Associate records (e.g., given an author name, return the author record associated with that name)
43
- #
44
- # To accomplish the afore mentioned definitions:
45
- #
46
- # # RAILS_ROOT/features/dupe_definitions/book.rb
47
- #
48
- # Dupe.define :author do |define|
49
- # define.bio 'Lorem ipsum delor.'
50
- # define.date_of_birth do |d|
51
- # Date.parse(t)
52
- # end
53
- # end
54
- #
55
- # Dupe.define :book do |define|
56
- # define.author do |author_name|
57
- # Dupe.find(:author) {|a| a.name == author_name}
58
- # end
59
- # end
60
- #
61
- # -----------------------------------------------------------------------------------------------------------------
62
- #
63
- # # RAILS_ROOT/features/step_definitions/library/find_book_steps.rb
64
- #
65
- # Given /^the following author:$/ do |author_table|
66
- # Dupe.create(:author, author_table.hashes)
67
- # end
68
- #
69
- # Given /^the following book:$/ do |book_table|
70
- # Dupe.create(:book, book_table.hashes)
71
- # end
72
- #
73
- # When cucumber encounters the "Given the following author:" line, the corresponding step definition
74
- # will ask Dupe to mock ActiveResource responses to find(:all) and find(:id) with the data
75
- # specified in the cucumber hash table immediately following the "Given the following author:" line.
76
- # Since we didn't specify a 'bio' value in our cucumber hash table, Dupe will give it the
77
- # default value 'Lorem ipsum delor.'. Also, it will transform the 'date_of_birth' value we provided in the hash
78
- # table into a time object.
79
- #
80
- # Similarly, for the :book cucumber hash table, Dupe will transform the author name we provided
81
- # into the author object we had already specified in the :author table.
82
- #
83
- # In terms of mocked responses, we could expect something like:
84
- #
85
- # # Author.find(1) --> GET /authors/1.xml
86
- # <?xml version="1.0" encoding="UTF-8"?>
87
- # <author>
88
- # <id type="integer">1</id>
89
- # <name>Arthur C. Clarke</name>
90
- # <bio>Lorem ipsum delor.</bio>
91
- # <date_of_birth>1917-12-16T00:00:00Z</date_of_birth>
92
- # </author>
93
- #
94
- # # Book.find(1) --> GET /books/1.xml
95
- # <?xml version="1.0" encoding="UTF-8"?>
96
- # <book>
97
- # <id type="integer">1</id>
98
- # <name>2001: A Space Odyssey</name>
99
- # <author>
100
- # <id type="integer">1</id>
101
- # <name>Arthur C. Clarke</name>
102
- # <bio>Lorem ipsum delor.</bio>
103
- # <date_of_birth>1917-12-16T00:00:00Z</date_of_birth>
104
- # </author>
105
- # </book>
106
- def define(factory) # yield: define
107
- setup_factory(factory)
108
- yield @factories[factory]
7
+ attr_reader :models #:nodoc:
8
+ attr_reader :database #:nodoc:
9
+
10
+ # set this to "true" if you Dupe to spit out mocked requests
11
+ # after each of your cucumber scenario's run
12
+ attr_accessor :debug
13
+
14
+ # Suppose we're creating a 'book' resource. Perhaps our app assumes every book has a title, so let's define a book resource
15
+ # that specifies just that:
16
+ #
17
+ # irb# Dupe.define :book do |attrs|
18
+ # --# attrs.title 'Untitled'
19
+ # --# attrs.author
20
+ # --# end
21
+ # ==> #<Dupe::Model:0x17b2694 ...>
22
+ #
23
+ # 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.
24
+ #
25
+ # irb# b = Dupe.create :book
26
+ # ==> <#Duped::Book author=nil title="Untitled" id=1>
27
+ #
28
+ #
29
+ # If we provide our own title, it should allow us to override the default value:
30
+ #
31
+ # irb# b = Dupe.create :book, :title => 'Monkeys!'
32
+ # ==> <#Duped::Book author=nil title="Monkeys!" id=2>
33
+ #
34
+ # === Attributes with procs as default values
35
+ #
36
+ # Sometimes it might be convenient to procedurally define the default value for an attribute:
37
+ #
38
+ # irb# Dupe.define :book do |attrs|
39
+ # --# attrs.title 'Untitled'
40
+ # --# attrs.author
41
+ # --# attrs.isbn do
42
+ # --# rand(1000000)
43
+ # --# end
44
+ # --# end
45
+ #
46
+ # Now, every time we create a book, it will get assigned a random ISBN number:
47
+ #
48
+ # irb# b = Dupe.create :book
49
+ # ==> <#Duped::Book author=nil title="Untitled" id=1 isbn=895825>
50
+ #
51
+ # irb# b = Dupe.create :book
52
+ # ==> <#Duped::Book author=nil title="Untitled" id=2 isbn=606472>
53
+ #
54
+ # 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 it's own resource. We can accomplish that by taking advantage of Dupe's "find_or_create" method:
55
+ #
56
+ # irb# Dupe.define :book do |attrs|
57
+ # --# attrs.title 'Untitled'
58
+ # --# attrs.author
59
+ # --# attrs.isbn do
60
+ # --# rand(1000000)
61
+ # --# end
62
+ # --# attrs.genre do
63
+ # --# Dupe.find_or_create :genre
64
+ # --# end
65
+ # --# end
66
+ #
67
+ # 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.
68
+ #
69
+ # First, let's confirm that no genres currently exist:
70
+ #
71
+ # irb# Dupe.find :genre
72
+ # Dupe::Database::TableDoesNotExistError: The table ':genre' does not exist.
73
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/database.rb:30:in `select'
74
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/dupe.rb:295:in `find'
75
+ # from (irb):135
76
+ #
77
+ # Next, let's create a book:
78
+ #
79
+ # irb# b = Dupe.create :book
80
+ # ==> <#Duped::Book genre=<#Duped::Genre id=1> author=nil title="Untitled" id=1 isbn=62572>
81
+ #
82
+ # Notice that it create a genre. If we tried to do another Dupe.find for the genre:
83
+ #
84
+ # irb# Dupe.find :genre
85
+ # ==> <#Duped::Genre id=1>
86
+ #
87
+ # Now, if create another book, it will associate with the genre that was just created:
88
+ #
89
+ # irb# b = Dupe.create :book
90
+ # ==> <#Duped::Book genre=<#Duped::Genre id=1> author=nil title="Untitled" id=2 isbn=729317>
91
+ #
92
+ #
93
+ #
94
+ # === Attributes with transformers
95
+ #
96
+ # Occasionally, you may find it useful to have attribute values transformed upon creation.
97
+ #
98
+ # 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.
99
+ #
100
+ # irb# Dupe.define :book do |attrs|
101
+ # --# attrs.title 'Untitled'
102
+ # --# attrs.author
103
+ # --# attrs.isbn do
104
+ # --# rand(1000000)
105
+ # --# end
106
+ # --# attrs.publish_date do |publish_date|
107
+ # --# Date.parse(publish_date)
108
+ # --# end
109
+ # --# end
110
+ #
111
+ # Now, let's create a book:
112
+ #
113
+ # irb# b = Dupe.create :book, :publish_date => '2009-12-29'
114
+ # ==> <#Duped::Book author=nil title="Untitled" publish_date=Tue, 29 Dec 2009 id=1 isbn=826291>
115
+ #
116
+ # irb# b.publish_date
117
+ # ==> Tue, 29 Dec 2009
118
+ #
119
+ # irb# b.publish_date.class
120
+ # ==> Date
121
+ #
122
+ # === Callbacks
123
+ #
124
+ # Suppose we'd like to make sure that our books get a unique label. We can accomplish that with an after_create callback:
125
+ #
126
+ # irb# Dupe.define :book do |attrs|
127
+ # --# attrs.title 'Untitled'
128
+ # --# attrs.author
129
+ # --# attrs.isbn do
130
+ # --# rand(1000000)
131
+ # --# end
132
+ # --# attrs.publish_date do |publish_date|
133
+ # --# Date.parse(publish_date)
134
+ # --# end
135
+ # --# attrs.after_create do |book|
136
+ # --# book.label = book.title.downcase.gsub(/\ +/, '-') + "--#{book.id}"
137
+ # --# end
138
+ # --# end
139
+ #
140
+ # irb# b = Dupe.create :book, :title => 'Rooby on Rails'
141
+ # ==> <#Duped::Book author=nil label="rooby-on-rails--1" title="Rooby on Rails" publish_date=nil id=1 isbn=842518>
142
+ #
143
+ def define(*args, &block) # yield: define
144
+ model_name, model_object = create_model_if_definition_parameters_are_valid(args, block)
145
+ model_object.tap do |m|
146
+ models[model_name] = m
147
+ database.create_table model_name
148
+ mocks = %{
149
+ network.define_service_mock(
150
+ :get,
151
+ %r{/#{model_name.to_s.pluralize}\\.xml$},
152
+ proc { Dupe.find(:#{model_name.to_s.pluralize}) }
153
+ )
154
+ network.define_service_mock(
155
+ :get,
156
+ %r{/#{model_name.to_s.pluralize}/(\\d+)\\.xml$},
157
+ proc {|id| Dupe.find(:#{model_name}) {|resource| resource.id == id.to_i}}
158
+ )
159
+ }
160
+ eval(mocks)
161
+ end
109
162
  end
110
163
 
111
164
  # This method will cause Dupe to mock resources for the record(s) provided.
112
165
  # The "records" value may be either a hash or an array of hashes.
113
- # For example, suppose you'd like to mock a single author ActiveResource object:
166
+ # For example, suppose you'd like to mock a single author:
114
167
  #
115
- # Dupe.create :author, :name => 'Arthur C. Clarke'
168
+ # author = Dupe.create :author, :name => 'Arthur C. Clarke'
169
+ # ==> <#Duped::Author name="Arthur C. Clarke" id=1>
116
170
  #
117
171
  # This will translate into the following two mocked resource calls:
118
172
  #
119
- # # Author.find(:all) --> GET /authors.xml
173
+ # # GET /authors.xml
120
174
  # <?xml version="1.0" encoding="UTF-8"?>
121
175
  # <authors>
122
176
  # <author>
@@ -125,7 +179,7 @@ class Dupe
125
179
  # </author>
126
180
  # </authors>
127
181
  #
128
- # # Author.find(1) --> GET /authors/1.xml
182
+ # # GET /authors/1.xml
129
183
  # <?xml version="1.0" encoding="UTF-8"?>
130
184
  # <author>
131
185
  # <id type="integer">1</id>
@@ -133,12 +187,13 @@ class Dupe
133
187
  # </author>
134
188
  #
135
189
  # However, suppose you wanted to mock two or more authors.
136
- #
137
- # Dupe.create :author, [{:name => 'Arthur C. Clarke'}, {:name => 'Robert Heinlein'}]
138
- #
190
+ #
191
+ # authors = Dupe.create :author, [{:name => 'Arthur C. Clarke'}, {:name => 'Robert Heinlein'}]
192
+ # ==> [<#Duped::Author name="Arthur C. Clarke" id=1>, <#Duped::Author name="Robert Heinlein" id=2>]
193
+ #
139
194
  # This will translate into the following three mocked resource calls:
140
195
  #
141
- # # Author.find(:all) --> GET /authors.xml
196
+ # # GET /authors.xml
142
197
  # <?xml version="1.0" encoding="UTF-8"?>
143
198
  # <authors>
144
199
  # <author>
@@ -151,24 +206,23 @@ class Dupe
151
206
  # </author>
152
207
  # </authors>
153
208
  #
154
- # # Author.find(1) --> GET /authors/1.xml
209
+ # # GET /authors/1.xml
155
210
  # <?xml version="1.0" encoding="UTF-8"?>
156
211
  # <author>
157
212
  # <id type="integer">1</id>
158
213
  # <name>Arthur C. Clarke</name>
159
214
  # </author>
160
215
  #
161
- # # Author.find(2) --> GET /authors/2.xml
216
+ # # GET /authors/2.xml
162
217
  # <?xml version="1.0" encoding="UTF-8"?>
163
218
  # <author>
164
219
  # <id type="integer">2</id>
165
220
  # <name>Robert Heinlein</name>
166
221
  # </author>
167
- def create(factory, records={})
168
- setup_factory(factory)
169
- raise Exception, "unknown records type" if !records.nil? and !records.is_a?(Array) and !records.is_a?(Hash)
170
- records = [records] if records.is_a?(Hash)
171
- @factories[factory].generate_services_for(records)
222
+ def create(model_name, records={})
223
+ model_name = model_name.to_s.singularize.to_sym
224
+ define model_name unless model_exists(model_name)
225
+ create_and_insert records, :into => model_name
172
226
  end
173
227
 
174
228
  # You can use this method to quickly stub out a large number of resources. For example:
@@ -183,9 +237,9 @@ class Dupe
183
237
  #
184
238
  # then stub would have generated 20 author records like:
185
239
  #
186
- # {:name => 'default', :id => 1}
240
+ # <#Duped::Author name="default" id=1>
187
241
  # ....
188
- # {:name => 'default', :id => 20}
242
+ # <#Duped::Author name="default" id=1>
189
243
  #
190
244
  # and it would also have mocked find(id) and find(:all) responses for these records (along with any other custom mocks you've
191
245
  # setup via Dupe.define_mocks). (Had you not defined an author resource, then the stub would have generated 20 author records
@@ -197,282 +251,215 @@ class Dupe
197
251
  #
198
252
  # which would generate 20 author records like:
199
253
  #
200
- # {:name => 'author 1', :id => 1}
201
- # ....
202
- # {:name => 'author 20', :id => 20}
203
- #
204
- # You may also override the sequence starting value:
205
- #
206
- # Dupe.stub 20, :authors, :like => {:name => proc {|n| "author #{n}"}}, :starting_with => 150
207
- #
208
- # This would generate 20 author records like:
209
- #
210
- # {:name => 'author 150', :id => 1}
254
+ # <#Duped::Author name="author 1" id=1>
211
255
  # ....
212
- # {:name => 'author 169', :id => 20}
256
+ # <#Duped::Author name="author 20" id=20>
213
257
  #
214
258
  # Naturally, stub will consult the Dupe.define definitions for anything it's attempting to stub
215
- # and will honor those definitions (default values, transformations) as you would expect.
216
- def stub(count, factory, options={})
217
- factory = factory.to_s.singularize.to_sym
218
- setup_factory(factory)
219
- @factories[factory].stub_services_with((options[:like] || {}), count.to_i, (options[:starting_with] || 1))
259
+ # and will honor those definitions (default values, transformations, callbacks) as you would expect.
260
+ def stub(count, model_name, options={})
261
+ start_at = options[:starting_with] || 1
262
+ record_template = options[:like] || {}
263
+ records = []
264
+ (start_at..(start_at + count - 1)).each do |i|
265
+ records <<
266
+ record_template.map do |k,v|
267
+ { k => (v.kind_of?(Proc) ? v.call(i) : v) }
268
+ end.inject({}) {|h, v| h.merge(v)}
269
+ end
270
+ create model_name, records
220
271
  end
221
272
 
222
- # === Global Configuration
223
- #
224
- # On a global level, configure supports the following options (expect this list to grow as the app grows):
225
- # debug: list the attempted requests and mocked responses that happened during the course of a scenario
273
+ # Dupe has a built-in querying system for finding resources you create.
226
274
  #
227
- # To turn on debugging, simply do:
228
- # Dupe.configure do |global_config|
229
- # global_config.debug true
230
- # end
231
- #
232
- # === Factory Configuration
233
- #
234
- # On a factory level, configure support the following options (expect this list to grow as the app grows):
235
- # record_identifiers: a list of attributes that are unique to each record in that resource factory.
236
- #
237
- # The "record_identifiers" configuration option allows you to override the array record identifiers for your resources ([:id], by default)
275
+ # irb# a = Dupe.create :author, :name => 'Monkey'
276
+ # ==> <#Duped::Author name="Monkey" id=1>
238
277
  #
239
- # For example, suppose the RESTful application your trying to consume supports lookups by both a textual 'label'
240
- # and a numeric 'id', and that it contains an author service where the author with id '1' has the label 'arthur-c-clarke'.
241
- # Your application should expect the same response whether or not you call <tt>Author.find(1)</tt> or <tt>Author.find('arthur-c-clarke')</tt>.
278
+ # irb# b = Dupe.create :book, :title => 'Bananas', :author => a
279
+ # ==> <#Duped::Book author=<#Duped::Author name="Monkey" id=1> title="Bananas" id=1>
242
280
  #
243
- # Thus, to ensure that Dupe mocks both, do the following:
244
- # Dupe.configure :author do |configure|
245
- # configure.record_identifiers :id, :label
246
- # end
247
- #
248
- # With this configuration, a <tt>Dupe.create :author, :name => 'Arthur C. Clarke', :label => 'arthur-c-clarke'</tt>
249
- # will result in the following mocked service calls:
250
- #
251
- # <tt>Author.find(1) --> (GET /authors/1.xml)</tt>
252
- #
253
- # <?xml version="1.0" encoding="UTF-8"?>
254
- # <author>
255
- # <id type="integer">1</id>
256
- # <name>Arthur C. Clarke</name>
257
- # <label>arthur-c-clarke</label>
258
- # </author>
259
- #
260
- #
261
- # <tt>Author.find('arthur-c-clarke') --> (GET /authors/arthur-c-clarke.xml)</tt>
262
- #
263
- # <?xml version="1.0" encoding="UTF-8"?>
264
- # <author>
265
- # <id type="integer">1</id>
266
- # <name>Arthur C. Clarke</name>
267
- # <label>arthur-c-clarke</label>
268
- # </author>
269
- def configure(factory=nil) # yield: configure
270
- yield global_configuration and return unless factory
271
- setup_factory(factory)
272
- yield @factories[factory].config
281
+ # irb# Dupe.find(:author) {|a| a.name == 'Monkey'}
282
+ # ==> <#Duped::Author name="Monkey" id=1>
283
+ #
284
+ # irb# Dupe.find(:book) {|b| b.author.name == 'Monkey'}
285
+ # ==> <#Duped::Book author=<#Duped::Author name="Monkey" id=1> title="Bananas" id=1>
286
+ #
287
+ # irb# Dupe.find(:author) {|a| a.id == 1}
288
+ # ==> <#Duped::Author name="Monkey" id=1>
289
+ #
290
+ # irb# Dupe.find(:author) {|a| a.id == 2}
291
+ # ==> nil
292
+ #
293
+ # In all cases, notice that we provided the singular form of a model name to Dupe.find.
294
+ # This ensures that we either get back either a single resource (if the query was successful), or _nil_.
295
+ #
296
+ # If we'd like to find several resources, we can use the plural form of the model name. For example:
297
+ #
298
+ # irb# a = Dupe.create :author, :name => 'Monkey', :published => true
299
+ # ==> <#Duped::Author published=true name="Monkey" id=1>
300
+ #
301
+ # irb# b = Dupe.create :book, :title => 'Bananas', :author => a
302
+ # ==> <#Duped::Book author=<#Duped::Author published=true name="Monkey" id=1> title="Bananas" id=1>
303
+ #
304
+ # irb# Dupe.create :author, :name => 'Tiger', :published => false
305
+ # ==> <#Duped::Author published=false name="Tiger" id=2>
306
+ #
307
+ # irb# Dupe.find(:authors)
308
+ # ==> [<#Duped::Author published=true name="Monkey" id=1>, <#Duped::Author published=false name="Tiger" id=2>]
309
+ #
310
+ # irb# Dupe.find(:authors) {|a| a.published == true}
311
+ # ==> [<#Duped::Author published=true name="Monkey" id=1>]
312
+ #
313
+ # irb# Dupe.find(:books)
314
+ # ==> [<#Duped::Book author=<#Duped::Author published=true name="Monkey" id=1> title="Bananas" id=1>]
315
+ #
316
+ # irb# Dupe.find(:books) {|b| b.author.published == false}
317
+ # ==> []
318
+ #
319
+ # Notice that by using the plural form of the model name, we ensure that we receive back an array -
320
+ # even in the case that the query did not find any results (it simply returns an empty array).
321
+ def find(model_name, &block) # yield: record
322
+ results = database.select model_name.to_s.singularize.to_sym, block
323
+ model_name.plural? ? results : results.first
273
324
  end
274
325
 
275
- # By default, Dupe will mock responses to ActiveResource <tt>find(:all)</tt> and <tt>find(id)</tt>.
276
- # However, it's likely that your cucumber scenarios will eventually fire off an ActiveResource request that's
277
- # something other than these basic lookups.
278
- #
279
- # Dupe.define_mocks allows you to add new resource mocks and override the built-in resource mocks.
280
- #
281
- # For example, suppose you had a Book ActiveResource model, and you want to use it to get the :count of all
282
- # Books in the back end system your consuming. <tt>Book.get(:count)</tt> would fire off an HTTP request to the
283
- # backend service like <tt>"GET /books/count.xml"</tt>, and assuming the service is set up to respond to that
284
- # request, you might expect to get something back like:
285
- #
286
- # <?xml version="1.0" encoding="UTF-8"?>
287
- # <hash>
288
- # <count type="integer">3</count>
289
- # </hash>
290
- #
291
- # To mock this for the purposes of cuking, you could do the following:
326
+ # This method will create a resource with the given specifications if one doesn't already exist.
292
327
  #
293
- # Dupe.define_mocks :book do |define|
294
- # define.count do |mock, records|
295
- # mock.get "/books/count.xml", {}, {:count => records.size}.to_xml
296
- # end
297
- # end
298
- #
299
- # The <tt>mock</tt> object is the ActiveResource::HttpMock object. Please see the documentation for that
300
- # if you would like to know more about what's possible with it.
301
- def define_mocks(factory) # yield: define
302
- setup_factory(factory)
303
- yield @factories[factory].mocker
328
+ # irb# Dupe.find :genre
329
+ # Dupe::Database::TableDoesNotExistError: The table ':genre' does not exist.
330
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/database.rb:30:in `select'
331
+ # from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/dupe.rb:295:in `find'
332
+ # from (irb):40
333
+ #
334
+ # irb# Dupe.find_or_create :genre
335
+ # ==> <#Duped::Genre id=1>
336
+ #
337
+ # irb# Dupe.find_or_create :genre
338
+ # ==> <#Duped::Genre id=1>
339
+ #
340
+ # You can also pass conditions to find_or_create as a hash:
341
+ #
342
+ # irb# Dupe.find_or_create :genre, :name => 'Science Fiction', :label => 'sci-fi'
343
+ # ==> <#Duped::Genre label="sci-fi" name="Science Fiction" id=2>
344
+ #
345
+ # irb# Dupe.find_or_create :genre, :name => 'Science Fiction', :label => 'sci-fi'
346
+ # ==> <#Duped::Genre label="sci-fi" name="Science Fiction" id=2>
347
+ def find_or_create(model_name, attributes={})
348
+ results = nil
349
+ if model_exists(model_name)
350
+ results = eval("find(:#{model_name}) #{build_conditions(attributes)}")
351
+ end
352
+
353
+ if !results
354
+ if model_name.singular?
355
+ create model_name, attributes
356
+ else
357
+ stub((rand(5)+1), model_name, :like => attributes)
358
+ end
359
+ elsif results.kind_of?(Array) && results.empty?
360
+ stub((rand(5)+1), model_name, :like => attributes)
361
+ else
362
+ results
363
+ end
364
+ end
365
+
366
+ def models #:nodoc:
367
+ @models ||= {}
304
368
  end
305
369
 
306
-
307
- # Search for a resource. This works a bit differently from both ActiveRecord's find and ActiveResource's find.
308
- # This is most often used for defining associations between objects (Dupe.define).
309
- # It will return a hash representation of the resource (or an array of hashes if we asked for multiple records).
310
- #
311
- # For example, suppose we have an author resource, and a book resource with a nested author attribute (in ActiveRecord
312
- # parlance, Book belongs_to Author, Author has_many Book).
313
- #
314
- # Now suppose we've created the following cucumber scenario:
315
- #
316
- # Scenario: Browsing books
317
- # Given the following author:
318
- # | name | date_of_birth |
319
- # | Arthur C. Clarke | 1917-12-16 |
320
- #
321
- # And the following books:
322
- # | name | author | published | genre |
323
- # | 2001: A Space Odyssey | Arthur C. Clarke | 1968 | sci-fi |
324
- # | A Fall of Moondust | Arthur C. Clarke | 1961 | fantasy |
325
- # | Rendezvous with Rama | Arthur C. Clarke | 1972 | sci-fi |
326
- #
327
- # When....
328
- #
329
- # To link up the book and author, we could create the following book definition
330
- #
331
- # Dupe.define :book do |book|
332
- # book.author {|name| Dupe.find(:author) {|a| a.name == name}}
333
- # end
334
- #
335
- # The line <tt>Dupe.find(:author) {|a| a.name == name}</tt> could be translated as
336
- # "find the first author record where the author's name equals `name`".
337
- #
338
- # Dupe decided to return only a single record because we specified <tt>find(:author)</tt>.
339
- # Had we instead specified <tt>find(:authors)</tt>, Dupe would have instead returned an array of results.
340
- #
341
- # More examples:
342
- #
343
- # # find all books written in the 1960's
344
- # Dupe.find(:books) {|b| b.published >= 1960 and b.published <= 1969}
345
- #
346
- # # return the first book found that was written by Arthur C. Clarke (nested resources example)
347
- # Dupe.find(:book) {|b| b.author.name == 'Arthur C. Clarke'}
348
- #
349
- # # find all sci-fi and fantasy books
350
- # Dupe.find(:books) {|b| b.genre == 'sci-fi' or b.genre == 'fantasy'}
351
- #
352
- # # find all books written by people named 'Arthur'
353
- # Dupe.find(:books) {|b| b.author.name.match /Arthur/}
354
- #
355
- # Also, if you have the need to explicitly specify :all or :first instead of relying on specifying the singular v. plural
356
- # version of your resource name (perhaps the singular and plural version of your resource are exactly the same):
357
- #
358
- # Dupe.find(:all, :deer) {|d| d.type == 'doe'}
359
- # Dupe.find(:first, :deer) {|d| d.name == 'Bambi'}
360
- def find(*args, &block) # yield: record
361
- all_or_first, factory_name = args[-2], args[-1]
362
- match = block ? block : proc {true}
363
- all_or_first = ((factory_name.to_s.plural?) ? :all : :first) unless all_or_first
364
- factory_name = factory_name.to_s.singularize.to_sym
365
- verify_factory_exists factory_name
366
- result = factories[factory_name].find_records_like match
367
- all_or_first == :all ? result : result.first
370
+ def network #:nodoc:
371
+ @network ||= Dupe::Network.new
368
372
  end
369
-
370
- def get_factory(factory) #:nodoc:
371
- setup_factory(factory)
372
- @factories[factory]
373
+
374
+ def database #:nodoc:
375
+ @database ||= Dupe::Database.new
373
376
  end
374
-
375
- def flush(factory=nil, destroy_definitions=false) #:nodoc:
376
- if factory and factories[factory]
377
- factories[factory].flush(destroy_definitions)
378
- else
379
- factories.each {|factory_name, factory| factory.flush(destroy_definitions)}
380
- end
377
+
378
+ # clears out all model definitions and database records / tables.
379
+ def reset
380
+ reset_models
381
+ reset_database
382
+ reset_network
381
383
  end
382
-
383
- def factories #:nodoc:
384
- @factories ||= {}
384
+
385
+ def reset_models
386
+ @models = {}
385
387
  end
386
-
387
- def global_configuration #:nodoc:
388
- @global_configuration ||= Configuration.new
388
+
389
+ def reset_database
390
+ @database = Dupe::Database.new
389
391
  end
390
-
392
+
393
+ def reset_network
394
+ @network = Dupe::Network.new
395
+ end
396
+
397
+ # set to true if you want to see mocked results spit out after each cucumber scenario
398
+ def debug
399
+ @debug ||= false
400
+ end
401
+
402
+
403
+
391
404
  private
405
+ def build_conditions(conditions)
406
+ return '' if conditions.empty?
407
+ select =
408
+ "{|record| " +
409
+ conditions.map do |k,v|
410
+ "record.#{k} == #{v.kind_of?(String) ? "\"#{v}\"" : v}"
411
+ end.join(" && ") + " }"
412
+ end
392
413
 
393
- def setup_factory(factory)
394
- factories[factory] = Dupe.new(factory) unless factories[factory]
414
+ def model_exists(model_name)
415
+ models[model_name.to_s.singularize.to_sym]
395
416
  end
396
-
397
- def reset(factory)
398
- factories[factory].flush if factories[factory]
417
+
418
+ def create_model(model_name)
419
+ models[model_name] = Dupe::Model.new(model_name) unless models[model_name]
399
420
  end
400
-
401
- def verify_factory_exists(factory_name)
402
- raise "Dupe doesn't know about the '#{factory_name}' resource" unless factories[factory_name]
421
+
422
+ def create_and_insert(records, into)
423
+ raise(
424
+ ArgumentError,
425
+ "You must pass a hash containing :into => :model_name " +
426
+ "as the second parameter to create_and_insert."
427
+ ) if !into || !into.kind_of?(Hash) || !into[:into]
428
+
429
+ if records.kind_of?(Array) and
430
+ records.inject(true) {|bool, r| bool and r.kind_of?(Hash)}
431
+ [].tap do |results|
432
+ records.each do |record|
433
+ results << models[into[:into]].create(record).tap {|r| database.insert r}
434
+ end
435
+ end
436
+ elsif records.kind_of?(Hash)
437
+ models[into[:into]].create(records).tap {|r| database.insert r}
438
+ else
439
+ raise ArgumentError, "You must call Dupe.create with either a hash or an array of hashes."
440
+ end
403
441
  end
404
- end
405
-
406
- def flush(destroy_definitions=false) #:nodoc:
407
- @records = []
408
- @sequence = Sequence.new
409
- @attributes = {} if destroy_definitions
410
- ActiveResource::HttpMock.reset_from_dupe!
411
- end
412
-
413
- def stub_services_with(record_template, count=1, starting_value=1) #:nodoc:
414
- records = stub_records(record_template, count, starting_value)
415
- generate_services_for(records, true)
416
- end
417
-
418
- def initialize(factory) #:nodoc:
419
- @factory_name = factory
420
- @attributes = {}
421
- @config = Configuration.new
422
- @mocker = MockServiceResponse.new(@factory_name)
423
- @records = []
424
- end
425
-
426
- def method_missing(method_name, *args, &block) #:nodoc:
427
- args = [nil] if args.empty?
428
- args << block
429
- define_attribute(method_name.to_sym, *args)
430
- end
431
-
432
- def generate_services_for(records, records_already_processed=false) #:nodoc:
433
- records = process_records records unless records_already_processed
434
- @mocker.run_mocks(@records, @config.config[:record_identifiers])
435
- records.length == 1 ? records.first : records
436
- end
437
-
438
- def find_records_like(match) #:nodoc:
439
- @records.select {|r| match.call Record.new(r)}
440
- end
441
-
442
- private
443
- def define_attribute(name, default_value=nil, prock=nil)
444
- @attributes[name] = Attribute.new(name, default_value, prock)
445
- end
446
-
447
- def process_records(records)
448
- records.map {|r| generate_record({:id => sequence}.merge(r))}
449
- end
450
-
451
- def generate_record(overrides={})
452
- define_missing_attributes(overrides.keys)
453
- record = {}
454
- @attributes.each do |attr_key, attr_class|
455
- override_default_value = overrides[attr_key] || overrides[attr_key.to_s]
456
- record[attr_key] = attr_class.value(override_default_value)
442
+
443
+ def create_model_if_definition_parameters_are_valid(args, definition)
444
+ if args.length == 1 and
445
+ args.first.kind_of?(Symbol) and
446
+ definition == nil
447
+
448
+ return args.first, Dupe::Model.new(args.first)
449
+
450
+ elsif args.length == 1 and
451
+ args.first.kind_of?(Symbol) and
452
+ definition.kind_of?(Proc) and
453
+ definition.arity == 1
454
+
455
+ model_name = args.first
456
+ return model_name, Dupe::Model.new(model_name).tap {|m| m.define definition}
457
+
458
+ else
459
+ raise ArgumentError.new(
460
+ "Unknown Dupe.define parameter format. Please consult the API for information on how to use Dupe.define"
461
+ )
462
+ end
457
463
  end
458
- @records << record
459
- record
460
- end
461
-
462
- def sequence
463
- (@sequence ||= Sequence.new).next
464
- end
465
-
466
- def define_missing_attributes(keys)
467
- keys.each {|k| define_attribute(k.to_sym) unless @attributes[k.to_sym]}
468
464
  end
469
-
470
- def stub_records(record_template, count, stub_number)
471
- overrides = record_template.merge({})
472
- overrides.keys.each {|k| overrides[k] = overrides[k].call(stub_number) if overrides[k].respond_to?(:call)}
473
- overrides = {:id => sequence}.merge(overrides) unless overrides[:id]
474
- return [generate_record(overrides)] if count <= 1
475
- [generate_record(overrides)] + stub_records(record_template, count-1, stub_number+1)
476
- end
477
-
478
465
  end