rets4r 0.8.5 → 1.1.18

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.
Files changed (79) hide show
  1. data/.document +5 -0
  2. data/{test/client/data/1.5/metadata.xml → .gemtest} +0 -0
  3. data/CHANGELOG +611 -66
  4. data/CONTRIBUTORS +6 -2
  5. data/Gemfile +1 -0
  6. data/LICENSE +22 -0
  7. data/MANIFEST +63 -0
  8. data/NEWS +203 -0
  9. data/{README → README.rdoc} +11 -4
  10. data/RUBYS +7 -7
  11. data/Rakefile +48 -0
  12. data/TODO +5 -1
  13. data/examples/client_get_object.rb +31 -42
  14. data/examples/client_login.rb +20 -18
  15. data/examples/client_mapper.rb +17 -0
  16. data/examples/client_metadata.rb +28 -28
  17. data/examples/client_parser.rb +9 -0
  18. data/examples/client_search.rb +25 -27
  19. data/examples/settings.yml +114 -0
  20. data/lib/rets4r.rb +14 -1
  21. data/lib/rets4r/auth.rb +70 -66
  22. data/lib/rets4r/client.rb +470 -650
  23. data/lib/rets4r/client/data.rb +13 -13
  24. data/lib/rets4r/client/dataobject.rb +27 -19
  25. data/lib/rets4r/client/exceptions.rb +116 -0
  26. data/lib/rets4r/client/links.rb +32 -0
  27. data/lib/rets4r/client/metadata.rb +12 -12
  28. data/lib/rets4r/client/parsers/compact.rb +42 -0
  29. data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
  30. data/lib/rets4r/client/parsers/metadata.rb +92 -0
  31. data/lib/rets4r/client/parsers/response_parser.rb +100 -0
  32. data/lib/rets4r/client/requester.rb +143 -0
  33. data/lib/rets4r/client/transaction.rb +30 -33
  34. data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
  35. data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
  36. data/lib/rets4r/core_ext/hash/keys.rb +46 -0
  37. data/lib/rets4r/core_ext/hash/slice.rb +39 -0
  38. data/lib/rets4r/listing_mapper.rb +17 -0
  39. data/lib/rets4r/listing_service.rb +35 -0
  40. data/lib/rets4r/loader.rb +8 -0
  41. data/lib/tasks/annotations.rake +121 -0
  42. data/lib/tasks/coverage.rake +13 -0
  43. data/rets4r.gemspec +24 -0
  44. data/spec/rets4r_compact_data_parser_spec.rb +7 -0
  45. data/test/data/1.5/bad_compact.xml +7 -0
  46. data/test/data/1.5/count_only_compact.xml +3 -0
  47. data/test/{client/data → data}/1.5/error.xml +0 -0
  48. data/test/{client/data → data}/1.5/invalid_compact.xml +0 -0
  49. data/test/{client/data → data}/1.5/login.xml +0 -0
  50. data/test/data/1.5/metadata.xml +0 -0
  51. data/test/{client/data → data}/1.5/search_compact.xml +0 -0
  52. data/test/data/1.5/search_compact_big.xml +136 -0
  53. data/test/{client/data → data}/1.5/search_unescaped_compact.xml +0 -0
  54. data/test/data/listing_service.yml +36 -0
  55. data/test/test_auth.rb +68 -0
  56. data/test/test_client.rb +342 -0
  57. data/test/test_client_links.rb +39 -0
  58. data/test/test_compact_nokogiri.rb +64 -0
  59. data/test/test_helper.rb +12 -0
  60. data/test/test_listing_mapper.rb +112 -0
  61. data/test/test_loader.rb +24 -0
  62. data/test/test_parser.rb +96 -0
  63. data/test/test_quality.rb +57 -0
  64. metadata +168 -53
  65. data/GPL +0 -340
  66. data/examples/metadata.xml +0 -42
  67. data/lib/rets4r/client/metadataindex.rb +0 -82
  68. data/lib/rets4r/client/parser.rb +0 -141
  69. data/lib/rets4r/client/parser/rexml.rb +0 -75
  70. data/lib/rets4r/client/parser/xmlparser.rb +0 -95
  71. data/test/client/parser/tc_rexml.rb +0 -17
  72. data/test/client/parser/tc_xmlparser.rb +0 -21
  73. data/test/client/tc_auth.rb +0 -68
  74. data/test/client/tc_client.rb +0 -320
  75. data/test/client/tc_metadataindex.rb +0 -36
  76. data/test/client/test_parser.rb +0 -128
  77. data/test/client/ts_all.rb +0 -8
  78. data/test/ts_all.rb +0 -1
  79. data/test/ts_client.rb +0 -1
