conversational 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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