rets 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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Manifest.txt +24 -0
  4. data/README.md +73 -1
  5. data/Rakefile +1 -1
  6. data/lib/rets.rb +202 -1
  7. data/lib/rets/client.rb +83 -94
  8. data/lib/rets/http_client.rb +42 -0
  9. data/lib/rets/metadata.rb +15 -3
  10. data/lib/rets/metadata/caching.rb +59 -0
  11. data/lib/rets/metadata/file_cache.rb +29 -0
  12. data/lib/rets/metadata/json_serializer.rb +27 -0
  13. data/lib/rets/metadata/lookup_table.rb +65 -0
  14. data/lib/rets/metadata/lookup_type.rb +3 -4
  15. data/lib/rets/metadata/marshal_serializer.rb +27 -0
  16. data/lib/rets/metadata/multi_lookup_table.rb +70 -0
  17. data/lib/rets/metadata/null_cache.rb +24 -0
  18. data/lib/rets/metadata/resource.rb +39 -29
  19. data/lib/rets/metadata/rets_class.rb +27 -23
  20. data/lib/rets/metadata/rets_object.rb +32 -0
  21. data/lib/rets/metadata/table.rb +9 -101
  22. data/lib/rets/metadata/table_factory.rb +19 -0
  23. data/lib/rets/metadata/yaml_serializer.rb +27 -0
  24. data/lib/rets/parser/compact.rb +61 -18
  25. data/lib/rets/parser/error_checker.rb +8 -1
  26. data/test/fixtures.rb +58 -0
  27. data/test/test_caching.rb +89 -0
  28. data/test/test_client.rb +44 -24
  29. data/test/test_error_checker.rb +18 -0
  30. data/test/test_file_cache.rb +42 -0
  31. data/test/test_http_client.rb +96 -60
  32. data/test/test_json_serializer.rb +26 -0
  33. data/test/test_marshal_serializer.rb +26 -0
  34. data/test/test_metadata.rb +62 -450
  35. data/test/test_metadata_class.rb +50 -0
  36. data/test/test_metadata_lookup_table.rb +21 -0
  37. data/test/test_metadata_lookup_type.rb +12 -0
  38. data/test/test_metadata_multi_lookup_table.rb +60 -0
  39. data/test/test_metadata_object.rb +20 -0
  40. data/test/test_metadata_resource.rb +140 -0
  41. data/test/test_metadata_root.rb +151 -0
  42. data/test/test_metadata_table.rb +21 -0
  43. data/test/test_metadata_table_factory.rb +24 -0
  44. data/test/test_parser_compact.rb +23 -28
  45. data/test/test_yaml_serializer.rb +26 -0
  46. metadata +29 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6a9bf5e37008d7651782e347a520f24cb291931f
4
- data.tar.gz: 22742e9f4a45db910f03358e45024ee25d8cb6e9
3
+ metadata.gz: cedf0e419db8f2c813a4c2c97fd210909b9b58af
4
+ data.tar.gz: b8eb9ad36844f79503c64f020830ef477586fa20
5
5
  SHA512:
6
- metadata.gz: a742d37803a7c1354bb4f8d2c087b9a20b745c984286df5c243022cbb053612cb281350f589ca259494e955b7404bfd37db18ff2812441b2a560ad33485976f0
7
- data.tar.gz: 250667d8536d504b41fc4192d8bb124237cd48bd663d061baccca0728b6408abd2d8776cd3ccf70ba6d7458a9a56e61f83a853510be725b264940be0e0c777b9
6
+ metadata.gz: e116e7a292cb5e8118cfff090aae8147a431299fd79b81572bf0afb6d093c0152f1ad35c671e2199898ddaf5b8ea12061a011184776179a239b44c8a8baf9b74
7
+ data.tar.gz: d8f6da1337852f4ba871eabb531e31765e5a1a1167f57a9aad5b02ce01c6a242d260d7f6e63492e8fd64c89f9cb4da12a1ec344c20b39f519195cf075cdce666
@@ -1,3 +1,22 @@
1
+ ### 0.10.0 / 2016-02-29
2
+
3
+ * fix: ensure cookie store exists #133
4
+ * feature: make cached capabilities case insensitive #136
5
+ * feature: add specific classes for each rets error #137
6
+ * feature: whitelist RETS search options #142
7
+ * feature: simplify metadata caching #134
8
+ * feature: use a SAX parser #98
9
+ * fix: save capabilities to avoid double logins #148
10
+ * feature: login on authorization error #155
11
+ * add basic support for DataDictionary feeds #156
12
+ * fix: count always returns a number #161
13
+ * feature: make lookup tables case insensitive #163
14
+ * feature: update to httpclient 2.7 #165
15
+ * fix: getObject now works with non-multipart responses #166
16
+ * fix: getObject works with multiple ids #167
17
+ * feature: store rets object metadata #168
18
+ * feature: add a code of conduct #171
19
+
1
20
  ### 0.9.0 / 2015-06-11
