stockboy 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +11 -0
  4. data/lib/stockboy/attribute.rb +15 -2
  5. data/lib/stockboy/attribute_map.rb +2 -1
  6. data/lib/stockboy/candidate_record.rb +30 -6
  7. data/lib/stockboy/job.rb +3 -3
  8. data/lib/stockboy/readers/csv.rb +20 -4
  9. data/lib/stockboy/translations/date.rb +35 -5
  10. data/lib/stockboy/translations/uk_date.rb +4 -8
  11. data/lib/stockboy/translations/us_date.rb +2 -12
  12. data/lib/stockboy/version.rb +1 -1
  13. data/spec/spec_helper.rb +1 -1
  14. data/spec/stockboy/attribute_map_spec.rb +7 -2
  15. data/spec/stockboy/attribute_spec.rb +22 -2
  16. data/spec/stockboy/candidate_record_spec.rb +16 -3
  17. data/spec/stockboy/configurator_spec.rb +4 -4
  18. data/spec/stockboy/filter_chain_spec.rb +1 -1
  19. data/spec/stockboy/filter_spec.rb +4 -4
  20. data/spec/stockboy/filters/missing_email_spec.rb +4 -4
  21. data/spec/stockboy/job_spec.rb +9 -9
  22. data/spec/stockboy/provider_repeater_spec.rb +3 -3
  23. data/spec/stockboy/provider_spec.rb +3 -3
  24. data/spec/stockboy/providers/file_spec.rb +6 -6
  25. data/spec/stockboy/providers/ftp_spec.rb +3 -3
  26. data/spec/stockboy/providers/imap_spec.rb +5 -5
  27. data/spec/stockboy/providers_spec.rb +4 -4
  28. data/spec/stockboy/readers/csv_spec.rb +13 -4
  29. data/spec/stockboy/readers/spreadsheet_spec.rb +5 -5
  30. data/spec/stockboy/readers/xml_spec.rb +4 -4
  31. data/spec/stockboy/readers_spec.rb +3 -3
  32. data/spec/stockboy/template_file_spec.rb +1 -1
  33. data/spec/stockboy/translations/date_spec.rb +1 -1
  34. data/spec/stockboy/translations/decimal_spec.rb +1 -1
  35. data/spec/stockboy/translations/integer_spec.rb +1 -1
  36. data/spec/stockboy/translations/time_spec.rb +1 -1
  37. data/spec/stockboy/translations/uk_date_spec.rb +1 -1
  38. data/spec/stockboy/translations/us_date_spec.rb +1 -1
  39. data/spec/stockboy/translations_spec.rb +3 -3
  40. data/stockboy.gemspec +1 -0
  41. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 5447e324b114a19041b667593624345a3e74255e
4
- data.tar.gz: 66cfa4b6d78f3a7c8e861d4f943e024f59973e30
3
+ metadata.gz: f39685f84983ede11ee77ffe5ab39982e37ddac7
4
+ data.tar.gz: c16642a9c384a71c72def32658d8e4764ab46b14
5
5
  SHA512:
6
- metadata.gz: 8021a777554ad4978179e4b428e452cc4050b772d4a15426d20478fe542f293503dc1204bdf7038c39f108657d01fe4712d2011abe47bdbd24f8b8cb01e1cef5
7
- data.tar.gz: 6a5bdf862e2de473579833bc5596f30c5f6d938418975697370aa51083b70353f7b60a92defb909c5d8ec6f14dab169b504de2576c0bdab7dd0daaf12c55e8f5
6
+ metadata.gz: 3d7f4377f7973b9e88d91ea807c12987385e594ca9f55b1594dbdc652ad8aea3cf674e431394ed840d60b11454404975cf4d9dc77b3a8a3409a760b1668316ef
7
+ data.tar.gz: 7dd65ee93fbf9a6df91f052b5505fe13d2b4d1ae2901a612a7ccd87589f1f5662cf8154f5d7d397a4d59779c23cd4ddf4abbee5c70f78a0204130c9cd329e9f5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 / 2015-01-23
4
+
5
+ * [FEATURE] Option to ignore unwanted attributes from output
6
+
3
7
  ## 0.9.0 / 2014-05-19
