rsmart_toolbox 0.1

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.
@@ -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