josephholsten-rets4r 1.1.16 → 1.1.17

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.
@@ -7,7 +7,8 @@ module RETS4R
7
7
  class CompactDataParser
8
8
  def parse_results(doc)
9
9
 
10
- delimiter = doc.get_elements('/RETS/DELIMITER')[0].attributes['value'].to_i.chr
10
+ delimiter = doc.get_elements('/RETS/DELIMITER')[0] &&
11
+ doc.get_elements('/RETS/DELIMITER')[0].attributes['value'].to_i.chr
11
12
  columns = doc.get_elements('/RETS/COLUMNS')[0]
12
13
  rows = doc.get_elements('/RETS/DATA')
13
14
 
@@ -16,7 +17,7 @@ module RETS4R
16
17
 
17
18
  def parse_data(column_element, row_elements, delimiter = "\t")
18
19
 
19
- column_names = column_element.text.split(delimiter)
20
+ column_names = column_element.to_s.split(delimiter)
20
21
 
21
22
  result = []
22
23
 
@@ -2,15 +2,28 @@ require 'rubygems'
2
2
  require 'nokogiri'
3
3
  module RETS4R
4
4
  class Client
5
- class CompactNokogiriParser < Nokogiri::XML::SAX::Document
6
- def parse_results(io)
7
- doc = CompactDocument.new
8
- parser = Nokogiri::XML::SAX::Parser.new(doc)
9
- parser.parse(io)
10
- doc.results
5
+ class CompactNokogiriParser
6
+
7
+ def initialize(io)
8
+ @doc = CompactDocument.new
9
+ @parser = Nokogiri::XML::SAX::Parser.new(@doc)
10
+ @io = io
11
+ end
12
+
13
+ def to_a
14
+ @parser.parse(@io) if @doc.results.empty?
15
+ @doc.results
11
16
  end
17
+
18
+ def each(&block)
19
+ @doc.proc = block.to_proc
20
+ @parser.parse(@io)
21
+ nil
22
+ end
23
+
12
24
  class CompactDocument < Nokogiri::XML::SAX::Document
13
25
  attr_reader :results
26
+ attr_writer :proc
14
27
 
15
28
  def initialize
16
29
  @results = []
@@ -35,10 +48,7 @@ module RETS4R
35
48
  @columns = @string.split(@delimiter)
36
49
  when 'DATA'
37
50
  @data_element = false
38
- @results << @columns.zip(@string.split(@delimiter)).inject({}) do | h,(k,v) |
39
- h[k] = v unless k.empty?
40
- next h
41
- end
51
+ handle_row
42
52
  end
43
53
  end
44
54
 
@@ -49,7 +59,23 @@ module RETS4R
49
59
  @string << string
50
60
  end
51
61
  end
62
+
63
+ private
64
+ def handle_row
65
+ data = make_hash
66
+ if @proc
67
+ @proc.call(data)
68
+ else
69
+ @results << data
70
+ end
71
+ end
72
+ def make_hash
73
+ @columns.zip(@string.split(@delimiter)).inject({}) do | h,(k,v) |
74
+ h[k] = v unless k.empty?
75
+ next h
76
+ end
77
+ end
52
78
  end
53
79
  end
54
80
  end
55
- end
81
+ end
@@ -61,10 +61,6 @@ module RETS4R
61
61
  raise RETSException, 'No transaction body was returned!'
62
62
  end
63
63
 
64
- File.open('/tmp/meh', 'w') do |file|
65
- file.write(xml)
66
- end
67
-
68
64
  doc = REXML::Document.new(xml)
69
65
 
70
66
  root = doc.root
@@ -91,7 +87,7 @@ module RETS4R
91
87
 
92
88
  def get_parser_by_name(name)
93
89
  case name
94
- when 'COMPACT'
90
+ when 'COMPACT', 'COMPACT-DECODED'
95
91
  type = RETS4R::Client::CompactDataParser
96
92
  else
97
93
  raise "Invalid format #{name}"
@@ -23,6 +23,9 @@ module RETS4R
23
23
  def ascii_delimiter
24
24
  self.delimiter.chr
25
25
  end
26
+
27
+ # For compatibility with the original library.
28
+ alias :data :response
26
29
  end
