netsuite_rails 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +3 -0
  3. data/Gemfile +17 -7
  4. data/README.md +9 -4
  5. data/lib/netsuite_rails/configuration.rb +3 -1
  6. data/lib/netsuite_rails/list_sync/poll_manager.rb +30 -0
  7. data/lib/netsuite_rails/list_sync.rb +2 -2
  8. data/lib/netsuite_rails/netsuite_rails.rb +13 -3
  9. data/lib/netsuite_rails/poll_timestamp.rb +5 -0
  10. data/lib/netsuite_rails/{poll_manager.rb → poll_trigger.rb} +18 -3
  11. data/lib/netsuite_rails/record_sync/poll_manager.rb +186 -0
  12. data/lib/netsuite_rails/record_sync/pull_manager.rb +10 -137
  13. data/lib/netsuite_rails/record_sync/push_manager.rb +59 -21
  14. data/lib/netsuite_rails/record_sync.rb +142 -140
  15. data/lib/netsuite_rails/spec/spec_helper.rb +34 -6
  16. data/lib/netsuite_rails/sync_trigger.rb +80 -75
  17. data/lib/netsuite_rails/tasks/netsuite.rb +26 -30
  18. data/lib/netsuite_rails/url_helper.rb +12 -3
  19. data/netsuite_rails.gemspec +3 -3
  20. data/spec/models/poll_manager_spec.rb +45 -0
  21. data/spec/models/poll_trigger_spec.rb +26 -0
  22. data/spec/models/record_sync/push_manager_spec.rb +0 -0
  23. data/spec/models/spec_helper_spec.rb +29 -0
  24. data/spec/models/sync_trigger_spec.rb +62 -0
  25. data/spec/models/url_helper_spec.rb +23 -0
  26. data/spec/spec_helper.rb +17 -1
  27. data/spec/support/config/database.yml +11 -0
  28. data/spec/support/dynamic_models/class_builder.rb +48 -0
  29. data/spec/support/dynamic_models/model_builder.rb +83 -0
  30. data/spec/support/example_models.rb +31 -0
  31. data/spec/support/netsuite_rails.rb +1 -0
  32. data/spec/support/test_application.rb +55 -0
  33. metadata +36 -10
  34. data/lib/netsuite_rails/list_sync/pull_manager.rb +0 -33
@@ -1,104 +1,109 @@
1
1
  module NetSuiteRails
2
- class SyncTrigger
3
- class << self
4
-
5
- # TODO think about a flag to push to NS on after_validation vs after_commit
6
- # TODO think about background async record syncing (re: multiple sales order updates)
7
- # TODO need to add hook for custom proc to determine if data should be pushed to netsuite
8
- # if a model has a pending/complete state we might want to only push on complete
9
-
10
- def attach(klass)
11
- if klass.include?(SubListSync)
12
- klass.after_save { SyncTrigger.sublist_trigger(self) }
13
- klass.after_destroy { SyncTrigger.sublist_trigger(self) }
14
- elsif klass.include?(RecordSync)
15
-
16
- # during the initial pull we don't want to push changes up
17
- klass.before_save do
18
- @netsuite_sync_record_import = self.new_record? && self.netsuite_id.present?
19
-
20
- # if false record will not save
21
- true
22
- end
2
+ module SyncTrigger
3
+ extend self
4
+
5
+ # TODO think about a flag to push to NS on after_validation vs after_commit
6
+ # TODO think about background async record syncing (re: multiple sales order updates)
7
+ # TODO need to add hook for custom proc to determine if data should be pushed to netsuite
8
+ # if a model has a pending/complete state we might want to only push on complete
9
+
10
+ def attach(klass)
11
+ if klass.include?(SubListSync)
12
+ klass.after_save { SyncTrigger.sublist_trigger(self) }
13
+ klass.after_destroy { SyncTrigger.sublist_trigger(self) }
14
+ elsif klass.include?(RecordSync)
15
+
16
+ # during the initial pull we don't want to push changes up
17
+ klass.before_save do
18
+ @netsuite_sync_record_import = self.new_record? && self.netsuite_id.present?
19
+
20
+ # if false record will not save
21
+ true
22
+ end
23
23
 
