jschairb-rets4r 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 (63) hide show
  1. data/.document +5 -0
  2. data/CHANGELOG +566 -0
  3. data/CONTRIBUTORS +7 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE +29 -0
  6. data/MANIFEST +62 -0
  7. data/NEWS +186 -0
  8. data/README.rdoc +43 -0
  9. data/RUBYS +56 -0
  10. data/Rakefile +50 -0
  11. data/TODO +35 -0
  12. data/examples/client_get_object.rb +49 -0
  13. data/examples/client_login.rb +39 -0
  14. data/examples/client_mapper.rb +17 -0
  15. data/examples/client_metadata.rb +42 -0
  16. data/examples/client_parser.rb +9 -0
  17. data/examples/client_search.rb +49 -0
  18. data/examples/settings.yml +114 -0
  19. data/lib/rets4r.rb +14 -0
  20. data/lib/rets4r/auth.rb +73 -0
  21. data/lib/rets4r/client.rb +487 -0
  22. data/lib/rets4r/client/data.rb +14 -0
  23. data/lib/rets4r/client/dataobject.rb +28 -0
  24. data/lib/rets4r/client/exceptions.rb +116 -0
  25. data/lib/rets4r/client/links.rb +32 -0
  26. data/lib/rets4r/client/metadata.rb +15 -0
  27. data/lib/rets4r/client/parsers/compact.rb +42 -0
  28. data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
  29. data/lib/rets4r/client/parsers/metadata.rb +92 -0
  30. data/lib/rets4r/client/parsers/response_parser.rb +100 -0
  31. data/lib/rets4r/client/requester.rb +143 -0
  32. data/lib/rets4r/client/transaction.rb +31 -0
  33. data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
  34. data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
  35. data/lib/rets4r/core_ext/hash/keys.rb +46 -0
  36. data/lib/rets4r/core_ext/hash/slice.rb +39 -0
  37. data/lib/rets4r/listing_mapper.rb +17 -0
  38. data/lib/rets4r/listing_service.rb +35 -0
  39. data/lib/rets4r/loader.rb +8 -0
  40. data/lib/tasks/annotations.rake +121 -0
  41. data/lib/tasks/coverage.rake +13 -0
  42. data/rets4r.gemspec +24 -0
  43. data/spec/rets4r_compact_data_parser_spec.rb +7 -0
  44. data/test/data/1.5/bad_compact.xml +7 -0
  45. data/test/data/1.5/count_only_compact.xml +3 -0
  46. data/test/data/1.5/error.xml +1 -0
  47. data/test/data/1.5/invalid_compact.xml +4 -0
  48. data/test/data/1.5/login.xml +16 -0
  49. data/test/data/1.5/metadata.xml +0 -0
  50. data/test/data/1.5/search_compact.xml +8 -0
  51. data/test/data/1.5/search_compact_big.xml +136 -0
  52. data/test/data/1.5/search_unescaped_compact.xml +8 -0
  53. data/test/data/listing_service.yml +36 -0
  54. data/test/test_auth.rb +68 -0
  55. data/test/test_client.rb +342 -0
  56. data/test/test_client_links.rb +39 -0
  57. data/test/test_compact_nokogiri.rb +64 -0
  58. data/test/test_helper.rb +12 -0
  59. data/test/test_listing_mapper.rb +112 -0
  60. data/test/test_loader.rb +24 -0
  61. data/test/test_parser.rb +96 -0
  62. data/test/test_quality.rb +57 -0
  63. metadata +211 -0