4
8
 
5
9
  * [FEATURE] Add JSON reader
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]
@@ -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}, translators=#{translators}>"
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(self.raw_hash, @table)
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(self.to_hash)
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[col.to] = tr[value]
119
+ fields[key] = tr[value]
106
120
  SourceRecord.new(fields, @table)
107
121
  rescue
108
- fields[col.to] = nil
122
+ fields[key] = nil
109
123
  break SourceRecord.new(fields, @table)
110
124
  end
111
125
  end
112
- @tr_table[col.to] = tr_input.public_send(col.to)
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=#{(Stockboy::Providers.all.key(provider.class) || provider.class)}"
200
- read = "reader=#{(Stockboy::Readers.all.key(reader.class) || reader.class)}"
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} #{[prov, read, attr, filt, cnts].join(', ')}>"
204
+ "#<#{self.class}:#{self.object_id} #{prov}, #{read}, #{attr}, #{filt}, #{cnts}>"
205
205
  end
206
206
 
207
207
  private
@@ -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.force_encoding(encoding) if encoding
99
- data = data.encode(universal_newline: true)
100
- .delete(0.chr)
101
- .chomp
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
- def separator(value)
50
- value.include?(?/) ? ?/ : ?-
51
- end
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
- # Match date strings with 2-digit year
54
- MATCH_YYYY = %r"\A\d{1,2}(/|-)\d{1,2}\1\d{4}\z"
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/us_date'
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::USDate
29
+ class UKDate < Stockboy::Translations::Date
30
+ include Stockboy::Translations::Date::PatternMatching
30
31
 
31
- private
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
- private
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
@@ -1,3 +1,3 @@
1
1
  module Stockboy
2
- VERSION = "0.9.0"
2
+ VERSION = "0.10.0"
3
3
  end
data/spec/spec_helper.rb CHANGED
@@ -17,7 +17,7 @@ if ENV['DEBUG']
17
17
  require 'pry'
18
18
  end
19
19
 
20
-
20
+ require 'rspec/its'
21
21
  require 'ostruct'
22
22
  require 'savon'
23
23
  require 'savon/mock/spec_helper'
@@ -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.should_receive(:new).with({id: '1', name: 'Arthur Dent'})
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 be_nil
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 be_nil
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.should_receive(:new).with(password:'foo')
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.should_receive(:new).with(col_sep: '|').and_return(reader_stub)
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.should_receive(:new).and_return(attribute_map)
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 be_nil
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
@@ -17,7 +17,7 @@ module Stockboy
17
17
  let(:chain) { FilterChain.new(no_angels: filter1, no_daleks: filter2) }
18
18
 
19
19
  it "calls reset on all members" do
20
- filter2.should_receive(:reset)
20
+ expect(filter2).to receive(:reset)
21
21
  chain.reset
22
22
  end
23
23
 
@@ -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 be_true
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 be_false
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 be_true
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 be_false
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 be_false
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 be_true
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 be_true
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 be_false
24
+ filter.call(input, output).should be false
25
25
  end
26
26
  end
@@ -85,22 +85,22 @@ module Stockboy
85
85
  end
86
86
 
87
87
  it "should read a file from a path" do
88
- File.should_receive(:read).with("#{jobs_path}/test_job.rb")
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.should_receive(:find)
94
- .with(:ftp)
95
- .and_return(TestProvider)
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.should_receive(:find)
102
- .with(:csv)
103
- .and_return(TestReader)
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 be_false
250
+ job.processed?.should be false
251
251
  job.process
