ns_connector 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/Gemfile +13 -0
  2. data/Gemfile.lock +80 -0
  3. data/Guardfile +9 -0
  4. data/HACKING +31 -0
  5. data/LICENSE.txt +7 -0
  6. data/README.rdoc +191 -0
  7. data/Rakefile +45 -0
  8. data/VERSION +1 -0
  9. data/lib/ns_connector.rb +4 -0
  10. data/lib/ns_connector/attaching.rb +42 -0
  11. data/lib/ns_connector/chunked_searching.rb +111 -0
  12. data/lib/ns_connector/config.rb +66 -0
  13. data/lib/ns_connector/errors.rb +79 -0
  14. data/lib/ns_connector/field_store.rb +19 -0
  15. data/lib/ns_connector/hash.rb +11 -0
  16. data/lib/ns_connector/resource.rb +288 -0
  17. data/lib/ns_connector/resources.rb +3 -0
  18. data/lib/ns_connector/resources/contact.rb +279 -0
  19. data/lib/ns_connector/resources/customer.rb +355 -0
  20. data/lib/ns_connector/resources/invoice.rb +466 -0
  21. data/lib/ns_connector/restlet.rb +137 -0
  22. data/lib/ns_connector/sublist.rb +21 -0
  23. data/lib/ns_connector/sublist_item.rb +25 -0
  24. data/misc/failed_sublist_saving_patch +547 -0
  25. data/scripts/run_restlet +25 -0
  26. data/scripts/test_shell +21 -0
  27. data/spec/attaching_spec.rb +48 -0
  28. data/spec/chunked_searching_spec.rb +75 -0
  29. data/spec/config_spec.rb +43 -0
  30. data/spec/resource_spec.rb +340 -0
  31. data/spec/resources/contact_spec.rb +8 -0
  32. data/spec/resources/customer_spec.rb +8 -0
  33. data/spec/resources/invoice_spec.rb +20 -0
  34. data/spec/restlet_spec.rb +135 -0
  35. data/spec/spec_helper.rb +16 -0
  36. data/spec/sublist_item_spec.rb +25 -0
  37. data/spec/sublist_spec.rb +45 -0
  38. data/spec/support/mock_data.rb +10 -0
  39. data/support/read_only_test +63 -0
  40. data/support/restlet.js +384 -0
  41. data/support/super_dangerous_write_test +85 -0
  42. metadata +221 -0
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(
3
+ File.dirname(__FILE__),
4
+ '..', 'lib'
5
+ )
6
+
7
+ require 'ns_connector'
8
+ require 'pp'
9
+
10
+ unless ARGV.size == 2 then
11
+ warn(
12
+ "Usage: #{$0}"\
13
+ "<configuration as ruby code> "\
14
+ "<arguments as ruby code>"
15
+ )
16
+ warn('e.g.')
17
+ warn(
18
+ "#{$0}"\
19
+ '"{:account_id => ...}" "{:action => ...}"'
20
+ )
21
+ exit 1
22
+ end
23
+
24
+ NSConnector::Config.set_config!(eval(ARGV[0]))
25
+ pp NSConnector::Restlet.execute!(eval(ARGV[1]))
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.join(
3
+ File.dirname(__FILE__),
4
+ '..', 'lib'
5
+ )
6
+
7
+ require 'ns_connector'
8
+ require 'pry'
9
+
10
+ if ARGV.empty? then
11
+ warn(
12
+ "Usage: #{$0}"\
13
+ "<configuration as ruby code>"
14
+ )
15
+ exit 1
16
+ end
17
+
18
+
19
+
20
+ NSConnector::Config.set_config!(eval(ARGV[0]))
21
+ NSConnector.pry
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ class MagicResource
4
+ extend NSConnector::Attaching
5
+ def initialize upstream_store
6
+ end
7
+ def self.type_id
8
+ 'magic'
9
+ end
10
+ end
11
+
12
+ class MagicTarget
13
+ extend NSConnector::Attaching
14
+ def initialize upstream_store
15
+ end
16
+ def self.type_id
17
+ 'target'
18
+ end
19
+ end
20
+
21
+ describe NSConnector::Attaching do
22
+ it 'attach! works' do
23
+ NSConnector::Restlet.should_receive(:execute!).with({
24
+ :action => 'attach',
25
+ :type_id => 'magic',
26
+ :target_type_id => 'target',
27
+ :attachee_id => 42,
28
+ :data => [1,2,3],
29
+ :attributes => nil,
30
+ })
31
+
32
+ MagicResource.attach!(MagicTarget, 42, [1,2,3])
33
+ end
34
+
35
+
36
+ it 'detach! works' do
37
+ NSConnector::Restlet.should_receive(:execute!).with({
38
+ :action => 'detach',
39
+ :type_id => 'magic',
40
+ :target_type_id => 'target',
41
+ :attachee_id => 42,
42
+ :data => [1,2,3]
43
+ })
44
+
45
+ MagicResource.detach!(MagicTarget, 42, [1,2,3])
46
+ end
47
+ end
48
+
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ class MagicResource
4
+ extend NSConnector::ChunkedSearching
5
+ def initialize upstream_store
6
+ end
7
+ def self.type_id
8
+ 'magic'
9
+ end
10
+ end
11
+
12
+ describe NSConnector::ChunkedSearching do
13
+ before :all do
14
+ # Keep things exciting
15
+ NSConnector::Config[:no_threads] = rand(15) + 1
16
+ end
17
+
18
+ def mock_chunked_search
19
+ (0..5).each do |i|
20
+ MagicResource.should_receive(:grab_chunk).
21
+ with('filters', i).ordered.and_return([1,2,3])
22
+ end
23
+
24
+ MagicResource.should_receive(:grab_chunk).
25
+ with('filters', 6).ordered.
26
+ and_raise(NSConnector::Errors::EndChunking, double)
27
+ end
28
+
29
+ it 'grabs a single chunk' do
30
+ NSConnector::Restlet.should_receive(:execute!).
31
+ with({
32
+ :action => 'search',
33
+ :type_id => 'magic',
34
+ :data => {
35
+ :filters => 'filters',
36
+ :chunk => 42
37
+ }
38
+ }).and_return(['retval'])
39
+
40
+ MagicResource.should_receive(:new).with('retval')
41
+ expect(MagicResource.grab_chunk('filters', 42)).to be_a(Array)
42
+ end
43
+
44
+ it 'listens to the config' do
45
+ MagicResource.should_receive(:threaded_search_by_chunks).
46
+ with('filters').ordered.
47
+ and_return(1)
48
+ MagicResource.should_receive(:normal_search_by_chunks).
49
+ with('filters').ordered.
50
+ and_return(2)
51
+
52
+ expect(MagicResource.search_by_chunks('filters')).to eql(1)
53
+ NSConnector::Config[:use_threads] = false
54
+ expect(MagicResource.search_by_chunks('filters')).to eql(2)
55
+ end
56
+
57
+ it 'does a normal search correctly' do
58
+ mock_chunked_search
59
+
60
+ ret = MagicResource.normal_search_by_chunks('filters')
61
+
62
+ expect(ret).to be_a(Array)
63
+ # 5 iterations returning 3 each
64
+ expect(ret).to have(6 * 3).things
65
+ end
66
+
67
+ it 'does a threaded search correctly' do
68
+ mock_chunked_search
69
+
70
+ ret = MagicResource.threaded_search_by_chunks('filters')
71
+ expect(ret).to be_a(Array)
72
+ expect(ret).to have(6 * 3).things
73
+ end
74
+
75
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+ describe NSConnector::Config do
3
+ before(:each) do
4
+ # Reset our 'global' config
5
+ # Yes, it's kinda not nice to have a global configuration like
6
+ # this, but it's better than passing the damned thing around
7
+ # everywhere. Thread safety should be fine for reads.
8
+ NSConnector::Config.set_config!({})
9
+ end
10
+
11
+ it 'sets a valid config' do
12
+ NSConnector::Config.set_config!(valid_config)
13
+ expect(NSConnector::Config.check_valid!).to be_true
14
+ end
15
+
16
+ it 'sets an invalid config' do
17
+ NSConnector::Config.set_config!(:invalid => true)
18
+ expect{NSConnector::Config.check_valid!}.to raise_error
19
+ end
20
+
21
+ it 'allows reading of keys' do
22
+ expect(NSConnector::Config[:account_id]).to be_nil
23
+
24
+ NSConnector::Config.set_config!(valid_config)
25
+
26
+ expect(NSConnector::Config[:account_id]).to eql('account_id')
27
+ expect(NSConnector::Config['account_id']).to eql('account_id')
28
+ end
29
+
30
+ it 'allows writing of keys' do
31
+ expect(NSConnector::Config[:account_id]).to be_nil
32
+ NSConnector::Config[:account_id] = 'account_id'
33
+ expect(NSConnector::Config[:account_id]).to eql('account_id')
34
+
35
+ NSConnector::Config['account_id'] = nil
36
+ expect(NSConnector::Config[:account_id]).to be_nil
37
+ end
38
+
39
+ it 'has defaults' do
40
+ expect(NSConnector::Config['use_threads']).
41
+ to eql(!!NSConnector::Config['use_threads'])
42
+ end
43
+ end
@@ -0,0 +1,340 @@
1
+ require 'spec_helper'
2
+ include NSConnector
3
+
4
+ class PseudoResource < Resource
5
+ # The NetSuite internal id for the object. For a Contact, it would be
6
+ # 'contact'
7
+ @type_id = 'pseudoresource'
8
+ @fields = ['id', 'firstname', 'lastname']
9
+ @sublists = {:notes => [:line]}
10
+ end
11
+
12
+ # We create another resource here to ensure no clashes in anything shared.
13
+ class OtherResource < Resource
14
+ @type_id = 'otherresource'
15
+ @fields = ['id', 'fax']
16
+ @sublists = {}
17
+ end
18
+
19
+
20
+ describe PseudoResource do
21
+ before :each do
22
+ @p = PseudoResource.new
23
+ end
24
+
25
+ context '#new' do
26
+ it 'has things we expect' do
27
+ PseudoResource.type_id.should eql('pseudoresource')
28
+ PseudoResource.fields.should eql(
29
+ ['id', 'firstname', 'lastname']
30
+ )
31
+ PseudoResource.sublists.should eql({:notes => [:line]})
32
+ end
33
+
34
+ it 'automatically loads field ids just once on #new' do
35
+ PseudoResource.new
36
+ expect(@p.fields).
37
+ to eql(['id', 'firstname', 'lastname'])
38
+
39
+ OtherResource.new
40
+ o = OtherResource.new
41
+ expect(o.fields).to eql(['id', 'fax'])
42
+ end
43
+
44
+ it 'allows us to pass stuff to new' do
45
+ new_resource = PseudoResource.new(:firstname => 'first')
46
+ expect(new_resource.firstname).to eql('first')
47
+ expect(new_resource.lastname).to be_nil
48
+ expect(new_resource).to_not be_in_netsuite
49
+
50
+ in_ns_resource = PseudoResource.new(
51
+ {:lastname => 'last'}, true
52
+ )
53
+ expect(in_ns_resource.lastname).to eql('last')
54
+ expect(in_ns_resource.firstname).to be_nil
55
+ expect(in_ns_resource).to be_in_netsuite
56
+ end
57
+
58
+ it 'allows fields to be read and set' do
59
+ expect(@p.id).to be_nil
60
+ expect(@p.firstname).to be_nil
61
+ expect(@p.lastname).to be_nil
62
+
63
+ @p.firstname = 'first'
64
+ expect(@p.firstname).to eql('first')
65
+ expect(@p.store).to eql({'firstname' => 'first'})
66
+
67
+ # Ensure there is no odd behaviour here
68
+ p2 = PseudoResource.new
69
+ p2.firstname = 'second'
70
+
71
+ expect(@p.firstname).to eql('first')
72
+ expect(p2.firstname).to eql('second')
73
+ end
74
+
75
+ it 'does not exist in netsuite as it is new' do
76
+ expect(PseudoResource.new).to_not be_in_netsuite
77
+ end
78
+
79
+ it 'saves as a new object when #save! is called' do
80
+ ns_reply = {
81
+ 'firstname' => 'Name',
82
+ 'lastname' => 'nothing',
83
+ 'id' => '42'
84
+ }
85
+
86
+ @p.firstname = 'name'
87
+
88
+ Restlet.should_receive(:execute!).
89
+ with({
90
+ :action => 'create',
91
+ :type_id => 'pseudoresource',
92
+ :fields => ['id', 'firstname', 'lastname'],
93
+ :data => {'firstname' => 'name'}
94
+ }).
95
+ once.
96
+ and_return(ns_reply)
97
+
98
+ expect(@p.save!).to eql(true)
99
+
100
+ expect(@p.firstname).to eql('Name')
101
+ expect(@p.lastname).to eql('nothing')
102
+ expect(@p.id).to eql('42')
103
+ end
104
+
105
+ it 'has a pretty inspect' do
106
+ expect(@p.inspect).to eql(
107
+ '#<NSConnector::PseudoResource:nil>'
108
+ )
109
+
110
+ @p.instance_variable_set('@store', {'id' => 1})
111
+ expect(@p.inspect).to eql(
112
+ '#<NSConnector::PseudoResource:1>'
113
+ )
114
+ end
115
+ end
116
+
117
+ context 'reading' do
118
+ it 'retrieves one resource with #find' do
119
+ Restlet.should_receive(:execute!).
120
+ with({
121
+ :action => 'retrieve',
122
+ :type_id => 'pseudoresource',
123
+ :fields => [
124
+ 'id',
125
+ 'firstname',
126
+ 'lastname'
127
+ ],
128
+ :data => {'id' => 42}
129
+ }).and_return({'firstname' => 'dude man'})
130
+
131
+ result = PseudoResource.find(42)
132
+ expect(result).to be_a(PseudoResource)
133
+ expect(result.firstname).to eql('dude man')
134
+ expect(result).to be_in_netsuite
135
+ end
136
+
137
+ it 'retrieves multiple resources with #advanced_search' do
138
+ expected_filters = [
139
+ ['entityId', nil, 'is', '42'],
140
+ ['email', nil, 'isempty']
141
+ ]
142
+
143
+ Restlet.should_receive(:execute!).
144
+ with({
145
+ :action => 'search',
146
+ :type_id => 'pseudoresource',
147
+ :fields => [
148
+ 'id',
149
+ 'firstname',
150
+ 'lastname'
151
+ ],
152
+ :data => {:filters => expected_filters}
153
+ }).and_return([
154
+ {'id' => 1, 'firstname' => 'unique'},
155
+ {'id' => 2, 'firstname' => 'two'}
156
+ ])
157
+
158
+ result = PseudoResource.advanced_search([
159
+ ['entityId', nil, 'is', '42'],
160
+ ['email', nil, 'isempty']
161
+ ])
162
+ expect(result).to be_a(Array)
163
+ expect(result).to have(2).things
164
+
165
+ expect(result.first).to be_a(PseudoResource)
166
+ expect(result.first.firstname).to eql('unique')
167
+ expect(result.first.id).to eql(1)
168
+ expect(result.first).to be_in_netsuite
169
+
170
+ expect(result.last.firstname).to eql('two')
171
+ end
172
+
173
+ it 'has convenience method #search_by' do
174
+ PseudoResource.
175
+ should_receive(:advanced_search).
176
+ with([['a', nil, 'is', 'b']]).
177
+ and_return('nothing')
178
+ expect(PseudoResource.search_by('a', 'b')).
179
+ to eql('nothing')
180
+ end
181
+
182
+ it 'has convenience method #all' do
183
+ PseudoResource.
184
+ should_receive(:advanced_search).
185
+ with([]).
186
+ and_return('nothing')
187
+ expect(PseudoResource.all).to eql('nothing')
188
+ end
189
+
190
+ it 'tries to chunk given BeginChunking' do
191
+ Restlet.should_receive(:execute!).
192
+ and_raise(Errors::BeginChunking, double)
193
+ PseudoResource.should_receive(:search_by_chunks).
194
+ and_return('hai')
195
+ expect(PseudoResource.all).to eql('hai')
196
+ end
197
+ end
198
+
199
+ shared_context 'found_resource' do
200
+ before :each do
201
+ Restlet.should_receive(:execute!).
202
+ with({
203
+ :action => 'retrieve',
204
+ :type_id => 'pseudoresource',
205
+ :fields => [
206
+ 'id',
207
+ 'firstname',
208
+ 'lastname'
209
+ ],
210
+ :data => {'id' => 42}
211
+ }).and_return(
212
+ {
213
+ 'id' => '1',
214
+ 'firstname' => 'orig',
215
+ 'lastname' => 'orig'
216
+ }
217
+ )
218
+ @found_resource = PseudoResource.find(42)
219
+ expect(@found_resource.firstname).to eql('orig')
220
+ expect(@found_resource).to be_in_netsuite
221
+ end
222
+ end
223
+
224
+ context 'updating' do
225
+ include_context 'found_resource'
226
+
227
+ it 'updates existing record in NetSuite' do
228
+ @found_resource.firstname = 'new'
229
+ expect(@found_resource.firstname).to eql('new')
230
+
231
+ Restlet.should_receive(:execute!).
232
+ with({
233
+ :action => 'update',
234
+ :type_id => 'pseudoresource',
235
+ :fields => [
236
+ 'id',
237
+ 'firstname',
238
+ 'lastname'
239
+ ],
240
+ :data => {
241
+ 'firstname' => 'new',
242
+ 'lastname' => 'orig',
243
+ 'id' => '1'
244
+ }
245
+ }).
246
+ once.
247
+ and_return({'firstname' => 'New'})
248
+
249
+ @found_resource.save!
250
+
251
+ expect(@found_resource.lastname).to be_nil
252
+ expect(@found_resource.firstname).to eql('New')
253
+ end
254
+ end
255
+
256
+ context 'deleting' do
257
+ include_context 'found_resource'
258
+
259
+ it 'tries to delete ID via class method' do
260
+ Restlet.should_receive(:execute!).
261
+ with({
262
+ :action => 'delete',
263
+ :type_id => 'pseudoresource',
264
+ :data => {
265
+ 'id' => 42
266
+ }
267
+ }).
268
+ once.and_return([])
269
+ PseudoResource.delete!(42)
270
+ end
271
+
272
+ it 'tries to delete ID via instance method' do
273
+ Restlet.should_receive(:execute!).
274
+ with({
275
+ :action => 'delete',
276
+ :type_id => 'pseudoresource',
277
+ :data => {
278
+ 'id' => 1
279
+ }
280
+ }).
281
+ once.and_return([])
282
+ @found_resource.delete!
283
+ end
284
+
285
+ it 'returns false trying to delete an object not in NS' do
286
+ expect(@p.delete!).to be_false
287
+ end
288
+ end
289
+
290
+ context 'sublists on new object' do
291
+ it 'is empty' do
292
+ p = PseudoResource.new
293
+ expect(p.notes).to be_empty
294
+ end
295
+ end
296
+
297
+ context 'raw search' do
298
+ it 'has #raw_search' do
299
+ Restlet.should_receive(:execute!).
300
+ with({
301
+ :action => 'raw_search',
302
+ :type_id => 'pseudoresource',
303
+ :fields => [
304
+ 'id', 'firstname', 'lastname'
305
+ ],
306
+ :data => {
307
+ :columns => [['a']],
308
+ :filters => [['b']]
309
+ }
310
+ }).
311
+ once.and_return(['hai'])
312
+ expect(PseudoResource.raw_search([['a']], [['b']])).
313
+ to eql(['hai'])
314
+ end
315
+ end
316
+
317
+ context 'link aliases' do
318
+ include_context 'found_resource'
319
+
320
+ it 'has #link' do
321
+ PseudoResource.should_receive(:attach!).
322
+ with(OtherResource, '1', [1,2], nil)
323
+ @found_resource.attach!(OtherResource, [1,2])
324
+
325
+ expect{@p.attach!(OtherResource, [1,2])}.to raise_error(
326
+ ::ArgumentError, /need an id/i
327
+ )
328
+ end
329
+
330
+ it 'has #unlink' do
331
+ PseudoResource.should_receive(:detach!).
332
+ with(OtherResource, '1', [1,2])
333
+ @found_resource.detach!(OtherResource, [1,2])
334
+
335
+ expect{@p.detach!(OtherResource, [1,2])}.to raise_error(
336
+ ::ArgumentError, /need an id/i
337
+ )
338
+ end
339
+ end
340
+ end