2
21
 
3
22
  * feature: update to httpclient 2.6
@@ -13,22 +13,46 @@ lib/rets/http_client.rb
13
13
  lib/rets/locking_http_client.rb
14
14
  lib/rets/measuring_http_client.rb
15
15
  lib/rets/metadata.rb
16
+ lib/rets/metadata/caching.rb
16
17
  lib/rets/metadata/containers.rb
18
+ lib/rets/metadata/file_cache.rb
19
+ lib/rets/metadata/json_serializer.rb
20
+ lib/rets/metadata/lookup_table.rb
17
21
  lib/rets/metadata/lookup_type.rb
22
+ lib/rets/metadata/marshal_serializer.rb
23
+ lib/rets/metadata/multi_lookup_table.rb
24
+ lib/rets/metadata/null_cache.rb
18
25
  lib/rets/metadata/resource.rb
19
26
  lib/rets/metadata/rets_class.rb
27
+ lib/rets/metadata/rets_object.rb
20
28
  lib/rets/metadata/root.rb
21
29
  lib/rets/metadata/table.rb
30
+ lib/rets/metadata/table_factory.rb
31
+ lib/rets/metadata/yaml_serializer.rb
22
32
  lib/rets/parser/compact.rb
23
33
  lib/rets/parser/error_checker.rb
24
34
  lib/rets/parser/multipart.rb
25
35
  test/fixtures.rb
26
36
  test/helper.rb
37
+ test/test_caching.rb
27
38
  test/test_client.rb
28
39
  test/test_error_checker.rb
40
+ test/test_file_cache.rb
29
41
  test/test_http_client.rb
42
+ test/test_json_serializer.rb
30
43
  test/test_locking_http_client.rb
44
+ test/test_marshal_serializer.rb
31
45
  test/test_metadata.rb
46
+ test/test_metadata_class.rb
47
+ test/test_metadata_lookup_table.rb
48
+ test/test_metadata_lookup_type.rb
49
+ test/test_metadata_multi_lookup_table.rb
50
+ test/test_metadata_object.rb
51
+ test/test_metadata_resource.rb
52
+ test/test_metadata_root.rb
53
+ test/test_metadata_table.rb
54
+ test/test_metadata_table_factory.rb
32
55
  test/test_parser_compact.rb
33
56
  test/test_parser_multipart.rb
57
+ test/test_yaml_serializer.rb
34
58
  test/vcr_cassettes/unauthorized_response.yml
data/README.md CHANGED
@@ -28,7 +28,79 @@ gem 'rets'
28
28
  ## EXAMPLE USAGE:
29
29
 
30
30
  We need work in this area! There are currently a few guideline examples in the `example` folder on connecting, fetching a property's data, and fetching a property's photos.
