rets 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4f011e9a58934f4b5152a5f01428b58275602342
4
- data.tar.gz: 188cdccdacaeb7b8c64df570208f073bfb00d9fb
3
+ metadata.gz: 2d46af696793fde645c0c712cdb2e8a708bb93a1
4
+ data.tar.gz: dd858c7811d76a390e178afd853f73a6f1d9807b
5
5
  SHA512:
6
- metadata.gz: 9ce589fca7e82987db2bdf0eb3432e9482bf1344b9d79a69d56b00f29219dd1495680c903bab456e8759e319ec6952245bb3bb88d69cbd1dcf4124a3abf8957e
7
- data.tar.gz: 80dbdc930daa3eedb1f9566b7decbb4b69964396f16300522d481c226b442ff71d5bbbcf4d0c8049ee6a3e0df88fd0f490e6b69d11041265ca9ec94d069de964
6
+ metadata.gz: 6358cf33cb29fb6801dc0e6046f281a41deb49b1163f8a8344f9441ee7737edbd70b2cb5b04190da9abfd5c29c7721062ae7caf7885815efef11fe28b93aea27
7
+ data.tar.gz: 4ac48ac0f4230210b4e52c9fcd49db1896510f4d819c2ebf7fb075618c1e7bfcfc4b76cccdbc135c5948c6a8903987c42d958387c5b77c3ebf8b5166421a3b36
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ### 0.8.0 / 2015-06-09
2
+
3
+ * reduce memory usage on parsing metadata
4
+ * correctly raise authorization error when given XHTML instead of XML
5
+ * unescape HTML encoded responses
6
+ * make httpclient version requirement more specific
7
+ * add ability to print metadata to a file
8
+ * remove Gemfile.lock from repository
9
+
1
10
  ### 0.7.0 / 2015-01-16
2
11
 
3
12
  * feature: optionally treat No Records Found as not an error
data/lib/rets.rb CHANGED
@@ -3,7 +3,7 @@ require 'digest/md5'
3
3
  require 'nokogiri'
4
4
 
5
5
  module Rets
6
- VERSION = '0.7.0'
6
+ VERSION = '0.8.0'
7
7
 
8
8
  MalformedResponse = Class.new(ArgumentError)
9
9
  UnknownResponse = Class.new(ArgumentError)
@@ -28,6 +28,16 @@ module Rets
28
28
  end
29
29
  end
30
30
 
31
+ class NoObjectFound < ArgumentError
32
+ ERROR_CODE = 20403
33
+ attr_reader :reply_text
34
+
35
+ def initialize(reply_text)
36
+ @reply_text = reply_text
37
+ super("Got error code #{ERROR_CODE} (#{reply_text})")
38
+ end
39
+ end
40
+
31
41
  class InvalidRequest < ArgumentError
32
42
  attr_reader :error_code, :reply_text
33
43
  def initialize(error_code, reply_text)
@@ -47,8 +57,10 @@ module Rets
47
57
  end
48
58
  end
49
59
 
60
+ require 'rets/http_client'
50
61
  require 'rets/client'
51
62
  require 'rets/metadata'
63
+ require 'rets/parser/error_checker'
52
64
  require 'rets/parser/compact'
53
65
  require 'rets/parser/multipart'
54
66
  require 'rets/measuring_http_client'
data/lib/rets/client.rb CHANGED
@@ -1,6 +1,5 @@
1
1
  require 'httpclient'
2
2
  require 'logger'
3
- require_relative 'http_client'
4
3
 
5
4
  module Rets
6
5
  class HttpError < StandardError ; end
@@ -61,7 +60,7 @@ module Rets
61
60
  # page 34 of http://www.realtor.org/retsorg.nsf/retsproto1.7d6.pdf#page=34
62
61
  def login
63
62
  res = http_get(login_url)
64
- Client::ErrorChecker.check(res)
63
+ Parser::ErrorChecker.check(res)
65
64
 
66
65
  self.capabilities = extract_capabilities(Nokogiri.parse(res.body))
67
66
  raise UnknownResponse, "Cannot read rets server capabilities." unless @capabilities
@@ -148,7 +147,9 @@ module Rets
148
147
  if opts[:count] == COUNT.only
