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.
Files changed (37) hide show
  1. data/.gitignore +3 -0
  2. data/Gemfile +4 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.markdown +240 -0
  5. data/Rakefile +2 -0
  6. data/conversational.gemspec +22 -0
  7. data/features/configure_blank_topic.feature +9 -0
  8. data/features/configure_exclusion_conversations.feature +20 -0
  9. data/features/configure_unknown_topic.feature +9 -0
  10. data/features/find_existing_conversation.feature +21 -0
  11. data/features/find_or_create_with.feature +33 -0
  12. data/features/retrieve_conversation_details.feature +13 -0
  13. data/features/step_definitions/conversation_steps.rb +60 -0
  14. data/features/step_definitions/pickle_steps.rb +87 -0
  15. data/features/support/conversation.rb +2 -0
  16. data/features/support/email_spec.rb +1 -0
  17. data/features/support/env.rb +58 -0
  18. data/features/support/mail.rb +6 -0
  19. data/features/support/paths.rb +33 -0
  20. data/features/support/pickle.rb +24 -0
  21. data/features/support/sample_conversation.rb +3 -0
  22. data/lib/conversational.rb +3 -0
  23. data/lib/conversational/active_record_additions.rb +122 -0
  24. data/lib/conversational/conversation.rb +285 -0
  25. data/lib/conversational/version.rb +4 -0
  26. data/lib/generators/conversational/migration/USAGE +5 -0
  27. data/lib/generators/conversational/migration/migration_generator.rb +23 -0
  28. data/lib/generators/conversational/migration/templates/migration.rb +14 -0
  29. data/lib/generators/conversational/skeleton/USAGE +6 -0
  30. data/lib/generators/conversational/skeleton/skeleton_generator.rb +17 -0
  31. data/lib/generators/conversational/skeleton/templates/conversation.rb +3 -0
  32. data/spec/active_record_additions_spec.rb +120 -0
  33. data/spec/conversation_definition_spec.rb +248 -0
  34. data/spec/conversation_spec.rb +34 -0
  35. data/spec/spec_helper.rb +24 -0
  36. data/spec/support/conversation.rb +3 -0
  37. metadata +113 -0