31
-
31
+
32
+ ## Metadata caching
33
+
34
+ Metadata, which is loaded when a client is first started, can be slow
35
+ to fetch. To avoid the cost of fetching metadata every time the
36
+ client is started, metadata can be cached.
37
+
38
+ To cache metadata, pass the :metadata_cache option to the client when
39
+ you start it. The library comes with a predefined metadata cache that
40
+ persists the metadata to a file. It is created with the path to which
41
+ the cached metadata should be written:
42
+
43
+ metadata_cache = Rets::Metadata::FileCache.new("/tmp/metadata")
44
+
45
+ When you create the RETS client, pass it the metadata cache:
46
+
47
+ client = Rets::Client.new(
48
+ ...
49
+ metadata_cache: metadata_cache
50
+ )
51
+
52
+ If you want to persist to something other than a file, create your own
53
+ metadata cache object and pass it in. It should have the same interface
54
+ as the built-in Metadata::FileCache class:
55
+
56
+ class MyMetadataCache
57
+
58
+ # Save the metadata. Should yield an IO-like object to a block;
59
+ # that block will serialize the metadata to that object.
60
+ def save(&block)
61
+ end
62
+
63
+ # Load the metadata. Should yield an IO-like object to a block;
64
+ # that block will deserialize the metadata from that object and
65
+ # return the metadata. Returns the metadata, or nil if it could
66
+ # not be loaded.
67
+ def load(&block)
68
+ end
69
+
70
+ end
71
+
72
+ By default, the metadata is serialized using Marshal. You may select
73
+ JSON or YAML instead, or define your own serialization mechanism, using the
74
+ :metadata_serializer option when you create the Rets::Client:
75
+
76
+ client = Rets::Client.new(
77
+ ...
78
+ metadata_serializer: Rets::Metadata::JsonSerializer.new
79
+ )
80
+
81
+ The built-in serializers are:
82
+
83
+ * Rets::Metadata::JsonSerializer
84
+ * Rets::Metadata::MarshalSerializer
85
+ * Rets::Metadata::YamlSerializer
86
+
87
+ To define your own serializer, create an object with this interface:
88
+
89
+ class MySerializer
90
+
91
+ # Serialize to a file. The library reserves the right to change
92
+ # the type or contents of o, so don't depend on it being
93
+ # anything in particular.
94
+ def save(file, o)
95
+ end
96
+
97
+ # Deserialize from a file. If the metadata cannot be
98
+ # deserialized, return nil.
99
+ def load(file)
100
+ end
101
+
102
+ end
103
+
32
104
  ## LICENSE:
33
105
 
34
106
  (The MIT License)
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ Hoe.plugin :gemspec
9
9
  Hoe.spec 'rets' do
10
10
  developer 'Estately, Inc. Open Source', 'opensource@estately.com'
11
11
 
12
- extra_deps << [ "httpclient", "~> 2.6.0" ]
12
+ extra_deps << [ "httpclient", "~> 2.7.0" ]
13
13
  extra_deps << [ "http-cookie", "~> 1.0.0" ]
14
14
  extra_deps << [ "nokogiri", "~> 1.5" ]
15
15
 
@@ -3,8 +3,9 @@ require 'digest/md5'
3
3
  require 'nokogiri'
4
4
 
5
5
  module Rets
6
- VERSION = '0.9.0'
6
+ VERSION = '0.10.0'
7
7
 
8
+ HttpError = Class.new(StandardError)
8
9
  MalformedResponse = Class.new(ArgumentError)
9
10
  UnknownResponse = Class.new(ArgumentError)
10
11
  NoLogout = Class.new(ArgumentError)
@@ -55,6 +56,206 @@ module Rets
55
56
  super("unknown capability #{capability_name}, available capabilities #{capabilities}")
56
57
  end
57
58
  end