149
148
  Parser::Compact.get_count(res.body)
150
149
  else
151
- results = Parser::Compact.parse_document(res.body.encode("UTF-8", "binary", :invalid => :replace, :undef => :replace))
150
+ results = Parser::Compact.parse_document(
151
+ res.body.encode("UTF-8", res.body.encoding, :invalid => :replace, :undef => :replace)
152
+ )
152
153
  if resolve
153
154
  rets_class = find_rets_class(opts[:search_type], opts[:class])
154
155
  decorate_results(results, rets_class)
@@ -328,7 +329,7 @@ module Rets
328
329
  # ... :(
329
330
  # Feel free to make this better. It has a test.
330
331
  raw_key_values.split(/\n/).
331
- map { |r| r.split(/=/, 2) }.
332
+ map { |r| r.split(/\=/, 2) }.
332
333
  each { |k,v| hash[k.strip.downcase] = v.strip }
333
334
 
334
335
  hash
@@ -357,50 +358,5 @@ module Rets
357
358
  super(IO::NULL)
358
359
  end
359
360
  end
360
-
361
- class ErrorChecker
362
- def self.check(response)
363
- # some RETS servers returns HTTP code 412 when session cookie expired, yet the response body
364
- # passes XML check. We need to special case for this situation.
365
- # This method is also called from multipart.rb where there are headers and body but no status_code
366
- if response.respond_to?(:status_code) && response.status_code == 412
367
- raise HttpError, "HTTP status: #{response.status_code}, body: #{response.body}"
368
- end
369
-
370
- # some RETS servers return success code in XML body but failure code 4xx in http status
371
- # If xml body is present we ignore http status
372
-
373
- if !response.body.empty?
374
- begin
375
- xml = Nokogiri::XML.parse(response.body, nil, nil, Nokogiri::XML::ParseOptions::STRICT)
376
-
377
- rets_element = xml.xpath("/RETS")
378
- if rets_element.empty?
379
- return
380
- end
381
- reply_text = (rets_element.attr("ReplyText") || rets_element.attr("replyText")).value
382
- reply_code = (rets_element.attr("ReplyCode") || rets_element.attr("replyCode")).value.to_i
383
-
384
- if reply_code == NoRecordsFound::ERROR_CODE
385
- raise NoRecordsFound.new(reply_text)
386
- elsif reply_code.nonzero?
387
- raise InvalidRequest.new(reply_code, reply_text)
388
- else
389
- return
390
- end
391
- rescue Nokogiri::XML::SyntaxError
392
- #Not xml
393
- end
394
- end
395
-
396
- if response.respond_to?(:ok?) && ! response.ok?
397
- if response.status_code == 401
398
- raise AuthorizationFailure.new(response.status_code, response.body)
399
- else
400
- raise HttpError, "HTTP status: #{response.status_code}, body: #{response.body}"
401
- end
402
- end
403
- end
404
- end
405
361
  end
406
362
  end
@@ -17,7 +17,7 @@ module Rets
17
17
  log_http_traffic("GET", url, params, headers) do
18
18
  res = http.get(url, params, headers)
19
19
  end
20
- Client::ErrorChecker.check(res)
20
+ Parser::ErrorChecker.check(res)
21
21
  res
22
22
  end
23
23
 
@@ -28,7 +28,7 @@ module Rets
28
28
  log_http_traffic("POST", url, params, headers) do
29
29
  res = http.post(url, params, headers)
30
30
  end
31
- Client::ErrorChecker.check(res)
31
+ Parser::ErrorChecker.check(res)
32
32
  res
33
33
  end
34
34
 
@@ -14,8 +14,7 @@ module Rets
14
14
  def self.uses(*fields)
15
15
  fields.each do |field|
16
16
  define_method(field) do
17
- instance_variable_get("@#{field}") ||
18
- instance_variable_set("@#{field}", extract(fragment, field.to_s.capitalize))
17
+ fields_hash[field] || fields_hash[field] = extract(fragment, field.to_s.capitalize)
19
18
  end
20
19
  end
21
20
  end
@@ -30,6 +29,12 @@ module Rets
30
29
  fragment.attr(attr)
31
30
  end
32
31
 
