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,122 +1,98 @@
1
1
  module Conversational
2
2
  module Conversation
3
- def self.included(base)
4
- base.extend ClassMethods
5
- ConversationDefinition.klass = base
6
- if defined?(ActiveRecord::Base) && base <= ActiveRecord::Base
7
- base.send(:include, ActiveRecordAdditions)
8
- else
9
- base.send(:include, InstanceAttributes)
10
- end
11
- end
12
3
 
13
- # Returns the specific sublass of conversation based from the topic
14
- # Example:
15
- #
16
- # <tt>
17
- # Class Conversation
18
- # include Conversational::Conversation
19
- # end
20
- #
21
- # Class HelloConversation < Conversation
22
- # end
23
- #
24
- # Class GoodbyeConversation < Conversation
25
- # end
26
- #
27
- # hello = Conversation.new("someone", "hello")
28
- # hello.details => #<HelloConversation topic: "hello", with: "someone">
29
- #
30
- # unknown = Conversation.new("someone", "cheese")
31
- # unknown.details => nil
32
- #
33
- # Conversation.unknown_topic_subclass = HelloConversation
34
- #
35
- # unknown = Conversation.new("someone", "cheese")
36
- # unknown.details => #<HelloConversation topic: "cheese", with: "someone">
37
- #
38
- # blank = Conversation.new("someone")
39
- # blank.details => nil
40
- #
41
- # Conversation.blank_topic_subclass = GoodbyeConversation
42
- #
43
- # blank = Conversation.new("someone")
44
- # blank.details => #<GoodbyeConversation topic: nil, with: "someone">
45
- #
46
- # Conversation.exclude HelloConversation
47
- #
48
- # hello = Conversation.new("someone", "hello")
49
- # hello.details => nil
50
- #
51
- # hello.details(:include_all => true) => #<HelloConversation topic: "hello", with: "someone">
52
- #
53
- # </tt>
54
- def details(options = {})
55
- details_subclass = ConversationDefinition.find_subclass_by_topic(
56
- topic, options
57
- )
58
- becomes(details_subclass) if details_subclass
59
- end
4
+ mattr_accessor :unknown_topic_subclass,
5
+ :blank_topic_subclass,
6
+ :parent,
7
+ :class_suffix
60
8
 
61
- def topic_defined?
62
- details_subclass = ConversationDefinition.topic_defined?(topic)
9
+ def self.included(base)
10
+ self.parent = base
11
+ base.send(:include, InstanceMethods)
12
+ base.send(:include, InstanceAttributes)
13
+ base.extend ClassMethods
63
14
  end
64
15
 
65
- protected
66
- # Called from a subclass to deliver the message
67
- def say(message)
68
- ConversationDefinition.notification.call(with, message)
69
- end
70
-
71
16
  module InstanceAttributes
72
- attr_accessor :with, :topic
17
+ attr_accessor :topic
73
18
 
74
19
  def initialize(options = {})
75
- self.with = options[:with]
76
20
  self.topic = options[:topic]
21
+ super
77
22
  end
23
+ end
78
24
 
