conversational 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +4 -7
  2. data/Gemfile +4 -0
  3. data/Gemfile.lock +32 -0
  4. data/README.markdown +23 -177
  5. data/Rakefile +6 -35
  6. data/conversational.gemspec +19 -69
  7. data/lib/conversational/conversation.rb +177 -98
  8. data/lib/conversational/version.rb +4 -0
  9. data/lib/conversational.rb +2 -3
  10. data/spec/conversation_spec.rb +188 -20
  11. data/spec/spec_helper.rb +3 -23
  12. metadata +76 -78
  13. data/VERSION +0 -1
  14. data/features/configure_blank_topic.feature +0 -9
  15. data/features/configure_exclusion_conversations.feature +0 -20
  16. data/features/configure_unknown_topic.feature +0 -9
  17. data/features/find_existing_conversation.feature +0 -21
  18. data/features/find_or_create_with.feature +0 -33
  19. data/features/retrieve_conversation_details.feature +0 -13
  20. data/features/step_definitions/conversation_steps.rb +0 -60
  21. data/features/step_definitions/pickle_steps.rb +0 -87
  22. data/features/support/conversation.rb +0 -2
  23. data/features/support/email_spec.rb +0 -1
  24. data/features/support/env.rb +0 -58
  25. data/features/support/mail.rb +0 -6
  26. data/features/support/paths.rb +0 -33
  27. data/features/support/pickle.rb +0 -24
  28. data/features/support/sample_conversation.rb +0 -3
  29. data/lib/conversational/active_record_additions.rb +0 -121
  30. data/lib/conversational/conversation_definition.rb +0 -98
  31. data/lib/generators/conversational/migration/USAGE +0 -5
  32. data/lib/generators/conversational/migration/migration_generator.rb +0 -23
  33. data/lib/generators/conversational/migration/templates/migration.rb +0 -14
  34. data/lib/generators/conversational/skeleton/USAGE +0 -6
  35. data/lib/generators/conversational/skeleton/skeleton_generator.rb +0 -17
  36. data/lib/generators/conversational/skeleton/templates/conversation.rb +0 -3
  37. data/spec/active_record_additions_spec.rb +0 -120
  38. data/spec/conversation_definition_spec.rb +0 -248
  39. data/spec/support/conversation.rb +0 -3