24
- klass.after_save do
25
- # need to implement this conditional on the save hook level
26
- # because the coordination class doesn't know about model persistence state
24
+ klass.after_save do
25
+ # need to implement this conditional on the save hook level
26
+ # because the coordination class doesn't know about model persistence state
27
27
 
28
- if @netsuite_sync_record_import
29
- # pull the record down if it has't been pulled yet
30
- # this is useful when this is triggered by a save on a parent record which has this
31
- # record as a related record
28
+ if @netsuite_sync_record_import
29
+ # pull the record down if it has't been pulled yet
30
+ # this is useful when this is triggered by a save on a parent record which has this
31
+ # record as a related record
32
32
 
33
- unless self.netsuite_pulled?
34
- SyncTrigger.record_pull_trigger(self)
35
- end
36
- else
37
- SyncTrigger.record_push_trigger(self)
33
+ unless self.netsuite_pulled?
34
+ SyncTrigger.record_pull_trigger(self)
38
35
  end
39
-
40
- @netsuite_sync_record_import = false
36
+ else
37
+ SyncTrigger.record_push_trigger(self)
41
38
  end
42
- end
43
39
 
44
- # TODO think on NetSuiteRails::ListSync
40
+ @netsuite_sync_record_import = false
41
+ end
45
42
  end
46
43
 
47
- def record_pull_trigger(local)
48
- return if NetSuiteRails::Configuration.netsuite_pull_disabled
44
+ # TODO think on NetSuiteRails::ListSync
45
+ end
49
46
 
50
- record_trigger_action(local, :netsuite_pull)
51
- end
47
+ def record_pull_trigger(local)
48
+ return if NetSuiteRails::Configuration.netsuite_pull_disabled
49
+
50
+ record_trigger_action(local, :netsuite_pull)
51
+ end
52
+
53
+ def record_push_trigger(netsuite_record_rep)
54
+ # don't update when fields are updated because of a netsuite_pull
55
+ return if netsuite_record_rep.netsuite_pulling?
52
56
 
53
- def record_push_trigger(netsuite_record_rep)
54
- # don't update when fields are updated because of a netsuite_pull
55
- return if netsuite_record_rep.netsuite_pulling?
57
+ return if NetSuiteRails::Configuration.netsuite_push_disabled
56
58
 
57
- return if NetSuiteRails::Configuration.netsuite_push_disabled
59
+ # don't update if a read only record
60
+ return if netsuite_record_rep.netsuite_sync == :read
58
61
 
59
- # don't update if a read only record
60
- return if netsuite_record_rep.netsuite_sync == :read
62
+ sync_options = netsuite_record_rep.netsuite_sync_options
61
63
 
62
- sync_options = netsuite_record_rep.netsuite_sync_options
64
+ # :if option is a block that returns a boolean
65
+ return if sync_options.has_key?(:if) && !netsuite_record_rep.instance_exec(&sync_options[:if])
63
66
 
64
- # :if option is a block that returns a boolean
65
- return if sync_options.has_key?(:if) && !netsuite_record_rep.instance_exec(&sync_options[:if])
67
+ record_trigger_action(netsuite_record_rep, :netsuite_push)
68
+ end
69
+
70
+ def record_trigger_action(local, action)
71
+ sync_options = local.netsuite_sync_options
72
+
73
+ action_options = {
66
74
 
67
- record_trigger_action(netsuite_record_rep, :netsuite_push)
75
+ }
76
+
77
+ if sync_options.has_key?(:credentials)
78
+ action_options[:credentials] = local.instance_exec(&sync_options[:credentials])
68
79
  end
69
80
 
70
- def record_trigger_action(local, action)
71
- sync_options = local.netsuite_sync_options
81
+ # TODO need to pass off the credentials to the NS push command
82
+
83
+ # You can force sync mode in different envoirnments with the global configuration variables
72
84
 