@@ -0,0 +1,87 @@
1
+ # this file generated by script/generate pickle
2
+
3
+ # create a model
4
+ Given(/^#{capture_model} exists?(?: with #{capture_fields})?$/) do |name, fields|
5
+ create_model(name, fields)
6
+ end
7
+
8
+ # create n models
9
+ Given(/^(\d+) #{capture_plural_factory} exist(?: with #{capture_fields})?$/) do |count, plural_factory, fields|
10
+ count.to_i.times { create_model(plural_factory.singularize, fields) }
11
+ end
12
+
13
+ # create models from a table
14
+ Given(/^the following #{capture_plural_factory} exists?:?$/) do |plural_factory, table|
15
+ create_models_from_table(plural_factory, table)
16
+ end
17
+
18
+ # find a model
19
+ Then(/^#{capture_model} should exist(?: with #{capture_fields})?$/) do |name, fields|
20
+ find_model!(name, fields)
21
+ end
22
+
23
+ # not find a model
24
+ Then(/^#{capture_model} should not exist(?: with #{capture_fields})?$/) do |name, fields|
25
+ find_model(name, fields).should be_nil
26
+ end
27
+
28
+ # find models with a table
29
+ Then(/^the following #{capture_plural_factory} should exists?:?$/) do |plural_factory, table|
30
+ find_models_from_table(plural_factory, table).should_not be_any(&:nil?)
31
+ end
32
+
33
+ # find exactly n models
34
+ Then(/^(\d+) #{capture_plural_factory} should exist(?: with #{capture_fields})?$/) do |count, plural_factory, fields|
35
+ find_models(plural_factory.singularize, fields).size.should == count.to_i
36
+ end
37
+
38
+ # assert equality of models
39
+ Then(/^#{capture_model} should be #{capture_model}$/) do |a, b|
40
+ model!(a).should == model!(b)
41
+ end
42
+
43
+ # assert model is in another model's has_many assoc
44
+ Then(/^#{capture_model} should be (?:in|one of|amongst) #{capture_model}(?:'s)? (\w+)$/) do |target, owner, association|
45
+ model!(owner).send(association).should include(model!(target))
46
+ end
47
+
48
+ # assert model is not in another model's has_many assoc
49
+ Then(/^#{capture_model} should not be (?:in|one of|amongst) #{capture_model}(?:'s)? (\w+)$/) do |target, owner, association|
50
+ model!(owner).send(association).should_not include(model!(target))
51
+ end
52
+
53
+ # assert model is another model's has_one/belongs_to assoc
54
+ Then(/^#{capture_model} should be #{capture_model}(?:'s)? (\w+)$/) do |target, owner, association|
55
+ model!(owner).send(association).should == model!(target)
56
+ end
57
+
58
+ # assert model is not another model's has_one/belongs_to assoc
59
+ Then(/^#{capture_model} should not be #{capture_model}(?:'s)? (\w+)$/) do |target, owner, association|
60
+ model!(owner).send(association).should_not == model!(target)
61
+ end
62
+
63
+ # assert model.predicate?
64
+ Then(/^#{capture_model} should (?:be|have) (?:an? )?#{capture_predicate}$/) do |name, predicate|
65
+ model!(name).should send("be_#{predicate.gsub(' ', '_')}")
66
+ end
67
+
68
+ # assert not model.predicate?
69
+ Then(/^#{capture_model} should not (?:be|have) (?:an? )?#{capture_predicate}$/) do |name, predicate|
70
+ model!(name).should_not send("be_#{predicate.gsub(' ', '_')}")
71
+ end
72
+
73
+ # model.attribute.should eql(value)
74
+ # model.attribute.should_not eql(value)
75
+ Then(/^#{capture_model}'s (\w+) (should(?: not)?) be #{capture_value}$/) do |name, attribute, expectation, expected|
76
+ actual_value = model(name).send(attribute)
77
+ expectation = expectation.gsub(' ', '_')
78
+
79
+ case expected
80
+ when 'nil', 'true', 'false'
81
+ actual_value.send(expectation, send("be_#{expected}"))
82
+ when /^-?[0-9_]+$/
83
+ actual_value.send(expectation, eql(expected.to_i))
84
+ else
85
+ actual_value.to_s.send(expectation, eql(expected))
86
+ end
87
+ end
@@ -0,0 +1,2 @@
1
+ require File.dirname(__FILE__) + '/../../spec/support/conversation'
2
+
@@ -0,0 +1 @@
1
+ require 'email_spec/cucumber'
@@ -0,0 +1,58 @@
1
+ # IMPORTANT: This file is generated by cucumber-rails - edit at your own peril.
2
+ # It is recommended to regenerate this file in the future when you upgrade to a
3
+ # newer version of cucumber-rails. Consider adding your own code to a new file
4
+ # instead of editing this one. Cucumber will automatically load all features/**/*.rb
5
+ # files.
6
+
7
+ ENV["RAILS_ENV"] ||= "test"
8
+ require File.expand_path(File.dirname(__FILE__) + '/../../config/environment')
9
+
10
+ require 'cucumber/formatter/unicode' # Remove this line if you don't want Cucumber Unicode support
11
+ require 'cucumber/rails/rspec'
12
+ require 'cucumber/rails/world'
13
+ require 'cucumber/rails/active_record'
14
+ require 'cucumber/web/tableish'
15
+
16
+ require 'capybara/rails'
17
+ require 'capybara/cucumber'
18
+ require 'capybara/session'
19
+ require 'cucumber/rails/capybara_javascript_emulation' # Lets you click links with onclick javascript handlers without using @culerity or @javascript
20
+ # Capybara defaults to XPath selectors rather than Webrat's default of CSS3. In
21
+ # order to ease the transition to Capybara we set the default here. If you'd
22
+ # prefer to use XPath just remove this line and adjust any selectors in your
23
+ # steps to use the XPath syntax.
24
+ Capybara.default_selector = :css
25
+
26
+ # If you set this to false, any error raised from within your app will bubble
27
+ # up to your step definition and out to cucumber unless you catch it somewhere
28
+ # on the way. You can make Rails rescue errors and render error pages on a
29
+ # per-scenario basis by tagging a scenario or feature with the @allow-rescue tag.
30
+ #
31
+ # If you set this to true, Rails will rescue all errors and render error
32
+ # pages, more or less in the same way your application would behave in the
33
+ # default production environment. It's not recommended to do this for all
34
+ # of your scenarios, as this makes it hard to discover errors in your application.
35
+ ActionController::Base.allow_rescue = false
36
+
37
+ # If you set this to true, each scenario will run in a database transaction.
38
+ # You can still turn off transactions on a per-scenario basis, simply tagging
39
+ # a feature or scenario with the @no-txn tag. If you are using Capybara,
40
+ # tagging with @culerity or @javascript will also turn transactions off.
41
+ #
42
+ # If you set this to false, transactions will be off for all scenarios,
43
+ # regardless of whether you use @no-txn or not.
44
+ #
45
+ # Beware that turning transactions off will leave data in your database
46
+ # after each scenario, which can lead to hard-to-debug failures in
47
+ # subsequent scenarios. If you do this, we recommend you create a Before
48
+ # block that will explicitly put your database in a known state.
49
+ Cucumber::Rails::World.use_transactional_fixtures = true
50
+ # How to clean your database when transactions are turned off. See
51
+ # http://github.com/bmabey/database_cleaner for more info.
52
+ if defined?(ActiveRecord::Base)
53
+ begin
54
+ require 'database_cleaner'
55
+ DatabaseCleaner.strategy = :truncation
56
+ rescue LoadError => ignore_if_database_cleaner_not_present
57
+ end
58
+ end
@@ -0,0 +1,6 @@
1
+ require 'mail'
2
+ Mail.defaults do
3
+ delivery_method :test
4
+ end
5
+
6
+
@@ -0,0 +1,33 @@
1
+ module NavigationHelpers
2
+ # Maps a name to a path. Used by the
3
+ #
4
+ # When /^I go to (.+)$/ do |page_name|
5
+ #
6
+ # step definition in web_steps.rb
7
+ #
8
+ def path_to(page_name)
9
+ case page_name
10
+
11
+ when /the home\s?page/
12
+ '/'
13
+
14
+ # Add more mappings here.
15
+ # Here is an example that pulls values out of the Regexp:
16
+ #
17
+ # when /^(.*)'s profile page$/i
18
+ # user_profile_path(User.find_by_login($1))
19
+
20
+ else
21
+ begin
22
+ page_name =~ /the (.*) page/
23
+ path_components = $1.split(/\s+/)
24
+ self.send(path_components.push('path').join('_').to_sym)
25
+ rescue Object => e
26
+ raise "Can't find mapping from \"#{page_name}\" to a path.\n" +
27
+ "Now, go and add a mapping in #{__FILE__}"
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ World(NavigationHelpers)
@@ -0,0 +1,24 @@
1
+ # this file generated by script/generate pickle [paths] [email]
2
+ #
3
+ # Make sure that you are loading your factory of choice in your cucumber environment
4
+ #
5
+ # For machinist add: features/support/machinist.rb
6
+ #
7
+ # require 'machinist/active_record' # or your chosen adaptor
8
+ # require File.dirname(__FILE__) + '/../../spec/blueprints' # or wherever your blueprints are
9
+ # Before { Sham.reset } # to reset Sham's seed between scenarios so each run has same random sequences
10
+ #
11
+ # For FactoryGirl add: features/support/factory_girl.rb
12
+ #
13
+ # require 'factory_girl'
14
+ # require File.dirname(__FILE__) + '/../../spec/factories' # or wherever your factories are
15
+ #
16
+ # You may also need to add gem dependencies on your factory of choice in <tt>config/environments/cucumber.rb</tt>
17
+
18
+ require 'pickle/world'
19
+ # Example of configuring pickle:
20
+ #
21
+ # Pickle.configure do |config|
22
+ # config.adapters = [:machinist]
23
+ # config.map 'I', 'myself', 'me', 'my', :to => 'user: "me"'
24
+ # end
@@ -0,0 +1,3 @@
1
+ class SampleConversation < Conversation
2
+ end
3
+
@@ -0,0 +1,3 @@
1
+ require 'conversational/conversation'
2
+ require 'conversational/active_record_additions'
3
+
@@ -0,0 +1,122 @@
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 = Conversational::Conversation.find_subclass_by_topic(topic)
102
+ if subclass.nil?
103
+ if topic && !topic.blank?
104
+ subclass_name = Conversational::Conversation.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
122
+
@@ -0,0 +1,285 @@
1
+ module Conversational
2
+ module Conversation
3
+
4
+ mattr_accessor :unknown_topic_subclass,
5
+ :blank_topic_subclass,
6
+ :parent,
7
+ :class_suffix
8
+
9
+ def self.included(base)
10
+ self.parent = base
11
+ base.send(:include, InstanceMethods)
12
+ base.extend ClassMethods
13
+ if defined?(ActiveRecord::Base) && base <= ActiveRecord::Base
14
+ base.send(:include, ActiveRecordAdditions)
15
+ else
16
+ base.send(:include, InstanceAttributes)
17
+ end
18
+ end
19
+
20
+ module InstanceAttributes
21
+ attr_accessor :with, :topic
22
+
23
+ def initialize(options = {})
24
+ self.with = options[:with]
25
+ self.topic = options[:topic]
26
+ end
27
+ end
28
+
29
+ module InstanceMethods
30
+ # Returns the specific sublass of conversation based from the topic
31
+ # Example:
32
+ #
33
+ # <tt>
34
+ # Class Conversation
35
+ # include Conversational::Conversation
36
+ # end
37
+ #
38
+ # Class HelloConversation < Conversation
39
+ # end
40
+ #
41
+ # Class GoodbyeConversation < Conversation
42
+ # end
43
+ #
44
+ # hello = Conversation.new("someone", "hello")
45
+ # hello.details => #<HelloConversation topic: "hello", with: "someone">
46
+ #
47
+ # unknown = Conversation.new("someone", "cheese")
48
+ # unknown.details => nil
49
+ #
50
+ # Conversation.unknown_topic_subclass = HelloConversation
51
+ #
52
+ # unknown = Conversation.new("someone", "cheese")
53
+ # unknown.details => #<HelloConversation topic: "cheese", with: "someone">
54
+ #
55
+ # blank = Conversation.new("someone")
56
+ # blank.details => nil
57
+ #
58
+ # Conversation.blank_topic_subclass = GoodbyeConversation
59
+ #
60
+ # blank = Conversation.new("someone")
61
+ # blank.details => #<GoodbyeConversation topic: nil, with: "someone">
62
+ #
63
+ # Conversation.exclude HelloConversation
64
+ #
65
+ # hello = Conversation.new("someone", "hello")
66
+ # hello.details => nil
67
+ #
68
+ # hello.details(:include_all => true) => #<HelloConversation topic: "hello", with: "someone">
69
+ #
70
+ # </tt>
71
+ def details(options = {})
72
+ details_subclass = Conversational::Conversation.find_subclass_by_topic(
73
+ topic, options
74
+ )
75
+ if details_subclass
76
+ self.respond_to?(:becomes) ?
77
+ becomes(details_subclass) :
78
+ Conversational::Conversation.becomes(
79
+ details_subclass, self
80
+ )
81
+ end
82
+ end
83
+
84
+ def topic_defined?
85
+ details_subclass = Conversational::Conversation.topic_defined?(topic)
86
+ end
87
+ end
88
+
89
+ module ClassMethods
90
+ def unknown_topic_subclass(value)
91
+ Conversational::Conversation.unknown_topic_subclass = value
92
+ end
93
+
94
+ def blank_topic_subclass(value)
95
+ Conversational::Conversation.blank_topic_subclass = value
96
+ end
97
+
98
+ def class_suffix(value)
99
+ Conversational::Conversation.class_suffix = value
100
+ end
101
+
102
+ # Register classes which will not be treated as conversations
103
+ # when you use #details or Conversation.find_or_create_with
104
+ #
105
+ # Example:
106
+ #
107
+ # Consider the following situation where you define AbstractConversation
108
+ # that MonkeyConversations inherits from. You want to work with MonkeyConversation
109
+ # but you never want to work with AbstractConversation directly.
110
+ #
111
+ # <tt>
112
+ # class AbstractConversation < Conversation
113
+ # end
114
+ #
115
+ # class MonkeyConversation < AbstractConversation
116
+ # end
117
+ #
118
+ # class UnknownTopicConversation < AbstractConversation
119
+ # end
120
+ #
121
+ # class IncomingTextMessage < ActiveRecord::Base
122
+ # end
123
+ #
124
+ # class IncomingTextMessagesController < ApplicationController
125
+ # def create
126
+ # IncomingTextMessage.create(params[:message_text], params[:number])
127
+ # end
128
+ # end
129
+ # </tt>
130
+ #
131
+ # Now what happens when a user sends in a message like "monkey"
132
+ # <tt>
133
+ # message = IncomingTextMessage.last
134
+ # topic = message.text.split(" ").first
135
+ # => "monkey"
136
+ # number = message.number
137
+ # => "123456789"
138
+ # conversation = Conversation.new(:with => number, :topic => topic).details
139
+ # => #<MonkeyConversation topic: "monkey", with: "123456789">
140
+ # </tt>
141
+
142
+ # No problem here you will work with MonkeyConversation directly. But what if
143
+ # the user sends in a message like "abstract"?
144
+ # <tt>
145
+ # message = IncomingTextMessage.last
146
+ # topic = message.text.split(" ").first
147
+ # => "abstract"
148
+ # number = message.number
149
+ # => "123456789"
150
+ # conversation = Conversation.new(:with => number, :topic => topic).details
151
+ # => #<AbstractConversation topic: "abstract", with: "123456789">
152
+ # </tt>
153
+ # Now you're are about to work with AbstractConversation directly
154
+ # which is not what you want. Let's fix it
155
+ # <tt>
156
+ # # config/initializers/conversation.rb
157
+ # Conversation.exclude AbstractConversation
158
+ # Conversation.unknown_topic_subclass = UnkownTopicConversation
159
+ # </tt>
160
+ #
161
+ # <tt>
162
+ # message = IncomingTextMessage.last
163
+ # topic = message.text.split(" ").first
164
+ # => "abstract"
165
+ # number = message.number
166
+ # => "123456789"
167
+ # conversation = Conversation.new(:with => number, :topic => topic).details
168
+ # => #<UnknownTopicConversation topic: "abstract", with: "123456789">
169
+ # </tt>
170
+ #
171
+ # <tt>Conversation.exclude</tt> accepts the following
172
+ # * Class: <tt>Conversation.exclude AbstractConversation</tt>
173
+ # * Array: <tt>Conversation.exclude [AbstractConversation, OtherConversation]</tt>
174
+ # * Symbol: <tt>Conversation.exclude :abstract_conversation</tt>
175
+ # * String: <tt>Conversation.exclude "abstract_conversation"</tt>
176
+ # * Regexp: <tt>Conversation.exclude /abstract/i</tt>
177
+
178
+ def exclude(classes)
179
+ Conversational::Conversation.exclude(classes)
180
+ end
181
+ end
182
+
183
+ def self.topic_defined?(topic)
184
+ self.find_subclass_by_topic(
185
+ topic,
186
+ :exclude_blank_unknown => true
187
+ )
188
+ end
189
+
190
+ def self.find_subclass_by_topic(topic, options = {})
191
+ subclass = nil
192
+ if topic.nil? || topic.blank?
193
+ unless options[:exclude_blank_unknown]
194
+ subclass = blank_topic_subclass.constantize if blank_topic_subclass
195
+ end
196
+ else
197
+ project_class_name = self.topic_subclass_name(topic)
198
+ begin
199
+ project_class = project_class_name.constantize
200
+ rescue
201
+ project_class = nil
202
+ end
203
+ # the subclass has been defined
204
+ # check that it is a subclass klass
205
+ if project_class && project_class <= parent &&
206
+ (options[:include_all] || !self.exclude?(project_class))
207
+ subclass = project_class
208
+ else
209
+ unless options[:exclude_blank_unknown]
210
+ subclass = unknown_topic_subclass.constantize if unknown_topic_subclass
211
+ end
212
+ end
213
+ end
214
+ subclass
215
+ end
216
+
217
+ def self.exclude(classes)
218
+ if classes
219
+ if classes.is_a?(Array)
220
+ classes.each do |class_name|
221
+ check_exclude_options!(class_name)
222
+ end
223
+ else
224
+ check_exclude_options!(classes)
225
+ end
226
+ end
227
+ @@excluded_classes = classes
228
+ end
229
+
230
+ def self.topic_subclass_name(topic)
231
+ topic.classify + (class_suffix || parent).to_s
232
+ end
233
+
234
+ private
235
+
236
+ def self.becomes(klass, from)
237
+ klass_instance = klass.new
238
+ from.instance_variables.each do |instance_variable|
239
+ klass_instance.instance_variable_set(
240
+ instance_variable,
241
+ from.instance_variable_get(instance_variable)
242
+ )
243
+ end
244
+ klass_instance
245
+ end
246
+
247
+ def self.exclude?(subclass)
248
+ if defined?(@@excluded_classes)
249
+ if @@excluded_classes.is_a?(Array)
250
+ @@excluded_classes.each do |excluded_class|
251
+ break if exclude_class?(subclass)
252
+ end
253
+ else
254
+ exclude_class?(subclass)
255
+ end
256
+ end
257
+ end
258
+
259
+ def self.exclude_class?(subclass)
260
+ if @@excluded_classes.is_a?(Class)
261
+ @@excluded_classes == subclass
262
+ elsif @@excluded_classes.is_a?(Regexp)
263
+ subclass.to_s =~ @@excluded_classes
264
+ else
265
+ excluded_class = @@excluded_classes.to_s
266
+ begin
267
+ excluded_class.classify.constantize == subclass
268
+ rescue
269
+ false
270
+ end
271
+ end
272
+ end
273
+
274
+ def self.check_exclude_options!(classes)
275
+ raise(
276
+ ArgumentError,
277
+ "You must specify an Array, Symbol, Regex, String or Class or nil. You specified a #{classes.class}"
278
+ ) unless classes.is_a?(Symbol) ||
279
+ classes.is_a?(Regexp) ||
280
+ classes.is_a?(String) ||
281
+ classes.is_a?(Class)
282
+ end
283
+ end
284
+ end
285
+