stockboy 0.9.0 → 0.10.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +11 -0
- data/lib/stockboy/attribute.rb +15 -2
- data/lib/stockboy/attribute_map.rb +2 -1
- data/lib/stockboy/candidate_record.rb +30 -6
- data/lib/stockboy/job.rb +3 -3
- data/lib/stockboy/readers/csv.rb +20 -4
- data/lib/stockboy/translations/date.rb +35 -5
- data/lib/stockboy/translations/uk_date.rb +4 -8
- data/lib/stockboy/translations/us_date.rb +2 -12
- data/lib/stockboy/version.rb +1 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/stockboy/attribute_map_spec.rb +7 -2
- data/spec/stockboy/attribute_spec.rb +22 -2
- data/spec/stockboy/candidate_record_spec.rb +16 -3
- data/spec/stockboy/configurator_spec.rb +4 -4
- data/spec/stockboy/filter_chain_spec.rb +1 -1
- data/spec/stockboy/filter_spec.rb +4 -4
- data/spec/stockboy/filters/missing_email_spec.rb +4 -4
- data/spec/stockboy/job_spec.rb +9 -9
- data/spec/stockboy/provider_repeater_spec.rb +3 -3
- data/spec/stockboy/provider_spec.rb +3 -3
- data/spec/stockboy/providers/file_spec.rb +6 -6
- data/spec/stockboy/providers/ftp_spec.rb +3 -3
- data/spec/stockboy/providers/imap_spec.rb +5 -5
- data/spec/stockboy/providers_spec.rb +4 -4
- data/spec/stockboy/readers/csv_spec.rb +13 -4
- data/spec/stockboy/readers/spreadsheet_spec.rb +5 -5
- data/spec/stockboy/readers/xml_spec.rb +4 -4
- data/spec/stockboy/readers_spec.rb +3 -3
- data/spec/stockboy/template_file_spec.rb +1 -1
- data/spec/stockboy/translations/date_spec.rb +1 -1
- data/spec/stockboy/translations/decimal_spec.rb +1 -1
- data/spec/stockboy/translations/integer_spec.rb +1 -1
- data/spec/stockboy/translations/time_spec.rb +1 -1
- data/spec/stockboy/translations/uk_date_spec.rb +1 -1
- data/spec/stockboy/translations/us_date_spec.rb +1 -1
- data/spec/stockboy/translations_spec.rb +3 -3
- data/stockboy.gemspec +1 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f39685f84983ede11ee77ffe5ab39982e37ddac7
|
4
|
+
data.tar.gz: c16642a9c384a71c72def32658d8e4764ab46b14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d7f4377f7973b9e88d91ea807c12987385e594ca9f55b1594dbdc652ad8aea3cf674e431394ed840d60b11454404975cf4d9dc77b3a8a3409a760b1668316ef
|
7
|
+
data.tar.gz: 7dd65ee93fbf9a6df91f052b5505fe13d2b4d1ae2901a612a7ccd87589f1f5662cf8154f5d7d397a4d59779c23cd4ddf4abbee5c70f78a0204130c9cd329e9f5
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -210,6 +210,17 @@ Translations are applied in order when given as an array. Since translators are
|
|
210
210
|
designed to handle invalid data, they will catch exceptions and return a `nil`
|
211
211
|
so it's a good idea to have default values at the end of the chain.
|
212
212
|
|
213
|
+
#### ignore:
|
214
|
+
If a field needs to be omitted when exporting to hash (to avoid downstream
|
215
|
+
clients clobbering existing values with nil), specify the option with a
|
216
|
+
callable object or a symbol that can be called against the output field value:
|
217
|
+
|
218
|
+
email ignore: ->(r) { not r.email.include?("@") }
|
219
|
+
score ignore: :zero?
|
220
|
+
|
221
|
+
The callable object receives the whole record context like when translating
|
222
|
+
with the "as" option.
|
223
|
+
|
213
224
|
#### Built-in attribute translators:
|
214
225
|
|
215
226
|
* [`:boolean`][bool]
|
data/lib/stockboy/attribute.rb
CHANGED
@@ -3,9 +3,22 @@ module Stockboy
|
|
3
3
|
# Struct-like value object for holding mapping & translation details from
|
4
4
|
# input data fields
|
5
5
|
#
|
6
|
-
class Attribute < Struct.new(:to, :from, :translators)
|
6
|
+
class Attribute < Struct.new(:to, :from, :translators, :ignore_condition)
|
7
|
+
def ignore?(context)
|
8
|
+
if Symbol === ignore_condition
|
9
|
+
context.public_send(to).public_send(ignore_condition)
|
10
|
+
elsif ignore_condition.respond_to?(:call)
|
11
|
+
ignore_condition.call(context)
|
12
|
+
else
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
7
17
|
def inspect
|
8
|
-
"#<Stockboy::Attribute to=#{to.inspect}, from=#{from.inspect}
|
18
|
+
"#<Stockboy::Attribute to=#{to.inspect}, from=#{from.inspect}%s%s>" % [
|
19
|
+
(", translators=#{translators}" if translators),
|
20
|
+
(", ignore=#{ignore_condition.inspect}" if ignore_condition)
|
21
|
+
]
|
9
22
|
end
|
10
23
|
end
|
11
24
|
end
|
@@ -52,8 +52,9 @@ module Stockboy
|
|
52
52
|
from = opts.fetch(:from, key)
|
53
53
|
from = from.to_s.freeze if from.is_a? Symbol
|
54
54
|
translators = Array(opts[:as]).map { |t| Translations.translator_for(to, t) }
|
55
|
+
ignore_condition = opts[:ignore]
|
55
56
|
define_singleton_method(key) { @map[key] }
|
56
|
-
@map[key] = Attribute.new(to, from, translators)
|
57
|
+
@map[key] = Attribute.new(to, from, translators, ignore_condition)
|
57
58
|
end
|
58
59
|
|
59
60
|
# Fetch the attribute corresponding to the source field name
|
@@ -19,6 +19,7 @@ module Stockboy
|
|
19
19
|
@map = map
|
20
20
|
@table = reuse_frozen_hash_keys(attrs, map)
|
21
21
|
@tr_table = Hash.new
|
22
|
+
@ignored_fields = []
|
22
23
|
freeze
|
23
24
|
end
|
24
25
|
|
@@ -27,11 +28,24 @@ module Stockboy
|
|
27
28
|
# @return [Hash]
|
28
29
|
#
|
29
30
|
def to_hash
|
31
|
+
bulk_hash.tap do |out|
|
32
|
+
tmp_context = SourceRecord.new(out, @table)
|
33
|
+
@map.each_with_object(out) do |col|
|
34
|
+
out.delete(col.to) if ignore?(col, tmp_context)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
alias_method :attributes, :to_hash
|
39
|
+
|
40
|
+
# Mapped output hash including ignored values
|
41
|
+
#
|
42
|
+
# @return [Hash]
|
43
|
+
#
|
44
|
+
def bulk_hash
|
30
45
|
Hash.new.tap do |out|
|
31
46
|
@map.each { |col| out[col.to] = translate(col) }
|
32
47
|
end
|
33
48
|
end
|
34
|
-
alias_method :attributes, :to_hash
|
35
49
|
|
36
50
|
# Return the original values mapped to attribute keys
|
37
51
|
#
|
@@ -80,7 +94,7 @@ module Stockboy
|
|
80
94
|
# input.email # => "ME@EXAMPLE.COM "
|
81
95
|
#
|
82
96
|
def input
|
83
|
-
SourceRecord.new(
|
97
|
+
SourceRecord.new(raw_hash, @table)
|
84
98
|
end
|
85
99
|
|
86
100
|
# Data structure representing the record's mapped & translated output values
|
@@ -91,7 +105,7 @@ module Stockboy
|
|
91
105
|
# output.email # => "me@example.com"
|
92
106
|
#
|
93
107
|
def output
|
94
|
-
MappedRecord.new(
|
108
|
+
MappedRecord.new(bulk_hash)
|
95
109
|
end
|
96
110
|
|
97
111
|
private
|
@@ -102,14 +116,24 @@ module Stockboy
|
|
102
116
|
fields = raw_hash
|
103
117
|
tr_input = col.translators.reduce(input) do |value, tr|
|
104
118
|
begin
|
105
|
-
fields[
|
119
|
+
fields[key] = tr[value]
|
106
120
|
SourceRecord.new(fields, @table)
|
107
121
|
rescue
|
108
|
-
fields[
|
122
|
+
fields[key] = nil
|
109
123
|
break SourceRecord.new(fields, @table)
|
110
124
|
end
|
111
125
|
end
|
112
|
-
@tr_table[
|
126
|
+
@tr_table[key] = tr_input.public_send(key)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def ignore?(col, context)
|
131
|
+
return true if @ignored_fields.include? col.to
|
132
|
+
if col.ignore?(context)
|
133
|
+
@ignored_fields << col.to
|
134
|
+
true
|
135
|
+
else
|
136
|
+
false
|
113
137
|
end
|
114
138
|
end
|
115
139
|
|
data/lib/stockboy/job.rb
CHANGED
@@ -196,12 +196,12 @@ module Stockboy
|
|
196
196
|
# @return [String]
|
197
197
|
#
|
198
198
|
def inspect
|
199
|
-
prov = "provider=#{
|
200
|
-
read = "reader=#{
|
199
|
+
prov = "provider=#{Providers.all.key(provider.class) || provider.class}"
|
200
|
+
read = "reader=#{Readers.all.key(reader.class) || reader.class}"
|
201
201
|
attr = "attributes=#{attributes.map(&:to)}"
|
202
202
|
filt = "filters=#{filters.keys}"
|
203
203
|
cnts = "record_counts=#{record_counts}"
|
204
|
-
"#<#{self.class}:#{self.object_id} #{
|
204
|
+
"#<#{self.class}:#{self.object_id} #{prov}, #{read}, #{attr}, #{filt}, #{cnts}>"
|
205
205
|
end
|
206
206
|
|
207
207
|
private
|
data/lib/stockboy/readers/csv.rb
CHANGED
@@ -94,11 +94,27 @@ module Stockboy::Readers
|
|
94
94
|
|
95
95
|
private
|
96
96
|
|
97
|
+
# Clean incoming data based on set encoding or best information
|
98
|
+
#
|
99
|
+
# 1. Assign the given input encoding setting if available
|
100
|
+
# 2. Scrub invalid characters for the encoding. (Scrubbing does not apply
|
101
|
+
# for BINARY input, which is undefined.)
|
102
|
+
# 3. Encode to UTF-8 with considerations for undefined input. The main
|
103
|
+
# issue are control characters that are absent in UTF-8 (and ISO-8859-1)
|
104
|
+
# but are common printable characters in Windows-1252, so we preserve
|
105
|
+
# this range as a best guess.
|
106
|
+
# 4. Delete null bytes that are inserted as terminators by some "CSV" output
|
107
|
+
# 5. Delete leading/trailing garbage lines based on settings
|
108
|
+
#
|
97
109
|
def sanitize(data)
|
98
|
-
data
|
99
|
-
data
|
100
|
-
|
101
|
-
|
110
|
+
data = data.dup
|
111
|
+
data.force_encoding encoding if encoding
|
112
|
+
data.scrub!
|
113
|
+
data.encode! Encoding::UTF_8, universal_newline: true, fallback: proc { |c|
|
114
|
+
c.force_encoding(Encoding::Windows_1252) if (127..159).cover? c.ord
|
115
|
+
}
|
116
|
+
data.delete! 0.chr
|
117
|
+
data.chomp!
|
102
118
|
from = row_start_index(data, skip_header_rows)
|
103
119
|
to = row_end_index(data, skip_footer_rows)
|
104
120
|
data[from..to]
|
@@ -46,12 +46,42 @@ module Stockboy::Translations
|
|
46
46
|
::Date.parse(value)
|
47
47
|
end
|
48
48
|
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
module PatternMatching
|
50
|
+
private
|
51
|
+
|
52
|
+
module ClassMethods
|
53
|
+
def match(*patterns)
|
54
|
+
@patterns = patterns
|
55
|
+
end
|
56
|
+
attr_reader :patterns
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.included(base)
|
60
|
+
base.extend ClassMethods
|
61
|
+
end
|
62
|
+
|
63
|
+
# Match date strings with 4-digit year
|
64
|
+
MATCH_YYYY = %r"\A\d{1,2}(/|-)\d{1,2}\1\d{4}\z"
|
65
|
+
SLASH = '/'.freeze
|
66
|
+
|
67
|
+
# @return [Date]
|
68
|
+
#
|
69
|
+
def parse_date(value)
|
70
|
+
::Date.strptime(value, date_format(value))
|
71
|
+
end
|
72
|
+
|
73
|
+
def date_format(value)
|
74
|
+
self.class.patterns[yy_index(value) + sep_index(value)].freeze
|
75
|
+
end
|
52
76
|
|
53
|
-
|
54
|
-
|
77
|
+
def yy_index(value)
|
78
|
+
value =~ MATCH_YYYY ? 0 : 2
|
79
|
+
end
|
80
|
+
|
81
|
+
def sep_index(value)
|
82
|
+
value.include?(SLASH) ? 1 : 0
|
83
|
+
end
|
84
|
+
end
|
55
85
|
|
56
86
|
end
|
57
87
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'stockboy/translator'
|
2
|
-
require 'stockboy/translations/
|
2
|
+
require 'stockboy/translations/date'
|
3
3
|
|
4
4
|
module Stockboy::Translations
|
5
5
|
|
@@ -26,14 +26,10 @@ module Stockboy::Translations
|
|
26
26
|
# record.check_in = "1/2/12"
|
27
27
|
# date.translate(record, :check_in) # => #<Date 2012-02-01>
|
28
28
|
#
|
29
|
-
class UKDate < Stockboy::Translations::
|
29
|
+
class UKDate < Stockboy::Translations::Date
|
30
|
+
include Stockboy::Translations::Date::PatternMatching
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
def date_format(value)
|
34
|
-
x = separator(value)
|
35
|
-
value =~ MATCH_YYYY ? "%d#{x}%m#{x}%Y" : "%d#{x}%m#{x}%y"
|
36
|
-
end
|
32
|
+
match '%d-%m-%Y', '%d/%m/%Y', '%d-%m-%y', '%d/%m/%y'
|
37
33
|
|
38
34
|
end
|
39
35
|
end
|
@@ -27,19 +27,9 @@ module Stockboy::Translations
|
|
27
27
|
# date.translate(record, :check_in) # => #<Date 2012-02-01>
|
28
28
|
#
|
29
29
|
class USDate < Stockboy::Translations::Date
|
30
|
+
include Stockboy::Translations::Date::PatternMatching
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
# @return [Date]
|
34
|
-
#
|
35
|
-
def parse_date(value)
|
36
|
-
::Date.strptime(value, date_format(value))
|
37
|
-
end
|
38
|
-
|
39
|
-
def date_format(value)
|
40
|
-
x = separator(value)
|
41
|
-
value =~ MATCH_YYYY ? "%m#{x}%d#{x}%Y" : "%m#{x}%d#{x}%y"
|
42
|
-
end
|
32
|
+
match '%m-%d-%Y', '%m/%d/%Y', '%m-%d-%y', '%m/%d/%y'
|
43
33
|
|
44
34
|
end
|
45
35
|
end
|
data/lib/stockboy/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -7,6 +7,7 @@ module Stockboy
|
|
7
7
|
subject do
|
8
8
|
AttributeMap.new do
|
9
9
|
email
|
10
|
+
score ignore: :zero?
|
10
11
|
updated_at from: 'statusDate', as: [proc{ |v| Date.parse(v) }]
|
11
12
|
end
|
12
13
|
end
|
@@ -43,6 +44,10 @@ module Stockboy
|
|
43
44
|
subject[:updated_at].translators.first.call("2012-01-01").should == Date.new(2012,1,1)
|
44
45
|
end
|
45
46
|
|
47
|
+
it "sets ignore conditions" do
|
48
|
+
expect( subject[:score].ignore_condition ).to be :zero?
|
49
|
+
end
|
50
|
+
|
46
51
|
it "has attr accessors" do
|
47
52
|
subject.email.should be_a Attribute
|
48
53
|
end
|
@@ -52,8 +57,8 @@ module Stockboy
|
|
52
57
|
end
|
53
58
|
|
54
59
|
it "is enumerable" do
|
55
|
-
subject.map(&:from).should == ["email", "statusDate"]
|
56
|
-
subject.map(&:to).should == [:email, :updated_at]
|
60
|
+
subject.map(&:from).should == ["email", "score", "statusDate"]
|
61
|
+
subject.map(&:to).should == [:email, :score, :updated_at]
|
57
62
|
end
|
58
63
|
|
59
64
|
describe "#insert" do
|
@@ -4,8 +4,28 @@ require 'stockboy/attribute'
|
|
4
4
|
module Stockboy
|
5
5
|
describe Attribute do
|
6
6
|
it "describes its attrs" do
|
7
|
-
attr = Attribute.new :outfield, "infield", [:one, :two]
|
8
|
-
attr.inspect.should == %{#<Stockboy::Attribute to=:outfield, from="infield", translators=[:one, :two]
|
7
|
+
attr = Attribute.new :outfield, "infield", [:one, :two], :nil?
|
8
|
+
attr.inspect.should == %{#<Stockboy::Attribute to=:outfield, from="infield", translators=[:one, :two], ignore=:nil?>}
|
9
|
+
end
|
10
|
+
|
11
|
+
describe :ignore? do
|
12
|
+
it "is false by default" do
|
13
|
+
attr = Attribute.new :email, "email", []
|
14
|
+
attr.ignore?(double email: "").should be false
|
15
|
+
end
|
16
|
+
|
17
|
+
it "extracts symbols from a record" do
|
18
|
+
attr = Attribute.new :email, "email", [], :blank?
|
19
|
+
attr.ignore?(double email: "").should be true
|
20
|
+
attr.ignore?(double email: "@").should be false
|
21
|
+
end
|
22
|
+
|
23
|
+
it "yields records to a proc" do
|
24
|
+
attr = Attribute.new :email, "email", [], ->(r) { not r.email.include? "@" }
|
25
|
+
attr.ignore?(double email: "").should be true
|
26
|
+
attr.ignore?(double email: "@").should be false
|
27
|
+
end
|
28
|
+
|
9
29
|
end
|
10
30
|
end
|
11
31
|
end
|
@@ -33,6 +33,13 @@ module Stockboy
|
|
33
33
|
subject = CandidateRecord.new(hash_attrs, map).to_hash
|
34
34
|
subject[:full_name].class.should == String
|
35
35
|
end
|
36
|
+
|
37
|
+
it "omits ignored attributes" do
|
38
|
+
invalid_email = ->(r) { r.email.include?('example.com') }
|
39
|
+
map = AttributeMap.new { id ; email ignore: invalid_email }
|
40
|
+
subject = CandidateRecord.new(hash_attrs, map).to_hash
|
41
|
+
subject.should == { :id => '1' }
|
42
|
+
end
|
36
43
|
end
|
37
44
|
|
38
45
|
describe "#raw_hash" do
|
@@ -82,7 +89,7 @@ module Stockboy
|
|
82
89
|
describe "#to_model" do
|
83
90
|
it "should instantiate a new model" do
|
84
91
|
model = Class.new(OpenStruct)
|
85
|
-
model.
|
92
|
+
expect(model).to receive(:new).with({id: '1', name: 'Arthur Dent'})
|
86
93
|
map = AttributeMap.new { id; name from: 'full_name' }
|
87
94
|
subject = CandidateRecord.new(hash_attrs, map)
|
88
95
|
|
@@ -113,6 +120,12 @@ module Stockboy
|
|
113
120
|
subject = CandidateRecord.new(hash_attrs, map)
|
114
121
|
subject.output.name.should == 'Arthur Dent'
|
115
122
|
end
|
123
|
+
|
124
|
+
it "returns translated values for ignored keys" do
|
125
|
+
map = AttributeMap.new { email ignore: ->(r){ r.email.include?("example.com") } }
|
126
|
+
subject = CandidateRecord.new(hash_attrs, map)
|
127
|
+
subject.output.email.should == "adent@example.com"
|
128
|
+
end
|
116
129
|
end
|
117
130
|
end
|
118
131
|
|
@@ -137,12 +150,12 @@ module Stockboy
|
|
137
150
|
|
138
151
|
it "returns nil when raw field is unmatched" do
|
139
152
|
key = subject.partition({beta: proc{ |raw| raw.name =~ /B/ }})
|
140
|
-
key.should
|
153
|
+
key.should be nil
|
141
154
|
end
|
142
155
|
|
143
156
|
it "returns nil when translated field is unmatched" do
|
144
157
|
key = subject.partition({beta: proc{ |raw,out| out.name =~ /B/ }})
|
145
|
-
key.should
|
158
|
+
key.should be nil
|
146
159
|
end
|
147
160
|
end
|
148
161
|
|
@@ -37,7 +37,7 @@ module Stockboy
|
|
37
37
|
end
|
38
38
|
|
39
39
|
it "initializes arguments" do
|
40
|
-
provider_class.
|
40
|
+
expect(provider_class).to receive(:new).with(password:'foo')
|
41
41
|
subject.provider :ftp, password: 'foo'
|
42
42
|
end
|
43
43
|
end
|
@@ -68,7 +68,7 @@ module Stockboy
|
|
68
68
|
|
69
69
|
it "initializes arguments" do
|
70
70
|
reader_stub = double(:reader)
|
71
|
-
reader_class.
|
71
|
+
expect(reader_class).to receive(:new).with(col_sep: '|').and_return(reader_stub)
|
72
72
|
subject.reader reader_class, col_sep: '|'
|
73
73
|
subject.config[:reader].should == reader_stub
|
74
74
|
end
|
@@ -77,7 +77,7 @@ module Stockboy
|
|
77
77
|
describe "#attributes" do
|
78
78
|
it "initializes a block" do
|
79
79
|
attribute_map = double
|
80
|
-
AttributeMap.
|
80
|
+
expect(AttributeMap).to receive(:new).and_return(attribute_map)
|
81
81
|
subject.attributes &proc{}
|
82
82
|
subject.config[:attributes].should be attribute_map
|
83
83
|
end
|
@@ -85,7 +85,7 @@ module Stockboy
|
|
85
85
|
it "replaces existing attributes" do
|
86
86
|
subject.attribute :first_name
|
87
87
|
subject.attributes do last_name end
|
88
|
-
subject.config[:attributes][:first_name].should
|
88
|
+
subject.config[:attributes][:first_name].should be nil
|
89
89
|
subject.config[:attributes][:last_name].should be_an Attribute
|
90
90
|
end
|
91
91
|
end
|
@@ -21,21 +21,21 @@ module Stockboy
|
|
21
21
|
|
22
22
|
context "matching raw value" do
|
23
23
|
it "returns true for match" do
|
24
|
-
filter.call(double(species:"babylichtus"), empty_values).should
|
24
|
+
filter.call(double(species:"babylichtus"), empty_values).should be true
|
25
25
|
end
|
26
26
|
|
27
27
|
it "returns false for no match" do
|
28
|
-
filter.call(double(species:"triceratops"), empty_values).should
|
28
|
+
filter.call(double(species:"triceratops"), empty_values).should be false
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
context "matching translated value" do
|
33
33
|
it "returns true for match" do
|
34
|
-
filter.call(empty_values, double(species:"babelfish")).should
|
34
|
+
filter.call(empty_values, double(species:"babelfish")).should be true
|
35
35
|
end
|
36
36
|
|
37
37
|
it "returns false for no match" do
|
38
|
-
filter.call(empty_values, double(species:"rhinoceros")).should
|
38
|
+
filter.call(empty_values, double(species:"rhinoceros")).should be false
|
39
39
|
end
|
40
40
|
end
|
41
41
|
end
|
@@ -5,22 +5,22 @@ describe Stockboy::Filters::MissingEmail do
|
|
5
5
|
subject(:filter) { described_class.new(:e) }
|
6
6
|
it 'allows email addresses' do
|
7
7
|
record = OpenStruct.new(e: 'me@example.com')
|
8
|
-
filter.call(record, record).should
|
8
|
+
filter.call(record, record).should be false
|
9
9
|
end
|
10
10
|
|
11
11
|
it 'catches empty strings' do
|
12
12
|
record = OpenStruct.new(e: '')
|
13
|
-
filter.call(record, record).should
|
13
|
+
filter.call(record, record).should be true
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'catches hyphen placeholders' do
|
17
17
|
record = OpenStruct.new(e: '-')
|
18
|
-
filter.call(record, record).should
|
18
|
+
filter.call(record, record).should be true
|
19
19
|
end
|
20
20
|
|
21
21
|
it 'uses translated output value' do
|
22
22
|
input = OpenStruct.new(e: '', other: 'me@example.com')
|
23
23
|
output = OpenStruct.new(e: input.other)
|
24
|
-
filter.call(input, output).should
|
24
|
+
filter.call(input, output).should be false
|
25
25
|
end
|
26
26
|
end
|
data/spec/stockboy/job_spec.rb
CHANGED
@@ -85,22 +85,22 @@ module Stockboy
|
|
85
85
|
end
|
86
86
|
|
87
87
|
it "should read a file from a path" do
|
88
|
-
File.
|
88
|
+
expect(File).to receive(:read).with("#{jobs_path}/test_job.rb")
|
89
89
|
Job.define("test_job")
|
90
90
|
end
|
91
91
|
|
92
92
|
it "assigns a registered provider from a symbol" do
|
93
|
-
Stockboy::Providers.
|
94
|
-
|
95
|
-
|
93
|
+
expect(Stockboy::Providers).to receive(:find)
|
94
|
+
.with(:ftp)
|
95
|
+
.and_return(TestProvider)
|
96
96
|
job = Job.define("test_job")
|
97
97
|
job.provider.should be_a TestProvider
|
98
98
|
end
|
99
99
|
|
100
100
|
it "assigns a registered reader from a symbol" do
|
101
|
-
Stockboy::Readers.
|
102
|
-
|
103
|
-
|
101
|
+
expect(Stockboy::Readers).to receive(:find)
|
102
|
+
.with(:csv)
|
103
|
+
.and_return(TestReader)
|
104
104
|
job = Job.define("test_job")
|
105
105
|
job.reader.should be_a TestReader
|
106
106
|
end
|
@@ -247,9 +247,9 @@ module Stockboy
|
|
247
247
|
end
|
248
248
|
|
249
249
|
it "indicates if the job has been processed" do
|
250
|
-
job.processed?.should
|
250
|
+
job.processed?.should be false
|
251
251
|
job.process
|
252
|
-
job.processed?.should
|
252
|
+
job.processed?.should be true
|
253
253
|
end
|
254
254
|
end
|
255
255
|
|
@@ -76,7 +76,7 @@ module Stockboy
|
|
76
76
|
|
77
77
|
describe "#data_size" do
|
78
78
|
subject(:repeater) { ProviderRepeater.new(provider) }
|
79
|
-
its(:data_size) { should
|
79
|
+
its(:data_size) { should be nil }
|
80
80
|
|
81
81
|
context "after iterating" do
|
82
82
|
before { repeater.data do |data| end }
|
@@ -86,7 +86,7 @@ module Stockboy
|
|
86
86
|
|
87
87
|
describe "#data_time" do
|
88
88
|
subject(:repeater) { ProviderRepeater.new(provider) }
|
89
|
-
its(:data_time) { should
|
89
|
+
its(:data_time) { should be nil }
|
90
90
|
|
91
91
|
context "after iterating" do
|
92
92
|
before { repeater.data do |data| end }
|
@@ -96,7 +96,7 @@ module Stockboy
|
|
96
96
|
|
97
97
|
describe "#data?" do
|
98
98
|
subject(:repeater) { ProviderRepeater.new(provider) }
|
99
|
-
its(:data?) { should
|
99
|
+
its(:data?) { should be nil }
|
100
100
|
|
101
101
|
context "after iterating" do
|
102
102
|
before { repeater.data do |data| end }
|
@@ -76,7 +76,7 @@ module Stockboy
|
|
76
76
|
|
77
77
|
describe "#data_size" do
|
78
78
|
subject(:provider) { ProviderSubclass.new }
|
79
|
-
its(:data_size) { should
|
79
|
+
its(:data_size) { should be nil }
|
80
80
|
|
81
81
|
context "after fetching" do
|
82
82
|
before { provider.fetch_data }
|
@@ -86,7 +86,7 @@ module Stockboy
|
|
86
86
|
|
87
87
|
describe "#data_time" do
|
88
88
|
subject(:provider) { ProviderSubclass.new }
|
89
|
-
its(:data_time) { should
|
89
|
+
its(:data_time) { should be nil }
|
90
90
|
|
91
91
|
context "after fetching" do
|
92
92
|
before { provider.fetch_data }
|
@@ -96,7 +96,7 @@ module Stockboy
|
|
96
96
|
|
97
97
|
describe "#data?" do
|
98
98
|
subject(:provider) { ProviderSubclass.new }
|
99
|
-
its(:data?) { should
|
99
|
+
its(:data?) { should be nil }
|
100
100
|
|
101
101
|
context "after fetching" do
|
102
102
|
before { provider.fetch_data }
|
@@ -52,7 +52,7 @@ module Stockboy
|
|
52
52
|
|
53
53
|
context "with an unmatched string" do
|
54
54
|
before { provider.file_name = "missing" }
|
55
|
-
it { should
|
55
|
+
it { should be nil }
|
56
56
|
end
|
57
57
|
end
|
58
58
|
|
@@ -61,8 +61,8 @@ module Stockboy
|
|
61
61
|
|
62
62
|
it "fails with an error if the file doesn't exist" do
|
63
63
|
provider.file_name = "missing-file.csv"
|
64
|
-
provider.data.should
|
65
|
-
provider.valid?.should
|
64
|
+
provider.data.should be nil
|
65
|
+
provider.valid?.should be false
|
66
66
|
provider.errors.first.should match /not found/
|
67
67
|
end
|
68
68
|
|
@@ -90,21 +90,21 @@ module Stockboy
|
|
90
90
|
it "skips old files with :since" do
|
91
91
|
expect_any_instance_of(::File).to receive(:mtime).and_return last_week
|
92
92
|
provider.since = recently
|
93
|
-
provider.data.should
|
93
|
+
provider.data.should be nil
|
94
94
|
provider.errors.first.should == "no new files since #{recently}"
|
95
95
|
end
|
96
96
|
|
97
97
|
it "skips large files with :file_smaller" do
|
98
98
|
expect_any_instance_of(::File).to receive(:size).and_return 1001
|
99
99
|
provider.file_smaller = 1000
|
100
|
-
provider.data.should
|
100
|
+
provider.data.should be nil
|
101
101
|
provider.errors.first.should == "file size larger than 1000"
|
102
102
|
end
|
103
103
|
|
104
104
|
it "skips small files with :file_larger" do
|
105
105
|
expect_any_instance_of(::File).to receive(:size).and_return 999
|
106
106
|
provider.file_larger = 1000
|
107
|
-
provider.data.should
|
107
|
+
provider.data.should be nil
|
108
108
|
provider.errors.first.should == "file size smaller than 1000"
|
109
109
|
end
|
110
110
|
end
|
@@ -49,8 +49,8 @@ module Stockboy
|
|
49
49
|
provider.client { |f| connection = f }
|
50
50
|
|
51
51
|
connection.should be_a Net::FTP
|
52
|
-
connection.binary.should
|
53
|
-
connection.passive.should
|
52
|
+
connection.binary.should be true
|
53
|
+
connection.passive.should be true
|
54
54
|
end
|
55
55
|
|
56
56
|
it "should return yielded result" do
|
@@ -94,7 +94,7 @@ module Stockboy
|
|
94
94
|
|
95
95
|
provider.file_newer = Time.new(2010,1,1)
|
96
96
|
|
97
|
-
provider.data.should
|
97
|
+
provider.data.should be nil
|
98
98
|
end
|
99
99
|
end
|
100
100
|
|
@@ -64,7 +64,7 @@ module Stockboy
|
|
64
64
|
|
65
65
|
context "with no messages found" do
|
66
66
|
before { mock_imap_search [] => [] }
|
67
|
-
it { should
|
67
|
+
it { should be nil }
|
68
68
|
end
|
69
69
|
|
70
70
|
context "with a found message" do
|
@@ -79,7 +79,7 @@ module Stockboy
|
|
79
79
|
|
80
80
|
context "validating incorrect filename string" do
|
81
81
|
before { provider.attachment = "wrong_report.csv" }
|
82
|
-
it { should
|
82
|
+
it { should be nil }
|
83
83
|
end
|
84
84
|
|
85
85
|
context "validating correct filename pattern" do
|
@@ -89,7 +89,7 @@ module Stockboy
|
|
89
89
|
|
90
90
|
context "validating incorrect filename pattern" do
|
91
91
|
before { provider.attachment = /^wrong.*\.csv$/ }
|
92
|
-
it { should
|
92
|
+
it { should be nil }
|
93
93
|
end
|
94
94
|
|
95
95
|
context "validating correct smaller size" do
|
@@ -99,7 +99,7 @@ module Stockboy
|
|
99
99
|
|
100
100
|
context "validating incorrect smaller size" do
|
101
101
|
before { provider.smaller_than = 10 }
|
102
|
-
it { should
|
102
|
+
it { should be nil }
|
103
103
|
end
|
104
104
|
|
105
105
|
context "validating correct larger size" do
|
@@ -109,7 +109,7 @@ module Stockboy
|
|
109
109
|
|
110
110
|
context "validating correct larger size" do
|
111
111
|
before { provider.larger_than = 2048 }
|
112
|
-
it { should
|
112
|
+
it { should be nil }
|
113
113
|
end
|
114
114
|
|
115
115
|
end
|
@@ -4,25 +4,25 @@ require 'stockboy/providers'
|
|
4
4
|
module Stockboy
|
5
5
|
describe Providers do
|
6
6
|
|
7
|
-
let(:provider) {
|
7
|
+
let(:provider) { Class.new }
|
8
8
|
|
9
9
|
describe ".register" do
|
10
10
|
it "registers a key and class" do
|
11
|
-
Providers.register(:snailmail, provider).should
|
11
|
+
Providers.register(:snailmail, provider).should be provider
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
describe ".find" do
|
16
16
|
it "returns a provider class" do
|
17
17
|
Providers.register(:snailmail, provider)
|
18
|
-
Providers.find(:snailmail).should
|
18
|
+
Providers.find(:snailmail).should be provider
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
describe ".[]" do
|
23
23
|
it "returns a provider class" do
|
24
24
|
Providers.register(:snailmail, provider)
|
25
|
-
Providers[:snailmail].should
|
25
|
+
Providers[:snailmail].should be provider
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -7,10 +7,10 @@ module Stockboy
|
|
7
7
|
subject(:reader) { Stockboy::Readers::CSV.new }
|
8
8
|
|
9
9
|
describe "default options" do
|
10
|
-
its(:row_sep) { should
|
11
|
-
its(:col_sep) { should
|
12
|
-
its(:quote_char) { should
|
13
|
-
its(:headers) { should
|
10
|
+
its(:row_sep) { should be nil }
|
11
|
+
its(:col_sep) { should be nil }
|
12
|
+
its(:quote_char) { should be nil }
|
13
|
+
its(:headers) { should be true }
|
14
14
|
its(:skip_header_rows) { should == 0 }
|
15
15
|
its(:skip_footer_rows) { should == 0 }
|
16
16
|
end
|
@@ -51,6 +51,15 @@ module Stockboy
|
|
51
51
|
[{"city" => "Vancouver", "state" => nil, "country" => "Canada"}]
|
52
52
|
end
|
53
53
|
|
54
|
+
it "scrubs invalid encoding characters in Unicode" do
|
55
|
+
reader.headers = %w[depart arrive]
|
56
|
+
reader.encoding = 'UTF-8'
|
57
|
+
garbage = 191.chr.force_encoding('UTF-8')
|
58
|
+
data = "Z#{garbage}rich,Genève"
|
59
|
+
reader.parse(data).should ==
|
60
|
+
[{"depart" => "Z\u{FFFD}rich", "arrive" => "Genève"}]
|
61
|
+
end
|
62
|
+
|
54
63
|
it "strips preamble header rows" do
|
55
64
|
reader.skip_header_rows = 2
|
56
65
|
data = "IGNORE\r\nCOMMENTS\r\nid,name\r\n42,Arthur Dent"
|
@@ -85,12 +85,12 @@ module Stockboy
|
|
85
85
|
|
86
86
|
describe "#sheet", pending: "Hard to test this due to roo. Needs a test case with fixtures" do
|
87
87
|
let(:sheets) { ['Towels', 'Lemons'] }
|
88
|
-
let(:
|
89
|
-
let(:spreadsheet) {
|
88
|
+
let(:be_selected) { receive(:default_sheet=) }
|
89
|
+
let(:spreadsheet) { double(:spreadsheet, sheets: sheets) }
|
90
90
|
before { subject.stub!(:with_spreadsheet).and_yield(spreadsheet) }
|
91
91
|
|
92
92
|
context "with :first" do
|
93
|
-
|
93
|
+
before { expect(spreadsheet).to be_selected.with('Towels') }
|
94
94
|
|
95
95
|
it "calls on first sheet by name" do
|
96
96
|
subject.sheet = :first
|
@@ -99,7 +99,7 @@ module Stockboy
|
|
99
99
|
end
|
100
100
|
|
101
101
|
context "with a string" do
|
102
|
-
|
102
|
+
before { expect(spreadsheet).to be_selected.with('Towels') }
|
103
103
|
|
104
104
|
it "passes unchanged" do
|
105
105
|
subject.sheet = 'Towels'
|
@@ -108,7 +108,7 @@ module Stockboy
|
|
108
108
|
end
|
109
109
|
|
110
110
|
context "with an integer" do
|
111
|
-
|
111
|
+
before { expect(spreadsheet).to be_selected.with('Towels') }
|
112
112
|
|
113
113
|
it "gets sheet name" do
|
114
114
|
subject.sheet = 1
|
@@ -15,8 +15,8 @@ module Stockboy
|
|
15
15
|
elements: ['SomeNested', 'Record']
|
16
16
|
)
|
17
17
|
|
18
|
-
reader.options[:strip_namespaces].should
|
19
|
-
reader.options[:advanced_typecasting].should
|
18
|
+
reader.options[:strip_namespaces].should be true
|
19
|
+
reader.options[:advanced_typecasting].should be true
|
20
20
|
reader.options[:convert_tags_to].should be_a Proc
|
21
21
|
reader.options[:parser].should == :nokogiri
|
22
22
|
reader.elements.should == ['some_nested', 'record']
|
@@ -32,8 +32,8 @@ module Stockboy
|
|
32
32
|
elements ['SomeNested', 'Record']
|
33
33
|
end
|
34
34
|
|
35
|
-
reader.options[:strip_namespaces].should
|
36
|
-
reader.options[:advanced_typecasting].should
|
35
|
+
reader.options[:strip_namespaces].should be true
|
36
|
+
reader.options[:advanced_typecasting].should be true
|
37
37
|
reader.options[:convert_tags_to].should be_a Proc
|
38
38
|
reader.options[:parser].should == :nokogiri
|
39
39
|
reader.elements.should == ['some_nested', 'record']
|
@@ -8,21 +8,21 @@ module Stockboy
|
|
8
8
|
|
9
9
|
describe ".register" do
|
10
10
|
it "registers a key for a reader class" do
|
11
|
-
Readers.register(:markup, reader_class).should
|
11
|
+
Readers.register(:markup, reader_class).should be reader_class
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
describe ".find" do
|
16
16
|
it "returns a reader class" do
|
17
17
|
Readers.register(:markup, reader_class)
|
18
|
-
Readers.find(:markup).should
|
18
|
+
Readers.find(:markup).should be reader_class
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
22
|
describe ".[]" do
|
23
23
|
it "returns a reader class" do
|
24
24
|
Readers.register(:markup, reader_class)
|
25
|
-
Readers[:markup].should
|
25
|
+
Readers[:markup].should be reader_class
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -68,11 +68,11 @@ module Stockboy
|
|
68
68
|
|
69
69
|
describe ".find" do
|
70
70
|
it "returns a callable translator" do
|
71
|
-
callable = ->(i){ i.upcase }
|
71
|
+
callable = ->(i){ i.message.upcase }
|
72
72
|
Translations.register :shout, callable
|
73
73
|
|
74
|
-
Translations.find(:shout).should
|
75
|
-
Translations[:shout].should
|
74
|
+
Translations.find(:shout).should be callable
|
75
|
+
Translations[:shout].should be callable
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
data/stockboy.gemspec
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stockboy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Vit
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2015-01-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec-its
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: roo
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|