73
- credentials = if sync_options.has_key?(:credentials)
74
- local.instance_exec(&sync_options[:credentials])
75
- end
85
+ if sync_options[:mode] == :sync || NetSuiteRails::Configuration.netsuite_sync_mode == :sync
86
+ local.send(action)
87
+ else
88
+ action_options[:modified_fields] = NetSuiteRails::RecordSync::PushManager.modified_local_fields(local).keys
76
89
 
77
- # TODO need to pass off the credentials to the NS push command
78
-
79
- # You can force sync mode in different envoirnments with the global configuration variables
90
+ # TODO support the rails4 DJ implementation
80
91
 
81
- if sync_options[:mode] == :sync || NetSuiteRails::Configuration.netsuite_sync_mode == :sync
82
- local.send(action)
92
+ if local.respond_to?(:delay)
93
+ local.delay.send(action, action_options)
83
94
  else
84
- # TODO support the rails4 DJ implementation
85
-
86
- if local.respond_to?(:delay)
87
- local.delay.send(action)
88
- else
89
- raise 'no supported delayed job method found'
90
- end
95
+ raise 'no supported delayed job method found'
91
96
  end
92
97
  end
98
+ end
93
99
 
94
- def sublist_trigger(sublist_item_rep)
95
- parent = sublist_item_rep.send(sublist_item_rep.class.netsuite_sublist_parent)
96
-
97
- if parent.class.include?(RecordSync)
98
- record_push_trigger(parent)
99
- end
100
+ def sublist_trigger(sublist_item_rep)
101
+ parent = sublist_item_rep.send(sublist_item_rep.class.netsuite_sublist_parent)
102
+
103
+ if parent.class.include?(RecordSync)
104
+ record_push_trigger(parent)
100
105
  end
101
-
102
106
  end
107
+
103
108
  end
104
109
  end
@@ -1,21 +1,8 @@
1
1
  namespace :netsuite do
2
2
 
3
- desc "Sync all NetSuite records using import_all"
4
- task :fresh_sync => :environment do
5
- NetSuiteRails::PollTimestamp.delete_all
6
-
7
- ENV['SKIP_EXISTING'] = "true"
8
-
9
- Rake::Task["netsuite:sync"].invoke
10
- end
11
-
12
- desc "sync all netsuite records"
13
- task :sync => :environment do
14
- # need to eager load to ensure that all classes are loaded into the poll manager
15
- Rails.application.eager_load!
16
-
3
+ def generate_options
17
4
  opts = {
18
- skip_existing: ENV['SKIP_EXISTING'].present?
5
+ skip_existing: ENV['SKIP_EXISTING'].present? && ENV['SKIP_EXISTING'] == "true"
19
6
  }
20
7
 
21
8
  if ENV['RECORD_MODELS'].present?
@@ -26,27 +13,36 @@ namespace :netsuite do
26
13
  opts[:list_models] = ENV['LIST_MODELS'].split(',').map(&:constantize)
27
14
  end
28
15
 
29
- # TODO make push disabled configurable
30
-
31
16
  # field values might change on import because of remote data structure changes
32
17
  # stop all pushes on sync & fresh_sync to avoid pushing up data that really hasn't
33
18
  # changed for each record
34
19
 
20
+ # TODO make push disabled configurable
35
21
  NetSuiteRails::Configuration.netsuite_push_disabled true
36
22
 
37
- NetSuiteRails::PollManager.sync(opts)
23
+ opts
38
24
  end
39
25
 
40
- end
26
+ desc "Sync all NetSuite records using import_all"
27
+ task :fresh_sync => :environment do
28
+ NetSuiteRails::PollTimestamp.delete_all
29
+
30
+ ENV['SKIP_EXISTING'] = "true"
31
+
32
+ Rake::Task["netsuite:sync"].invoke
33
+ end
34
+
35
+ desc "sync all netsuite records"
36
+ task :sync => :environment do
37
+ # need to eager load to ensure that all classes are loaded into the poll manager
38
+ Rails.application.eager_load!
41
39
 