59
+
60
+ class SystemError < InvalidRequest
61
+ ERROR_CODE = 10000
62
+ end
63
+
64
+ class ZeroBalance < InvalidRequest
65
+ ERROR_CODE = 20003
66
+ end
67
+
68
+ class BrokerCodeRequired < InvalidRequest
69
+ ERROR_CODE = 20012
70
+ end
71
+
72
+ class AdditionalLoginNotPermitted < InvalidRequest
73
+ ERROR_CODE = 20022
74
+ end
75
+
76
+ class ServerLoginError < InvalidRequest
77
+ ERROR_CODE = 20036
78
+ end
79
+
80
+ class ClientAuthenticationFailed < InvalidRequest
81
+ ERROR_CODE = 20037
82
+ end
83
+
84
+ class UserAgentAuthenticationRequired < InvalidRequest
85
+ ERROR_CODE = 20041
86
+ end
87
+
88
+ class ServerTemporarilyDisabled < InvalidRequest
89
+ ERROR_CODE = 20050
90
+ end
91
+
92
+ class InsecurePassword < InvalidRequest
93
+ ERROR_CODE = 20140
94
+ end
95
+
96
+ class SameAsPreviousPassword < InvalidRequest
97
+ ERROR_CODE = 20141
98
+ end
99
+
100
+ class InvalidUserName < InvalidRequest
101
+ ERROR_CODE = 20142
102
+ end
103
+
104
+ class UnknownQueryField < InvalidRequest
105
+ ERROR_CODE = 20200
106
+ end
107
+
108
+ class InvalidSelect < InvalidRequest
109
+ ERROR_CODE = 20202
110
+ end
111
+
112
+ class MiscellaneousSearchError < InvalidRequest
113
+ ERROR_CODE = 20203
114
+ end
115
+
116
+ class InvalidQuerySyntax < InvalidRequest
117
+ ERROR_CODE = 20206
118
+ end
119
+
120
+ class UnauthorizedQuery < InvalidRequest
121
+ ERROR_CODE = 20207
122
+ end
123
+
124
+ class MaximumRecordsExceeded < InvalidRequest
125
+ ERROR_CODE = 20208
126
+ end
127
+
128
+ class RequestTimeout < InvalidRequest
129
+ ERROR_CODE = 20209
130
+ end
131
+
132
+ class TooManyOutstandingQueries < InvalidRequest
133
+ ERROR_CODE = 20210
134
+ end
135
+
136
+ class QueryTooComplex < InvalidRequest
137
+ ERROR_CODE = 20211
138
+ end
139
+
140
+ class InvalidKeyRequest < InvalidRequest
141
+ ERROR_CODE = 20212
142
+ end
143
+
144
+ class InvalidKey < InvalidRequest
145
+ ERROR_CODE = 20213
146
+ end
147
+
148
+ class InvalidParameter < InvalidRequest
149
+ ERROR_CODE = 20301
150
+ end
151
+
152
+ class UnableToSaveRecord < InvalidRequest
153
+ ERROR_CODE = 20301
154
+ end
155
+
156
+ class MiscellaneousUpdateError < InvalidRequest
157
+ ERROR_CODE = 20301
158
+ end
159
+
160
+ class WarningResponseNotGivenForAllWarnings < InvalidRequest
161
+ ERROR_CODE = 20311
162
+ end
163
+
164
+ class WarningResponseGivenForWarningNotRequired < InvalidRequest
165
+ ERROR_CODE = 20312
166
+ end
167
+
168
+ class InvalidResource < InvalidRequest
169
+ ERROR_CODE = 20400
170
+ end
171
+
172
+ class InvalidType < InvalidRequest
173
+ ERROR_CODE = 20401
174
+ end
175
+
176
+ class InvalidIdentifier < InvalidRequest
177
+ ERROR_CODE = 20402
178
+ end
179
+
180
+ class UnsupportedMimeType < InvalidRequest
181
+ ERROR_CODE = 20406
182
+ end
183
+
184
+ class UnauthorizedRetrieval < InvalidRequest
185
+ ERROR_CODE = 20407
186
+ end
187
+
188
+ class ResourceUnavailable < InvalidRequest
189
+ ERROR_CODE = 20408
190
+ end
191
+
192
+ class ObjectUnavailable < InvalidRequest
193
+ ERROR_CODE = 20409
194
+ end
195
+
196
+ class RequestTooLarge < InvalidRequest
197
+ ERROR_CODE = 20410
198
+ end
199
+
200
+ class ExecutionTimeout < InvalidRequest
201
+ ERROR_CODE = 20411
202
+ end
203
+
204
+ class TooManyOutstandingRequests < InvalidRequest
205
+ ERROR_CODE = 20412
206
+ end
207
+
208
+ class InvalidResourceRequested < InvalidRequest
209
+ ERROR_CODE = 20500
210
+ end
211
+
212
+ class InvalidMetadataType < InvalidRequest
213
+ ERROR_CODE = 20501
214
+ end
215
+
216
+ class InvalidIdentifierRequested < InvalidRequest
217
+ ERROR_CODE = 20502
218
+ end
219
+
220
+ class NoMetadataFound < InvalidRequest
221
+ ERROR_CODE = 20503
222
+ end
223
+
224
+ class UnsupportedMetadataMimeType < InvalidRequest
225
+ ERROR_CODE = 20506
226
+ end
227
+
228
+ class UnauthorizedMetadataRetrieval < InvalidRequest
229
+ ERROR_CODE = 20507
230
+ end
231
+
232
+ class MetadataResourceUnavailable < InvalidRequest
233
+ ERROR_CODE = 20508
234
+ end
235
+
236
+ class MetadataUnavailable < InvalidRequest
237
+ ERROR_CODE = 20509
238
+ end
239
+
240
+ class MetadataRequestTooLarge < InvalidRequest
241
+ ERROR_CODE = 20510
242
+ end
243
+
244
+ class MetadataRequestTimeout < InvalidRequest
245
+ ERROR_CODE = 20511
246
+ end
247
+
248
+ class TooManyOutstandingMetadataRequests < InvalidRequest
249
+ ERROR_CODE = 20512
250
+ end
251
+
252
+ class RequestedDTDVersionUnavailable < InvalidRequest
253
+ ERROR_CODE = 20514
254
+ end
255
+
256
+ class NotLoggedIn < InvalidRequest
257
+ ERROR_CODE = 20701
258
+ end
58
259
  end