32
+ private
33
+
34
+ def fields_hash
35
+ @fields ||= {}
36
+ end
37
+
33
38
  end
34
39
 
35
40
  class RowContainer < Container
@@ -9,8 +9,11 @@ module Rets
9
9
  self.long_value = lookup_type_fragment["LongValue"]
10
10
  end
11
11
 
12
- def print_tree
13
- puts " #{long_value} -> #{value}"
12
+ # Print the tree to a file
13
+ #
14
+ # [out] The file to print to. Defaults to $stdout.
15
+ def print_tree(out = $stdout)
16
+ out.puts " #{long_value} -> #{value}"
14
17
  end
15
18
  end
16
19
  end
@@ -69,10 +69,14 @@ module Rets
69
69
  nil
70
70
  end
71
71
 
72
- def print_tree
73
- puts "Resource: #{id} (Key Field: #{key_field})"
74
-
75
- rets_classes.each(&:print_tree)
72
+ # Print the tree to a file
73
+ #
74
+ # [out] The file to print to. Defaults to $stdout.
75
+ def print_tree(out = $stdout)
76
+ out.puts "Resource: #{id} (Key Field: #{key_field})"
77
+ rets_classes.each do |rets_class|
78
+ rets_class.print_tree(out)
79
+ end
76
80
  end
77
81
 
78
82
  def find_rets_class(rets_class_name)
@@ -33,11 +33,16 @@ module Rets
33
33
  rets_class
34
34
  end
35
35
 
36
- def print_tree
37
- puts " Class: #{name}"
38
- puts " Visible Name: #{visible_name}"
39
- puts " Description : #{description}"
40
- tables.each(&:print_tree)
36
+ # Print the tree to a file
37
+ #
38
+ # [out] The file to print to. Defaults to $stdout.
39
+ def print_tree(out = $stdout)
40
+ out.puts " Class: #{name}"
41
+ out.puts " Visible Name: #{visible_name}"
42
+ out.puts " Description : #{description}"
43
+ tables.each do |table|
44
+ table.print_tree(out)
45
+ end
41
46
  end
42
47
 
43
48
  def find_table(name)
@@ -106,9 +106,12 @@ module Rets
106
106
  @tree ||= build_tree
107
107
  end
108
108
 
109
- def print_tree
109
+ # Print the tree to a file
110
+ #
111
+ # [out] The file to print to. Defaults to $stdout.
112
+ def print_tree(out = $stdout)
110
113
  tree.each do |name, value|
111
- value.print_tree
114
+ value.print_tree(out)
112
115
  end
113
116
  end
114
117
 
@@ -28,15 +28,18 @@ module Rets
28
28
  self.long_name = table_fragment["LongName"]
29
29
  end
30
30
 
31
- def print_tree
32
- puts " Table: #{name}"
33
- puts " Resource: #{resource.id}"
34
- puts " ShortName: #{ table_fragment["ShortName"] }"
35
- puts " LongName: #{ table_fragment["LongName"] }"
36
- puts " StandardName: #{ table_fragment["StandardName"] }"
37
- puts " Units: #{ table_fragment["Units"] }"
38
- puts " Searchable: #{ table_fragment["Searchable"] }"
39
- puts " Required: #{table_fragment['Required']}"
31
+ # Print the tree to a file
32
+ #
33
+ # [out] The file to print to. Defaults to $stdout.
34
+ def print_tree(out = $stdout)
35
+ out.puts " Table: #{name}"
36
+ out.puts " Resource: #{resource.id}"
37
+ out.puts " ShortName: #{ table_fragment["ShortName"] }"
38
+ out.puts " LongName: #{ table_fragment["LongName"] }"
39
+ out.puts " StandardName: #{ table_fragment["StandardName"] }"
40
+ out.puts " Units: #{ table_fragment["Units"] }"
41
+ out.puts " Searchable: #{ table_fragment["Searchable"] }"
42
+ out.puts " Required: #{table_fragment['Required']}"
40
43
  end
41
44
 
42
45
  def resolve(value)
@@ -69,18 +72,23 @@ module Rets
69
72
  resource.lookup_types[lookup_name]
70
73
  end
71
74
 