42
- # TODO could use this for a "updates local records with a netsuite_id with remote NS data"
43
- # Model.select([:netsuite_id, :id]).find_in_batches do |batch|
44
- # NetSuite::Records::CustomRecord.get_list(
45
- # list: batch.map(&:netsuite_id),
46
- # type_id: Model::NETSUITE_RECORD_TYPE_ID
47
- # ).each do |record|
48
- # model = Model.find_by_netsuite_id(record.internal_id)
49
- # model.extract_from_netsuite_record(record)
50
- # model.save!
51
- # end
52
- # end
40
+ NetSuiteRails::PollTrigger.sync(self.generate_options)
41
+ end
42
+
43
+ desc "sync all local netsuite records"
44
+ task :sync_local => :environment do
45
+ NetSuiteRails::PollTrigger.update_local_records(generate_options)
46
+ end
47
+
48
+ end
@@ -6,13 +6,22 @@ module NetSuiteRails
6
6
  def self.netsuite_url(record = self)
7
7
  prefix = "https://system#{".sandbox" if NetSuite::Configuration.sandbox}.netsuite.com/app"
8
8
 
9
- record_class = record.netsuite_record_class
10
- internal_id = record.netsuite_id
9
+ if record.class.to_s.start_with?('NetSuite::Records')
10
+ record_class = record.class
11
+ internal_id = record.internal_id
12
+ is_custom_record = false
13
+ else
14
+ record_class = record.netsuite_record_class
15
+ internal_id = record.netsuite_id
16
+ is_custom_record = record.netsuite_custom_record?
17
+ end
18
+
19
+ # TODO support NS classes, should jump right to the list for the class
11
20
 
12
21
  # https://system.sandbox.netsuite.com/app/common/scripting/scriptrecordlist.nl
13
22
  # https://system.sandbox.netsuite.com/app/common/scripting/script.nl
14
23
 
15
- if record.netsuite_custom_record?
24
+ if is_custom_record
16
25
  "#{prefix}/common/custom/custrecordentry.nl?id=#{internal_id}&rectype=#{record.class.netsuite_custom_record_type_id}"
17
26
  elsif [ NetSuite::Records::InventoryItem, NetSuite::Records::NonInventorySaleItem, NetSuite::Records::AssemblyItem].include?(record_class)
18
27
  "#{prefix}/common/item/item.nl?id=#{internal_id}"
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |s|
6
6
  s.name = "netsuite_rails"
7
- s.version = "0.1.0"
7
+ s.version = "0.2.0"
8
8
  s.authors = ["Michael Bianco"]
9
9
  s.email = ["mike@cliffsidemedia.com"]
10
10
  s.summary = %q{Write Rails applications that integrate with NetSuite}
@@ -16,10 +16,10 @@ Gem::Specification.new do |s|
16
16
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
17
17
  s.require_paths = ["lib"]
18
18
 
19
- s.add_dependency 'netsuite', '~> 0.3'
19
+ s.add_dependency 'netsuite', '~> 0.3.2'
20
20
  s.add_dependency 'rails', '>= 3.2.16'
21
21
 
22
22
  s.add_development_dependency "bundler", "~> 1.6"
23
23
  s.add_development_dependency "rake"
24
- s.add_development_dependency "rspec"
24
+ s.add_development_dependency "rspec", '~> 3.1'
25
25
  end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe NetSuiteRails::RecordSync::PollManager do