27
30
  end
28
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
data/lib/rets4r/loader.rb CHANGED
@@ -1,12 +1,8 @@
1
1
  module RETS4R
2
2
  class Loader
3
- def self.load(file)
4
- parser = RETS4R::Client::CompactNokogiriParser.new
5
- listings = parser.parse_results(file)
6
-
7
- listings.each {|original|
8
- yield original
9
- }
3
+ def self.load(file, &block)
4
+ parser = RETS4R::Client::CompactNokogiriParser.new(file)
5
+ parser.each(&block)
10
6
  end
11
7
  end
12
- end
8
+ end
@@ -6,14 +6,30 @@ require 'rets4r'
6
6
  class CompactNokogiriTest < Test::Unit::TestCase
7
7
  def test_should_do_stuff
8
8
  file = File.expand_path(File.join('test', 'data', '1.5', 'search_compact.xml'))
9
- listings = RETS4R::Client::CompactNokogiriParser.new.parse_results(open(file))
9
+ listings = RETS4R::Client::CompactNokogiriParser.new(open(file)).to_a
10
10
  assert_equal({"Third"=>"Datum3", "Second"=>"Datum2", "First"=>"Datum1"}, listings[0])
11
11
  assert_equal({"Third"=>"Datum6", "Second"=>"Datum5", "First"=>"Datum4"}, listings[1])
12
12
  end
13
13
  def test_should_handle_big_data
14
14
  file = File.expand_path(File.join('test', 'data', '1.5', 'bad_compact.xml'))
15
- listings = RETS4R::Client::CompactNokogiriParser.new.parse_results(open(file))
15
+ listings = RETS4R::Client::CompactNokogiriParser.new(open(file)).to_a
16
16
  assert_equal 1, listings.length
17
17
  assert_equal 79, listings.first.keys.length
18
18
  end
19
+ def test_each_should_yield_between_results
20
+ file = File.expand_path(
21
+ File.join('test', 'data', '1.5', 'search_compact_big.xml'))
22
+ stat = File::Stat.new(file)
23
+ unless stat.size > stat.blksize
24
+ flunk "This test probably won't work on this machine.
25
+ It needs a test input file larger than the native block size."
26
+ end
27
+ stream = open(file)
28
+ positions = []
29
+ listings = RETS4R::Client::CompactNokogiriParser.new(stream).each do |row|
30
+ positions << stream.pos
31
+ end
32
+ assert positions.first < positions.last,
33
+ "data was yielded durring the reading of the stream"
34
+ end
19
35
  end