72
- def print_tree
73
- puts " LookupTable: #{name}"
74
- puts " Resource: #{resource.id}"
75
- puts " Required: #{table_fragment['Required']}"
76
- puts " Searchable: #{ table_fragment["Searchable"] }"
77
- puts " Units: #{ table_fragment["Units"] }"
78
- puts " ShortName: #{ table_fragment["ShortName"] }"
79
- puts " LongName: #{ table_fragment["LongName"] }"
80
- puts " StandardName: #{ table_fragment["StandardName"] }"
81
- puts " Types:"
82
-
83
- lookup_types.each(&:print_tree)
75
+ # Print the tree to a file
76
+ #
77
+ # [out] The file to print to. Defaults to $stdout.
78
+ def print_tree(out = $stdout)
79
+ out.puts " LookupTable: #{name}"
80
+ out.puts " Resource: #{resource.id}"
81
+ out.puts " Required: #{table_fragment['Required']}"
82
+ out.puts " Searchable: #{ table_fragment["Searchable"] }"
83
+ out.puts " Units: #{ table_fragment["Units"] }"
84
+ out.puts " ShortName: #{ table_fragment["ShortName"] }"
85
+ out.puts " LongName: #{ table_fragment["LongName"] }"
86
+ out.puts " StandardName: #{ table_fragment["StandardName"] }"
87
+ out.puts " Types:"
88
+
89
+ lookup_types.each do |lookup_type|
90
+ lookup_type.print_tree(out)
91
+ end
84
92
  end
85
93
 
86
94
  def lookup_type(value)
@@ -3,8 +3,6 @@ module Rets
3
3
  class Compact
4
4
  TAB = /\t/
5
5
 
6
- INCLUDE_NULL_FIELDS = -1
7
-
8
6
  InvalidDelimiter = Class.new(ArgumentError)
9
7
 
10
8
  def self.parse_document(xml)
@@ -17,16 +15,12 @@ module Rets
17
15
  raise InvalidDelimiter, "Empty or invalid delimiter found, unable to parse."
18
16
  end
19
17
 
20
- column_node = doc.at("//COLUMNS")
21
- if column_node.nil?
22
- columns = ''
23
- else
24
- columns = column_node.text
25
- end
26
- rows = doc.xpath("//DATA")
18
+ column_node = doc.at("//COLUMNS")
19
+ column_names = column_node.nil? ? [] : column_node.text.split(delimiter)
27
20
 
21
+ rows = doc.xpath("//DATA")
28
22
  rows.map do |data|
29
- self.parse(columns, data.text, delimiter)
23
+ self.parse_row(column_names, data.text, delimiter)
30
24
  end
31
25
  end
32
26
 
@@ -35,13 +29,12 @@ module Rets
35
29
  # Delimiter must be a regexp because String#split behaves differently when
36
30
  # given a string pattern. (It removes leading spaces).
37
31
  #
38
- def self.parse(columns, data, delimiter = TAB)
32
+ def self.parse_row(column_names, data, delimiter = TAB)
39
33
  raise ArgumentError, "Delimiter must be a regular expression" unless Regexp === delimiter
40
34
 
41
- column_names = columns.split(delimiter)
42
- data_values = data.split(delimiter, INCLUDE_NULL_FIELDS)
35
+ data_values = data.split(delimiter).map { |x| CGI.unescapeHTML(x) }
43
36
 
44
- zipped_key_values = column_names.zip(data_values).map { |k, v| [k, v.to_s] }
37
+ zipped_key_values = column_names.zip(data_values).map { |k, v| [k.freeze, v.to_s] }
45
38
 
46
39
  hash = Hash[*zipped_key_values.flatten]
47
40
  hash.reject { |key, value| key.empty? && value.to_s.empty? }
@@ -1,6 +1,5 @@
1
1
  module Rets
2
2
  module Parser
3
-
4
3
  # Inspired by Mail.
5
4
  class Multipart
6
5
  CRLF = "\r\n"
@@ -33,7 +32,7 @@ module Rets
33
32
 
34
33
  def self.check_for_invalids_parts!(parts)
35
34
  return unless parts.length == 1 && parts.first.headers['content-type'] == 'text/xml'