252
- job.processed?.should be_true
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 be_nil }
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 be_nil }
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 be_nil }
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 be_nil }
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 be_nil }
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 be_nil }
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 be_nil }
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 be_nil
65
- provider.valid?.should be_false
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 be_nil
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 be_nil
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 be_nil
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 be_true
53
- connection.passive.should be_true
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 be_nil
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 be_nil }
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 be_nil }
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 be_nil }
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 be_nil }
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 be_nil }
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) { double("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 === provider
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 === provider
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 === provider
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 be_nil }
11
- its(:col_sep) { should be_nil }
12
- its(:quote_char) { should be_nil }
13
- its(:headers) { should be_true }
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(:expectation) { mock(:spreadsheet, sheets: sheets).should_receive(:default_sheet=) }
89
- let(:spreadsheet) { expectation }
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
- let(:spreadsheet) { expectation.with('Towels') }
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
- let(:spreadsheet) { expectation.with('Towels') }
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
- let(:spreadsheet) { expectation.with('Towels') }
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 be_true
19
- reader.options[:advanced_typecasting].should be_true
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 be_true
36
- reader.options[:advanced_typecasting].should be_true
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 == reader_class
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 == reader_class
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 == reader_class
25
+ Readers[:markup].should be reader_class
26
26
  end
27
27
  end
28
28
 
@@ -18,7 +18,7 @@ module Stockboy
18
18
 
19
19
  describe ".template_path" do
20
20
  it "returns nil when missing" do
21
- TemplateFile.find("not_here").should be_nil
21
+ TemplateFile.find("not_here").should be nil
22
22
  end
23
23
 
24
24
  it "returns a file path when found" do
@@ -10,7 +10,7 @@ module Stockboy
10
10
  describe "#call" do
11
11
  it "returns nil for an empty string" do
12
12
  result = subject.call start: ""
13
- result.should be_nil
13
+ result.should be nil
14
14
  end
15
15
 
16
16
  it "returns a date unmodified" do
@@ -9,7 +9,7 @@ module Stockboy
9
9
  describe "#call" do
10
10
  it "returns nil for an empty string" do
11
11
  result = subject.call total: ""
12
- result.should be_nil
12
+ result.should be nil
13
13
  end
14
14
 
15
15
  it "returns a decimal" do
@@ -9,7 +9,7 @@ module Stockboy
9
9
  describe "#call" do
10
10
  it "returns nil for an empty string" do
11
11
  result = subject.call id: ""
12
- result.should be_nil
12
+ result.should be nil
13
13
  end
14
14
 
15
15
  it "returns an integer" do
@@ -9,7 +9,7 @@ module Stockboy
9
9
  describe "#call" do
10
10
  it "returns nil for an empty string" do
11
11
  result = subject.call start: ""
12
- result.should be_nil
12
+ result.should be nil
13
13
  end
14
14
 
15
15
  it "returns a time" do
@@ -9,7 +9,7 @@ module Stockboy
9
9
  describe "#call" do
10
10
  it "returns nil for an empty string" do
11
11
  result = subject.call start: ""
12
- result.should be_nil
12
+ result.should be nil
13
13
  end
14
14
 
15
15
  it "translates DD/MM/YYYY" do
@@ -9,7 +9,7 @@ module Stockboy
9
9
  describe "#call" do
10
10
  it "returns nil for an empty string" do
11
11
  result = subject.call start: ""
12
- result.should be_nil
12
+ result.should be nil
13
13
  end
14
14
 
15
15
  it "translates MM/DD/YYYY" do
@@ -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 == callable
75
- Translations[:shout].should == callable
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
@@ -23,6 +23,7 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.add_development_dependency "rake"
25
25
  s.add_development_dependency "rspec"
26
+ s.add_development_dependency "rspec-its"
26
27
  # s.add_development_dependency "vcr"
27
28
 
28
29
  s.add_runtime_dependency "roo", ">= 1.12.2"
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.9.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: 2014-05-19 00:00:00.000000000 Z
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