4
+ include ExampleModels
5
+
6
+ # TODO fake a couple items in the list
7
+
8
+ let(:empty_search_results) { OpenStruct.new(results: [ OpenStruct.new(internal_id: 0) ]) }
9
+
10
+ it "should poll record sync objects" do
11
+ allow(NetSuite::Records::Customer).to receive(:search).and_return(empty_search_results)
12
+
13
+ StandardRecord.netsuite_poll(import_all: true)
14
+
15
+ expect(NetSuite::Records::Customer).to have_received(:search)
16
+ end
17
+
18
+ skip "should poll and then get_list on saved search" do
19
+ # TODO SS enabled record
20
+ # TODO mock search to return one result
21
+ # TODO mock out get_list
22
+ end
23
+
24
+ it "should poll list sync objects" do
25
+ allow(NetSuite::Records::CustomList).to receive(:get).and_return(OpenStruct.new(custom_value_list: OpenStruct.new(custom_value: [])))
26
+
27
+ StandardList.netsuite_poll(import_all: true)
28
+
29
+ expect(NetSuite::Records::CustomList).to have_received(:get)
30
+ end
31
+
32
+ it "should sync only available local records" do
33
+ NetSuiteRails::Configuration.netsuite_push_disabled true
34
+ StandardRecord.create! netsuite_id: 123
35
+ NetSuiteRails::Configuration.netsuite_push_disabled false
36
+
37
+ allow(NetSuite::Records::Customer).to receive(:get_list).and_return([OpenStruct.new(internal_id: 123)])
38
+ allow(NetSuiteRails::RecordSync::PollManager).to receive(:process_search_result_item)
39
+
40
+ NetSuiteRails::PollTrigger.update_local_records
41
+
42
+ expect(NetSuite::Records::Customer).to have_received(:get_list)
43
+ end
44
+
45
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe NetSuiteRails::PollTrigger do
4
+ include ExampleModels
5
+
6
+ it "should properly sync for the first time" do
7
+ allow(StandardRecord).to receive(:netsuite_poll).with(hash_including(:import_all => true))
8
+
9
+ NetSuiteRails::PollTrigger.sync list_models: []
10
+
11
+ expect(StandardRecord).to have_received(:netsuite_poll)
12
+ end
13
+
14
+ it "should trigger syncing when the time has passed is greater than frequency" do
15
+ allow(StandardRecord).to receive(:netsuite_poll)
16
+
17
+ StandardRecord.netsuite_sync_options[:frequency] = 5.minutes
18
+ timestamp = NetSuiteRails::PollTimestamp.for_class(StandardRecord)
19
+ timestamp.value = DateTime.now - 7.minutes
20
+ timestamp.save!
21
+
22
+ NetSuiteRails::PollTrigger.sync list_models: []
23
+
24
+ expect(StandardRecord).to have_received(:netsuite_poll)
25
+ end
26
+ end
File without changes
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+
3
+ require 'netsuite_rails/spec/spec_helper'
4
+
5
+ describe NetSuiteRails::TestHelpers do
6
+ include NetSuiteRails::TestHelpers
7
+ include ExampleModels
8
+
9
+ let(:fake_search_results) { OpenStruct.new(results: [ OpenStruct.new(internal_id: 0) ]) }
10
+
11
+ before do
12
+ allow(NetSuite::Records::Customer).to receive(:search).and_return(fake_search_results)
13
+ allow(NetSuite::Records::Customer).to receive(:get)
14
+ end
15
+
16
+ it "should accept a standard NS gem object" do
17
+ get_last_netsuite_object(NetSuite::Records::Customer)
18
+
19
+ expect(NetSuite::Records::Customer).to have_received(:search)
20
+ expect(NetSuite::Records::Customer).to have_received(:get)
21
+ end
22
+
23
+ it "should accept a record sync enabled object" do
24
+ get_last_netsuite_object(StandardRecord.new)
25
+
26
+ expect(NetSuite::Records::Customer).to have_received(:search)
27
+ expect(NetSuite::Records::Customer).to have_received(:get)
28
+ end
29
+ end
@@ -0,0 +1,62 @@
1
+ require 'spec_helper'
2
+
3
+ describe NetSuiteRails::SyncTrigger do
4
+ include ExampleModels
5
+
6
+ before do
7
+ allow(NetSuiteRails::RecordSync::PushManager).to receive(:push_add)
8
+ allow(NetSuiteRails::RecordSync::PushManager).to receive(:push_update)
9
+ end
10
+
11
+ it "should push new record when saved" do
12
+ s = StandardRecord.new
13
+ s.phone = Faker::PhoneNumber.phone_number
14
+ s.save!
15
+
16
+ expect(NetSuiteRails::RecordSync::PushManager).to have_received(:push_add)
17
+ expect(NetSuiteRails::RecordSync::PushManager).to_not have_received(:push_update)
18
+ end
19
+
20
+ it "should not push update on a pull record" do
21
+ s = StandardRecord.new netsuite_id: 123
22
+ allow(s).to receive(:netsuite_pull)
23
+ s.save!
24
+
25
+ expect(s).to have_received(:netsuite_pull)
26
+ expect(NetSuiteRails::RecordSync::PushManager).to_not have_received(:push_add)
27
+ expect(NetSuiteRails::RecordSync::PushManager).to_not have_received(:push_update)
28
+ end
29
+
30
+ it "should push an update on an existing record" do
31
+ s = StandardRecord.new netsuite_id: 123
32
+ allow(s).to receive(:netsuite_pull)
33
+ s.save!
34
+
35
+ s.phone = Faker::PhoneNumber.phone_number
36
+ s.save!
37
+
38
+ expect(NetSuiteRails::RecordSync::PushManager).to_not have_received(:push_add)
39
+ expect(NetSuiteRails::RecordSync::PushManager).to have_received(:push_update)
40
+ end
41
+
42
+ it "should push the modified attributes to the model" do
43
+ s = StandardRecord.new netsuite_id: 123
44
+ allow(s).to receive(:netsuite_pull)
45
+ s.save!
46
+
47
+ # delayed_job isn't included in this gem; hack it into the current record instance
48
+ s.instance_eval { def delay; self; end }
49
+ allow(s).to receive(:delay).and_return(s)
50
+
51
+ NetSuiteRails::Configuration.netsuite_sync_mode :async
52
+
53
+ s.phone = Faker::PhoneNumber.phone_number
54
+ s.save!
55
+
56
+ NetSuiteRails::Configuration.netsuite_sync_mode :sync
57
+
58
+ expect(s).to have_received(:delay)
59
+ expect(NetSuiteRails::RecordSync::PushManager).to have_received(:push_update).with(anything, anything, {:modified_fields=>{:phone=> :phone}})
60
+ end
61
+
62
+ end
@@ -0,0 +1,23 @@
1
+ require 'spec_helper'
2
+
3
+ describe NetSuiteRails::UrlHelper do
4
+ include ExampleModels
5
+
6
+ it 'should handle a netsuite record' do
7
+ c = NetSuite::Records::Customer.new internal_id: 123
8
+ url = NetSuiteRails::UrlHelper.netsuite_url(c)
9
+
10
+ expect(url).to include("cust")
11
+ end
12
+
13
+ it "should handle a record sync enabled record" do
14
+ s = StandardRecord.new netsuite_id: 123
15
+ url = NetSuiteRails::UrlHelper.netsuite_url(s)
16
+
17
+ expect(url).to include("cust")
18
+ end
19
+
20
+ it "should handle a list sync enabled record" do
21
+
22
+ end
23
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,4 +1,20 @@
1
+ require 'rails/all'
2
+
3
+ require 'shoulda/matchers'
4
+ require 'rspec/rails'
5
+ require 'faker'
6
+ require 'pry'
7
+
8
+ require 'netsuite_rails'
9
+
10
+ Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f }
11
+
12
+ TestApplication::Application.initialize!
13
+ NetSuiteRails::PollTimestamp.delete_all
14
+
1
15
  RSpec.configure do |config|