36
- Client::ErrorChecker.check(parts.first)
35
+ ErrorChecker.check(parts.first)
37
36
  end
38
37
  end
39
38
  end
data/test/fixtures.rb CHANGED
@@ -1,4 +1,16 @@
1
- RETS_ERROR = <<-XML
1
+ RETS_NO_RECORDS_ERROR = <<-XML
2
+ <?xml version="1.0"?>
3
+ <RETS ReplyCode="20201" ReplyText="Error message">
4
+ </RETS>
5
+ XML
6
+
7
+ RETS_NO_OBJECT_ERROR = <<-XML
8
+ <?xml version="1.0"?>
9
+ <RETS ReplyCode="20403" ReplyText="Error message">
10
+ </RETS>
11
+ XML
12
+
13
+ RETS_INVALID_REQUEST_ERROR = <<-XML
2
14
  <?xml version="1.0"?>
3
15
  <RETS ReplyCode="123" ReplyText="Error message">
4
16
  </RETS>
@@ -205,8 +217,41 @@ SAMPLE_COMPACT_2 = <<XML
205
217
  <DATA> 15n1172 ZipCode_f1172 Zip ZipCo_1172 Zip 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
206
218
  <DATA> 15n1177 ADDRESS1_f1177 Address Line 1 ADDRE_1177 Address1 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
207
219
  <DATA> 15n1182 MLSYN_f1182 MLS Y/N MLSYN_1182 MLSYN 1 Character 0 1 0 0 0 0 0 0 0 </DATA>
208
- <DATA> 15n1184 OFFICENAME_f1184 Name Office Name OFFIC_1184 Office Name 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
220
+ <DATA> 15n1184 OFFICENAME_f1184 Office Name Office&#x2019;s Name OFFIC_1184 Office Name 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
209
221
  <DATA> 15n1193 OfficeCode_f1193 OfficeID Office Code Offic_1193 Office Code 12 Character 0 1 0 0 0 0 0 0 1 </DATA>
210
222
  </METADATA-TABLE>
211
223
  </RETS>
212
224
  XML
225
+
226
+ HTML_AUTH_FAILURE = <<EOF
227
+ <html><head><title>Apache Tomcat/6.0.26 - Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 401 - </h1><HR size="1" noshade="noshade"><p><b>type</b> Status report</p><p><b>message</b> <u></u></p><p><b>description</b> <u>This request requires HTTP authentication ().</u></p><HR size="1" noshade="noshade"><h3>Apache Tomcat/6.0.26</h3></body></html>
228
+ EOF
229
+
230
+ XHTML_AUTH_FAILURE = <<EOF
231
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
232
+ <html xmlns="http://www.w3.org/1999/xhtml">
233
+ <head>
234
+ <title>401 - Unauthorized: Access is denied due to invalid credentials.</title>
235
+ </head>
236
+ <body>
237
+ <h1>401 - Unauthorized: Access is denied due to invalid credentials.</h1>
238
+ <p>You do not have permission to view this directory or page using the credentials that you supplied.</p>
239
+ </body>
240
+ </html>
241
+ EOF
242
+
243
+ SAMPLE_COMPACT_WITH_SPECIAL_CHARS = <<EOF
244
+ <RETS ReplyCode=\"0\" ReplyText=\"Operation Success.\">
245
+ <DELIMITER value=\"09\" />
246
+ <COLUMNS> PublicRemarksNew WindowCoverings YearBuilt Zoning ZoningCompatibleYN </COLUMNS>
247
+ <DATA> porte-coch&amp;#xE8;re welcomes 1999 00 </DATA>
248
+ </RETS>
249
+ EOF
250
+
251
+ SAMPLE_COMPACT_WITH_SPECIAL_CHARS_2 = <<EOF
252
+ <RETS ReplyCode=\"0\" ReplyText=\"Operation Success.\">
253
+ <DELIMITER value=\"09\" />
254
+ <COLUMNS> PublicRemarksNew WindowCoverings YearBuilt Zoning ZoningCompatibleYN </COLUMNS>
255
+ <DATA> text with &lt;tag&gt; 1999 00 </DATA>
256
+ </RETS>
257
+ EOF
data/test/test_client.rb CHANGED
@@ -99,6 +99,41 @@ class TestClient < MiniTest::Test
99
99
  end
