netsuite_rails 0.1.0 → 0.2.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 (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