cicloid-conversational 0.3.2.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +21 -0
- data/README.markdown +240 -0
- data/Rakefile +2 -0
- data/conversational.gemspec +22 -0
- data/features/configure_blank_topic.feature +9 -0
- data/features/configure_exclusion_conversations.feature +20 -0
- data/features/configure_unknown_topic.feature +9 -0
- data/features/find_existing_conversation.feature +21 -0
- data/features/find_or_create_with.feature +33 -0
- data/features/retrieve_conversation_details.feature +13 -0
- data/features/step_definitions/conversation_steps.rb +60 -0
- data/features/step_definitions/pickle_steps.rb +87 -0
- data/features/support/conversation.rb +2 -0
- data/features/support/email_spec.rb +1 -0
- data/features/support/env.rb +58 -0
- data/features/support/mail.rb +6 -0
- data/features/support/paths.rb +33 -0
- data/features/support/pickle.rb +24 -0
- data/features/support/sample_conversation.rb +3 -0
- data/lib/conversational.rb +3 -0
- data/lib/conversational/active_record_additions.rb +122 -0
- data/lib/conversational/conversation.rb +285 -0
- data/lib/conversational/version.rb +4 -0
- data/lib/generators/conversational/migration/USAGE +5 -0
- data/lib/generators/conversational/migration/migration_generator.rb +23 -0
- data/lib/generators/conversational/migration/templates/migration.rb +14 -0
- data/lib/generators/conversational/skeleton/USAGE +6 -0
- data/lib/generators/conversational/skeleton/skeleton_generator.rb +17 -0
- data/lib/generators/conversational/skeleton/templates/conversation.rb +3 -0
- data/spec/active_record_additions_spec.rb +120 -0
- data/spec/conversation_definition_spec.rb +248 -0
- data/spec/conversation_spec.rb +34 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/conversation.rb +3 -0
- metadata +113 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/migration'
|
3
|
+
module Conversational
|
4
|
+
class MigrationGenerator < Rails::Generators::Base
|
5
|
+
include Rails::Generators::Migration
|
6
|
+
|
7
|
+
def self.source_root
|
8
|
+
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.next_migration_number(dirname)
|
12
|
+
if ActiveRecord::Base.timestamped_migrations
|
13
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
14
|
+
else
|
15
|
+
"%.3d" % (current_migration_number(dirname) + 1)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_migration_file
|
20
|
+
migration_template 'migration.rb', 'db/migrate/create_conversations.rb'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateConversations < ActiveRecord::Migration
|
2
|
+
def self.up
|
3
|
+
create_table :conversations do |t|
|
4
|
+
t.string :with, :null => false
|
5
|
+
t.string :state
|
6
|
+
t.string :topic
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.down
|
12
|
+
drop_table :conversations
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
module Conversational
|
3
|
+
class SkeletonGenerator < Rails::Generators::Base
|
4
|
+
|
5
|
+
def self.source_root
|
6
|
+
@source_root ||= File.join(File.dirname(__FILE__), 'templates')
|
7
|
+
end
|
8
|
+
|
9
|
+
def create_conversations_directory
|
10
|
+
empty_directory "app/conversations"
|
11
|
+
end
|
12
|
+
|
13
|
+
def copy_conversation_file
|
14
|
+
copy_file "conversation.rb", "app/conversations/conversation.rb"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# See support/conversation.rb
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Conversational::ActiveRecordAdditions do
|
6
|
+
let(:valid_attributes) { {:with => "someone"} }
|
7
|
+
describe "scopes" do
|
8
|
+
let!(:conversation) { Conversation.create!(valid_attributes) }
|
9
|
+
describe ".converser" do
|
10
|
+
it "should find the conversation with someone" do
|
11
|
+
Conversation.converser("someone").last.should == conversation
|
12
|
+
end
|
13
|
+
end
|
14
|
+
describe ".recent" do
|
15
|
+
context "the conversation was updated less than 24 hours ago" do
|
16
|
+
context "passing no arguments" do
|
17
|
+
it "should find the conversation" do
|
18
|
+
Conversation.recent.last.should == conversation
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
context "the conversation was updated more than 24 hours ago" do
|
23
|
+
before {
|
24
|
+
Conversation.record_timestamps = false
|
25
|
+
conversation.updated_at = 24.hours.ago
|
26
|
+
conversation.save!
|
27
|
+
Conversation.record_timestamps = true
|
28
|
+
}
|
29
|
+
context "passing no arguments" do
|
30
|
+
it "should not find the conversation" do
|
31
|
+
Conversation.recent.last.should be_nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
context "passing an argument" do
|
35
|
+
it "should find the conversation" do
|
36
|
+
Conversation.recent(25.hours.ago).last.should == conversation
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
describe ".in_progress" do
|
42
|
+
context "conversation is in progress" do
|
43
|
+
it "should find the conversation" do
|
44
|
+
Conversation.in_progress.last.should == conversation
|
45
|
+
end
|
46
|
+
end
|
47
|
+
context "conversation is finished" do
|
48
|
+
before {
|
49
|
+
conversation.state = "finished"
|
50
|
+
conversation.save!
|
51
|
+
}
|
52
|
+
it "should not find the conversation" do
|
53
|
+
Conversation.in_progress.last.should be_nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
describe ".with" do
|
58
|
+
context "conversation is in progress and recent" do
|
59
|
+
it "should find the conversation" do
|
60
|
+
Conversation.with("someone").last.should == conversation
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe ".find_or_create_with" do
|
67
|
+
context "when no existing conversation exists with 'someone'" do
|
68
|
+
context "but a subclass for this topic exists and has not been excluded" do
|
69
|
+
let(:subclass) { mock("Subclass") }
|
70
|
+
before {
|
71
|
+
Conversational::ConversationDefinition.stub!(
|
72
|
+
:find_subclass_by_topic
|
73
|
+
).with("something").and_return(subclass)
|
74
|
+
}
|
75
|
+
it "should create an instance of the subclass" do
|
76
|
+
subclass.should_receive(:create!).with(
|
77
|
+
:with => "someone", :topic => "something"
|
78
|
+
)
|
79
|
+
Conversation.find_or_create_with("someone", "something")
|
80
|
+
end
|
81
|
+
end
|
82
|
+
context "and a subclass for this topic also does not exist" do
|
83
|
+
before {
|
84
|
+
Conversational::ConversationDefinition.stub!(
|
85
|
+
:find_subclass_by_topic
|
86
|
+
).and_return(nil)
|
87
|
+
}
|
88
|
+
context "and the topic is blank" do
|
89
|
+
it "should raise an error" do
|
90
|
+
lambda {
|
91
|
+
Conversation.find_or_create_with("someone", nil)
|
92
|
+
}.should raise_error(/not defined a blank_topic_subclass/)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
context "though the topic is not blank" do
|
96
|
+
it "should still raise an error" do
|
97
|
+
lambda {
|
98
|
+
Conversation.find_or_create_with(
|
99
|
+
"someone", "something"
|
100
|
+
)
|
101
|
+
}.should raise_error(/not defined SomethingConversation/)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
context "a conversation already exists" do
|
107
|
+
let(:conversation) { Conversation.new(valid_attributes) }
|
108
|
+
let(:defined_conversation) { mock("defined_conversation") }
|
109
|
+
before {
|
110
|
+
Conversation.stub_chain(:with, :last).and_return(conversation)
|
111
|
+
}
|
112
|
+
it "should return the conversation as an instance of the subclass" do
|
113
|
+
conversation.should_receive(:details).and_return(defined_conversation)
|
114
|
+
Conversation.find_or_create_with(
|
115
|
+
"someone", "something"
|
116
|
+
).should == defined_conversation
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Conversational::ConversationDefinition do
|
4
|
+
def define_conversation(params=nil)
|
5
|
+
defined_conversation = {}
|
6
|
+
unless params && (params[:blank] || params[:unknown])
|
7
|
+
defined_conversation[:class] = mock("DefinedConversation")
|
8
|
+
defined_conversation[:instance] = mock("defined_conversation")
|
9
|
+
topic = "defined"
|
10
|
+
topic.stub!(:+).and_return(topic)
|
11
|
+
topic.stub!(:classify).and_return(topic)
|
12
|
+
topic.stub!(:constantize).and_return(defined_conversation[:class])
|
13
|
+
defined_conversation[:topic] = topic
|
14
|
+
defined_conversation[:class].stub!(:<=).and_return(true)
|
15
|
+
else
|
16
|
+
if params[:unknown]
|
17
|
+
defined_conversation[:class] = mock("UnknownConversation")
|
18
|
+
defined_conversation[:instance] = mock("unknown_conversation")
|
19
|
+
Conversational::ConversationDefinition.unknown_topic_subclass = defined_conversation[:class]
|
20
|
+
end
|
21
|
+
if params[:blank]
|
22
|
+
defined_conversation[:class] = mock("BlankConversation")
|
23
|
+
defined_conversation[:instance] = mock("blank_conversation")
|
24
|
+
Conversational::ConversationDefinition.blank_topic_subclass = defined_conversation[:class]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
defined_conversation
|
28
|
+
end
|
29
|
+
|
30
|
+
describe ".exclude" do
|
31
|
+
it "should accept a string" do
|
32
|
+
lambda {
|
33
|
+
Conversational::ConversationDefinition.exclude "something"
|
34
|
+
}.should_not raise_error
|
35
|
+
end
|
36
|
+
it "should accept a symbol" do
|
37
|
+
lambda {
|
38
|
+
Conversational::ConversationDefinition.exclude :defined_conversation
|
39
|
+
}.should_not raise_error
|
40
|
+
end
|
41
|
+
it "should accept a regex" do
|
42
|
+
lambda {
|
43
|
+
Conversational::ConversationDefinition.exclude /something/i
|
44
|
+
}.should_not raise_error
|
45
|
+
end
|
46
|
+
it "should accept a Class" do
|
47
|
+
some_class = mock("SomeClass", :superclass => Class)
|
48
|
+
some_class.stub!(:is_a?).with(Class).and_return(true)
|
49
|
+
lambda {
|
50
|
+
Conversational::ConversationDefinition.exclude some_class
|
51
|
+
}.should_not raise_error
|
52
|
+
end
|
53
|
+
it "should accept an Array where the elements are a Class, String, Symbol or Regexp" do
|
54
|
+
some_class = mock("SomeClass", :superclass => Class)
|
55
|
+
some_class.stub!(:is_a?).with(Class).and_return(true)
|
56
|
+
lambda {
|
57
|
+
Conversational::ConversationDefinition.exclude ["Something", some_class, /something/i, :something]
|
58
|
+
}.should_not raise_error
|
59
|
+
end
|
60
|
+
it "should accept nil" do
|
61
|
+
lambda {
|
62
|
+
Conversational::ConversationDefinition.exclude nil
|
63
|
+
}.should_not raise_error
|
64
|
+
end
|
65
|
+
it "should not accept anything else" do
|
66
|
+
some_class = mock("SomeClass")
|
67
|
+
lambda {
|
68
|
+
Conversational::ConversationDefinition.exclude some_class
|
69
|
+
}.should raise_error(/You specified a /)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe ".find_subclass_by_topic" do
|
74
|
+
context "a conversation definition with this topic has been defined" do
|
75
|
+
let(:defined_conversation) { define_conversation }
|
76
|
+
context "and it subclasses conversation" do
|
77
|
+
before {
|
78
|
+
defined_conversation[:class].stub!(:<=).and_return(true)
|
79
|
+
}
|
80
|
+
context "and it has not been excluded" do
|
81
|
+
before {
|
82
|
+
Conversational::ConversationDefinition.exclude nil
|
83
|
+
}
|
84
|
+
it "should return the conversation definition class" do
|
85
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
86
|
+
defined_conversation[:topic]
|
87
|
+
).should == defined_conversation[:class]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
context "but it has been excluded" do
|
91
|
+
before {
|
92
|
+
Conversational::ConversationDefinition.stub!(:exclude?).and_return(true)
|
93
|
+
}
|
94
|
+
context "and it has not been explicitly included" do
|
95
|
+
context "and an unknown topic subclass has been defined" do
|
96
|
+
let!(:unknown_conversation) { define_conversation(:unknown => true) }
|
97
|
+
it "should return the unknown conversation subclass" do
|
98
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
99
|
+
defined_conversation[:topic]
|
100
|
+
).should == unknown_conversation[:class]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
context "and an unknown topic subclass has not been defined" do
|
104
|
+
before {
|
105
|
+
Conversational::ConversationDefinition.unknown_topic_subclass = nil
|
106
|
+
}
|
107
|
+
it "should return nil" do
|
108
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
109
|
+
defined_conversation[:topic]
|
110
|
+
).should be_nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
context "but it was explicitly included for this query" do
|
115
|
+
it "should return the conversation definition class" do
|
116
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
117
|
+
defined_conversation[:topic], :include_all => true
|
118
|
+
).should == defined_conversation[:class]
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
context "and an unknown topic subclass has not been defined" do
|
123
|
+
before {
|
124
|
+
Conversational::ConversationDefinition.unknown_topic_subclass = nil
|
125
|
+
}
|
126
|
+
context "and the conversation has been excluded" do
|
127
|
+
context "by setting Conversation.exclude 'defined_conversation'" do
|
128
|
+
before {
|
129
|
+
excluded_class = "defined_conversation"
|
130
|
+
excluded_class.stub_chain(
|
131
|
+
:classify,
|
132
|
+
:constantize).and_return(
|
133
|
+
defined_conversation[:class]
|
134
|
+
)
|
135
|
+
Conversational::ConversationDefinition.exclude excluded_class
|
136
|
+
}
|
137
|
+
it "should return nil" do
|
138
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
139
|
+
defined_conversation[:topic]
|
140
|
+
).should be_nil
|
141
|
+
end
|
142
|
+
end
|
143
|
+
context "by setting Conversation.exclude DefinedConversation" do
|
144
|
+
before {
|
145
|
+
excluded_class = defined_conversation[:class]
|
146
|
+
defined_conversation[:class].stub!(:superclass).and_return(Class)
|
147
|
+
defined_conversation[:class].stub!(:is_a?).with(
|
148
|
+
Class
|
149
|
+
).and_return(true)
|
150
|
+
Conversational::ConversationDefinition.exclude(
|
151
|
+
defined_conversation[:class]
|
152
|
+
)
|
153
|
+
}
|
154
|
+
it "should return nil" do
|
155
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
156
|
+
defined_conversation[:topic]
|
157
|
+
).should be_nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
context "by setting Conversation.exclude /defined/i" do
|
161
|
+
before {
|
162
|
+
Conversational::ConversationDefinition.exclude /defined/i
|
163
|
+
}
|
164
|
+
it "should return nil" do
|
165
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
166
|
+
defined_conversation[:topic]
|
167
|
+
).should be_nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
context "by setting Conversation.exclude [/defined/i]" do
|
171
|
+
before {
|
172
|
+
Conversational::ConversationDefinition.exclude [/defined/i]
|
173
|
+
}
|
174
|
+
it "should return nil" do
|
175
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
176
|
+
defined_conversation[:topic]
|
177
|
+
).should be_nil
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
context "but it is not a type of conversation" do
|
184
|
+
before {
|
185
|
+
defined_conversation[:class].stub!(:<=).and_return(false)
|
186
|
+
}
|
187
|
+
context "but an unknown topic subclass has been defined" do
|
188
|
+
let!(:unknown_conversation) { define_conversation(:unknown => true) }
|
189
|
+
it "should return the unknown conversation subclass" do
|
190
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
191
|
+
defined_conversation[:topic]
|
192
|
+
).should == unknown_conversation[:class]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
context "and an unknown topic subclass has not been defined" do
|
196
|
+
before {
|
197
|
+
Conversational::ConversationDefinition.unknown_topic_subclass = nil
|
198
|
+
}
|
199
|
+
it "should return nil" do
|
200
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
201
|
+
defined_conversation[:topic]
|
202
|
+
).should be_nil
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
context "and a conversation definition with this topic has not been defined" do
|
208
|
+
context "but an unknown conversation definition has been defined" do
|
209
|
+
let!(:unknown_conversation) { define_conversation(:unknown => true) }
|
210
|
+
it "should return the unknown conversation subclass" do
|
211
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
212
|
+
"something"
|
213
|
+
).should == unknown_conversation[:class]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
context "and an unknown conversation definition has not been defined" do
|
217
|
+
before {
|
218
|
+
Conversational::ConversationDefinition.unknown_topic_subclass = nil
|
219
|
+
}
|
220
|
+
it "should return nil" do
|
221
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
222
|
+
"someone"
|
223
|
+
).should be_nil
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
context "and the topic for conversation is blank" do
|
228
|
+
context "but a blank conversation definition has been defined" do
|
229
|
+
let!(:blank_conversation) { define_conversation(:blank => true) }
|
230
|
+
it "should return the blank conversation subclass" do
|
231
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
232
|
+
nil
|
233
|
+
).should == blank_conversation[:class]
|
234
|
+
end
|
235
|
+
end
|
236
|
+
context "and a blank conversation definition has not been defined" do
|
237
|
+
before(:each) do
|
238
|
+
Conversational::ConversationDefinition.blank_topic_subclass = nil
|
239
|
+
end
|
240
|
+
it "should return nil" do
|
241
|
+
Conversational::ConversationDefinition.find_subclass_by_topic(
|
242
|
+
nil
|
243
|
+
).should be_nil
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|