59
260
 
60
261
  require 'rets/http_client'
@@ -1,17 +1,11 @@
1
- require 'http-cookie'
2
- require 'httpclient'
3
1
  require 'logger'
4
2
 
5
3
  module Rets
6
- class HttpError < StandardError ; end
7
-
8
4
  class Client
9
- DEFAULT_OPTIONS = {}
10
-
11
5
  COUNT = Struct.new(:exclude, :include, :only).new(0,1,2)
6
+ CASE_INSENSITIVE_PROC = Proc.new { |h,k| h.key?(k.downcase) ? h[k.downcase] : nil }
12
7
 
13
- attr_accessor :login_url, :options, :logger
14
- attr_writer :capabilities, :metadata
8
+ attr_accessor :cached_metadata, :client_progress, :logger, :login_url, :options
15
9
 
16
10
  def initialize(options)
17
11
  @options = options
@@ -19,41 +13,15 @@ module Rets
19
13
  end
20
14
 
21
15
  def clean_setup
22
- self.options = DEFAULT_OPTIONS.merge(@options)
23
- self.login_url = self.options[:login_url]
24
-
25
- @cached_metadata = nil
26
- @capabilities = nil
27
- @metadata = nil
28
- @tries = nil
29
- self.capabilities = nil
30
-
31
- self.logger = @options[:logger] || FakeLogger.new
32
- @client_progress = ClientProgressReporter.new(self.logger, options[:stats_collector], options[:stats_prefix])
33
- @cached_metadata = @options[:metadata]
34
- if @options[:http_proxy]
35
- @http = HTTPClient.new(options.fetch(:http_proxy))
36
-
37
- if @options[:proxy_username]
38
- @http.set_proxy_auth(options.fetch(:proxy_username), options.fetch(:proxy_password))
39
- end
40
- else
41
- @http = HTTPClient.new
42
- end
43
-
44
- if @options[:receive_timeout]
45
- @http.receive_timeout = @options[:receive_timeout]
46
- end
47
-
48
- @http.set_cookie_store(options[:cookie_store]) if options[:cookie_store]
49
-
50
- @http_client = Rets::HttpClient.new(@http, @options, @logger, @login_url)
51
- if options[:http_timing_stats_collector]
52
- @http_client = Rets::MeasuringHttpClient.new(@http_client, options.fetch(:http_timing_stats_collector), options.fetch(:http_timing_stats_prefix))
53
- end
54
- if options[:lock_around_http_requests]
55
- @http_client = Rets::LockingHttpClient.new(@http_client, options.fetch(:locker), options.fetch(:lock_name), options.fetch(:lock_options))
56
- end
16
+ @metadata = nil
17
+ @tries = nil
18
+ @login_url = options[:login_url]
19
+ @cached_metadata = options[:metadata]
20
+ @cached_capabilities = options[:capabilities]
21
+ @logger = options[:logger] || FakeLogger.new
22
+ @client_progress = ClientProgressReporter.new(logger, options[:stats_collector], options[:stats_prefix])
23
+ @http_client = Rets::HttpClient.from_options(options, logger)
24
+ @caching = Metadata::Caching.make(options)
57
25
  end
58
26
 
59
27
  # Attempts to login by making an empty request to the URL provided in
@@ -63,9 +31,11 @@ module Rets
63
31
  res = http_get(login_url)
64
32
  Parser::ErrorChecker.check(res)
65
33
 
66
- self.capabilities = extract_capabilities(Nokogiri.parse(res.body))
67
- raise UnknownResponse, "Cannot read rets server capabilities." unless @capabilities
68
- @capabilities
34
+ new_capabilities = extract_capabilities(Nokogiri.parse(res.body))
35
+ unless new_capabilities
36
+ raise UnknownResponse, "Cannot read rets server capabilities."
37
+ end
38
+ @capabilities = new_capabilities
69
39
  end
70
40
 
71
41
  def logout
@@ -110,39 +80,58 @@ module Rets
110
80
 