100
100
  end
101
101
 
102
+ def test_response_text_encoding_from_ascii
103
+ @client.stubs(:capability_url).with("Search").returns("search_url")
104
+
105
+ response = mock
106
+ response.stubs(:body).returns(("An ascii string").encode("binary", "UTF-8"))
107
+ @client.stubs(:http_post).with("search_url", anything).returns(response)
108
+
109
+ Rets::Parser::Compact.expects(:parse_document).with("An ascii string")
110
+
111
+ @client.find_every({}, false)
112
+ end
113
+
114
+ def test_response_text_encoding_from_utf_8
115
+ @client.stubs(:capability_url).with("Search").returns("search_url")
116
+
117
+ response = mock
118
+ response.stubs(:body).returns("Some string with non-ascii characters \u0119")
119
+ @client.stubs(:http_post).with("search_url", anything).returns(response)
120
+
121
+ Rets::Parser::Compact.expects(:parse_document).with("Some string with non-ascii characters \u0119")
122
+
123
+ @client.find_every({}, false)
124
+ end
125
+
126
+ def test_response_text_encoding_from_utf_16
127
+ @client.stubs(:capability_url).with("Search").returns("search_url")
128
+
129
+ response = mock
130
+ response.stubs(:body).returns("Some string with non-utf-8 characters \xC2")
131
+ @client.stubs(:http_post).with("search_url", anything).returns(response)
132
+
133
+ Rets::Parser::Compact.expects(:parse_document).with("Some string with non-utf-8 characters \uFFFD")
134
+
135
+ @client.find_every({}, false)
136
+ end
102
137
 
103
138
  def test_find_retries_when_receiving_no_records_found
104
139
  @client.stubs(:find_every).raises(Rets::NoRecordsFound.new('')).then.returns([1])
@@ -10,8 +10,8 @@ class TestParserCompact < MiniTest::Test
10
10
  def test_parse_document_uses_default_delimiter_when_none_provided
11
11
  # we assert that the delimeter character getting to parse is a tab
12
12
  # even though COMPACT defines no delimiter tag
13
- Rets::Parser::Compact.expects(:parse).with("A\tB", "1\t2", /\t/)
14
- Rets::Parser::Compact.expects(:parse).with("A\tB", "4\t5", /\t/)
13
+ Rets::Parser::Compact.expects(:parse_row).with(%w(A B), "1\t2", /\t/)
14
+ Rets::Parser::Compact.expects(:parse_row).with(%w(A B), "4\t5", /\t/)
15
15
  Rets::Parser::Compact.parse_document(COMPACT)
16
16
  end
17
17
 
@@ -30,29 +30,31 @@ class TestParserCompact < MiniTest::Test
30
30
  end
31
31
 
32
32
  def test_parse_returns_key_value_pairs
33
- result = Rets::Parser::Compact.parse("A\tB", "1\t2")
33
+ result = Rets::Parser::Compact.parse_row(%w(A B), "1\t2")
34
34
 
35
35
  assert_equal({"A" => "1", "B" => "2"}, result)
36
36
  end
37
37
 
38
38
  # RMLS does this. :|
39
39
  def test_remaining_columns_produce_empty_string_values
40
- columns = "A B C D"
41
- data = "1 2"
40
+ column_names = %w(A B C D)
41
+ data = "1 2"
42
42
 
43
- assert_equal({"A" => "1", "B" => "2", "C" => "", "D" => ""}, Rets::Parser::Compact.parse(columns, data, / /))
43
+ assert_equal({"A" => "1", "B" => "2", "C" => "", "D" => ""},
44
+ Rets::Parser::Compact.parse_row(column_names, data, / /))
44
45
  end
45
46
 
46
47
  def test_leading_empty_columns_are_preserved_with_delimiter
47
- columns = "A\tB\tC\tD"
48
- data = "\t\t3\t4" # first two columns are empty data.
48
+ column_names = %w(A B C D)
49
+ data = "\t\t3\t4" # first two columns are empty data.
49
50
 
