dupe 0.3.7 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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