79
- private
80
- def becomes(klass)
81
- klass_instance = klass.new
82
- self.instance_variables.each do |instance_variable|
83
- klass_instance.instance_variable_set(
84
- instance_variable,
85
- self.instance_variable_get(instance_variable)
25
+ module InstanceMethods
26
+ # Returns the specific sublass of conversation based from the topic
27
+ # Example:
28
+ #
29
+ # <tt>
30
+ # Class Conversation
31
+ # include Conversational::Conversation
32
+ # end
33
+ #
34
+ # Class HelloConversation < Conversation
35
+ # end
36
+ #
37
+ # Class GoodbyeConversation < Conversation
38
+ # end
39
+ #
40
+ # hello = Conversation.new("someone", "hello")
41
+ # hello.details => #<HelloConversation topic: "hello", with: "someone">
42
+ #
43
+ # unknown = Conversation.new("someone", "cheese")
44
+ # unknown.details => nil
45
+ #
46
+ # Conversation.unknown_topic_subclass = HelloConversation
47
+ #
48
+ # unknown = Conversation.new("someone", "cheese")
49
+ # unknown.details => #<HelloConversation topic: "cheese", with: "someone">
50
+ #
51
+ # blank = Conversation.new("someone")
52
+ # blank.details => nil
53
+ #
54
+ # Conversation.blank_topic_subclass = GoodbyeConversation
55
+ #
56
+ # blank = Conversation.new("someone")
57
+ # blank.details => #<GoodbyeConversation topic: nil, with: "someone">
58
+ #
59
+ # Conversation.exclude HelloConversation
60
+ #
61
+ # hello = Conversation.new("someone", "hello")
62
+ # hello.details => nil
63
+ #
64
+ # hello.details(:include_all => true) => #<HelloConversation topic: "hello", with: "someone">
65
+ #
66
+ # </tt>
67
+ def details(options = {})
68
+ details_subclass = Conversational::Conversation.find_subclass_by_topic(
69
+ topic, options
70
+ )
71
+ if details_subclass
72
+ self.respond_to?(:becomes) ?
73
+ becomes(details_subclass) :
74
+ Conversational::Conversation.becomes(
75
+ details_subclass, self
86
76
  )
87
- end
88
- klass_instance
89
77
  end
90
- end
91
-
92
- module ClassMethods
93
- def unknown_topic_subclass=(klass)
94
- ConversationDefinition.unknown_topic_subclass = klass
95
78
  end
96
79
 
97
- def unknown_topic_subclass
98
- ConversationDefinition.unknown_topic_subclass
80
+ def topic_defined?
81
+ details_subclass = Conversational::Conversation.topic_defined?(topic)
99
82
  end
83
+ end
100
84
 
101
- def blank_topic_subclass=(klass)
102
- ConversationDefinition.blank_topic_subclass = klass
85
+ module ClassMethods
86
+ def unknown_topic_subclass(value)
87
+ Conversational::Conversation.unknown_topic_subclass = Conversational::Conversation.stringify(value)
103
88
  end
104
89
 
105
- def blank_topic_subclass
106
- ConversationDefinition.blank_topic_subclass
90
+ def blank_topic_subclass(value)
91
+ Conversational::Conversation.blank_topic_subclass = Conversational::Conversation.stringify(value)
107
92
  end
108
93
 
109
- # Register a service for sending notifications
110
- #
111
- # Example:
112
- #
113
- # <tt>
114
- # Conversation.converse do |with, message|
115
- # OutgoingTextMessage.create!(with, message).send
116
- # end
117
- # </tt>
118
- def converse(&blk)
119
- ConversationDefinition.notification = blk
94
+ def class_suffix(value)
95
+ Conversational::Conversation.class_suffix = Conversational::Conversation.stringify(value)
120
96
  end
121
97
 
122
98
  # Register classes which will not be treated as conversations
@@ -196,8 +172,111 @@ module Conversational
196
172
  # * Regexp: <tt>Conversation.exclude /abstract/i</tt>
197
173
 
198
174
  def exclude(classes)
199
- ConversationDefinition.exclude(classes)
175
+ Conversational::Conversation.exclude(classes)
176
+ end
177
+ end
178
+
179
+ def self.topic_defined?(topic)
180
+ self.find_subclass_by_topic(
181
+ topic,
182
+ :exclude_blank_unknown => true
183
+ )
184
+ end
185
+
186
+ def self.find_subclass_by_topic(topic, options = {})
187
+ subclass = nil
188
+ if topic.nil? || topic.blank?
189
+ unless options[:exclude_blank_unknown]
190
+ subclass = blank_topic_subclass.constantize if blank_topic_subclass
191
+ end
192
+ else
193
+ project_class_name = self.topic_subclass_name(topic)
194
+ begin
195
+ project_class = project_class_name.constantize
196
+ rescue
197
+ project_class = nil
198
+ end
199
+ # the subclass has been defined
200
+ # check that it is a subclass klass
201
+ if project_class && project_class <= parent &&
202
+ (options[:include_all] || !self.exclude?(project_class))
203
+ subclass = project_class
204
+ else
205
+ unless options[:exclude_blank_unknown]
206
+ subclass = unknown_topic_subclass.constantize if unknown_topic_subclass
207
+ end
208
+ end
209
+ end
210
+ subclass
211
+ end
212
+
213
+ def self.exclude(classes)
214
+ if classes
215
+ if classes.is_a?(Array)
216
+ classes.each do |class_name|
217
+ check_exclude_options!(class_name)
218
+ end
219
+ else
220
+ check_exclude_options!(classes)
221
+ end
200
222
  end
223
+ @@excluded_classes = classes
224
+ end
225
+
226
+ def self.topic_subclass_name(topic)
227
+ topic.classify + (class_suffix || parent).to_s
228
+ end
229
+
230
+ private
231
+
232
+ def self.becomes(klass, from)
233
+ klass_instance = klass.new
234
+ from.instance_variables.each do |instance_variable|
235
+ klass_instance.instance_variable_set(
236
+ instance_variable,
237
+ from.instance_variable_get(instance_variable)
238
+ )
239
+ end
240
+ klass_instance
241
+ end
242
+
243
+ def self.exclude?(subclass)
244
+ if defined?(@@excluded_classes)
245
+ if @@excluded_classes.is_a?(Array)
246
+ @@excluded_classes.each do |excluded_class|
247
+ break if exclude_class?(subclass)
248
+ end
249
+ else
250
+ exclude_class?(subclass)
251
+ end
252
+ end
253
+ end
254
+
255
+ def self.exclude_class?(subclass)
256
+ if @@excluded_classes.is_a?(Regexp)
257
+ subclass.to_s =~ @@excluded_classes
258
+ else
259
+ excluded_class = @@excluded_classes.to_s
260
+ begin
261
+ excluded_class.classify.constantize == subclass
262
+ rescue
263
+ false
264
+ end
265
+ end
266
+ end
267
+
268
+ def self.check_exclude_options!(classes)
269
+ raise(
270
+ ArgumentError,
271
+ "You must specify an Array, Symbol, Regex, Class, String or nil. You specified a #{classes.class}"
272
+ ) unless classes.is_a?(Symbol) ||
273
+ classes.is_a?(Regexp) ||
274
+ classes.is_a?(String) ||
275
+ classes.is_a?(Class)
276
+ end
277
+
278
+ def self.stringify(value)
279
+ value.nil? ? value : value.to_s
201
280
  end
202
281
  end
203
282
  end
@@ -0,0 +1,4 @@
1
+ module Conversational
2
+ VERSION = "0.4.0"
3
+ end
4
+
@@ -1,4 +1,3 @@
1
- require 'conversational/conversation'
2
- require 'conversational/conversation_definition'
3
- require 'conversational/active_record_additions'
1
+ require "active_support/core_ext"
2
+ require "conversational/conversation"
4
3
 
@@ -1,34 +1,202 @@
1
1
  require 'spec_helper'
2
2
 
3
+ # TODO test .class_suffix
3
4
  describe Conversational::Conversation do
4
5
 
6
+ class Conversation
7
+ include Conversational::Conversation
8
+ end
9
+
10
+ class DrinkingConversation < Conversation
11
+ end
12
+
13
+ class SmokingConversation < Conversation
14
+ end
15
+
16
+ class DrivingBlahBlah < Conversation
17
+ end
18
+
19
+ class FlyingConversation
20
+ end
21
+
22
+ it "should respond to '.unknown_topic_subclass'" do
23
+ Conversation.should respond_to(:unknown_topic_subclass)
24
+ end
25
+
26
+ it "should respond to '.blank_topic_subclass'" do
27
+ Conversation.should respond_to(:blank_topic_subclass)
28
+ end
29
+
30
+ it "should respond to '.class_suffix'" do
31
+ Conversation.should respond_to(:class_suffix)
32
+ end
33
+
34
+ let!(:conversation) { Conversation.new }
35
+
36
+ before do
37
+ Conversation.exclude(nil)
38
+ Conversation.unknown_topic_subclass(nil)
39
+ Conversation.blank_topic_subclass(nil)
40
+ Conversation.class_suffix(nil)
41
+ end
42
+
43
+ describe ".exclude" do
44
+ it "should accept a string" do
45
+ lambda {
46
+ Conversation.exclude "something"
47
+ }.should_not raise_error
48
+ end
49
+ it "should accept a symbol" do
50
+ lambda {
51
+ Conversation.exclude :defined_conversation
52
+ }.should_not raise_error
53
+ end
54
+ it "should accept a regex" do
55
+ lambda {
56
+ Conversation.exclude /something/i
57
+ }.should_not raise_error
58
+ end
59
+ it "should accept a Class" do
60
+ lambda {
61
+ Conversation.exclude(DrinkingConversation)
62
+ }.should_not raise_error
63
+ end
64
+ it "should accept an Array where the elements are a Class, String, Symbol or Regexp" do
65
+ lambda {
66
+ Conversation.exclude ["Something", DrinkingConversation, /something/i, :something]
67
+ }.should_not raise_error
68
+ end
69
+ it "should accept nil" do
70
+ lambda {
71
+ Conversation.exclude nil
72
+ }.should_not raise_error
73
+ end
74
+ it "should not accept anything else" do
75
+ lambda {
76
+ Conversation.exclude({})
77
+ }.should raise_error(/You specified a Hash/)
78
+ end
79
+ end
80
+
81
+ describe "#topic" do
82
+ it "should set the topic" do
83
+ conversation.topic = "hello"
84
+ conversation.topic.should == "hello"
85
+ end
86
+ end
87
+
88
+ describe "#topic_defined?" do
89
+ shared_examples_for "#topic_defined? for an excluded class" do
90
+ it "should return nil" do
91
+ conversation.topic_defined?.should be_nil
92
+ end
93
+ end
94
+
95
+ context "a class with this topic is defined" do
96
+ before { conversation.topic = "drinking" }
97
+ it "should be true" do
98
+ conversation.topic_defined?.should be_true
99
+ end
100
+
101
+ context "but it is not a subclass of the module which includes Conversation" do
102
+ before { conversation.topic = "flying" }
103
+ it_should_behave_like "#topic_defined? for an excluded class"
104
+ end
105
+
106
+ context "but it has been excluded" do
107
+ context "by passing the class" do
108
+ before { Conversation.exclude(DrinkingConversation) }
109
+ it_should_behave_like "#topic_defined? for an excluded class"
110
+ end
111
+
112
+ context "by passing an Array of classes" do
113
+ before { Conversation.exclude([DrinkingConversation])}
114
+ it_should_behave_like "#topic_defined? for an excluded class"
115
+ end
116
+
117
+ context "by passing a regexp" do
118
+ before { Conversation.exclude(/^Drinking/) }
119
+ it_should_behave_like "#topic_defined? for an excluded class"
120
+ end
121
+
122
+ context "by passing a symbol" do
123
+ before { Conversation.exclude(:drinking_conversation) }
124
+ it_should_behave_like "#topic_defined? for an excluded class"
125
+ end
126
+
127
+ context "by passing a string" do
128
+ before { Conversation.exclude("DrinkingConversation") }
129
+ it_should_behave_like "#topic_defined? for an excluded class"
130
+ end
131
+ end
132
+ end
133
+
134
+ context "a conversation for this topic is not defined" do
135
+ before do
136
+ Conversation.unknown_topic_subclass(SmokingConversation)
137
+ conversation.topic = "drinking_tea"
138
+ end
139
+ it_should_behave_like "#topic_defined? for an excluded class"
140
+ end
141
+ end
142
+
5
143
  describe "#details" do
6
- let(:conversation) { Class.new }
7
- before {
8
- conversation.extend(Conversational::Conversation)
9
- conversation.stub!(:topic).and_return("something")
10
- }
11
- context "a subclass for this topic exists" do
12
- let(:subclass) { mock("Subclass") }
13
- before {
14
- Conversational::ConversationDefinition.stub!(
15
- :find_subclass_by_topic
16
- ).and_return(subclass)
17
- }
144
+ shared_examples_for "#details for an excluded class" do
145
+ it "should return nil" do
146
+ conversation.details.should be_nil
147
+ end
148
+ context "when '.unknown_topic_subclass' is set" do
149
+ before {Conversation.unknown_topic_subclass(SmokingConversation)}
150
+ it "should return an instance of the unknown_topic_subclass" do
151
+ conversation.details.should be_a(SmokingConversation)
152
+ end
153
+ end
154
+ end
155
+
156
+ context "a conversation for this topic has been defined" do
157
+ before { conversation.topic = "drinking" }
18
158
  it "should return the instance as a subclass" do
19
- conversation.should_receive(:becomes).with(subclass)
20
- conversation.details
159
+ conversation.details.should be_a(DrinkingConversation)
160
+ end
161
+
162
+ context "but it is not a subclass of the module which includes Conversation" do
163
+ before { conversation.topic = "flying" }
164
+ it_should_behave_like "#details for an excluded class"
21
165
  end
166
+
167
+ context "but it has been excluded" do
168
+ before { Conversation.exclude(DrinkingConversation) }
169
+ it_should_behave_like "#details for an excluded class"
170
+ end
171
+ end
172
+
173
+ context "a conversation for this topic has not been defined" do
174
+ before { conversation.topic = "drinking_tea" }
175
+ it_should_behave_like "#details for an excluded class"
22
176
  end
23
- context "a subclass for this topic does not exist" do
24
- before {
25
- Conversational::ConversationDefinition.stub!(
26
- :find_subclass_by_topic
27
- ).and_return(nil)
28
- }
177
+
178
+ shared_examples_for "#details for a blank or nil topic" do
29
179
  it "should return nil" do
30
180
  conversation.details.should be_nil
31
181
  end
182
+
183
+ context "'.blank_topic_subclass' is set" do
184
+ before {Conversation.blank_topic_subclass(FlyingConversation)}
185
+ it "should return an instance of the blank_topic_subclass" do
186
+ conversation.details.should be_a(FlyingConversation)
187
+ end
188
+ end
189
+ end
190
+
191
+ context "the conversation has a blank topic" do
192
+ before { conversation.topic = "" }
193
+ it_should_behave_like "#details for a blank or nil topic"
194
+ end
195
+
196
+ context "the conversation has no topic" do
197
+ before { conversation.topic = nil }
198
+ it_should_behave_like "#details for a blank or nil topic"
32
199
  end
33
200
  end
34
201
  end
202
+
data/spec/spec_helper.rb CHANGED
@@ -1,24 +1,4 @@
1
- # This file is copied to ~/spec when you run 'ruby script/generate rspec'
2
- # from the project root directory.
3
- ENV["RAILS_ENV"] ||= 'test'
4
- require File.dirname(__FILE__) + "/../config/environment" unless defined?(Rails)
5
- require 'rspec/rails'
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+ require "conversational"
6
4
 
7
- # Requires supporting files with custom matchers and macros, etc,
8
- # in ./support/ and its subdirectories.
9
- Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
10
-
11
- Rspec.configure do |config|
12
- # == Mock Framework
13
- #
14
- # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
15
- #
16
- # config.mock_with :mocha
17
- # config.mock_with :flexmock
18
- # config.mock_with :rr
19
- config.mock_with :rspec
20
-
21
- # If you'd prefer not to run each of your examples within a transaction,
22
- # uncomment the following line.
23
- # config.use_transactional_examples = false
24
- end