conversational 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. data/.gitignore +8 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.markdown +239 -0
  4. data/Rakefile +38 -0
  5. data/VERSION +1 -0
  6. data/conversational.gemspec +78 -0
  7. data/features/configure_blank_topic.feature +9 -0
  8. data/features/configure_exclusion_conversations.feature +20 -0
  9. data/features/configure_unknown_topic.feature +9 -0
  10. data/features/find_existing_conversation.feature +21 -0
  11. data/features/find_or_create_with.feature +33 -0
  12. data/features/retrieve_conversation_details.feature +13 -0
  13. data/features/step_definitions/conversation_steps.rb +60 -0
  14. data/features/step_definitions/pickle_steps.rb +87 -0
  15. data/features/support/conversation.rb +2 -0
  16. data/features/support/email_spec.rb +1 -0
  17. data/features/support/env.rb +58 -0
  18. data/features/support/mail.rb +6 -0
  19. data/features/support/paths.rb +33 -0
  20. data/features/support/pickle.rb +24 -0
  21. data/features/support/sample_conversation.rb +3 -0
  22. data/lib/conversational/active_record_additions.rb +121 -0
  23. data/lib/conversational/conversation.rb +188 -0
  24. data/lib/conversational/conversation_definition.rb +86 -0
  25. data/lib/conversational.rb +4 -0
  26. data/lib/generators/conversational/migration/USAGE +5 -0
  27. data/lib/generators/conversational/migration/migration_generator.rb +23 -0
  28. data/lib/generators/conversational/migration/templates/migration.rb +14 -0
  29. data/lib/generators/conversational/skeleton/USAGE +6 -0
  30. data/lib/generators/conversational/skeleton/skeleton_generator.rb +17 -0
  31. data/lib/generators/conversational/skeleton/templates/conversation.rb +3 -0
  32. data/spec/active_record_additions_spec.rb +120 -0
  33. data/spec/conversation_definition_spec.rb +248 -0
  34. data/spec/conversation_spec.rb +34 -0
  35. data/spec/spec_helper.rb +24 -0
  36. data/spec/support/conversation.rb +3 -0
  37. metadata +101 -0
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ .bundle
2
+ db/*.sqlite3
3
+ log/*.log
4
+ tmp/**/*
5
+ config/database.yml
6
+ *~
7
+ config/initializers/ebay_config*
8
+ rerun.txt
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2010 David Wilkie
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.markdown ADDED
@@ -0,0 +1,239 @@
1
+ # Conversational
2
+
3
+ Conversational makes it easier to accept incoming text messages (SMS) and respond to them in your application. You could also use Conversational to respond to incoming email or IM.
4
+
5
+ Conversational allows you to have have *stateful* or *stateless* interactions with your users.
6
+
7
+ ## Stateless Conversations
8
+
9
+ This is the most common way of dealing with incoming messages in SMS apps. Stateless conversations don't know anything about a previous request and therefore require you to text in an explicit command. Let's look at an example:
10
+
11
+ Say you have an application which allows you to interact with Facebook over SMS. You want be able to write on on your friends wall and change your status by sending an SMS to your app. You also want to receive SMS alerts when a friend writes on your wall or you have a new friend request.
12
+
13
+ Let's tackle the incoming messages first.
14
+
15
+ ### Incoming Text Messages
16
+
17
+ Say your commands are:
18
+
19
+ * us is ready for a beer
20
+ * wow johnny wanna go 4 a beer?
21
+
22
+ Where "us" is update status and "wow" is write on wall
23
+
24
+ Assuming you have set up a controller in your application to accept the incoming text messages when they are posted from your SMS gateway you could use Conversational as follows:
25
+
26
+ class Conversation
27
+ include Conversational::Conversation
28
+ converse do |with, message|
29
+ OutgoingTextMessage.send(with, message)
30
+ end
31
+ end
32
+
33
+ class UsConversation < Conversation
34
+ def move_along(message)
35
+ # code to update status
36
+ say "Successfully updated your status"
37
+ end
38
+ end
39
+
40
+ class WowConversation < Conversation
41
+ def move_along(message)
42
+ # Code to write on wall
43
+ say "Successfully wrote on wall"
44
+ end
45
+ end
46
+
47
+ class IncomingTextMessageController
48
+ def create
49
+ message = params[:message]
50
+ topic = params[:message].split(" ").first
51
+ number = params[:number]
52
+ Conversation.new(:with => number, :topic => topic).details.move_along(message)
53
+ end
54
+ end
55
+
56
+ There's quite a bit going on here so let's have a bit more of a look.
57
+
58
+ In the controller a new Conversation is created with the number of the incoming message and the topic as the first word of the text message. In our case the topic will be either "us" or "wow". Calling `details` on an instance of Conversation will try and return an instance of a class in your project that subclasses your main Conversation class and has the same name as the topic. In our case our main Conversation class *is* called Conversation so a topic of "us" will map to `UsConversation`. Similarly "wow" maps to `WowConversation`. So say we text in "us is ready for a beer" an instance of `UsConversation` is returned and move_along is then called on the instance.
59
+
60
+ Inside our subclassed conversations there is a method available to us called `say`. Say simple executes the converse block you set up inside your main Conversation class.In our case this will call `OutgoingTextMessage.send` passing in the number and the message. Obviously this example doesn't contain any error checking. If we text in something starting with other than "us" or "wow" we'll get an error because `details` will return nil and `move_along` will be called on `nil`
61
+
62
+ ### Outgoing Text Messages
63
+
64
+ To handle your Facebook alerts you can simply make use of Conversation's `say` method: You might do something like this:
65
+
66
+ class FacebookAlert < Conversation
67
+ def wrote_on_wall(facebook_notification)
68
+ # Code to get the wall details
69
+ say "Someone wrote on your wall..."
70
+ end
71
+
72
+ def friend_request(facebook_notification)
73
+ # Code to get the name of the person who befriended you
74
+ say "You have a new friend request from ..."
75
+ end
76
+ end
77
+
78
+ Then when you get a facebook alert simply do either:
79
+
80
+ FacebookAlert.new(:with => "your number").wrote_on_wall(facebook_notification)
81
+ FacebookAlert.new(:with => "your number").friend_request(facebook_notification)
82
+
83
+ ## Stateful Conversations
84
+
85
+ Conversational also allows you to have *stateful* conversations. A stateful conversation knows about prevous interactions and therefore allows you to respond differently. Note this is currently only supported in Rails.
86
+
87
+ Let's build on the prevous example using stateful conversations.
88
+
89
+ Our application so far is *stateless*. Currently if we get a friend request we have no way of accepting or rejecting it. If we were to continue building a stateless application we would simple add a couple of new commands such as:
90
+
91
+ * "afr &lt;friend&gt;"
92
+ * "rfr &lt;friend&gt;"
93
+ where "afr" is accept friend request and "rfr" is reject friend request.
94
+
95
+ But instead of doing that let's allow us to respond to a friend request notification with yes or no.
96
+
97
+ So we'll change our existing friend request alert to: "You have a new friend request from Johnnie Cash. Do you want to accept? Text yes or no."
98
+
99
+ With the current stateless implementation if they text "yes" then `details` will look to see if `YesConversation` is defined in our project, won't be able to find and return `nil` So here's how to make our application stateful.
100
+
101
+ class Conversation < ActiveRecord::Base
102
+ include Conversational::Conversation
103
+ converse do |with, message|
104
+ OutgoingTextMessage.send(with, message)
105
+ end
106
+ protected
107
+ def finish
108
+ state = "finished"
109
+ self.save!
110
+ end
111
+ end
112
+
113
+ class IncomingTextMessageController
114
+ def create
115
+ message = params[:message]
116
+ topic = params[:message].split(" ").first
117
+ number = params[:number]
118
+ Conversation.find_or_create_with(number, topic).move_along(message)
119
+ end
120
+ end
121
+
122
+ class FacebookAlertConversation < Conversation
123
+ def move_along(message)
124
+ if message == "yes"
125
+ # code to accept friend request
126
+ say "You successfully accepted the friend request"
127
+ elsif message == "no"
128
+ # say "You successfully rejected the friend request"
129
+ else
130
+ say "Invalid response. Reply with yes or no"
131
+ end
132
+ finish
133
+ end
134
+
135
+ def friend_request(facebook_notification)
136
+ # Code to get the name of the person who befriended you
137
+ say "You have a new friend request from ...Do you want to accept? Text yes or no."
138
+ end
139
+ end
140
+
141
+ The first change you'll notice is that our Conversation base class now extends from ActiveRecord::Base. This does a couple of things. But first we will need a migration file (which can be generated with `rails g conversational:migration`). Once we have that we can simply migrate our database `rake db:migrate`. Extending from ActiveRecord::Base adds a couple of methods for us which i'll describe in some more detail later. For our example we only care about one of them.
142
+
143
+ Jumping over to our controller you can see that now we are calling `Conversation.find_or_create_with` This method was added when we extended Conversation from ActiveRecord::Base. The method tries to return the last *recent*, *open* conversation with this number. By *open* we mean that it's state is *not* finished and by *recent* we mean that it was updated within the last 24 hours. More on how you can override this later. If it finds one it will return an instance of this conversation subclass. If it doesn't it will create a new conversation with the topic specified and return it as an instance of its subclass (just like `details`).
144
+
145
+ Now take a look at our `FacebookAlert` class. The first thing is that we renamed it to `FacebookAlertConversation`. This is important so that it is now recognised as a type of conversation.
146
+
147
+ There is also a new method `move_along` which looks at the message to see if the user replied with "yes" or "no" and responds appropriately. Notice it also calls `finish`.
148
+
149
+ If we jump back and take a look at our main Conversation class we see that `finish` marks the conversation state as finished so it will be found by `find_or_create_with` It is important that you remember to call `finish` on all conversations where you don't expect a response.
150
+
151
+ So how does this all tie together?
152
+
153
+ The application receives an alert from Facebook with a new friend request and calls:
154
+
155
+ FacebookAlert.create!(
156
+ :with => "your number", :topic => "facebook_alert"
157
+ ).friend_request(facebook_notification)
158
+
159
+ This sends the following message to you: "You have a new friend request from...Do you want to accept? Text yes or no."
160
+
161
+ Notice that it calls `create!` and *not* `new` as we want to save this conversation. Also notice that topic is set to "facebook_alert" which is the name of the class minus "Conversation". This is important so `find_or_create_with` can find the conversation. Also notice that `friend_request` does *not* call finish. This conversation isn't over yet!
162
+
163
+ Now sometime later you reply with "yes". The controller calls
164
+ `Conversation.find_or_create_with` which finds your open conversation and returns the an instance as a `FacebookAlertConversation`
165
+
166
+ The controller then calls `move_along` on this conversation which looks at your message, sees that you replied with "yes" and replies with "You successfully accepted the friend request". It also calls `finish` which marks the conversation as finished, so that next time you text something in it won't find any open conversations.
167
+
168
+ There are still a few more things we need to do in order to make this application work properly. Right now if we text in something other than "us", "wow" or "yes" we will get an exception. Let's fix it in the following section
169
+
170
+ ## Configuration
171
+
172
+ In our example application we are responding to requests based on the first word of their text message. But what if they text in something unknown to us or they text in something blank?
173
+
174
+ You can configure Conversation to deal with this situation as follows:
175
+
176
+ class Conversation
177
+ unknown_topic_subclass = UnknownTopicNotification
178
+ blank_topic_subclass = BlankTopicNotification
179
+ end
180
+
181
+ class UnknownTopicNotification < Conversation
182
+ def move_along
183
+ say "Sorry. Unknown Command"
184
+ end
185
+ end
186
+
187
+ class BlankTopicNotification < Conversation
188
+ def move_along
189
+ say "Hey. What do you want?"
190
+ end
191
+ end
192
+
193
+ Now when the user texts in with "hey jonnie", `details` will try and find a conversation defined as `HeyConversation`, won't be able to find it and will return an instance of `UnknownTopicConversation`
194
+
195
+ `move_along` then causes a message to be sent saying "Sorry. Unknown Command."
196
+
197
+ The same thing happens for a blank conversation.
198
+
199
+ There is one more subtle issue with our application. What if a user texts in "facebook_alert"? The reply will be: "Invalid response. Reply with yes or no" when it should be "Sorry. Unknown Command". This is because if `find_or_create_with` cannot find an existing conversation it will try and create one with the topic "facebook_alert" if `FacebookAlertConversation` is defined in our application (which it is). To solve this prolem we can use `exclude`
200
+
201
+ class Conversation
202
+ exclude FacebookAlertConversation
203
+ end
204
+
205
+ ## Overriding Defaults
206
+
207
+ When you extend your base conversation class from ActiveRecord::Base in addition to `find_or_create_with` you will also get the following class methods:
208
+
209
+ `converser("someone")` returns all conversations with "someone"
210
+
211
+ `in_progress` returns all conversations that are not "finished".
212
+
213
+ `recent(time)` returns all conversations in the last *time* or within the last 24 if *time* is not suppied
214
+
215
+ `with("someone")` a convienience method for `converse("someone").in_progress.recent`
216
+
217
+
218
+ ## Installation
219
+
220
+ Add the following to your Gemfile: `gem "conversational"`
221
+
222
+ ## Setup
223
+
224
+ `rails g conversational:skeleton`
225
+ Generates a base conversation class under app/conversations
226
+
227
+ `rails g conversational:migration`
228
+ Generates a migration file if you want to use Conversational with Rails
229
+
230
+ ## More Examples
231
+
232
+ Here's an [example](http://github.com/dwilkie/drinking) *stateful* conversation app about drinking
233
+
234
+ ## Notes
235
+
236
+ Conversational is compatible with Rails 3
237
+ To use with Rails 2.3.x you must install Conversation version 0.1.0 which is an old and obsolete version.
238
+
239
+ Copyright (c) 2010 David Wilkie, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+
5
+ desc 'Default: run unit tests.'
6
+ task :default => :test
7
+
8
+ desc 'Test the conversational plugin.'
9
+ Rake::TestTask.new(:test) do |t|
10
+ t.libs << 'lib'
11
+ t.libs << 'spec'
12
+ t.libs << 'features'
13
+ t.pattern = 'spec/**/*_spec.rb'
14
+ t.verbose = true
15
+ end
16
+
17
+ begin
18
+ require 'jeweler'
19
+ Jeweler::Tasks.new do |gemspec|
20
+ gemspec.name = "conversational"
21
+ gemspec.summary = "Have conversations with your users over SMS"
22
+ gemspec.email = "dwilkie@gmail.com"
23
+ gemspec.homepage = "http://github.com/dwilkie/conversational"
24
+ gemspec.authors = ["David Wilkie"]
25
+ end
26
+ rescue LoadError
27
+ puts "Jeweler not available. Install it with: gem install jeweler"
28
+ end
29
+
30
+ desc 'Generate documentation for the conversational plugin.'
31
+ Rake::RDocTask.new(:rdoc) do |rdoc|
32
+ rdoc.rdoc_dir = 'rdoc'
33
+ rdoc.title = 'Conversational'
34
+ rdoc.options << '--line-numbers' << '--inline-source'
35
+ rdoc.rdoc_files.include('README')
36
+ rdoc.rdoc_files.include('lib/**/*.rb')
37
+ end
38
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.3.0
@@ -0,0 +1,78 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{conversational}
8
+ s.version = "0.3.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["David Wilkie"]
12
+ s.date = %q{2010-06-01}
13
+ s.email = %q{dwilkie@gmail.com}
14
+ s.extra_rdoc_files = [
15
+ "README.markdown"
16
+ ]
17
+ s.files = [
18
+ ".gitignore",
19
+ "MIT-LICENSE",
20
+ "README.markdown",
21
+ "Rakefile",
22
+ "VERSION",
23
+ "conversational.gemspec",
24
+ "features/configure_blank_topic.feature",
25
+ "features/configure_exclusion_conversations.feature",
26
+ "features/configure_unknown_topic.feature",
27
+ "features/find_existing_conversation.feature",
28
+ "features/find_or_create_with.feature",
29
+ "features/retrieve_conversation_details.feature",
30
+ "features/step_definitions/conversation_steps.rb",
31
+ "features/step_definitions/pickle_steps.rb",
32
+ "features/support/conversation.rb",
33
+ "features/support/email_spec.rb",
34
+ "features/support/env.rb",
35
+ "features/support/mail.rb",
36
+ "features/support/paths.rb",
37
+ "features/support/pickle.rb",
38
+ "features/support/sample_conversation.rb",
39
+ "lib/conversational.rb",
40
+ "lib/conversational/active_record_additions.rb",
41
+ "lib/conversational/conversation.rb",
42
+ "lib/conversational/conversation_definition.rb",
43
+ "lib/generators/conversational/migration/USAGE",
44
+ "lib/generators/conversational/migration/migration_generator.rb",
45
+ "lib/generators/conversational/migration/templates/migration.rb",
46
+ "lib/generators/conversational/skeleton/USAGE",
47
+ "lib/generators/conversational/skeleton/skeleton_generator.rb",
48
+ "lib/generators/conversational/skeleton/templates/conversation.rb",
49
+ "spec/active_record_additions_spec.rb",
50
+ "spec/conversation_definition_spec.rb",
51
+ "spec/conversation_spec.rb",
52
+ "spec/spec_helper.rb",
53
+ "spec/support/conversation.rb"
54
+ ]
55
+ s.homepage = %q{http://github.com/dwilkie/conversational}
56
+ s.rdoc_options = ["--charset=UTF-8"]
57
+ s.require_paths = ["lib"]
58
+ s.rubygems_version = %q{1.3.6}
59
+ s.summary = %q{Have conversations with your users over SMS}
60
+ s.test_files = [
61
+ "spec/conversation_spec.rb",
62
+ "spec/active_record_additions_spec.rb",
63
+ "spec/conversation_definition_spec.rb",
64
+ "spec/spec_helper.rb",
65
+ "spec/support/conversation.rb"
66
+ ]
67
+
68
+ if s.respond_to? :specification_version then
69
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
70
+ s.specification_version = 3
71
+
72
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
73
+ else
74
+ end
75
+ else
76
+ end
77
+ end
78
+
@@ -0,0 +1,9 @@
1
+ Feature: Specify a blank conversation topic template
2
+ In order to converse when the topic is blank
3
+ As a Conversation user
4
+ I want to be able to specify a conversation template to use when the topic is blank
5
+
6
+ Scenario: Specify a blank conversation topic
7
+ Given I configured Conversation with the following: Conversation.blank_topic_subclass = SampleConversation
8
+ When I start up a conversation with an blank topic
9
+ Then the conversation details should be a SampleConversation
@@ -0,0 +1,20 @@
1
+ Feature: Configure exclusion conversations
2
+ In order to allow conversations to exist in a project that should not be found
3
+ when using Conversation.details or Conversation.find_or_create_with
4
+ As a Conversation user
5
+ I want to be able to configure which conversations should be excluded
6
+
7
+ Scenario Outline: Configure exclusion conversations
8
+ Given I configured Conversation with the following: <configuration>
9
+ And a conversation exists with topic: "sample", with: "someone@example.com"
10
+ Then the conversation details should be nil
11
+
12
+ Examples:
13
+ | configuration |
14
+ | Conversation.exclude [SampleConversation] |
15
+ | Conversation.exclude SampleConversation |
16
+ | Conversation.exclude "sample_conversation" |
17
+ | Conversation.exclude "SampleConversation" |
18
+ | Conversation.exclude :sample_conversation |
19
+ | Conversation.exclude /sample/i |
20
+ | Conversation.exclude [/sample/, "sample_conversation", SampleConversation] |
@@ -0,0 +1,9 @@
1
+ Feature: Specify an unknown conversation topic template
2
+ In order to converse when the topic is unknown
3
+ As a Conversation user
4
+ I want to be able to specify a conversation template to use when the topic is unknown
5
+
6
+ Scenario: Specify an unknown conversation topic
7
+ Given I configured Conversation with the following: Conversation.unknown_topic_subclass = SampleConversation
8
+ When I start up a conversation with an unknown topic
9
+ Then the conversation details should be a SampleConversation
@@ -0,0 +1,21 @@
1
+ Feature: Find existing conversations
2
+ In order to find an active conversation with a user
3
+ As a Conversation user
4
+ I want to be able to find the conversation easily
5
+
6
+ Background:
7
+ Given a conversation exists with topic: "sample", with: "someone@example.com"
8
+
9
+ Scenario: Find an open conversation updated within the last 24 hours
10
+ Given 23 hours have elapsed since the conversation was last updated
11
+ Then I should be able to find a conversation with: "someone@example.com"
12
+
13
+ Scenario: Find an open conversation updated more than that 24 hours
14
+ Given 24 hours have elapsed since the conversation was last updated
15
+ Then I should not be able to find a conversation with: "someone@example.com"
16
+
17
+ Scenario: Find a finished conversation
18
+ Given 1 hour has elapsed since the conversation was last updated
19
+ And the conversation is finished
20
+ Then I should not be able to find a conversation with: "someone@example.com"
21
+
@@ -0,0 +1,33 @@
1
+ Feature: Find existing conversation or create a new one
2
+ In order to easily create a new conversation if an existing one cannot be found
3
+ As a Conversation user
4
+ I want to be able to call find_or_create_with supplying who with and the topic
5
+
6
+ Scenario: No conversations exist
7
+ Given no conversations exist with: "someone"
8
+ When I call find_or_create_with("someone", "sample")
9
+ Then a conversation should exist with topic: "sample", with: "someone"
10
+
11
+ Scenario: A recent conversation exists
12
+ Given a conversation exists with topic: "sample", with: "someone"
13
+ When I call find_or_create_with("someone", "sample")
14
+ Then 1 conversations should exist
15
+
16
+ Scenario: An old conversation exists
17
+ Given a conversation exists with topic: "sample", with: "someone"
18
+ And 24 hours have elapsed since the conversation was last updated
19
+ When I call find_or_create_with("someone", "sample")
20
+ Then 2 conversations should exist
21
+
22
+ Scenario: An finished conversation exists
23
+ Given a conversation exists with topic: "sample", with: "someone"
24
+ And the conversation is finished
25
+ When I call find_or_create_with("someone", "sample")
26
+ Then 2 conversations should exist
27
+
28
+ Scenario: No conversation exists and the subclass has been excluded
29
+ Given a conversation exists with topic: "sample", with: "someone"
30
+ And I configured Conversation with the following: Conversation.exclude [SampleConversation]
31
+ When I call find_or_create_with("someone", "sample")
32
+ Then 1 conversations should exist
33
+
@@ -0,0 +1,13 @@
1
+ Feature: Get the conversation specifics
2
+ In order to get the specific conversation based on the topic and then move it along
3
+ As a Conversation user
4
+ I want to be able to get the specific conversation from the general one based off the topic
5
+
6
+ Scenario: Get the details from a conversation instance
7
+ Given a conversation exists with topic: "sample", with: "someone@example.com"
8
+ Then the conversation details should be a SampleConversation
9
+
10
+ Scenario: Get the details from a conversation instance
11
+ Given a conversation exists with topic: "sample", with: "someone@example.com"
12
+ And I configured Conversation with the following: Conversation.exclude [SampleConversation]
13
+ Then conversation.details(:include_all => true) should be a SampleConversation
@@ -0,0 +1,60 @@
1
+ Given /^(an|\d+) hours? (?:has|have) elapsed since #{capture_model} was last updated$/ do |time, conversation|
2
+ conversation = model!(conversation)
3
+ time = parse_email_count(time)
4
+ Conversation.record_timestamps = false
5
+ conversation.updated_at = time.hours.ago
6
+ conversation.save!
7
+ Conversation.record_timestamps = true
8
+ end
9
+
10
+ Given /^#{capture_model} is (.+)$/ do |conversation, state|
11
+ conversation = model!(conversation)
12
+ conversation.state = state
13
+ conversation.save!
14
+ end
15
+
16
+ Given /^no conversations exist with: "([^\"]*)"/ do |with|
17
+ find_models("conversation", "with: \"#{with}\"").each do |instance|
18
+ instance.destroy
19
+ end
20
+ end
21
+
22
+ Given /^I configured Conversation with the following: (.+)$/ do |configuration|
23
+ instance_eval(configuration)
24
+ end
25
+
26
+ When /^I start a new conversation(?: with #{capture_fields})?$/ do |fields|
27
+
28
+ end
29
+
30
+ When /^I call find_or_create_with\("([^\"]*)", "([^\"]*)"\)$/ do |with, topic|
31
+ Conversation.find_or_create_with(with, topic)
32
+ end
33
+
34
+ When /^I start up a conversation with an? (blank|unknown) topic$/ do |template_type|
35
+ topic = ""
36
+ topic = "unknown" if template_type == "unknown"
37
+ Given "a conversation exists with topic: \"#{topic}\", with: \"someone\""
38
+ end
39
+
40
+ Then /^the conversation details should be a (\w+)$/ do |template_name|
41
+ model!("conversation").details.class.should == template_name.constantize
42
+ end
43
+
44
+ Then /^I should (not )?be able to find a conversation with: "([^\"]*)"$/ do |negative, with|
45
+ conversation = Conversation.with(with).last
46
+ unless negative
47
+ conversation.should == model!("conversation")
48
+ else
49
+ conversation.should be_nil
50
+ end
51
+ end
52
+
53
+ Then /^#{capture_model} details should (?:be|have) (?:an? )?#{capture_predicate}$/ do |name, predicate|
54
+ model!(name).details.should send("be_#{predicate.gsub(' ', '_')}")
55
+ end
56
+
57
+ Then /^conversation\.details\(:include_all => true\) should be a SampleConversation$/ do
58
+ model!("conversation").details(:include_all => true).class.should == SampleConversation
59
+ end
60
+
@@ -0,0 +1,87 @@
1
+ # this file generated by script/generate pickle
2
+
3
+ # create a model
4
+ Given(/^#{capture_model} exists?(?: with #{capture_fields})?$/) do |name, fields|
5
+ create_model(name, fields)
6
+ end
7
+
8
+ # create n models
9
+ Given(/^(\d+) #{capture_plural_factory} exist(?: with #{capture_fields})?$/) do |count, plural_factory, fields|
10
+ count.to_i.times { create_model(plural_factory.singularize, fields) }
11
+ end
12
+
13
+ # create models from a table
14
+ Given(/^the following #{capture_plural_factory} exists?:?$/) do |plural_factory, table|
15
+ create_models_from_table(plural_factory, table)
16
+ end
17
+
18
+ # find a model
19
+ Then(/^#{capture_model} should exist(?: with #{capture_fields})?$/) do |name, fields|
20
+ find_model!(name, fields)
21
+ end
22
+
23
+ # not find a model
24
+ Then(/^#{capture_model} should not exist(?: with #{capture_fields})?$/) do |name, fields|
25
+ find_model(name, fields).should be_nil
26
+ end
27
+
28
+ # find models with a table
29
+ Then(/^the following #{capture_plural_factory} should exists?:?$/) do |plural_factory, table|
30
+ find_models_from_table(plural_factory, table).should_not be_any(&:nil?)
31
+ end
32
+
33
+ # find exactly n models
34
+ Then(/^(\d+) #{capture_plural_factory} should exist(?: with #{capture_fields})?$/) do |count, plural_factory, fields|
35
+ find_models(plural_factory.singularize, fields).size.should == count.to_i
36
+ end
37
+
38
+ # assert equality of models
39
+ Then(/^#{capture_model} should be #{capture_model}$/) do |a, b|
40
+ model!(a).should == model!(b)
41
+ end
42
+
43
+ # assert model is in another model's has_many assoc
44
+ Then(/^#{capture_model} should be (?:in|one of|amongst) #{capture_model}(?:'s)? (\w+)$/) do |target, owner, association|
45
+ model!(owner).send(association).should include(model!(target))
46
+ end
47
+
48
+ # assert model is not in another model's has_many assoc
49
+ Then(/^#{capture_model} should not be (?:in|one of|amongst) #{capture_model}(?:'s)? (\w+)$/) do |target, owner, association|
50
+ model!(owner).send(association).should_not include(model!(target))
51
+ end
52
+
53
+ # assert model is another model's has_one/belongs_to assoc
54
+ Then(/^#{capture_model} should be #{capture_model}(?:'s)? (\w+)$/) do |target, owner, association|
55
+ model!(owner).send(association).should == model!(target)
56
+ end
57
+
58
+ # assert model is not another model's has_one/belongs_to assoc
59
+ Then(/^#{capture_model} should not be #{capture_model}(?:'s)? (\w+)$/) do |target, owner, association|
60
+ model!(owner).send(association).should_not == model!(target)
61
+ end
62
+
63
+ # assert model.predicate?
64
+ Then(/^#{capture_model} should (?:be|have) (?:an? )?#{capture_predicate}$/) do |name, predicate|
65
+ model!(name).should send("be_#{predicate.gsub(' ', '_')}")
66
+ end
67
+
68
+ # assert not model.predicate?
69
+ Then(/^#{capture_model} should not (?:be|have) (?:an? )?#{capture_predicate}$/) do |name, predicate|
70
+ model!(name).should_not send("be_#{predicate.gsub(' ', '_')}")
71
+ end
72
+
73
+ # model.attribute.should eql(value)
74
+ # model.attribute.should_not eql(value)
75
+ Then(/^#{capture_model}'s (\w+) (should(?: not)?) be #{capture_value}$/) do |name, attribute, expectation, expected|
76
+ actual_value = model(name).send(attribute)
77
+ expectation = expectation.gsub(' ', '_')
78
+
79
+ case expected
80
+ when 'nil', 'true', 'false'
81
+ actual_value.send(expectation, send("be_#{expected}"))
82
+ when /^-?[0-9_]+$/
83
+ actual_value.send(expectation, eql(expected.to_i))
84
+ else
85
+ actual_value.to_s.send(expectation, eql(expected))
86
+ end
87
+ end
@@ -0,0 +1,2 @@
1
+ require File.dirname(__FILE__) + '/../../spec/support/conversation'
2
+
@@ -0,0 +1 @@
1
+ require 'email_spec/cucumber'