sean-rets 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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