jschairb-rets4r 1.1.18

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