sean-rets 0.6.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.
@@ -0,0 +1,113 @@
1
+ module Rets
2
+ module Metadata
3
+ class TableFactory
4
+ def self.build(table_fragment, resource)
5
+ enum?(table_fragment) ? LookupTable.new(table_fragment, resource) : Table.new(table_fragment)
6
+ end
7
+
8
+ def self.enum?(table_fragment)
9
+ lookup_value = table_fragment["LookupName"].strip
10
+ interpretation = table_fragment["Interpretation"].strip
11
+
12
+ interpretation =~ /Lookup/ && !lookup_value.empty?
13
+ end
14
+ end
15
+
16
+ class Table
17
+ attr_accessor :type
18
+ attr_accessor :name
19
+ attr_accessor :long_name
20
+ attr_accessor :table_fragment
21
+
22
+ def initialize(table_fragment)
23
+ self.table_fragment = table_fragment
24
+ self.type = table_fragment["DataType"]
25
+ self.name = table_fragment["SystemName"]
26
+ self.long_name = table_fragment["LongName"]
27
+ end
28
+
29
+ def print_tree
30
+ puts " Table: #{name}"
31
+ puts " ShortName: #{ table_fragment["ShortName"] }"
32
+ puts " LongName: #{ table_fragment["LongName"] }"
33
+ puts " StandardName: #{ table_fragment["StandardName"] }"
34
+ puts " Units: #{ table_fragment["Units"] }"
35
+ puts " Searchable: #{ table_fragment["Searchable"] }"
36
+ puts " Required: #{table_fragment['Required']}"
37
+ end
38
+
39
+ def resolve(value)
40
+ value.to_s.strip
41
+ end
42
+ end
43
+
44
+ class LookupTable
45
+ attr_accessor :resource
46
+ attr_accessor :lookup_name
47
+ attr_accessor :name
48
+ attr_accessor :interpretation
49
+ attr_accessor :long_name
50
+ attr_accessor :table_fragment
51
+
52
+ def initialize(table_fragment, resource)
53
+ self.table_fragment = table_fragment
54
+ self.resource = resource
55
+ self.name = table_fragment["SystemName"]
56
+ self.interpretation = table_fragment["Interpretation"]
57
+ self.lookup_name = table_fragment["LookupName"]
58
+ self.long_name = table_fragment["LongName"]
59
+ end
60
+
61
+ def multi?
62
+ interpretation == "LookupMulti"
63
+ end
64
+
65
+ def lookup_types
66
+ resource.lookup_types[lookup_name]
67
+ end
68
+
69
+ def print_tree
70
+ puts " LookupTable: #{name}"
71
+ puts " Required: #{table_fragment['Required']}"
72
+ puts " Searchable: #{ table_fragment["Searchable"] }"
73
+ puts " Units: #{ table_fragment["Units"] }"
74
+ puts " ShortName: #{ table_fragment["ShortName"] }"
75
+ puts " LongName: #{ table_fragment["LongName"] }"
76
+ puts " StandardName: #{ table_fragment["StandardName"] }"
77
+ puts " Types:"
78
+
79
+ lookup_types.each(&:print_tree)
80
+ end
81
+
82
+ def lookup_type(value)
83
+ lookup_types.detect {|lt| lt.value == value }
84
+ end
85
+
86
+ def resolve(value)
87
+ if value.empty?
88
+ return [] if multi?
89
+ return value.to_s.strip
90
+ end
91
+
92
+ values = multi? ? value.split(","): [value]
93
+
94
+ values = values.map do |v|
95
+
96
+ #Remove surrounding quotes
97
+ clean_value = v.scan(/^["']?(.*?)["']?$/).join
98
+
99
+
100
+ lookup_type = lookup_type(clean_value)
101
+
102
+ resolved_value = lookup_type ? lookup_type.long_value : nil
103
+
104
+ warn("Discarding unmappable value of #{clean_value.inspect}") if resolved_value.nil? && $VERBOSE
105
+
106
+ resolved_value
107
+ end
108
+
109
+ multi? ? values.map {|v| v.to_s.strip } : values.first.to_s.strip
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,62 @@
1
+ module Rets
2
+ module Parser
3
+ class Compact
4
+ TAB = /\t/
5
+
6
+ INCLUDE_NULL_FIELDS = -1
7
+
8
+ InvalidDelimiter = Class.new(ArgumentError)
9
+
10
+ def self.parse_document(xml)
11
+ doc = Nokogiri.parse(xml.to_s)
12
+
13
+ delimiter = doc.at("//DELIMITER")
14
+ delimiter = delimiter ? Regexp.new(Regexp.escape(delimiter.attr(:value).to_i.chr)) : TAB
15
+
16
+ if delimiter == // || delimiter == /,/
17
+ raise InvalidDelimiter, "Empty or invalid delimiter found, unable to parse."
18
+ end
19
+
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")
27
+
28
+ rows.map do |data|
29
+ self.parse(columns, data.text, delimiter)
30
+ end
31
+ end
32
+
33
+ # Parses a single row of RETS-COMPACT data.
34
+ #
35
+ # Delimiter must be a regexp because String#split behaves differently when
36
+ # given a string pattern. (It removes leading spaces).
37
+ #
38
+ def self.parse(columns, data, delimiter = TAB)
39
+ raise ArgumentError, "Delimiter must be a regular expression" unless Regexp === delimiter
40
+
41
+ column_names = columns.split(delimiter)
42
+ data_values = data.split(delimiter, INCLUDE_NULL_FIELDS)
43
+
44
+ zipped_key_values = column_names.zip(data_values).map { |k, v| [k, v.to_s] }
45
+
46
+ hash = Hash[*zipped_key_values.flatten]
47
+ hash.reject { |key, value| key.empty? && value.to_s.empty? }
48
+ end
49
+
50
+ def self.get_count(xml)
51
+ doc = Nokogiri.parse(xml.to_s)
52
+ if node = doc.at("//COUNT")
53
+ return node.attr('Records').to_i
54
+ elsif node = doc.at("//RETS-STATUS")
55
+ # Handle <RETS-STATUS ReplyCode="20201" ReplyText="No matching records were found" />
56
+ return 0 if node.attr('ReplyCode') == '20201'
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ module Rets
2
+ module Parser
3
+
4
+ # Inspired by Mail.
5
+ class Multipart
6
+ CRLF = "\r\n"
7
+ WSP = "\s"
8
+ HEADER_LINE = /^([!-9;-~]+:\s*.+)$/
9
+
10
+ Part = Struct.new(:headers, :body)
11
+
12
+ def self.parse(raw, boundary)
13
+ parts = []
14
+ boundary_regexp = /--#{Regexp.quote(boundary)}(--)?#{CRLF}/
15
+
16
+ # WTF some RETS servers declare response body including jpeg binary is encoded in utf8
17
+ raw.force_encoding 'ascii-8bit' if raw.respond_to?(:force_encoding)
18
+
19
+ raw.split(boundary_regexp).each do |chunk|
20
+ header_part, body_part = chunk.split(/#{CRLF}#{WSP}*#{CRLF}/m, 2)
21
+
22
+ if header_part =~ HEADER_LINE
23
+ headers = header_part.split(/\r\n/).map { |kv| p = kv.split(/:\s?/); [p[0].downcase, p[1..-1].join(':')] }
24
+ headers = Hash[*headers.flatten]
25
+ parts << Part.new(headers, body_part)
26
+ else
27
+ next # not a valid chunk.
28
+ end
29
+ end
30
+ check_for_invalids_parts!(parts)
31
+ parts
32
+ end
33
+
34
+ def self.check_for_invalids_parts!(parts)
35
+ return unless parts.length == 1 && parts.first.headers['content-type'] == 'text/xml'
36
+ Client::ErrorChecker.check(parts.first)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,212 @@
1
+ RETS_ERROR = <<-XML
2
+ <?xml version="1.0"?>
3
+ <RETS ReplyCode="123" ReplyText="Error message">
4
+ </RETS>
5
+ XML
6
+
7
+ RETS_REPLY = <<-XML
8
+ <?xml version="1.0"?>
9
+ <RETS ReplyCode="0" ReplyText="OK">
10
+ </RETS>
11
+ XML
12
+
13
+ CAPABILITIES = <<-XML
14
+ <RETS ReplyCode="0" ReplyText="OK">
15
+ <RETS-RESPONSE>
16
+
17
+ Abc=123
18
+ Def=ghi=jk
19
+
20
+ </RETS-RESPONSE>
21
+ </RETS>
22
+ XML
23
+
24
+ COUNT_ONLY = <<XML
25
+ <RETS ReplyCode="0" ReplyText="Success">
26
+ <COUNT Records="1234" />
27
+ </RETS>
28
+ XML
29
+
30
+ RETS_STATUS_NO_MATCHING_RECORDS = <<XML
31
+ <?xml version="1.0"?>
32
+ <RETS ReplyCode="0" ReplyText="Operation Successful">
33
+ <RETS-STATUS ReplyCode="20201" ReplyText="No matching records were found" />
34
+ </RETS>
35
+ XML
36
+
37
+ CAPABILITIES_WITH_WHITESPACE = <<XML
38
+ <RETS ReplyCode="0" ReplyText="Operation Successful">
39
+ <RETS-RESPONSE>
40
+ Action = /RETS/Action
41
+ </RETS-RESPONSE>
42
+ </RETS>
43
+ XML
44
+
45
+ # 44 is the ASCII code for comma; an invalid delimiter.
46
+ INVALID_DELIMETER = <<-XML
47
+ <?xml version="1.0"?>
48
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
49
+ <DELIMITER value="44" />
50
+ <COLUMNS>A\tB</COLUMNS>
51
+ <DATA>1\t2</DATA>
52
+ <DATA>4\t5</DATA>
53
+ </METADATA>
54
+ XML
55
+
56
+ COMPACT = <<-XML
57
+ <?xml version="1.0"?>
58
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
59
+ <COLUMNS>A\tB</COLUMNS>
60
+ <DATA>1\t2</DATA>
61
+ <DATA>4\t5</DATA>
62
+ </METADATA>
63
+ XML
64
+
65
+
66
+ EMPTY_COMPACT = <<-XML
67
+ <METADATA-TABLE Resource="OpenHouse" Class="OpenHouse" Version="01.01.00000" Date="2011-07-29T12:09:16">
68
+ </METADATA-TABLE>
69
+ XML
70
+
71
+ METADATA_UNKNOWN = <<-XML
72
+ <?xml version="1.0"?>
73
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
74
+ <METADATA-FOO Version="01.72.10306" Date="2011-03-15T19:51:22">
75
+ <UNKNOWN />
76
+ </METADATA-FOO>
77
+ XML
78
+
79
+ METADATA_SYSTEM = <<-XML
80
+ <?xml version="1.0"?>
81
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
82
+ <METADATA-SYSTEM Version="01.72.10306" Date="2011-03-15T19:51:22">
83
+ <SYSTEM />
84
+ <COMMENTS />
85
+ </METADATA-SYSTEM>
86
+ XML
87
+
88
+ METADATA_RESOURCE = <<-XML
89
+ <?xml version="1.0"?>
90
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
91
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
92
+ <COLUMNS> ResourceID StandardName VisibleName Description KeyField ClassCount ClassVersion ClassDate ObjectVersion ObjectDate SearchHelpVersion SearchHelpDate EditMaskVersion EditMaskDate LookupVersion LookupDate UpdateHelpVersion UpdateHelpDate ValidationExpressionVersion ValidationExpressionDate ValidationLookupVersionValidationLookupDate ValidationExternalVersion ValidationExternalDate </COLUMNS>
93
+ <DATA> ActiveAgent ActiveAgent Active Agent Search Contains information about active agents. MemberNumber 1 01.72.10304 2011-03-03T00:29:23 01.72.10000 2010-08-16T15:08:20 01.72.10305 2011-03-09T21:33:41 01.72.10284 2011-02-24T06:56:43 </DATA>
94
+ <DATA> Agent Agent Agent Search Contains information about all agents. MemberNumber 1 01.72.10303 2011-03-03T00:29:23 01.72.10000 2010-08-16T15:08:20 01.72.10305 2011-03-09T21:33:41 01.72.10284 2011-02-24T06:56:43 </DATA>
95
+ <DATA> History History History Search Contains information about accumulated changes to each listing. TransactionRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T22:08:30 01.72.10000 2010-08-16T15:08:20 </DATA>
96
+ <DATA> MemberAssociation Member Association Contains MLS member Association information. MemberAssociationKey 1 01.72.10277 2011-02-23T19:11:10 01.72.10214 2011-01-06T16:41:05 01.72.10220 2011-01-06T16:41:06 </DATA>
97
+ <DATA> Office Office Office Search Contains information about broker offices. OfficeNumber 1 01.72.10302 2011-03-03T00:29:23 01.72.10000 2010-08-16T15:08:20 01.72.10305 2011-03-09T21:33:41 01.72.10284 2011-02-24T06:56:43 </DATA>
98
+ <DATA> OfficeAssociation Office Association Contains MLS office Association information. OfficeAssociationKey 1 01.72.10306 2011-03-15T19:51:22 01.72.10245 2011-01-06T16:41:08 01.72.10251 2011-01-06T16:41:08 </DATA>
99
+ <DATA> OpenHouse OpenHouse Open House Search Contains information about public open house activities. OpenHouseRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10134 2010-11-12T13:57:32 01.72.10000 2010-08-16T15:08:20 </DATA>
100
+ <DATA> Property Property Property Search Contains information about listed properties. ListingRid 8 01.72.10288 2011-02-24T06:59:11 01.72.10000 2010-08-16T15:08:20 01.72.10289 2011-02-24T06:59:19 01.72.10290 2011-03-01T11:06:31 </DATA>
101
+ <DATA> PropertyDeleted Deleted Property Search Contains information about deleted properties. ListingRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T22:08:30 01.72.10000 2010-08-16T22:08:34 </DATA>
102
+ <DATA> PropertyWithheld Withheld Property Search Contains information about withheld properties. ListingRid 8 01.72.10201 2011-01-05T19:34:36 01.72.10000 2010-08-16T15:08:20 01.72.10200 2011-01-05T19:34:34 01.72.10000 2010-08-16T22:08:34 </DATA>
103
+ <DATA> Prospect Prospect Prospect Search Contains information about sales or listing propects. ProspectRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T15:08:20 </DATA>
104
+ <DATA> Tour Tour Tour Search Contains information about private tour activities. TourRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T22:08:30 01.72.10000 2010-08-16T15:08:20 </DATA>
105
+ <DATA> VirtualMedia Virtual Media Contains information about virtual media for MLS listings. VirtualMediaRid 1 01.72.10126 2010-11-12T13:47:41 01.72.10127 2010-11-12T13:47:41 01.72.10086 2010-11-10T09:59:11 </DATA>
106
+ </METADATA-RESOURCE>
107
+ </RETS>
108
+ XML
109
+
110
+ METADATA_OBJECT = "<RETS ReplyCode=\"0\" ReplyText=\"V2.6.0 728: Success\">\r\n<METADATA-OBJECT Resource=\"Property\" Version=\"1.12.24\" Date=\"Wed, 1 Dec 2010 00:00:00 GMT\">\r\n<COLUMNS>\tMetadataEntryID\tObjectType\tStandardName\tMimeType\tVisibleName\tDescription\tObjectTimeStamp\tObjectCount\t</COLUMNS>\r\n<DATA>\t50045650619\tMedium\tMedium\timage/jpeg\tMedium\tA 320 x 240 Size Photo\tLastPhotoDate\tTotalPhotoCount\t</DATA>\r\n<DATA>\t20101753230\tDocumentPDF\tDocumentPDF\tapplication/pdf\tDocumentPDF\tDocumentPDF\t\t\t</DATA>\r\n<DATA>\t50045650620\tPhoto\tPhoto\timage/jpeg\tPhoto\tA 640 x 480 Size Photo\tLastPhotoDate\tTotalPhotoCount\t</DATA>\r\n<DATA>\t50045650621\tThumbnail\tThumbnail\timage/jpeg\tThumbnail\tA 128 x 96 Size Photo\tLastPhotoDate\tTotalPhotoCount\t</DATA>\r\n</METADATA-OBJECT>\r\n</RETS>\r\n"
111
+
112
+ MULITPART_RESPONSE = [
113
+ "--simple boundary",
114
+ "Content-Type: image/jpeg",
115
+ "Content-Length: 10",
116
+ "Content-ID: 90020062739",
117
+ "Object-ID: 1",
118
+ "",
119
+ "xxxxxxxx",
120
+ "--simple boundary",
121
+ "Content-Type: image/jpeg",
122
+ "Content-Length: 10",
123
+ "Content-ID: 90020062739",
124
+ "Object-ID: 2",
125
+ "",
126
+ "yyyyyyyy",
127
+ "--simple boundary",
128
+ ""
129
+ ].join("\r\n")
130
+
131
+ MULTIPART_RESPONSE_URLS = [
132
+ '--rets.object.content.boundary.1330546052739',
133
+ 'Content-ID: 38845440',
134
+ 'Object-ID: 1',
135
+ 'Content-Type: text/xml',
136
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-1.jpg',
137
+ '',
138
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
139
+ '',
140
+ '--rets.object.content.boundary.1330546052739',
141
+ 'Content-ID: 38845440',
142
+ 'Object-ID: 2',
143
+ 'Content-Type: text/xml',
144
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-2.jpg',
145
+ '',
146
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
147
+ '',
148
+ '--rets.object.content.boundary.1330546052739',
149
+ 'Content-ID: 38845440',
150
+ 'Object-ID: 3',
151
+ 'Content-Type: text/xml',
152
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-3.jpg',
153
+ '',
154
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
155
+ '',
156
+ '--rets.object.content.boundary.1330546052739',
157
+ 'Content-ID: 38845440',
158
+ 'Object-ID: 4',
159
+ 'Content-Type: text/xml',
160
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-4.jpg',
161
+ '',
162
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
163
+ '',
164
+ '--rets.object.content.boundary.1330546052739',
165
+ 'Content-ID: 38845440',
166
+ 'Object-ID: 5',
167
+ 'Content-Type: text/xml',
168
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-5.jpg',
169
+ '',
170
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
171
+ '',
172
+ '--rets.object.content.boundary.1330546052739--'
173
+ ].join("\r\n")
174
+
175
+ SAMPLE_COMPACT = <<XML
176
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
177
+ <METADATA-TABLE Resource="ActiveAgent" Class="MEMB" Version="01.72.10236" Date="2011-03-03T00:29:23">
178
+ <COLUMNS> MetadataEntryID SystemName StandardName LongName DBName ShortName MaximumLength DataType Precision Searchable Interpretation Alignment UseSeparator EditMaskID LookupName MaxSelect Units Index Minimum Maximum Default Required SearchHelpID Unique ModTimeStamp ForeignKeyName ForeignField InKeyindex </COLUMNS>
179
+ <DATA> 7 City City City City 11 Character 0 1 Left 0 0 0 0 0 0 0 City 0 0 1 MemberNumber 0 </DATA>
180
+ <DATA> 9 ContactAddlPhoneType1 Contact Additional Phone Type 1 AddlPhTyp1 Contact Addl Ph Type 1 1 Character 0 1 Lookup Left 0 ContactAddlPhoneType 0 0 0 0 0 0 ContactAddlPhoneType 0 0 1 MemberNumber 0 </DATA>
181
+ <DATA> 11 ContactAddlPhoneType2 Contact Additional Phone Type 2 AddlPhTyp2 Contact Addl Ph Type 2 1 Character 0 1 Lookup Left 0 ContactAddlPhoneType 0 0 0 0 0 0 ContactAddlPhoneType 0 0 1 MemberNumber 0 </DATA>
182
+ <DATA> 13 ContactAddlPhoneType3 Contact Additional Phone Type 3 AddlPhTyp3 Contact Addl Ph Type 3 1 Character 0 1 Lookup Left 0 ContactAddlPhoneType 0 0 0 0 0 0 ContactAddlPhoneType 0 0 1 MemberNumber 0 </DATA>
183
+ <DATA> 15 ContactPhoneAreaCode1 Contact Phone Area Code 1 ContPhAC1 Contact Phone AC 1 3 Character 0 1 Left 0 0 0 0 0 0 0 ContactPhoneAreaCode 0 0 1 MemberNumber 0 </DATA>
184
+ <DATA> 17 ContactPhoneAreaCode2 Contact Phone Area Code 2 ContPhAC2 Contact Phone AC 2 3 Character 0 1 Left 0 0 0 0 0 0 0 ContactPhoneAreaCode 0 0 1 MemberNumber 0 </DATA>
185
+ <DATA> 19 ContactPhoneAreaCode3 Contact Phone Area Code 3 ContPhAC3 Contact Phone AC 3 3 Character 0 1 Left 0 0 0 0 0 0 0 ContactPhoneAreaCode 0 0 1 MemberNumber 0 </DATA>
186
+ </METADATA-TABLE>
187
+ </RETS>
188
+ XML
189
+
190
+ SAMPLE_COMPACT_2 = <<XML
191
+ <?xml version="1.0" encoding="utf-8"?>
192
+ <RETS ReplyCode="0" ReplyText="Success">
193
+ <METADATA-TABLE Class="15" Date="2010-10-28T05:41:31Z" Resource="Office" Version="26.27.62891">
194
+ <COLUMNS> ModTimeStamp MetadataEntryID SystemName StandardName LongName DBName ShortName MaximumLength DataType Precision Searchable Interpretation Alignment UseSeparator EditMaskID LookupName MaxSelect Units Index Minimum Maximum Default Required SearchHelpID Unique ForeignKeyName ForeignField InKeyIndex </COLUMNS>
195
+ <DATA> sysid15 sysid sysid sysid sysid 10 Int 0 1 0 0 1 0 0 1 1 </DATA>
196
+ <DATA> 15n1155 OfficePhone_f1155 OfficePhone Phone Offic_1155 Phone 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
197
+ <DATA> 15n1158 AccessFlag_f1158 Office Status Acces_1158 Status 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
198
+ <DATA> 15n1163 MODIFIED_f1163 Modified MODIF_1163 Modified 20 DateTime 0 1 0 0 0 0 0 0 0 </DATA>
199
+ <DATA> 15n1165 DESREALTOR_f1165 DesRealtor DESRE_1165 DesRealtor 75 Character 0 1 0 0 0 0 0 0 0 </DATA>
200
+ <DATA> 15n1166 DESREALTORUID_f1166 Designated Realtor Uid DESRE_1166 RealtorUid 20 Character 0 1 0 0 0 0 0 0 0 </DATA>
201
+ <DATA> 15n1167 INT_NO_f1167 Internet Syndication (Y/N) INT_N_1167 Int.Syn. 1 Character 0 1 Lookup 0 YESNO 1 0 0 0 0 0 </DATA>
202
+ <DATA> 15n1168 STATE_f1168 State STATE_1168 State 50 Character 0 1 Lookup 0 1_523 1 0 0 0 0 0 </DATA>
203
+ <DATA> 15n1169 CITY_f1169 City CITY_1169 City 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
204
+ <DATA> 15n1170 IDX_NO_f1170 IDX (Y/N) IDX_N_1170 IDX 1 Character 0 1 0 0 0 0 0 0 0 </DATA>
205
+ <DATA> 15n1172 ZipCode_f1172 Zip ZipCo_1172 Zip 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
206
+ <DATA> 15n1177 ADDRESS1_f1177 Address Line 1 ADDRE_1177 Address1 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
207
+ <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>
209
+ <DATA> 15n1193 OfficeCode_f1193 OfficeID Office Code Offic_1193 Office Code 12 Character 0 1 0 0 0 0 0 0 1 </DATA>
210
+ </METADATA-TABLE>
211
+ </RETS>
212
+ XML
@@ -0,0 +1,14 @@
1
+ require "minitest"
2
+ require "mocha/setup"
3
+
4
+ require_relative "../lib/rets"
5
+
6
+ require_relative "fixtures"
7
+
8
+ require 'vcr'
9
+
10
+ VCR.configure do |c|
11
+ c.cassette_library_dir = 'test/vcr_cassettes'
12
+ c.hook_into :webmock
13
+ end
14
+
@@ -0,0 +1,238 @@
1
+ require_relative "helper"
2
+
3
+ class TestClient < MiniTest::Test
4
+
5
+ def setup
6
+ @client = Rets::Client.new(:login_url => "http://example.com/login")
7
+ end
8
+
9
+ def test_extract_capabilities
10
+ assert_equal(
11
+ {"abc" => "123", "def" => "ghi=jk"},
12
+ @client.extract_capabilities(Nokogiri.parse(CAPABILITIES))
13
+ )
14
+ end
15
+
16
+ def test_extract_capabilities_with_whitespace
17
+ assert_equal(
18
+ {"action" => "/RETS/Action"},
19
+ @client.extract_capabilities(Nokogiri.parse(CAPABILITIES_WITH_WHITESPACE))
20
+ )
21
+ end
22
+
23
+ def test_capability_url_returns_parsed_url
24
+ @client.capabilities = { "foo" => "/foo" }
25
+
26
+ assert_equal "http://example.com/foo", @client.capability_url("foo")
27
+ end
28
+
29
+ def test_capabilities_calls_login_when_nil
30
+ @client.expects(:login)
31
+ @client.capabilities
32
+ end
33
+
34
+ def test_tries_increments_with_each_call
35
+ assert_equal 1, @client.tries
36
+ assert_equal 2, @client.tries
37
+ end
38
+
39
+ def test_metadata_when_not_initialized_with_metadata
40
+ new_raw_metadata = stub(:new_raw_metadata)
41
+
42
+ client = Rets::Client.new(:login_url => "http://example.com")
43
+ client.stubs(:retrieve_metadata).returns(new_raw_metadata)
44
+
45
+ assert_same new_raw_metadata, client.metadata.marshal_dump
46
+ end
47
+
48
+ def test_initialize_with_old_metadata_cached_contstructs_new_metadata_from_request
49
+ metadata = stub(:current? => false)
50
+ new_raw_metadata = stub(:new_raw_metadata)
51
+
52
+ client = Rets::Client.new(:login_url => "http://example.com", :metadata => metadata)
53
+ client.stubs(:capabilities).returns({})
54
+ client.stubs(:retrieve_metadata).returns(new_raw_metadata)
55
+
56
+ assert_same new_raw_metadata, client.metadata.marshal_dump
57
+ end
58
+
59
+ def test_initialize_with_current_metadata_cached_return_cached_metadata
60
+ metadata = stub(:current? => true)
61
+ client = Rets::Client.new(:login_url => "http://example.com", :metadata => metadata)
62
+ client.stubs(:capabilities => {})
63
+
64
+ assert_same metadata, client.metadata
65
+ end
66
+
67
+ def test_initialize_takes_logger
68
+ logger = Object.new
69
+
70
+ client = Rets::Client.new(:login_url => "http://example.com", :logger => logger)
71
+
72
+ assert_equal logger, client.logger
73
+ end
74
+
75
+ def test_default_logger_returns_api_compatible_silent_logger
76
+ logger = @client.logger
77
+ logger.fatal "foo"
78
+ logger.error "foo"
79
+ logger.warn "foo"
80
+ logger.info "foo"
81
+ logger.debug "foo"
82
+ end
83
+
84
+
85
+ def test_find_first_calls_find_every_with_limit_one
86
+ @client.expects(:find_every).with({:limit => 1, :foo => :bar}, nil).returns([1,2,3])
87
+
88
+ assert_equal 1, @client.find(:first, :foo => :bar, :limit => 5), "User-specified limit should be ignored"
89
+ end
90
+
91
+ def test_find_all_calls_find_every
92
+ @client.expects(:find_every).with({:limit => 5, :foo => :bar}, nil).returns([1,2,3])
93
+
94
+ assert_equal [1,2,3], @client.find(:all, :limit => 5, :foo => :bar)
95
+ end
96
+
97
+ def test_find_raises_on_unknown_quantity
98
+ assert_raises ArgumentError do
99
+ @client.find(:incorrect, :foo => :bar)
100
+ end
101
+ end
102
+
103
+ def test_find_retries_on_errors
104
+ @client.stubs(:find_every).raises(Rets::AuthorizationFailure.new(401, 'Not Authorized')).then.raises(Rets::InvalidRequest.new(20134, 'Not Found')).then.returns([])
105
+ @client.find(:all, :foo => :bar)
106
+ end
107
+
108
+ def test_find_retries_on_errors_preserves_resolve
109
+ @client.stubs(:find_every).raises(Rets::AuthorizationFailure.new(401, 'Not Authorized')).then.raises(Rets::InvalidRequest.new(20134, 'Not Found')).then.with({:foo => :bar}, true).returns([])
110
+ @client.find(:all, {:foo => :bar, :resolve => true})
111
+ end
112
+
113
+ def test_find_eventually_reraises_errors
114
+ @client.stubs(:find_every).raises(Rets::AuthorizationFailure.new(401, 'Not Authorized'))
115
+ assert_raises Rets::AuthorizationFailure do
116
+ @client.find(:all, :foo => :bar)
117
+ end
118
+ end
119
+
120
+ def test_fixup_keys
121
+ assert_equal({ "Foo" => "bar" }, @client.fixup_keys(:foo => "bar"))
122
+ assert_equal({ "FooFoo" => "bar" }, @client.fixup_keys(:foo_foo => "bar"))
123
+ end
124
+
125
+ def test_all_objects_calls_objects
126
+ @client.expects(:objects).with("*", :foo => :bar)
127
+
128
+ @client.all_objects(:foo => :bar)
129
+ end
130
+
131
+ def test_objects_handles_string_argument
132
+ @client.expects(:fetch_object).with("*", :foo => :bar)
133
+ @client.stubs(:create_parts_from_response)
134
+
135
+ @client.objects("*", :foo => :bar)
136
+ end
137
+
138
+ def test_objects_handle_array_argument
139
+ @client.expects(:fetch_object).with("1,2", :foo => :bar)
140
+ @client.stubs(:create_parts_from_response)
141
+
142
+ @client.objects([1,2], :foo => :bar)
143
+ end
144
+
145
+ def test_objects_raises_on_other_arguments
146
+ assert_raises ArgumentError do
147
+ @client.objects(Object.new, :foo => :bar)
148
+ end
149
+ end
150
+
151
+ def test_create_parts_from_response_returns_multiple_parts_when_multipart_response
152
+ response = {}
153
+ response.stubs(:header => { "content-type" => ['multipart; boundary="simple boundary"']})
154
+ response.stubs(:body => MULITPART_RESPONSE)
155
+
156
+ Rets::Parser::Multipart.expects(:parse).
157
+ with(MULITPART_RESPONSE, "simple boundary").
158
+ returns([])
159
+
160
+ @client.create_parts_from_response(response)
161
+ end
162
+
163
+ def test_parse_boundary_wo_quotes
164
+ response = {}
165
+ response.stubs(:header => { "content-type" => ['multipart; boundary=simple boundary; foo;']})
166
+ response.stubs(:body => MULITPART_RESPONSE)
167
+
168
+ Rets::Parser::Multipart.expects(:parse).
169
+ with(MULITPART_RESPONSE, "simple boundary").
170
+ returns([])
171
+
172
+ @client.create_parts_from_response(response)
173
+ end
174
+
175
+ def test_create_parts_from_response_returns_a_single_part_when_not_multipart_response
176
+ response = {}
177
+ response.stubs(:header => { "content-type" => ['text/plain']})
178
+ response.stubs(:headers => { "content-type" => ['text/plain']})
179
+ response.stubs(:body => "fakebody")
180
+
181
+ parts = @client.create_parts_from_response(response)
182
+
183
+ assert_equal 1, parts.size
184
+
185
+ part = parts.first
186
+
187
+ assert_equal "text/plain", part.headers["content-type"]
188
+ assert_equal "fakebody", part.body
189
+ end
190
+
191
+ def test_object_calls_fetch_object
192
+ response = stub(:body => "foo")
193
+
194
+ @client.expects(:fetch_object).with("1", :foo => :bar).returns(response)
195
+
196
+ assert_equal "foo", @client.object("1", :foo => :bar)
197
+ end
198
+
199
+ def test_metadata_caches
200
+ metadata = stub(:current? => true)
201
+ @client.metadata = metadata
202
+ @client.stubs(:capabilities => {})
203
+
204
+ assert_same metadata, @client.metadata, "Should be memoized"
205
+ end
206
+
207
+ def test_decorate_result_handles_bad_metadata
208
+ result = {'foo' => 'bar'}
209
+ rets_class = stub
210
+ rets_class.expects(:find_table).with('foo').returns(nil)
211
+ response = @client.decorate_result(result, rets_class)
212
+ assert_equal response, result
213
+ end
214
+
215
+ def test_clean_setup_with_receive_timeout
216
+ HTTPClient.any_instance.expects(:receive_timeout=).with(1234)
217
+ @client = Rets::Client.new(
218
+ login_url: 'http://example.com/login',
219
+ receive_timeout: 1234
220
+ )
221
+ end
222
+
223
+ def test_clean_setup_with_proxy_auth
224
+ @login_url = 'http://example.com/login'
225
+ @proxy_url = 'http://example.com/proxy'
226
+ @proxy_username = 'username'
227
+ @proxy_password = 'password'
228
+ HTTPClient.any_instance.expects(:set_proxy_auth).with(@proxy_username, @proxy_password)
229
+
230
+ @client = Rets::Client.new(
231
+ login_url: @login_url,
232
+ http_proxy: @proxy_url,
233
+ proxy_username: @proxy_username,
234
+ proxy_password: @proxy_password
235
+ )
236
+ end
237
+
238
+ end