rsmart_toolbox 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,447 @@
1
+ # rSmart client library and command-line tool to help interact with rSmart's cloud APIs.
2
+ # Copyright (C) 2014 The rSmart Group, Inc.
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'spec_helper'
18
+ require 'rsmart_toolbox/etl'
19
+
20
+ ETL = RsmartToolbox::ETL
21
+
22
+ RSpec.describe "RsmartToolbox::ETL" do
23
+
24
+ describe "#error" do
25
+ it "it returns a TextParseError when passed a String" do
26
+ expect(ETL::error("foo")).to be_kind_of TextParseError
27
+ end
28
+
29
+ it "reformats the message with additional context information" do
30
+ e = ETL::error("foo")
31
+ expect(e.message).to include "foo"
32
+ expect(e.message).to match /^ERROR:\s+Line\s+(\d+):\s+.+$/
33
+ end
34
+
35
+ it "supports passing Exceptions and maintains type" do
36
+ e1 = NotImplementedError.new "foo"
37
+ e2 = ETL::error(e1)
38
+ expect(ETL::error(e2)).to be_kind_of NotImplementedError
39
+ end
40
+
41
+ it "supports passing Exceptions and maintains message" do
42
+ e1 = NotImplementedError.new "foo"
43
+ e2 = ETL::error(e1)
44
+ expect(e2.message).to include e1.message
45
+ expect(e2.message).to match /^ERROR:\s+Line\s+(\d+):\s+.+$/
46
+ end
47
+
48
+ it "raises an ArgumentError if passed an unsupported type" do
49
+ expect { ETL::error("foo".to_i) }.to raise_error(ArgumentError)
50
+ end
51
+ end
52
+
53
+ describe "#warning" do
54
+ it "it returns a TextParseError when passed a String" do
55
+ expect(ETL::warning("foo")).to be_kind_of TextParseError
56
+ end
57
+
58
+ it "reformats the message with additional context information" do
59
+ e = ETL::warning("foo")
60
+ expect(e.message).to include "foo"
61
+ expect(e.message).to match /^WARN:\s+Line\s+(\d+):\s+.+$/
62
+ end
63
+
64
+ it "supports passing Exceptions and maintains type" do
65
+ e1 = NotImplementedError.new "foo"
66
+ e2 = ETL::warning(e1)
67
+ expect(ETL::warning(e2)).to be_kind_of NotImplementedError
68
+ end
69
+
70
+ it "supports passing Exceptions and maintains message" do
71
+ e1 = NotImplementedError.new "foo"
72
+ e2 = ETL::warning(e1)
73
+ expect(e2.message).to include e1.message
74
+ expect(e2.message).to match /^WARN:\s+Line\s+(\d+):\s+.+$/
75
+ end
76
+
77
+ it "raises an ArgumentError if passed an unsupported type" do
78
+ expect { ETL::warning("foo".to_i) }.to raise_error(ArgumentError)
79
+ end
80
+ end
81
+
82
+ describe '#valid_value' do
83
+ it "tests semantic equality against a set of valid values" do
84
+ expect(ETL.valid_value(1, [1, 2, 3])).to eq(true)
85
+ expect(ETL.valid_value(2, [1, 2, 3])).to eq(true)
86
+ expect(ETL.valid_value(3, [1, 2, 3])).to eq(true)
87
+ expect(ETL.valid_value(1, [2, 3])).to eq(false)
88
+ expect(ETL.valid_value("1", ["1"])).to eq(true)
89
+ expect(ETL.valid_value("1", ["2"])).to eq(false)
90
+ expect(ETL.valid_value("1", [1])).to eq(false)
91
+ end
92
+
93
+ it "provides a case_sensitive option" do
94
+ expect(ETL.valid_value("one", ["ONE"], case_sensitive: false)).to eq(true)
95
+ expect(ETL.valid_value("one", ["ONE"], case_sensitive: true)).to eq(false)
96
+ expect(ETL.valid_value("one", ["one"], case_sensitive: true)).to eq(true)
97
+ expect(ETL.valid_value("one", ["ONE"], case_sensitive: "foo")).to eq(false)
98
+ expect(ETL.valid_value("one", ["ONE"])).to eq(false)
99
+ end
100
+
101
+ it "allows for a valid_values that is a regular expression" do
102
+ expect(ETL.valid_value("word", /^(\w+)$/)).to eq(true)
103
+ expect(ETL.valid_value("Z", /^(B|A|Z)?$/)).to eq(true)
104
+ expect(ETL.valid_value("", /^(B|A|Z)?$/)).to eq(true)
105
+ expect(ETL.valid_value("upper", /^(UPPER)$/i)).to eq(true)
106
+
107
+ expect(ETL.valid_value("false", /^(true)$/)).to eq(false)
108
+ expect(ETL.valid_value("", "^(B|A|Z)+$")).to eq(false)
109
+ end
110
+ end
111
+
112
+ describe "#parse_boolean" do
113
+ true_valid_values = ['active', 'a', 'true', 't', 'yes', 'y', '1']
114
+ false_valid_values = ['inactive', 'i', 'false', 'f', 'no', 'n', '0']
115
+
116
+ it "converts all valid, exact case 'true' Strings to true Booleans" do
117
+ true_valid_values.each do |valid_value|
118
+ expect(ETL.parse_boolean(valid_value)).to eq(true)
119
+ end
120
+ end
121
+
122
+ it "converts all valid, lowercase 'true' Strings to true Booleans" do
123
+ true_valid_values.each do |valid_value|
124
+ expect(ETL.parse_boolean(valid_value.downcase)).to eq(true)
125
+ end
126
+ end
127
+
128
+ it "converts all valid, mixed case 'true' Strings to true Booleans" do
129
+ true_valid_values.each do |valid_value|
130
+ expect(ETL.parse_boolean(valid_value.capitalize)).to eq(true)
131
+ end
132
+ end
133
+
134
+ it "converts all valid, exact case 'false' Strings to false Booleans" do
135
+ false_valid_values.each do |valid_value|
136
+ expect(ETL.parse_boolean(valid_value)).to eq(false)
137
+ end
138
+ end
139
+
140
+ it "converts all valid, lowercase 'false' Strings to false Booleans" do
141
+ false_valid_values.each do |valid_value|
142
+ expect(ETL.parse_boolean(valid_value.downcase)).to eq(false)
143
+ end
144
+ end
145
+
146
+ it "converts all valid, mixed case 'false' Strings to false Booleans" do
147
+ false_valid_values.each do |valid_value|
148
+ expect(ETL.parse_boolean(valid_value.capitalize)).to eq(false)
149
+ end
150
+ end
151
+
152
+ it "handles Booleans in addition to Strings" do
153
+ expect(ETL.parse_boolean(true)).to eq(true)
154
+ expect(ETL.parse_boolean(false)).to eq(false)
155
+ end
156
+
157
+ it "converts '' Strings to nil" do
158
+ expect(ETL.parse_boolean('')).to eq(nil)
159
+ expect { ETL.parse_boolean('') }.not_to raise_error
160
+ end
161
+
162
+ it "converts nil to nil" do
163
+ expect(ETL.parse_boolean(nil)).to eq(nil)
164
+ expect { ETL.parse_boolean(nil) }.not_to raise_error
165
+ end
166
+
167
+ it "throws an Exception when an invalid string is passed" do
168
+ expect { ETL.parse_boolean("foober") }.to raise_error(TextParseError)
169
+ end
170
+
171
+ it "supports use of the :required option" do
172
+ expect { ETL.parse_boolean(nil, required: true) }.to raise_error(TextParseError)
173
+ expect { ETL.parse_boolean(nil, required: false) }.not_to raise_error
174
+ end
175
+
176
+ it "supports use of the :default option" do
177
+ expect(ETL.parse_boolean("", default: true)).to eq true
178
+ expect(ETL.parse_boolean(nil, default: true)).to eq true
179
+ expect(ETL.parse_boolean("", default: "yes")).to eq true
180
+ expect(ETL.parse_boolean(nil, default: "yes")).to eq true
181
+
182
+ expect(ETL.parse_boolean("", default: false)).to eq false
183
+ expect(ETL.parse_boolean(nil, default: false)).to eq false
184
+ expect(ETL.parse_boolean("", default: "no")).to eq false
185
+ expect(ETL.parse_boolean(nil, default: "no")).to eq false
186
+ end
187
+ end
188
+
189
+ describe "#escape_single_quotes" do
190
+ it "Escapes any single quotes in a String with a '\' character" do
191
+ expect(ETL.escape_single_quotes("That's it")).to eq("That\\\'s it")
192
+ expect(ETL.escape_single_quotes("Thats it")).to eq("Thats it")
193
+ expect(ETL.escape_single_quotes("")).to eq("")
194
+ expect(ETL.escape_single_quotes(nil)).to eq(nil)
195
+ end
196
+ end
197
+
198
+ describe "#parse_string" do
199
+ it "Escapes any single quotes in a String with a '\' character" do
200
+ expect(ETL.parse_string("That's it")).to eq("That\\\'s it")
201
+ expect(ETL.parse_string("Thats it")).to eq("Thats it")
202
+ end
203
+
204
+ it "Returns empty string if nil or an empty string is passed" do
205
+ expect(ETL.parse_string("")).to eq("")
206
+ expect(ETL.parse_string(nil)).to eq("")
207
+ end
208
+
209
+ it "Supports a :required option" do
210
+ expect { ETL.parse_string("", required: true) }.to raise_error(TextParseError)
211
+ expect { ETL.parse_string(nil, required: true) }.to raise_error(TextParseError)
212
+ expect { ETL.parse_string("", required: false) }.not_to raise_error
213
+ expect { ETL.parse_string(nil, required: false) }.not_to raise_error
214
+ end
215
+
216
+ it "Supports a :default option if no String is found" do
217
+ expect(ETL.parse_string("", default: "foo")).to eq("foo")
218
+ expect(ETL.parse_string(nil, default: "foo")).to eq("foo")
219
+ end
220
+
221
+ it "Ignores the :default option if a String is found" do
222
+ expect(ETL.parse_string("bar", default: "foo")).to eq("bar")
223
+ end
224
+
225
+ it "performs a :length validation" do
226
+ expect { ETL.parse_string("123", length: 1) }.to raise_error(TextParseError)
227
+ end
228
+
229
+ it "allows you to disable :strict :length validation" do
230
+ expect { ETL.parse_string("123", length: 1, strict: false) }.not_to raise_error
231
+ end
232
+
233
+ it "Supports a :valid_values validation semantics" do
234
+ expect { ETL.parse_string("123", valid_values: /456/) }.to raise_error(TextParseError)
235
+ expect { ETL.parse_string("123", valid_values: ['456']) }.to raise_error(TextParseError)
236
+ end
237
+ end
238
+
239
+ describe "#parse_string!" do
240
+ it "Modifies the insert_str and values_str based on a CSV::Row match" do
241
+ insert_str = ""; values_str = "";
242
+ row = CSV::Row.new(['rolodex_id'.to_sym], ['123ABC'], true)
243
+ ETL.parse_string!(row, insert_str, values_str, name: "ROLODEX_ID")
244
+ expect(insert_str).to eq "ROLODEX_ID,"
245
+ expect(values_str).to eq "'123ABC',"
246
+ end
247
+
248
+ it "is not required by default and mutates with an empty string" do
249
+ insert_str = ""; values_str = "";
250
+ row = CSV::Row.new(['rolodex_id'.to_sym], [''], true)
251
+ ETL.parse_string!(row, insert_str, values_str, name: "ROLODEX_ID")
252
+ expect(insert_str).to eq "ROLODEX_ID,"
253
+ expect(values_str).to eq "'',"
254
+ end
255
+ end
256
+
257
+ describe "#parse_integer" do
258
+ it "Converts Strings into Integers" do
259
+ expect(ETL.parse_integer("1")).to eq(1)
260
+ expect(ETL.parse_integer("0")).to eq(0)
261
+ end
262
+
263
+ it "Supports passing Integers for convenience" do
264
+ expect(ETL.parse_integer(1)).to eq(1)
265
+ expect(ETL.parse_integer(0)).to eq(0)
266
+ end
267
+
268
+ it "Returns nil if no value is found instead of 0" do
269
+ expect(ETL.parse_integer("")).to eq(nil)
270
+ expect(ETL.parse_integer(nil)).to eq(nil)
271
+ end
272
+
273
+ it "Raises an TextParseError if String is nil or empty and is required" do
274
+ expect { ETL.parse_integer(nil, required: true) }.to raise_error(TextParseError)
275
+ expect { ETL.parse_integer("", required: true ) }.to raise_error(TextParseError)
276
+ end
277
+
278
+ it "Supports :default option" do
279
+ expect(ETL.parse_integer("", default: '1', required: false)).to eq(1)
280
+ expect(ETL.parse_integer("", default: 2, required: false)).to eq(2)
281
+ end
282
+
283
+ it "Enforces strict length validation to avoid loss of precision" do
284
+ expect { ETL.parse_integer("22", length: 1, strict: true) }.to raise_error(TextParseError)
285
+ end
286
+
287
+ it "Supports a :valid_values validation semantics" do
288
+ expect { ETL.parse_integer("123", valid_values: /456/) }.to raise_error(TextParseError)
289
+ expect { ETL.parse_integer("123", valid_values: ['456']) }.to raise_error(TextParseError)
290
+ end
291
+ end
292
+
293
+ describe "#parse_integer!" do
294
+ it "Modifies the insert_str and values_str based on a CSV::Row match" do
295
+ insert_str = ""; values_str = ""; name = "VALID_CLASS_REPORT_FREQ_ID"
296
+ row = CSV::Row.new([name.downcase.to_sym], ['123'], true)
297
+ ETL.parse_integer!(row, insert_str, values_str, name: name)
298
+ expect(insert_str).to eq "#{name},"
299
+ expect(values_str).to eq "123,"
300
+ end
301
+
302
+ # TODO how to handle mutation of column name and value when nil is returned from parse_integer?
303
+ end
304
+
305
+ describe "#parse_float" do
306
+ it "Converts Strings into Floats" do
307
+ expect(ETL.parse_float("1.1")).to eq(1.1)
308
+ expect(ETL.parse_float("0.0")).to eq(0.0)
309
+ end
310
+
311
+ it "Supports passing floats for convenience" do
312
+ expect(ETL.parse_float(1.1)).to eq(1.1)
313
+ expect(ETL.parse_float(0.0)).to eq(0.0)
314
+ end
315
+
316
+ it "Returns nil if no value is found instead of 0" do
317
+ expect(ETL.parse_float("")).to eq(nil)
318
+ expect(ETL.parse_float(nil)).to eq(nil)
319
+ end
320
+
321
+ it "Raises an TextParseError if String is nil or empty and is required" do
322
+ expect { ETL.parse_float(nil, required: true) }.to raise_error(TextParseError)
323
+ expect { ETL.parse_float("", required: true ) }.to raise_error(TextParseError)
324
+ end
325
+
326
+ it "Supports :default option" do
327
+ expect(ETL.parse_float("", default: '3.3', required: false)).to eq(3.3)
328
+ expect(ETL.parse_float("", default: 2.2, required: false)).to eq(2.2)
329
+ end
330
+
331
+ it "Enforces strict length validation to avoid loss of precision" do
332
+ expect { ETL.parse_float("2.2", length: 1, strict: true) }.to raise_error(TextParseError)
333
+ end
334
+
335
+ it "Supports a :valid_values validation semantics" do
336
+ expect { ETL.parse_float("123.1", valid_values: /456/) }.to raise_error(TextParseError)
337
+ expect { ETL.parse_float("123.1", valid_values: ['456']) }.to raise_error(TextParseError)
338
+ end
339
+ end
340
+
341
+ describe "#parse_actv_ind!" do
342
+ it "Modifies the insert_str and values_str based on a CSV::Row match" do
343
+ insert_str = ""; values_str = "";
344
+ row = CSV::Row.new(['actv_ind'.to_sym], ['Y'], true)
345
+ ETL.parse_actv_ind!(row, insert_str, values_str)
346
+ expect(insert_str).to eq("ACTV_IND,")
347
+ expect(values_str).to eq("'Y',")
348
+ end
349
+
350
+ it "allows for lowercase input Strings" do
351
+ insert_str = ""; values_str = "";
352
+ row = CSV::Row.new(['actv_ind'.to_sym], ['n'], true)
353
+ ETL.parse_actv_ind!(row, insert_str, values_str)
354
+ expect(insert_str).to eq("ACTV_IND,")
355
+ expect(values_str).to eq("'N',")
356
+ end
357
+
358
+ it "Returns a default value of 'Y' and does not raise an TextParseError if nil or empty" do
359
+ insert_str = ""; values_str = "";
360
+ row = CSV::Row.new(['actv_ind'.to_sym], [nil], true)
361
+ expect { ETL.parse_actv_ind!(row, insert_str, values_str) }.not_to raise_error
362
+ expect(insert_str).to eq("ACTV_IND,")
363
+ expect(values_str).to eq("'Y',")
364
+ insert_str = ""; values_str = "";
365
+ row = CSV::Row.new(['actv_ind'.to_sym], [''], true)
366
+ expect { ETL.parse_actv_ind!(row, insert_str, values_str) }.not_to raise_error
367
+ expect(insert_str).to eq("ACTV_IND,")
368
+ expect(values_str).to eq("'Y',")
369
+ end
370
+
371
+ it "Raises an TextParseError if not a valid 'Y/N' value" do
372
+ insert_str = ""; values_str = "";
373
+ row = CSV::Row.new(['actv_ind'.to_sym], ["Q"], true)
374
+ expect { ETL.parse_actv_ind!(row, insert_str, values_str) }.to raise_error(TextParseError)
375
+ end
376
+
377
+ it "Raises an TextParseError if length exceeds 1 characters" do
378
+ insert_str = ""; values_str = "";
379
+ row = CSV::Row.new(['actv_ind'.to_sym], ["x" * 2], true)
380
+ expect { ETL.parse_actv_ind!(row, insert_str, values_str) }.to raise_error(TextParseError)
381
+ end
382
+ end
383
+
384
+ describe "#to_symbol" do
385
+ it "is downcased" do
386
+ sample = ETL.to_symbol "ROLODEX_ID"
387
+ expect(sample).to eq("rolodex_id".to_sym)
388
+ end
389
+
390
+ it "spaces are replaced with underscores" do
391
+ sample = ETL.to_symbol "ROLODEX_ID "
392
+ expect(sample).to eq("rolodex_id_".to_sym)
393
+ end
394
+
395
+ it "non-word characters are dropped" do
396
+ sample = ETL.to_symbol "ROLODEX_ID&"
397
+ expect(sample).to eq("rolodex_id".to_sym)
398
+ end
399
+ end
400
+
401
+ describe "#parse_csv_command_line_options" do
402
+ executable = "foo.rb"
403
+ args = ["/some/file.csv"]
404
+
405
+ it "sets opt[:sql_filename] to a reasonable default value when none is suppied" do
406
+ opt = ETL.parse_csv_command_line_options(executable, args)
407
+ expect(opt[:sql_filename]).to eq("/some/file.sql")
408
+ end
409
+
410
+ it "sets opt[:csv_filename] to args[0] when a value is supplied" do
411
+ opt = ETL.parse_csv_command_line_options(executable, args)
412
+ expect(opt[:csv_filename]).to eq("/some/file.csv")
413
+ end
414
+
415
+ it "allows the caller to specify opt[:sql_filename] on the command line" do
416
+ subject = { sql_filename: "/some/other/file.sql" }
417
+ opt = ETL.parse_csv_command_line_options(executable, args, subject)
418
+ expect(opt[:sql_filename]).to eq("/some/other/file.sql")
419
+ end
420
+
421
+ it "provides good default CSV parsing options" do
422
+ opt = ETL.parse_csv_command_line_options(executable, args)
423
+ expect(opt[:csv_options][:headers]).to eq(:first_row)
424
+ expect(opt[:csv_options][:header_converters]).to eq(:symbol)
425
+ expect(opt[:csv_options][:skip_blanks]).to eq(true)
426
+ expect(opt[:csv_options][:col_sep]).to eq(',')
427
+ expect(opt[:csv_options][:quote_char]).to eq('"')
428
+ end
429
+
430
+ it "allows you to override the CSV parsing options" do
431
+ opt = ETL.parse_csv_command_line_options(executable, args, csv_options: {col_sep: '|', quote_char: '`'})
432
+ expect(opt[:csv_options][:col_sep]).to eq('|')
433
+ expect(opt[:csv_options][:quote_char]).to eq('`')
434
+ end
435
+
436
+ it "exits with code 1 if no csv_filename is provided on the command line" do
437
+ begin
438
+ ETL.parse_csv_command_line_options(executable, [])
439
+ rescue SystemExit => e
440
+ expect(e.status).to eq 1 # exited with failure status
441
+ else
442
+ raise "Unexpected Exception found: #{e.class}"
443
+ end
444
+ end
445
+ end
446
+
447
+ end
@@ -0,0 +1,27 @@
1
+ # rSmart client library and command-line tool to help interact with rSmart's cloud APIs.
2
+ # Copyright (C) 2014 The rSmart Group, Inc.
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU Affero General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU Affero General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU Affero General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+
17
+ require 'spec_helper'
18
+ require 'rsmart_toolbox'
19
+
20
+ RSpec.describe "RsmartToolbox" do
21
+
22
+ it "has a VERSION number" do
23
+ expect( RsmartToolbox::VERSION ).not_to be_nil
24
+ expect( RsmartToolbox::VERSION ).to match /^(\d+)\.*(\d+)\.*(\d+)*\.*(\d+)*$/
25
+ end
26
+
27
+ end