111
81
  def find_with_retries(opts = {})
112
82
  retries = 0
113
- resolve = opts.delete(:resolve)
114
- find_with_given_retry(retries, resolve, opts)
83
+ find_with_given_retry(retries, opts)
115
84
  end
116
85
 
117
- def find_with_given_retry(retries, resolve, opts)
86
+ def find_with_given_retry(retries, opts)
118
87
  begin
119
- find_every(opts, resolve)
88
+ find_every(opts)
120
89
  rescue NoRecordsFound => e
121
90
  if opts.fetch(:no_records_not_an_error, false)
122
- @client_progress.no_records_found
91
+ client_progress.no_records_found
123
92
  opts[:count] == COUNT.only ? 0 : []
124
93
  else
125
- handle_find_failure(retries, resolve, opts, e)
94
+ handle_find_failure(retries, opts, e)
126
95
  end
127
- rescue AuthorizationFailure, InvalidRequest => e
128
- handle_find_failure(retries, resolve, opts, e)
96
+ rescue InvalidRequest, HttpError => e
97
+ handle_find_failure(retries, opts, e)
98
+ rescue AuthorizationFailure => e
99
+ login
100
+ handle_find_failure(retries, opts, e)
129
101
  end
130
102
  end
131
103
 
132
- def handle_find_failure(retries, resolve, opts, e)
104
+ def handle_find_failure(retries, opts, e)
133
105
  if retries < opts.fetch(:max_retries, 3)
134
106
  retries += 1
135
- @client_progress.find_with_retries_failed_a_retry(e, retries)
107
+ client_progress.find_with_retries_failed_a_retry(e, retries)
136
108
  clean_setup
137
- find_with_given_retry(retries, resolve, opts)
109
+ find_with_given_retry(retries, opts)
138
110
  else
139
- @client_progress.find_with_retries_exceeded_retry_count(e)
111
+ client_progress.find_with_retries_exceeded_retry_count(e)
140
112
  raise e
141
113
  end
142
114
  end
143
115
 
144
- def find_every(opts, resolve)
145
- params = {"QueryType" => "DMQL2", "Format" => "COMPACT"}.merge(fixup_keys(opts))
116
+ def find_every(opts)
117
+ raise ArgumentError.new("missing option :search_type (provide the name of a RETS resource)") unless opts[:search_type]
118
+ raise ArgumentError.new("missing option :class (provide the name of a RETS class)") unless opts[:class]
119
+
120
+ params = {
121
+ "SearchType" => opts.fetch(:search_type),
122
+ "Class" => opts.fetch(:class),
123
+
124
+ "Count" => opts[:count],
125
+ "Format" => opts.fetch(:format, "COMPACT"),
126
+ "Limit" => opts[:limit],
127
+ "Offset" => opts[:offset],
128
+ "Select" => opts[:select],
129
+ "RestrictedIndicator" => opts[:RestrictedIndicator],
130
+ "StandardNames" => opts[:standard_name],
131
+ "Payload" => opts[:payload],
132
+ "Query" => opts[:query],
133
+ "QueryType" => opts.fetch(:query_type, "DMQL2"),
134
+ }.reject { |k,v| v.nil? }
146
135
  res = http_post(capability_url("Search"), params)
147
136
 
148
137
  if opts[:count] == COUNT.only
@@ -151,7 +140,7 @@ module Rets
151
140
  results = Parser::Compact.parse_document(
152
141
  res.body.encode("UTF-8", res.body.encoding, :invalid => :replace, :undef => :replace)
153
142
  )
154
- if resolve
143
+ if opts[:resolve]
155
144
  rets_class = find_rets_class(opts[:search_type], opts[:class])
156
145
  decorate_results(results, rets_class)
157
146
  else
@@ -177,7 +166,7 @@ module Rets
177
166
  result[key] = table.resolve(value.to_s)
178
167
  else
179
168
  #can't resolve just leave the value be
180
- @client_progress.could_not_resolve_find_metadata(key)
169
+ client_progress.could_not_resolve_find_metadata(key)
181
170
  end
182
171
  end
183
172
  end
@@ -191,7 +180,7 @@ module Rets
191
180
  def objects(object_ids, opts = {})
192
181
  response = case object_ids
193
182
  when String then fetch_object(object_ids, opts)