@@ -0,0 +1,100 @@
1
+ require 'rets4r/client/transaction'
2
+ require 'rets4r/client/parsers/compact'
3
+ require 'rexml/document'
4
+
5
+ module RETS4R
6
+ class Client
7
+ class ResponseParser
8
+ def parse_key_value(xml)
9
+ parse_common(xml) do |doc|
10
+ parsed = nil
11
+ first_child = doc.get_elements('/RETS/RETS-RESPONSE')[0] ? doc.get_elements('/RETS/RETS-RESPONSE')[0] : doc.get_elements('/RETS')[0]
12
+ unless first_child.nil?
13
+ parsed = {}
14
+ first_child.text.each do |line|
15
+ (key, value) = line.strip.split('=')
16
+ key.strip! if key
17
+ value.strip! if value
18
+ parsed[key] = value
19
+ end
20
+ else
21
+ raise 'Response was not a proper RETS XML doc!'
22
+ end
23
+
24
+ if parsed.nil?
25
+ raise "Response was not valid key/value format"
26
+ else
27
+ parsed
28
+ end
29
+ end
30
+ end
31
+
32
+ def parse_results(xml, format)
33
+ parse_common(xml) do |doc|
34
+ parser = get_parser_by_name(format)
35
+ parser.parse_results(doc)
36
+ end
37
+ end
38
+
39
+ def parse_count(xml)
40
+ parse_common(xml) do |doc|
41
+ doc.get_elements('/RETS/COUNT')[0].attributes['Records']
42
+ end
43
+ end
44
+
45
+ def parse_metadata(xml, format)
46
+ parse_common(xml) do |doc|
47
+ return REXML::Document.new(xml)
48
+ end
49
+ end
50
+
51
+ def parse_object_response(xml)
52
+ parse_common(xml) do |doc|
53
+ # XXX
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def parse_common(xml, &block)
60
+ if xml == ''
61
+ raise RETSException, 'No transaction body was returned!'
62
+ end
63
+
64
+ doc = REXML::Document.new(xml)
65
+
66
+ root = doc.root
67
+ if root.nil? || root.name != 'RETS'
68
+ raise "Response had invalid root node. Document was: #{doc.inspect}"
69
+ end
70
+
71
+ transaction = Transaction.new
72
+ transaction.reply_code = root.attributes['ReplyCode']
73
+ transaction.reply_text = root.attributes['ReplyText']
74
+ transaction.maxrows = (doc.get_elements('/RETS/MAXROWS').length > 0)
75
+
76
+
77
+ # XXX: If it turns out we need to parse the response of errors, then this will
78
+ # need to change.
79
+ if transaction.reply_code.to_i > 0 && transaction.reply_code.to_i != 20201
80
+ exception_type = Client::EXCEPTION_TYPES[transaction.reply_code.to_i] || RETSTransactionException
81
+ raise exception_type, "#{transaction.reply_code} - #{transaction.reply_text}"
82
+ end
83
+
84
+ transaction.response = yield doc
85
+ return transaction
86
+ end
87
+
88
+ def get_parser_by_name(name)
89
+ case name
90
+ when 'COMPACT', 'COMPACT-DECODED'
91
+ type = RETS4R::Client::CompactDataParser
92
+ else
93
+ raise "Invalid format #{name}"
94
+ end
95
+ type.new
96
+ end
97
+ end
98
+ end
99
+ end
100
+
@@ -0,0 +1,143 @@
1
+ require 'rets4r'
2
+ require 'rets4r/client/exceptions'
3
+
4
+ module RETS4R
5
+ class Client
6
+ class Requester
7
+ DEFAULT_USER_AGENT = "rets4r/#{::RETS4R::VERSION}"
8
+ DEFAULT_RETS_VERSION = '1.7'
9
+
10
+ attr_accessor :logger, :headers, :pre_request_block, :nc, :username, :password, :method
11
+ def initialize
12
+ @nc = 0
13
+ @headers = {
14
+ 'User-Agent' => DEFAULT_USER_AGENT,
15
+ 'Accept' => '*/*',
16
+ 'RETS-Version' => "RETS/#{DEFAULT_RETS_VERSION}",
17
+ }
18
+ @pre_request_block = nil
19
+ end
20
+
21
+ def user_agent
22
+ @headers['User-Agent']
23
+ end
24
+
25
+ def user_agent=(name)
26
+ set_header('User-Agent', name)
27
+ end
28
+
29
+ def rets_version=(version)
30
+ if (SUPPORTED_RETS_VERSIONS.include? version)
31
+ set_header('RETS-Version', "RETS/#{version}")
32
+ else
33
+ raise Unsupported.new("The client does not support RETS version '#{version}'.")
34
+ end
35
+ end
36
+
37
+ def rets_version
38
+ (@headers['RETS-Version'] || "").gsub("RETS/", "")
39
+ end
40
+
41
+ def set_header(name, value)
42
+ if value.nil? then
43
+ @headers.delete(name)
44
+ else
45
+ @headers[name] = value
46
+ end
47
+
48
+ logger.debug("Set header '#{name}' to '#{value}'") if logger
49
+ end
50
+
51
+ def user_agent
52
+ @headers['User-Agent']
53
+ end
54
+
55
+ # Given a hash, it returns a URL encoded query string.
56
+ def create_query_string(hash)
57
+ parts = hash.map {|key,value| "#{CGI.escape(key)}=#{CGI.escape(value)}" unless key.nil? || value.nil?}
58
+ return parts.join('&')
59
+ end
60
+ # This is the primary transaction method, which the other public methods make use of.
61
+ # Given a url for the transaction (endpoint) it makes a request to the RETS server.
62
+ #
63
+ #--
64
+ # This needs to be better documented, but for now please see the public transaction methods
65
+ # for how to make use of this method.
66
+ #++
67
+ def request(url, data = {}, header = {}, method = @method, retry_auth = DEFAULT_RETRY)
68
+ response = ''
69
+
70
+ http = Net::HTTP.new(url.host, url.port)
71
+ http.read_timeout = 600
72
+
73
+ if logger && logger.debug?
74
+ http.set_debug_output HTTPDebugLogger.new(logger)
75
+ end
76
+
77
+ http.start do |http|
78
+ begin
79
+ uri = url.path
80
+
81
+ if ! data.empty? && method == METHOD_GET
82
+ uri += "?#{create_query_string(data)}"
83
+ end
84
+
85
+ headers = @headers
86
+ headers.merge(header) unless header.empty?
87
+
88
+ @pre_request_block.call(self, http, headers) if @pre_request_block
89
+
90
+ logger.debug(headers.inspect) if logger
91
+
92
+ post_data = data.map {|k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join('&') if method == METHOD_POST
93
+ response = method == METHOD_POST ? http.post(uri, post_data, headers) :
94
+ http.get(uri, headers)
95
+
96
+
97
+ if response.code == '401'
98
+ # Authentication is required
99
+ raise AuthRequired
100
+ elsif response.code.to_i >= 300
101
+ # We have a non-successful response that we cannot handle
102
+ raise HTTPError.new(response)
103
+ else
104
+ cookies = []
105
+ if set_cookies = response.get_fields('set-cookie') then
106
+ set_cookies.each do |cookie|
107
+ cookies << cookie.split(";").first
108
+ end
109
+ end
110
+ set_header('Cookie', cookies.join("; ")) unless cookies.empty?
111
+ # totally wrong. session id is only ever under the Cookie header
112
+ #set_header('RETS-Session-ID', response['RETS-Session-ID']) if response['RETS-Session-ID']
113
+ set_header('RETS-Session-ID',nil)
114
+ end
115
+ rescue AuthRequired
116
+ @nc += 1
117
+
118
+ if retry_auth > 0
119
+ retry_auth -= 1
120
+ auth = Auth.authenticate(response,
121
+ @username,
122
+ @password,
123
+ url.path,
124
+ method,
125
+ @headers['RETS-Request-ID'],
126
+ @headers['User-Agent'],
127
+ @nc)
128
+ set_header('Authorization', auth)
129
+ retry
130
+ else
131
+ raise LoginError.new(response.message)
132
+ end
133
+ end
134
+
135
+ logger.debug(response.body) if logger
136
+ end
137
+
138
+ return response
139
+ end
140
+
141
+ end
142
+ end
143
+ end
@@ -1,34 +1,31 @@
1
1
  module RETS4R
2
- class Client
3
- class Transaction
4
- attr_accessor :reply_code, :reply_text, :response, :metadata, :header, :data, :maxrows,
5
- :count, :delimiter, :secondary_response
6
-
7
- def initialize
8
- self.maxrows = false
9
- self.header = []
10
- self.data = []
11
- self.delimiter = ?\t
12
- end
13
-
14
- def success?
15
- return true if self.reply_code == '0'
16
- return false
17
- end
18
-
19
- def has_data?
20
- return true if self.data.length > 0
21
- return false
22
- end
23
-
24
- def maxrows?
25
- return true if self.maxrows
26
- return false
27
- end
28
-
29
- def ascii_delimiter
30
- self.delimiter.chr
31
- end
32
- end
33
- end
34
- end
2
+ class Client
3
+ class Transaction
4
+ attr_accessor :reply_code, :reply_text, :response, :metadata,
5
+ :header, :maxrows, :delimiter, :secondary_response
6
+
7
+ def initialize
8
+ self.maxrows = false
9
+ self.header = []
10
+ self.delimiter = ?\t
11
+ end
12
+
13
+ def success?
14
+ return true if self.reply_code == '0'
15
+ return false
16
+ end
17
+
18
+ def maxrows?
19
+ return true if self.maxrows
20
+ return false
21
+ end
22
+
23
+ def ascii_delimiter
24
+ self.delimiter.chr
25
+ end
26
+
27
+ # For compatibility with the original library.
28
+ alias :data :response
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ # from ActiveSupport
2
+ class Array
3
+ # Extracts options from a set of arguments. Removes and returns the last
4
+ # element in the array if it's a hash, otherwise returns a blank hash.
5
+ #
6
+ # def options(*args)
7
+ # args.extract_options!
8
+ # end
9
+ #
10
+ # options(1, 2) # => {}
11
+ # options(1, 2, :a => :b) # => {:a=>:b}
12
+ def extract_options!
13
+ last.is_a?(::Hash) ? pop : {}
14
+ end
15
+ end
@@ -0,0 +1,58 @@
1
+ require 'rets4r/core_ext/array/extract_options'
2
+ # from ActiveSupport
3
+
4
+ # Extends the class object with class and instance accessors for class attributes,
5
+ # just like the native attr* accessors for instance attributes.
6
+ #
7
+ # class Person
8
+ # cattr_accessor :hair_colors
9
+ # end
10
+ #
11
+ # Person.hair_colors = [:brown, :black, :blonde, :red]
12
+ class Class
13
+ def cattr_reader(*syms)
14
+ syms.flatten.each do |sym|
15
+ next if sym.is_a?(Hash)
16
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
17
+ unless defined? @@#{sym} # unless defined? @@hair_colors
18
+ @@#{sym} = nil # @@hair_colors = nil
19
+ end # end
20
+ #
21
+ def self.#{sym} # def self.hair_colors
22
+ @@#{sym} # @@hair_colors
23
+ end # end
24
+ #
25
+ def #{sym} # def hair_colors
26
+ @@#{sym} # @@hair_colors
27
+ end # end
28
+ EOS
29
+ end
30
+ end
31
+
32
+ def cattr_writer(*syms)
33
+ options = syms.extract_options!
34
+ syms.flatten.each do |sym|
35
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
36
+ unless defined? @@#{sym} # unless defined? @@hair_colors
37
+ @@#{sym} = nil # @@hair_colors = nil
38
+ end # end
39
+ #
40
+ def self.#{sym}=(obj) # def self.hair_colors=(obj)
41
+ @@#{sym} = obj # @@hair_colors = obj
42
+ end # end
43
+ #
44
+ #{" #
45
+ def #{sym}=(obj) # def hair_colors=(obj)
46
+ @@#{sym} = obj # @@hair_colors = obj
47
+ end # end
48
+ " unless options[:instance_writer] == false } # # instance writer above is generated unless options[:instance_writer] == false
49
+ EOS
50
+ self.send("#{sym}=", yield) if block_given?
51
+ end
52
+ end
53
+
54
+ def cattr_accessor(*syms, &blk)
55
+ cattr_reader(*syms)
56
+ cattr_writer(*syms, &blk)
57
+ end
58
+ end
@@ -0,0 +1,46 @@
1
+ # from ActiveSupport
2
+ class Hash
3
+ # Return a new hash with all keys converted to strings.
4
+ def stringify_keys
5
+ dup.stringify_keys!
6
+ end
7
+
8
+ # Destructively convert all keys to strings.
9
+ def stringify_keys!
10
+ keys.each do |key|
11
+ self[key.to_s] = delete(key)
12
+ end
13
+ self
14
+ end
15
+
16
+ # Return a new hash with all keys converted to symbols, as long as
17
+ # they respond to +to_sym+.
18
+ def symbolize_keys
19
+ dup.symbolize_keys!
20
+ end
21
+
22
+ # Destructively convert all keys to symbols, as long as they respond
23
+ # to +to_sym+.
24
+ def symbolize_keys!
25
+ keys.each do |key|
26
+ self[(key.to_sym rescue key) || key] = delete(key)
27
+ end
28
+ self
29
+ end
30
+
31
+ alias_method :to_options, :symbolize_keys
32
+ alias_method :to_options!, :symbolize_keys!
33
+
34
+ # Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
35
+ # Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
36
+ # as keys, this will fail.
37
+ #
38
+ # ==== Examples
39
+ # { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
40
+ # { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
41
+ # { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
42
+ def assert_valid_keys(*valid_keys)
43
+ unknown_keys = keys - [valid_keys].flatten
44
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
45
+ end
46
+ end
@@ -0,0 +1,39 @@
1
+ # from ActiveSupport
2
+ class Hash
3
+ # Slice a hash to include only the given keys. This is useful for
4
+ # limiting an options hash to valid keys before passing to a method:
5
+ #
6
+ # def search(criteria = {})
7
+ # assert_valid_keys(:mass, :velocity, :time)
8
+ # end
9
+ #
10
+ # search(options.slice(:mass, :velocity, :time))
11
+ #
12
+ # If you have an array of keys you want to limit to, you should splat them:
13
+ #
14
+ # valid_keys = [:mass, :velocity, :time]
15
+ # search(options.slice(*valid_keys))
16
+ def slice(*keys)
17
+ keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
18
+ hash = self.class.new
19
+ keys.each { |k| hash[k] = self[k] if has_key?(k) }
20
+ hash
21
+ end
22
+
23
+ # Replaces the hash with only the given keys.
24
+ # Returns a hash contained the removed key/value pairs
25
+ # {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d =>4}
26
+ def slice!(*keys)
27
+ keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
28
+ omit = slice(*self.keys - keys)
29
+ hash = slice(*keys)
30
+ replace(hash)
31
+ omit
32
+ end
33
+
34
+ def extract!(*keys)
35
+ result = {}
36
+ keys.each {|key| result[key] = delete(key) }
37
+ result
38
+ end
39
+ end