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,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