194
- when Array then fetch_object(object_ids.join(","), opts)
183
+ when Array then fetch_object(object_ids.join(":"), opts)
195
184
  else raise ArgumentError, "Expected instance of String or Array, but got #{object_ids.inspect}."
196
185
  end
197
186
 
@@ -214,10 +203,11 @@ module Rets
214
203
 
215
204
  return parts
216
205
  else
206
+ logger.debug "Rets::Client: Found 1 part (the whole body)"
207
+
217
208
  # fake a multipart for interface compatibility
218
209
  headers = {}
219
- response.headers.each { |k,v| headers[k] = v[0] }
220
-
210
+ response.headers.each { |k,v| headers[k.downcase] = v }
221
211
  part = Parser::Multipart::Part.new(headers, response.body)
222
212
 
223
213
  return [part]
@@ -250,35 +240,24 @@ module Rets
250
240
  http_post(capability_url("GetObject"), params, extra_headers)
251
241
  end
252
242
 
253
- # Changes keys to be camel cased, per the RETS standard for queries.
254
- def fixup_keys(hash)
255
- fixed_hash = {}
256
-
257
- hash.each do |key, value|
258
- camel_cased_key = key.to_s.capitalize.gsub(/_(\w)/) { $1.upcase }
259
-
260
- fixed_hash[camel_cased_key] = value
261
- end
262
-
263
- fixed_hash
264
- end
265
-
266
- def metadata
243
+ def metadata(types=nil)
267
244
  return @metadata if @metadata
268
-
269
- if @cached_metadata && (@options[:skip_metadata_uptodate_check] ||
270
- @cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"]))
271
- @client_progress.use_cached_metadata
272
- self.metadata = @cached_metadata
245
+ @cached_metadata ||= @caching.load(@logger)
246
+ if cached_metadata && (options[:skip_metadata_uptodate_check] ||
247
+ cached_metadata.current?(capabilities["MetadataTimestamp"], capabilities["MetadataVersion"]))
248
+ client_progress.use_cached_metadata
249
+ @metadata = cached_metadata
273
250
  else
274
- @client_progress.bad_cached_metadata(@cached_metadata)
275
- self.metadata = Metadata::Root.new(logger, retrieve_metadata)
251
+ client_progress.bad_cached_metadata(cached_metadata)
252
+ @metadata = Metadata::Root.new(logger, retrieve_metadata(types))
253
+ @caching.save(metadata)
276
254
  end
255
+ @metadata
277
256
  end
278
257
 
279
- def retrieve_metadata
258
+ def retrieve_metadata(types=nil)
280
259
  raw_metadata = {}
281
- Metadata::METADATA_TYPES.each {|type|
260
+ (types || Metadata::METADATA_TYPES).each {|type|
282
261
  raw_metadata[type] = retrieve_metadata_type(type)
283
262
  }
284
263
  raw_metadata
@@ -301,7 +280,13 @@ module Rets
301
280
  #
302
281
  # [1] In fact, sometimes only a path is returned from the server.
303
282
  def capabilities
304
- @capabilities || login
283
+ if @capabilities
284
+ @capabilities
285
+ elsif @cached_capabilities
286
+ @capabilities = add_case_insensitive_default_proc(@cached_capabilities)
287
+ else
288
+ login
289
+ end
305
290
  end
306
291
 
307
292
  def capability_url(name)
@@ -325,15 +310,19 @@ module Rets
325
310
  def extract_capabilities(document)
326
311
  raw_key_values = document.xpath("/RETS/RETS-RESPONSE").text.strip
327
312
 
328
- hash = Hash.new{|h,k| h.key?(k.downcase) ? h[k.downcase] : nil }
329
-
330
313
  # ... :(
331
314
  # Feel free to make this better. It has a test.
332
- raw_key_values.split(/\n/).
315
+ hash = raw_key_values.split(/\n/).
333
316
  map { |r| r.split(/\=/, 2) }.
334
- each { |k,v| hash[k.strip.downcase] = v.strip }
317
+ each_with_object({}) { |(k,v), h| h[k.strip.downcase] = v.strip }
318
+
319
+ add_case_insensitive_default_proc(hash)
320
+ end
335
321
 
336
- hash
322
+ def add_case_insensitive_default_proc(hash)
323
+ new_hash = hash.dup
324
+ new_hash.default_proc = CASE_INSENSITIVE_PROC
325
+ new_hash
337
326
  end
338
327
 
339
328
  def save_cookie_store