16
+ config.color = true
17
+
2
18
  config.expect_with :rspec do |expectations|
3
19
  expectations.include_chain_clauses_in_custom_matcher_descriptions = true
4
20
  end
@@ -6,4 +22,4 @@ RSpec.configure do |config|
6
22
  config.mock_with :rspec do |mocks|
7
23
  mocks.verify_partial_doubles = true
8
24
  end
9
- end
25
+ end
@@ -0,0 +1,11 @@
1
+ development:
2
+ adapter: sqlite3
3
+ database: db/development.sqlite3
4
+ pool: 5
5
+ timeout: 5000
6
+
7
+ test:
8
+ adapter: sqlite3
9
+ database: db/test.sqlite3
10
+ pool: 5
11
+ timeout: 5000
@@ -0,0 +1,48 @@
1
+ # https://github.com/thoughtbot/shoulda-matchers/raw/2a4b9f1e163fa9c5b84f223962d5f8e099032420/spec/support/class_builder.rb
2
+
3
+ module ClassBuilder
4
+ def self.included(example_group)
5
+ example_group.class_eval do
6
+ after do
7
+ teardown_defined_constants
8
+ end
9
+ end
10
+ end
11
+
12
+ def define_class(class_name, base = Object, &block)
13
+ class_name = class_name.to_s.camelize
14
+
15
+ if Object.const_defined?(class_name)
16
+ Object.__send__(:remove_const, class_name)
17
+ end
18
+
19
+ # FIXME: ActionMailer 3.2 calls `name.underscore` immediately upon
20
+ # subclassing. Class.new.name == nil. So, Class.new(ActionMailer::Base)
21
+ # errors out since it's trying to do `nil.underscore`. This is very ugly but
22
+ # allows us to test against ActionMailer 3.2.x.
23
+ eval <<-A_REAL_CLASS_FOR_ACTION_MAILER_3_2
24
+ class ::#{class_name} < #{base}
25
+ end
26
+ A_REAL_CLASS_FOR_ACTION_MAILER_3_2
27
+
28
+ Object.const_get(class_name).tap do |constant_class|
29
+ constant_class.unloadable
30
+
31
+ if block_given?
32
+ constant_class.class_eval(&block)
33
+ end
34
+
35
+ if constant_class.respond_to?(:reset_column_information)
36
+ constant_class.reset_column_information
37
+ end
38
+ end
39
+ end
40
+
41
+ def teardown_defined_constants
42
+ ActiveSupport::Dependencies.clear
43
+ end
44
+ end
45
+
46
+ RSpec.configure do |config|
47
+ config.include ClassBuilder
48
+ end
@@ -0,0 +1,83 @@
1
+ # https://github.com/thoughtbot/shoulda-matchers/blob/master/spec/support/unit/helpers/model_builder.rb
2
+
3
+ module ModelBuilder
4
+ def self.included(example_group)
5
+ example_group.class_eval do
6
+ before do
7
+ @created_tables ||= []
8
+ end
9
+
10
+ after do
11
+ drop_created_tables
12
+ ActiveSupport::Dependencies.clear
13
+ end
14
+ end
15
+ end
16
+
17
+ def create_table(table_name, options = {}, &block)
18
+ connection = ActiveRecord::Base.connection
19
+
20
+ begin
21
+ connection.execute("DROP TABLE IF EXISTS #{table_name}")
22
+ connection.create_table(table_name, options, &block)
23
+ @created_tables << table_name
24
+ connection
25
+ rescue Exception => e
26
+ connection.execute("DROP TABLE IF EXISTS #{table_name}")
27
+ raise e
28
+ end
29
+ end
30
+
31
+ def define_model_class(class_name, &block)
32
+ define_class(class_name, ActiveRecord::Base, &block)
33
+ end
34
+
35
+ def define_active_model_class(class_name, options = {}, &block)
36
+ define_class(class_name) do
37
+ include ActiveModel::Validations
38
+
39
+ options[:accessors].each do |column|
40
+ attr_accessor column.to_sym
41
+ end
42
+
43
+ if block_given?
44
+ class_eval(&block)
45
+ end
46
+ end
47
+ end
48
+
49
+ def define_model(name, columns = {}, &block)
50
+ class_name = name.to_s.pluralize.classify
51
+ table_name = class_name.tableize
52
+ table_block = lambda do |table|
53
+ columns.each do |name, specification|
54
+ if specification.is_a?(Hash)
55
+ table.column name, specification[:type], specification[:options]
56
+ else
57
+ table.column name, specification
58
+ end
59
+ end
60
+ end
61
+
62
+ if columns.key?(:id) && columns[:id] == false
63
+ columns.delete(:id)
64
+ create_table(table_name, id: false, &table_block)
65
+ else
66
+ create_table(table_name, &table_block)
67
+ end
68
+
69
+ define_model_class(class_name, &block)
70
+ end
71
+
72
+ def drop_created_tables
73
+ connection = ActiveRecord::Base.connection
74
+
75
+ @created_tables.each do |table_name|
76
+ connection.execute("DROP TABLE IF EXISTS #{table_name}")
77
+ end
78
+ end
79
+ end
80
+
81
+ RSpec.configure do |config|
82
+ config.include ModelBuilder
83
+ end