@@ -1,121 +0,0 @@
1
- module Conversational
2
- module ActiveRecordAdditions
3
- def self.included(base)
4
- base.extend ClassMethods
5
- end
6
-
7
- module ClassMethods
8
- attr_accessor :finishing_keywords
9
-
10
- def converser(with)
11
- scoped.where("with = ?", with)
12
- end
13
-
14
- def in_progress
15
- scoped.where("state != ? OR state IS NULL", "finished")
16
- end
17
-
18
- def recent(time = nil)
19
- time ||= 24.hours.ago
20
- scoped.where("updated_at > ?", time)
21
- end
22
-
23
- def with(with)
24
- scoped.converser(with).in_progress.recent
25
- end
26
-
27
- # Finds an existing conversation with using the defaults or
28
- # creates a new conversation and returns the specific conversation based
29
- # on the conversation topic.
30
- # Example:
31
- #
32
- # <tt>
33
- # Class Conversation < ActiveRecord::Base
34
- # include Conversational::Conversation
35
- # end
36
- #
37
- # Class HelloConversation < Conversation
38
- # end
39
- #
40
- # Class GoodbyeConversation < Conversation
41
- # end
42
- #
43
- # Conversation.create!("someone", "hello")
44
- # existing_conversation = Conversation.find_or_create_with(
45
- # "someone",
46
- # "goodbye"
47
- # ) => #<HelloConversation topic: "hello", with: "someone">
48
- #
49
- # Conversation.exclude HelloConversation
50
- #
51
- # existing_conversation = Conversation.find_or_create_with(
52
- # "someone",
53
- # "goodbye"
54
- # ) => #<HelloConversation topic: "hello", with: "someone">
55
- #
56
- # existing_conversation.destroy
57
- #
58
- # non_existing_conversation = Conversation.find_or_create_with(
59
- # "someone",
60
- # "goodbye"
61
- # ) => #<GoodbyeConversation topic: "goodbye", with: "someone">
62
- #
63
- # non_existing_conversation.destroy
64
- #
65
- # Conversation.exclude GoodbyeConversation
66
- #
67
- # non_existing_conversation = Conversation.find_or_create_with(
68
- # "someone",
69
- # "goodbye"
70
- # ) => BOOM! (Raises Error)
71
- #
72
- # unknown_conversation = Conversation.find_or_create_with(
73
- # "someone",
74
- # "cheese"
75
- # ) => BOOM! (Raises Error)
76
- #
77
- # Conversation.unknown_topic_subclass = HelloConversation
78
- #
79
- # unknown_conversation = Conversation.find_or_create_with(
80
- # "someone",
81
- # "cheese"
82
- # ) => #<HelloConversation topic: "hello", with: "someone">
83
- #
84
- # unknown_conversation.destroy
85
- #
86
- # blank_conversation = Conversation.find_or_create_with(
87
- # "someone"
88
- # ) => BOOM! (Raises Error)
89
- #
90
- # Conversation.blank_topic_subclass = GoodbyeConversation
91
- #
92
- # blank_conversation = Conversation.find_or_create_with(
93
- # "someone"
94
- # ) => #<GoodbyeConversation topic: "goodbye", with: "someone">
95
- #
96
- # </tt>
97
- def find_or_create_with(with, topic)
98
- if default_find = self.with(with).last
99
- default_find.details(:include_all => true)
100
- else
101
- subclass = ConversationDefinition.find_subclass_by_topic(topic)
102
- if subclass.nil?
103
- if topic && !topic.blank?
104
- subclass_name = ConversationDefinition.topic_subclass_name(topic)
105
- raise(
106
- ArgumentError,
107
- "You have either not defined #{subclass_name} it does not subclass #{self.to_s}, or it has been excluded. You can either define #{subclass_name} as a subclass of #{self.to_s} or define an unknown_topic_subclass for #{self.to_s}"
108
- )
109
- else
110
- raise(
111
- ArgumentError,
112
- "You have not defined a blank_topic_subclass for #{self.to_s} so conversations without a topic are not allowed."
113
- )
114
- end
115
- end
116
- subclass.create!(:with => with, :topic => topic)
117
- end
118
- end
119
- end
120
- end
121
- end
@@ -1,98 +0,0 @@
1
- module Conversational
2
- class ConversationDefinition
3
- cattr_accessor :unknown_topic_subclass
4
- cattr_accessor :blank_topic_subclass
5
- cattr_accessor :notification
6
- cattr_writer :klass
7
-
8
- def self.exclude(classes)
9
- if classes
10
- if classes.is_a?(Array)
11
- classes.each do |class_name|
12
- check_exclude_options!(class_name)
13
- end
14
- else
15
- check_exclude_options!(classes)
16
- end
17
- end
18
- @@excluded_classes = classes
19
- end
20
-
21
- def self.find_subclass_by_topic(topic, options = {})
22
- subclass = nil
23
- if topic.nil? || topic.blank?
24
- unless options[:exclude_blank_unknown]
25
- subclass = blank_topic_subclass if blank_topic_subclass
26
- end
27
- else
28
- project_class_name = self.topic_subclass_name(topic)
29
- begin
30
- project_class = project_class_name.constantize
31
- rescue
32
- project_class = nil
33
- end
34
- # the subclass has been defined
35
- # check that it is a subclass klass
36
- if project_class && project_class <= @@klass &&
37
- (options[:include_all] || !self.exclude?(project_class))
38
- subclass = project_class
39
- else
40
- unless options[:exclude_blank_unknown]
41
- subclass = unknown_topic_subclass if unknown_topic_subclass
42
- end
43
- end
44
- end
45
- subclass
46
- end
47
-
48
- def self.topic_defined?(topic)
49
- self.find_subclass_by_topic(
50
- topic,
51
- :exclude_blank_unknown => true
52
- )
53
- end
54
-
55
- def self.topic_subclass_name(topic)
56
- topic.classify + @@klass.to_s
57
- end
58
-
59
- private
60
- def self.exclude?(subclass)
61
- if defined?(@@excluded_classes)
62
- if @@excluded_classes.is_a?(Array)
63
- @@excluded_classes.each do |excluded_class|
64
- break if exclude_class?(subclass)
65
- end
66
- else
67
- exclude_class?(subclass)
68
- end
69
- end
70
- end
71
-
72
- def self.exclude_class?(subclass)
73
- if @@excluded_classes.is_a?(Class)
74
- @@excluded_classes == subclass
75
- elsif @@excluded_classes.is_a?(Regexp)
76
- subclass.to_s =~ @@excluded_classes
77
- else
78
- excluded_class = @@excluded_classes.to_s
79
- begin
80
- excluded_class.classify.constantize == subclass
81
- rescue
82
- false
83
- end
84
- end
85
- end
86
-
87
- def self.check_exclude_options!(classes)
88
- raise(
89
- ArgumentError,
90
- "You must specify an Array, Symbol, Regex, String or Class or nil. You specified a #{classes.class}"
91
- ) unless classes.is_a?(Symbol) ||
92
- classes.is_a?(Regexp) ||
93
- classes.is_a?(String) ||
94
- classes.is_a?(Class)
95
- end
96
- end
97
- end
98
-
@@ -1,5 +0,0 @@
1
- Description:
2
- Creates a migration file for when you want to use Conversation with ActiveRecord
3
-
4
- Example:
5
- rails g conversation:migration
@@ -1,23 +0,0 @@
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
@@ -1,14 +0,0 @@
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
@@ -1,6 +0,0 @@
1
- Description:
2
- Creates a blank Conversation class and a directory to store your conversations
3
- under app/conversations
4
-
5
- Example:
6
- rails g conversation:skeleton
@@ -1,17 +0,0 @@
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
@@ -1,3 +0,0 @@
1
- class Conversation
2
- include Conversational::Conversation
3
- end
@@ -1,120 +0,0 @@
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
@@ -1,248 +0,0 @@
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
@@ -1,3 +0,0 @@
1
- class Conversation < ActiveRecord::Base
2
- include Conversational::Conversation
3
- end