50
- assert_equal({"A" => "", "B" => "", "C" => "3", "D" => "4"}, Rets::Parser::Compact.parse(columns, data, /\t/))
51
+ assert_equal({"A" => "", "B" => "", "C" => "3", "D" => "4"},
52
+ Rets::Parser::Compact.parse_row(column_names, data, /\t/))
51
53
  end
52
54
 
53
55
  def test_parse_only_accepts_regexp
54
56
  assert_raises ArgumentError do
55
- Rets::Parser::Compact.parse("a", "b", " ")
57
+ Rets::Parser::Compact.parse_row(["a"], "b", " ")
56
58
  end
57
59
  end
58
60
 
@@ -83,4 +85,15 @@ class TestParserCompact < MiniTest::Test
83
85
  assert_equal "", rows.first["ModTimeStamp"]
84
86
  end
85
87
 
88
+ def test_parse_html_encoded_chars
89
+ rows = Rets::Parser::Compact.parse_document(SAMPLE_COMPACT_WITH_SPECIAL_CHARS)
90
+
91
+ assert_equal "porte-coch\u{E8}re welcomes ", rows.last["PublicRemarksNew"]
92
+ end
93
+
94
+ def test_parse_html_encoded_chars_2
95
+ rows = Rets::Parser::Compact.parse_document(SAMPLE_COMPACT_WITH_SPECIAL_CHARS_2)
96
+
97
+ assert_equal "text with <tag>", rows.last["PublicRemarksNew"]
98
+ end
86
99
  end
@@ -30,7 +30,7 @@ class TestParserMultipart < MiniTest::Test
30
30
  end
31
31
 
32
32
  def test_parse_with_error
33
- raw = "\r\n--89467f8e0c6b48158c8f1883910212ec\r\nContent-Type: text/xml\r\nContent-ID: foo\r\nObject-ID: *\r\n\r\n<RETS ReplyCode=\"20403\" ReplyText=\"No Object Found\" />\r\n\r\n--89467f8e0c6b48158c8f1883910212ec--\r\n"
33
+ raw = "\r\n--89467f8e0c6b48158c8f1883910212ec\r\nContent-Type: text/xml\r\nContent-ID: foo\r\nObject-ID: *\r\n\r\n<RETS ReplyCode=\"999\" ReplyText=\"An Unexplaned Error\" />\r\n\r\n--89467f8e0c6b48158c8f1883910212ec--\r\n"
34
34
  boundary = "89467f8e0c6b48158c8f1883910212ec"
35
35
  assert_raises Rets::InvalidRequest do
36
36
  Rets::Parser::Multipart.parse(raw, boundary)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Estately, Inc. Open Source
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-16 00:00:00.000000000 Z
11
+ date: 2015-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httpclient
@@ -100,14 +100,14 @@ dependencies:
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '3.6'
103
+ version: '3.13'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '3.6'
110
+ version: '3.13'
111
111
  description: |-
112
112
  [![Build Status](https://secure.travis-ci.org/estately/rets.png?branch=master)](http://travis-ci.org/estately/rets)
113
113
  A pure-ruby library for fetching data from [RETS] servers.
@@ -153,7 +153,8 @@ files:
153
153
  - test/test_parser_multipart.rb
154
154
  - test/vcr_cassettes/unauthorized_response.yml
155
155
  homepage: http://github.com/estately/rets
156
- licenses: []
156
+ licenses:
157
+ - MIT
157
158
  metadata: {}
158
159
  post_install_message:
159
160
  rdoc_options:
@@ -172,15 +173,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
172
173
  - !ruby/object:Gem::Version
173
174
  version: '0'
174
175
  requirements: []
175
- rubyforge_project: rets
176
- rubygems_version: 2.2.0
176
+ rubyforge_project:
177
+ rubygems_version: 2.2.3
177
178
  signing_key:
178
179
  specification_version: 4
179
180
  summary: "[![Build Status](https://secure.travis-ci.org/estately/rets.png?branch=master)](http://travis-ci.org/estately/rets)
180
181
  A pure-ruby library for fetching data from [RETS] servers"
181
- test_files:
182
- - test/test_client.rb
183
- - test/test_locking_http_client.rb
184
- - test/test_metadata.rb
185
- - test/test_parser_compact.rb
186
- - test/test_parser_multipart.rb
182
+ test_files: []