@@ -0,0 +1,3 @@
1
+ <RETS ReplyCode="0" ReplyText="SUCCESS">
2
+ <COUNT Records="1024"/>
3
+ </RETS>
@@ -0,0 +1,136 @@
1
+ <RETS ReplyCode="0" ReplyText="SUCCESS">
2
+ <COUNT Records="4"/>
3
+ <DELIMITER value="09" />
4
+ <COLUMNS> First Second Third </COLUMNS>
5
+ <DATA> Datum1 Datum2 Datum3</DATA>
6
+ <DATA> Datum4 Datum5 Datum6</DATA>
7
+ <DATA> Datum4 Datum5 Datum6</DATA>
8
+ <DATA> Datum1 Datum2 Datum3</DATA>
9
+ <DATA> Datum1 Datum2 Datum3</DATA>
10
+ <DATA> Datum4 Datum5 Datum6</DATA>
11
+ <DATA> Datum4 Datum5 Datum6</DATA>
12
+ <DATA> Datum1 Datum2 Datum3</DATA>
13
+ <DATA> Datum4 Datum5 Datum6</DATA>
14
+ <DATA> Datum4 Datum5 Datum6</DATA>
15
+ <DATA> Datum1 Datum2 Datum3</DATA>
16
+ <DATA> Datum1 Datum2 Datum3</DATA>
17
+ <DATA> Datum4 Datum5 Datum6</DATA>
18
+ <DATA> Datum4 Datum5 Datum6</DATA>
19
+ <DATA> Datum1 Datum2 Datum3</DATA>
20
+ <DATA> Datum4 Datum5 Datum6</DATA>
21
+ <DATA> Datum4 Datum5 Datum6</DATA>
22
+ <DATA> Datum1 Datum2 Datum3</DATA>
23
+ <DATA> Datum1 Datum2 Datum3</DATA>
24
+ <DATA> Datum4 Datum5 Datum6</DATA>
25
+ <DATA> Datum4 Datum5 Datum6</DATA>
26
+ <DATA> Datum1 Datum2 Datum3</DATA>
27
+ <DATA> Datum4 Datum5 Datum6</DATA>
28
+ <DATA> Datum4 Datum5 Datum6</DATA>
29
+ <DATA> Datum1 Datum2 Datum3</DATA>
30
+ <DATA> Datum1 Datum2 Datum3</DATA>
31
+ <DATA> Datum4 Datum5 Datum6</DATA>
32
+ <DATA> Datum4 Datum5 Datum6</DATA>
33
+ <DATA> Datum1 Datum2 Datum3</DATA>
34
+ <DATA> Datum4 Datum5 Datum6</DATA>
35
+ <DATA> Datum4 Datum5 Datum6</DATA>
36
+ <DATA> Datum1 Datum2 Datum3</DATA>
37
+ <DATA> Datum1 Datum2 Datum3</DATA>
38
+ <DATA> Datum4 Datum5 Datum6</DATA>
39
+ <DATA> Datum4 Datum5 Datum6</DATA>
40
+ <DATA> Datum1 Datum2 Datum3</DATA>
41
+ <DATA> Datum4 Datum5 Datum6</DATA>
42
+ <DATA> Datum4 Datum5 Datum6</DATA>
43
+ <DATA> Datum1 Datum2 Datum3</DATA>
44
+ <DATA> Datum1 Datum2 Datum3</DATA>
45
+ <DATA> Datum4 Datum5 Datum6</DATA>
46
+ <DATA> Datum4 Datum5 Datum6</DATA>
47
+ <DATA> Datum1 Datum2 Datum3</DATA>
48
+ <DATA> Datum4 Datum5 Datum6</DATA>
49
+ <DATA> Datum4 Datum5 Datum6</DATA>
50
+ <DATA> Datum1 Datum2 Datum3</DATA>
51
+ <DATA> Datum1 Datum2 Datum3</DATA>
52
+ <DATA> Datum4 Datum5 Datum6</DATA>
53
+ <DATA> Datum4 Datum5 Datum6</DATA>
54
+ <DATA> Datum1 Datum2 Datum3</DATA>
55
+ <DATA> Datum4 Datum5 Datum6</DATA>
56
+ <DATA> Datum4 Datum5 Datum6</DATA>
57
+ <DATA> Datum1 Datum2 Datum3</DATA>
58
+ <DATA> Datum1 Datum2 Datum3</DATA>
59
+ <DATA> Datum4 Datum5 Datum6</DATA>
60
+ <DATA> Datum4 Datum5 Datum6</DATA>
61
+ <DATA> Datum1 Datum2 Datum3</DATA>
62
+ <DATA> Datum4 Datum5 Datum6</DATA>
63
+ <DATA> Datum4 Datum5 Datum6</DATA>
64
+ <DATA> Datum1 Datum2 Datum3</DATA>
65
+ <DATA> Datum1 Datum2 Datum3</DATA>
66
+ <DATA> Datum4 Datum5 Datum6</DATA>
67
+ <DATA> Datum4 Datum5 Datum6</DATA>
68
+ <DATA> Datum1 Datum2 Datum3</DATA>
69
+ <DATA> Datum4 Datum5 Datum6</DATA>
70
+ <DATA> Datum4 Datum5 Datum6</DATA>
71
+ <DATA> Datum1 Datum2 Datum3</DATA>
72
+ <DATA> Datum1 Datum2 Datum3</DATA>
73
+ <DATA> Datum4 Datum5 Datum6</DATA>
74
+ <DATA> Datum4 Datum5 Datum6</DATA>
75
+ <DATA> Datum1 Datum2 Datum3</DATA>
76
+ <DATA> Datum4 Datum5 Datum6</DATA>
77
+ <DATA> Datum4 Datum5 Datum6</DATA>
78
+ <DATA> Datum1 Datum2 Datum3</DATA>
79
+ <DATA> Datum1 Datum2 Datum3</DATA>
80
+ <DATA> Datum4 Datum5 Datum6</DATA>
81
+ <DATA> Datum4 Datum5 Datum6</DATA>
82
+ <DATA> Datum1 Datum2 Datum3</DATA>
83
+ <DATA> Datum4 Datum5 Datum6</DATA>
84
+ <DATA> Datum4 Datum5 Datum6</DATA>
85
+ <DATA> Datum1 Datum2 Datum3</DATA>
86
+ <DATA> Datum1 Datum2 Datum3</DATA>
87
+ <DATA> Datum4 Datum5 Datum6</DATA>
88
+ <DATA> Datum4 Datum5 Datum6</DATA>
89
+ <DATA> Datum1 Datum2 Datum3</DATA>
90
+ <DATA> Datum4 Datum5 Datum6</DATA>
91
+ <DATA> Datum4 Datum5 Datum6</DATA>
92
+ <DATA> Datum1 Datum2 Datum3</DATA>
93
+ <DATA> Datum1 Datum2 Datum3</DATA>
94
+ <DATA> Datum4 Datum5 Datum6</DATA>
95
+ <DATA> Datum4 Datum5 Datum6</DATA>
96
+ <DATA> Datum1 Datum2 Datum3</DATA>
97
+ <DATA> Datum4 Datum5 Datum6</DATA>
98
+ <DATA> Datum4 Datum5 Datum6</DATA>
99
+ <DATA> Datum1 Datum2 Datum3</DATA>
100
+ <DATA> Datum1 Datum2 Datum3</DATA>
101
+ <DATA> Datum4 Datum5 Datum6</DATA>
102
+ <DATA> Datum4 Datum5 Datum6</DATA>
103
+ <DATA> Datum1 Datum2 Datum3</DATA>
104
+ <DATA> Datum4 Datum5 Datum6</DATA>
105
+ <DATA> Datum4 Datum5 Datum6</DATA>
106
+ <DATA> Datum1 Datum2 Datum3</DATA>
107
+ <DATA> Datum1 Datum2 Datum3</DATA>
108
+ <DATA> Datum4 Datum5 Datum6</DATA>
109
+ <DATA> Datum4 Datum5 Datum6</DATA>
110
+ <DATA> Datum1 Datum2 Datum3</DATA>
111
+ <DATA> Datum4 Datum5 Datum6</DATA>
112
+ <DATA> Datum4 Datum5 Datum6</DATA>
113
+ <DATA> Datum1 Datum2 Datum3</DATA>
114
+ <DATA> Datum1 Datum2 Datum3</DATA>
115
+ <DATA> Datum4 Datum5 Datum6</DATA>
116
+ <DATA> Datum4 Datum5 Datum6</DATA>
117
+ <DATA> Datum1 Datum2 Datum3</DATA>
118
+ <DATA> Datum4 Datum5 Datum6</DATA>
119
+ <DATA> Datum4 Datum5 Datum6</DATA>
120
+ <DATA> Datum1 Datum2 Datum3</DATA>
121
+ <DATA> Datum1 Datum2 Datum3</DATA>
122
+ <DATA> Datum4 Datum5 Datum6</DATA>
123
+ <DATA> Datum4 Datum5 Datum6</DATA>
124
+ <DATA> Datum1 Datum2 Datum3</DATA>
125
+ <DATA> Datum4 Datum5 Datum6</DATA>
126
+ <DATA> Datum4 Datum5 Datum6</DATA>
127
+ <DATA> Datum1 Datum2 Datum3</DATA>
128
+ <DATA> Datum1 Datum2 Datum3</DATA>
129
+ <DATA> Datum1 Datum2 Datum3</DATA>
130
+ <DATA> Datum4 Datum5 Datum6</DATA>
131
+ <DATA> Datum4 Datum5 Datum6</DATA>
132
+ <DATA> Datum1 Datum2 Datum3</DATA>
133
+ <DATA> Datum4 Datum5 Datum6</DATA>
134
+ <DATA> Datum4 Datum5 Datum6</DATA>
135
+ <MAXROWS/>
136
+ </RETS>