@@ -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
@@ -0,0 +1,31 @@
1
+ module RETS4R
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
@@ -0,0 +1,17 @@
1
+ module RETS4R
2
+ class ListingMapper
3
+ def initialize(params = {})
4
+ @select = params[:select] || ListingService.connection[:select]
5
+ end
6
+ def select
7
+ @select
8
+ end
9
+ def map(original)
10
+ listing = {}
11
+ @select.each_pair {|rets_key, record_key|
12
+ listing[record_key] = original[rets_key]
13
+ }
14
+ listing
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ require 'rets4r/core_ext/class/attribute_accessors'
2
+ require 'rets4r/core_ext/hash/keys'
3
+ require 'rets4r/core_ext/hash/slice'
4
+
5
+ module RETS4R
6
+ class ListingService
7
+ # RECORD_COUNT_ONLY=Librets::SearchRequest::RECORD_COUNT_ONLY
8
+ RECORD_COUNT_ONLY='fixme'
9
+ # Contains the listing service configurations - typically stored in
10
+ # config/listing_service.yml - as a Hash.
11
+ cattr_accessor :configurations, :instance_writer => false
12
+ cattr_accessor :env, :instance_writer => false
13
+
14
+ class << self
15
+
16
+ # Connection configuration for the specified environment, or the current
17
+ # environment if none is given.
18
+ def connection(spec = nil)
19
+ case spec
20
+ when nil
21
+ connection(RETS4R::ListingService.env)
22
+ when Symbol, String
23
+ if configuration = configurations[spec.to_s]
24
+ configuration.symbolize_keys
25
+ else
26
+ raise ArgumentError, "#{spec} listing service is not configured"
27
+ end
28
+ else
29
+ raise ArgumentError, "#{spec} listing service is not configured"
30
+ end
31
+ end
32
+
33
+ end # class << self
34
+ end
35
+ end
@@ -0,0 +1,8 @@
1
+ module RETS4R
2
+ class Loader
3
+ def self.load(io, &block)
4
+ parser = RETS4R::Client::CompactNokogiriParser.new(io)
5
+ parser.each(&block)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,121 @@
1
+ # Implements the logic behind the rake tasks for annotations like
2
+ #
3
+ # rake notes
4
+ # rake notes:optimize
5
+ #
6
+ # and friends. See <tt>rake -T notes</tt> and <tt>railties/lib/tasks/annotations.rake</tt>.
7
+ #
8
+ # Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that
9
+ # represent the line where the annotation lives, its tag, and its text. Note
10
+ # the filename is not stored.
11
+ #
12
+ # Annotations are looked for in comments and modulus whitespace they have to
13
+ # start with the tag optionally followed by a colon. Everything up to the end
14
+ # of the line (or closing ERb comment tag) is considered to be their text.
15
+ class SourceAnnotationExtractor
16
+ class Annotation < Struct.new(:line, :tag, :text)
17
+
18
+ # Returns a representation of the annotation that looks like this:
19
+ #
20
+ # [126] [TODO] This algorithm is simple and clearly correct, make it faster.
21
+ #
22
+ # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above.
23
+ # Otherwise the string contains just line and text.
24
+ def to_s(options={})
25
+ s = "[%3d] " % line
26
+ s << "[#{tag}] " if options[:tag]
27
+ s << text
28
+ end
29
+ end
30
+
31
+ # Prints all annotations with tag +tag+ under the root directories +app+, +lib+,
32
+ # and +test+ (recursively). Only filenames with extension +.builder+, +.rb+,
33
+ # +.rxml+, +.rjs+, +.rhtml+, or +.erb+ are taken into account. The +options+
34
+ # hash is passed to each annotation's +to_s+.
35
+ #
36
+ # This class method is the single entry point for the rake tasks.
37
+ def self.enumerate(tag, options={})
38
+ extractor = new(tag)
39
+ extractor.display(extractor.find, options)
40
+ end
41
+
42
+ attr_reader :tag
43
+
44
+ def initialize(tag)
45
+ @tag = tag
46
+ end
47
+
48
+ # Returns a hash that maps filenames under +dirs+ (recursively) to arrays
49
+ # with their annotations. Only files with annotations are included, and only
50
+ # those with extension +.builder+, +.rb+, +.rxml+, +.rjs+, +.rhtml+, and +.erb+
51
+ # are taken into account.
52
+ def find(dirs=%w(app lib test))
53
+ dirs.inject({}) { |h, dir| h.update(find_in(dir)) }
54
+ end
55
+
56
+ # Returns a hash that maps filenames under +dir+ (recursively) to arrays
57
+ # with their annotations. Only files with annotations are included, and only
58
+ # those with extension +.builder+, +.rb+, +.rxml+, +.rjs+, +.rhtml+, and +.erb+
59
+ # are taken into account.
60
+ def find_in(dir)
61
+ results = {}
62
+
63
+ Dir.glob("#{dir}/*") do |item|
64
+ next if File.basename(item)[0] == ?.
65
+
66
+ if File.directory?(item)
67
+ results.update(find_in(item))
68
+ elsif item =~ /\.(builder|(r(?:b|xml|js)))$/
69
+ results.update(extract_annotations_from(item, /#\s*(#{tag}):?\s*(.*)$/))
70
+ elsif item =~ /\.(rhtml|erb)$/
71
+ results.update(extract_annotations_from(item, /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/))
72
+ end
73
+ end
74
+
75
+ results
76
+ end
77
+
78
+ # If +file+ is the filename of a file that contains annotations this method returns
79
+ # a hash with a single entry that maps +file+ to an array of its annotations.
80
+ # Otherwise it returns an empty hash.
81
+ def extract_annotations_from(file, pattern)
82
+ lineno = 0
83
+ result = File.readlines(file).inject([]) do |list, line|
84
+ lineno += 1
85
+ next list unless line =~ pattern
86
+ list << Annotation.new(lineno, $1, $2)
87
+ end
88
+ result.empty? ? {} : { file => result }
89
+ end
90
+
91
+ # Prints the mapping from filenames to annotations in +results+ ordered by filename.
92
+ # The +options+ hash is passed to each annotation's +to_s+.
93
+ def display(results, options={})
94
+ results.keys.sort.each do |file|
95
+ puts "#{file}:"
96
+ results[file].each do |note|
97
+ puts " * #{note.to_s(options)}"
98
+ end
99
+ puts
100
+ end
101
+ end
102
+ end
103
+
104
+ desc "Enumerate all annotations"
105
+ task :notes do
106
+ SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", :tag => true
107
+ end
108
+
109
+ namespace :notes do
110
+ ["OPTIMIZE", "FIXME", "TODO"].each do |annotation|
111
+ desc "Enumerate all #{annotation} annotations"
112
+ task annotation.downcase.intern do
113
+ SourceAnnotationExtractor.enumerate annotation
114
+ end
115
+ end
116
+
117
+ desc "Enumerate a custom annotation, specify with ANNOTATION=WTFHAX"
118
+ task :custom do
119
+ SourceAnnotationExtractor.enumerate ENV['ANNOTATION']
120
+ end
121
+ end