rets 0.9.0 → 0.10.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/Manifest.txt +24 -0
- data/README.md +73 -1
- data/Rakefile +1 -1
- data/lib/rets.rb +202 -1
- data/lib/rets/client.rb +83 -94
- data/lib/rets/http_client.rb +42 -0
- data/lib/rets/metadata.rb +15 -3
- data/lib/rets/metadata/caching.rb +59 -0
- data/lib/rets/metadata/file_cache.rb +29 -0
- data/lib/rets/metadata/json_serializer.rb +27 -0
- data/lib/rets/metadata/lookup_table.rb +65 -0
- data/lib/rets/metadata/lookup_type.rb +3 -4
- data/lib/rets/metadata/marshal_serializer.rb +27 -0
- data/lib/rets/metadata/multi_lookup_table.rb +70 -0
- data/lib/rets/metadata/null_cache.rb +24 -0
- data/lib/rets/metadata/resource.rb +39 -29
- data/lib/rets/metadata/rets_class.rb +27 -23
- data/lib/rets/metadata/rets_object.rb +32 -0
- data/lib/rets/metadata/table.rb +9 -101
- data/lib/rets/metadata/table_factory.rb +19 -0
- data/lib/rets/metadata/yaml_serializer.rb +27 -0
- data/lib/rets/parser/compact.rb +61 -18
- data/lib/rets/parser/error_checker.rb +8 -1
- data/test/fixtures.rb +58 -0
- data/test/test_caching.rb +89 -0
- data/test/test_client.rb +44 -24
- data/test/test_error_checker.rb +18 -0
- data/test/test_file_cache.rb +42 -0
- data/test/test_http_client.rb +96 -60
- data/test/test_json_serializer.rb +26 -0
- data/test/test_marshal_serializer.rb +26 -0
- data/test/test_metadata.rb +62 -450
- data/test/test_metadata_class.rb +50 -0
- data/test/test_metadata_lookup_table.rb +21 -0
- data/test/test_metadata_lookup_type.rb +12 -0
- data/test/test_metadata_multi_lookup_table.rb +60 -0
- data/test/test_metadata_object.rb +20 -0
- data/test/test_metadata_resource.rb +140 -0
- data/test/test_metadata_root.rb +151 -0
- data/test/test_metadata_table.rb +21 -0
- data/test/test_metadata_table_factory.rb +24 -0
- data/test/test_parser_compact.rb +23 -28
- data/test/test_yaml_serializer.rb +26 -0
- metadata +29 -5
@@ -1,36 +1,40 @@
|
|
1
1
|
module Rets
|
2
2
|
module Metadata
|
3
3
|
class RetsClass
|
4
|
-
|
5
|
-
attr_accessor :name
|
6
|
-
attr_accessor :visible_name
|
7
|
-
attr_accessor :description
|
8
|
-
attr_accessor :resource
|
9
|
-
|
10
|
-
def initialize(rets_class_fragment, resource)
|
11
|
-
self.resource = resource
|
12
|
-
self.tables = []
|
13
|
-
self.name = rets_class_fragment["ClassName"]
|
14
|
-
self.visible_name = rets_class_fragment["VisibleName"]
|
15
|
-
self.description = rets_class_fragment["Description"]
|
16
|
-
end
|
4
|
+
attr_reader :name, :visible_name, :standard_name, :description, :tables
|
17
5
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
6
|
+
def initialize(name, visible_name, standard_name, description, tables)
|
7
|
+
@name = name
|
8
|
+
@visible_name = visible_name
|
9
|
+
@description = description
|
10
|
+
@standard_name = standard_name
|
21
11
|
|
22
|
-
|
23
|
-
|
12
|
+
@tables = tables
|
13
|
+
end
|
24
14
|
|
25
|
-
|
15
|
+
def self.find_table_container(metadata, resource_id, class_name)
|
16
|
+
metadata[:table].detect { |t| t.resource == resource_id && t.class == class_name }
|
17
|
+
end
|
26
18
|
|
19
|
+
def self.builds_tables(table_container, resource_id, lookup_types)
|
27
20
|
if table_container
|
28
|
-
table_container.tables.
|
29
|
-
|
21
|
+
table_container.tables.map do |table_fragment|
|
22
|
+
TableFactory.build(table_fragment, resource_id, lookup_types)
|
30
23
|
end
|
24
|
+
else
|
25
|
+
[]
|
31
26
|
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.build(rets_class_fragment, resource_id, lookup_types, metadata)
|
30
|
+
class_name = rets_class_fragment["ClassName"]
|
31
|
+
visible_name = rets_class_fragment["VisibleName"]
|
32
|
+
standard_name = rets_class_fragment["StandardName"]
|
33
|
+
description = rets_class_fragment["Description"]
|
32
34
|
|
33
|
-
|
35
|
+
table_container = find_table_container(metadata, resource_id, class_name)
|
36
|
+
tables = builds_tables(table_container, resource_id, lookup_types)
|
37
|
+
new(class_name, visible_name, standard_name, description, tables)
|
34
38
|
end
|
35
39
|
|
36
40
|
# Print the tree to a file
|
@@ -46,7 +50,7 @@ module Rets
|
|
46
50
|
end
|
47
51
|
|
48
52
|
def find_table(name)
|
49
|
-
tables.detect { |value| value.name == name }
|
53
|
+
tables.detect { |value| value.name.downcase == name.downcase }
|
50
54
|
end
|
51
55
|
end
|
52
56
|
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Rets
|
2
|
+
module Metadata
|
3
|
+
class RetsObject
|
4
|
+
attr_reader :name, :mime_type, :description
|
5
|
+
|
6
|
+
def initialize(name, mime_type, description)
|
7
|
+
@name = name
|
8
|
+
@mime_type = mime_type
|
9
|
+
@description = description
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.build(rets_object_fragment)
|
13
|
+
name = rets_object_fragment["VisibleName"]
|
14
|
+
mime_type = rets_object_fragment["MIMEType"]
|
15
|
+
description = rets_object_fragment["Description"]
|
16
|
+
new(name, mime_type, description)
|
17
|
+
end
|
18
|
+
|
19
|
+
def print_tree(out = $stdout)
|
20
|
+
out.puts " Object: #{name}"
|
21
|
+
out.puts " MimeType: #{mime_type}"
|
22
|
+
out.puts " Description: #{description}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def ==(other)
|
26
|
+
name == other.name &&
|
27
|
+
mime_type == other.mime_type &&
|
28
|
+
description == other.description
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/rets/metadata/table.rb
CHANGED
@@ -1,31 +1,14 @@
|
|
1
1
|
module Rets
|
2
2
|
module Metadata
|
3
|
-
class TableFactory
|
4
|
-
def self.build(table_fragment, resource)
|
5
|
-
enum?(table_fragment) ? LookupTable.new(table_fragment, resource) : Table.new(table_fragment, resource)
|
6
|
-
end
|
7
|
-
|
8
|
-
def self.enum?(table_fragment)
|
9
|
-
lookup_value = table_fragment["LookupName"].strip
|
10
|
-
interpretation = table_fragment["Interpretation"].strip
|
11
|
-
|
12
|
-
interpretation =~ /Lookup/ && !lookup_value.empty?
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
3
|
class Table
|
17
|
-
|
18
|
-
attr_accessor :name
|
19
|
-
attr_accessor :long_name
|
20
|
-
attr_accessor :table_fragment
|
21
|
-
attr_accessor :resource
|
4
|
+
attr_reader :table_fragment, :resource_id, :name, :long_name
|
22
5
|
|
23
|
-
def initialize(table_fragment,
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
6
|
+
def initialize(table_fragment, resource_id)
|
7
|
+
@table_fragment = table_fragment
|
8
|
+
@resource_id = resource_id
|
9
|
+
|
10
|
+
@name = table_fragment["SystemName"]
|
11
|
+
@long_name = table_fragment["LongName"]
|
29
12
|
end
|
30
13
|
|
31
14
|
# Print the tree to a file
|
@@ -33,9 +16,9 @@ module Rets
|
|
33
16
|
# [out] The file to print to. Defaults to $stdout.
|
34
17
|
def print_tree(out = $stdout)
|
35
18
|
out.puts " Table: #{name}"
|
36
|
-
out.puts " Resource: #{
|
19
|
+
out.puts " Resource: #{resource_id}"
|
37
20
|
out.puts " ShortName: #{ table_fragment["ShortName"] }"
|
38
|
-
out.puts " LongName: #{
|
21
|
+
out.puts " LongName: #{ long_name }"
|
39
22
|
out.puts " StandardName: #{ table_fragment["StandardName"] }"
|
40
23
|
out.puts " Units: #{ table_fragment["Units"] }"
|
41
24
|
out.puts " Searchable: #{ table_fragment["Searchable"] }"
|
@@ -46,80 +29,5 @@ module Rets
|
|
46
29
|
value.to_s.strip
|
47
30
|
end
|
48
31
|
end
|
49
|
-
|
50
|
-
class LookupTable
|
51
|
-
attr_accessor :resource
|
52
|
-
attr_accessor :lookup_name
|
53
|
-
attr_accessor :name
|
54
|
-
attr_accessor :interpretation
|
55
|
-
attr_accessor :long_name
|
56
|
-
attr_accessor :table_fragment
|
57
|
-
|
58
|
-
def initialize(table_fragment, resource)
|
59
|
-
self.table_fragment = table_fragment
|
60
|
-
self.resource = resource
|
61
|
-
self.name = table_fragment["SystemName"]
|
62
|
-
self.interpretation = table_fragment["Interpretation"]
|
63
|
-
self.lookup_name = table_fragment["LookupName"]
|
64
|
-
self.long_name = table_fragment["LongName"]
|
65
|
-
end
|
66
|
-
|
67
|
-
def multi?
|
68
|
-
interpretation == "LookupMulti"
|
69
|
-
end
|
70
|
-
|
71
|
-
def lookup_types
|
72
|
-
resource.lookup_types[lookup_name]
|
73
|
-
end
|
74
|
-
|
75
|
-
# Print the tree to a file
|
76
|
-
#
|
77
|
-
# [out] The file to print to. Defaults to $stdout.
|
78
|
-
def print_tree(out = $stdout)
|
79
|
-
out.puts " LookupTable: #{name}"
|
80
|
-
out.puts " Resource: #{resource.id}"
|
81
|
-
out.puts " Required: #{table_fragment['Required']}"
|
82
|
-
out.puts " Searchable: #{ table_fragment["Searchable"] }"
|
83
|
-
out.puts " Units: #{ table_fragment["Units"] }"
|
84
|
-
out.puts " ShortName: #{ table_fragment["ShortName"] }"
|
85
|
-
out.puts " LongName: #{ table_fragment["LongName"] }"
|
86
|
-
out.puts " StandardName: #{ table_fragment["StandardName"] }"
|
87
|
-
out.puts " Types:"
|
88
|
-
|
89
|
-
lookup_types.each do |lookup_type|
|
90
|
-
lookup_type.print_tree(out)
|
91
|
-
end
|
92
|
-
end
|
93
|
-
|
94
|
-
def lookup_type(value)
|
95
|
-
lookup_types.detect {|lt| lt.value == value }
|
96
|
-
end
|
97
|
-
|
98
|
-
def resolve(value)
|
99
|
-
if value.empty?
|
100
|
-
return [] if multi?
|
101
|
-
return value.to_s.strip
|
102
|
-
end
|
103
|
-
|
104
|
-
values = multi? ? value.split(","): [value]
|
105
|
-
|
106
|
-
values = values.map do |v|
|
107
|
-
|
108
|
-
#Remove surrounding quotes
|
109
|
-
clean_value = v.scan(/^["']?(.*?)["']?$/).join
|
110
|
-
|
111
|
-
|
112
|
-
lookup_type = lookup_type(clean_value)
|
113
|
-
|
114
|
-
resolved_value = lookup_type ? lookup_type.long_value : nil
|
115
|
-
|
116
|
-
warn("Discarding unmappable value of #{clean_value.inspect}") if resolved_value.nil? && $VERBOSE
|
117
|
-
|
118
|
-
resolved_value
|
119
|
-
end
|
120
|
-
|
121
|
-
multi? ? values.map {|v| v.to_s.strip } : values.first.to_s.strip
|
122
|
-
end
|
123
|
-
end
|
124
32
|
end
|
125
33
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Rets
|
2
|
+
module Metadata
|
3
|
+
class TableFactory
|
4
|
+
|
5
|
+
def self.build(table_fragment, resource_id, lookup_types)
|
6
|
+
if table_fragment["LookupName"].empty?
|
7
|
+
Table.new(table_fragment, resource_id)
|
8
|
+
else
|
9
|
+
if table_fragment["Interpretation"] == "LookupMulti"
|
10
|
+
MultiLookupTable.build(table_fragment, resource_id, lookup_types)
|
11
|
+
else
|
12
|
+
LookupTable.build(table_fragment, resource_id, lookup_types)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Rets
|
4
|
+
module Metadata
|
5
|
+
|
6
|
+
# Serialize/Deserialize metadata using Marshal.
|
7
|
+
class YamlSerializer
|
8
|
+
|
9
|
+
# Serialize to a file. The library reserves the right to change
|
10
|
+
# the type or contents of o, so don't depend on it being
|
11
|
+
# anything in particular.
|
12
|
+
def save(file, o)
|
13
|
+
YAML.dump(o, file)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Deserialize from a file. If the metadata cannot be
|
17
|
+
# deserialized, return nil.
|
18
|
+
def load(file)
|
19
|
+
YAML.load(file)
|
20
|
+
rescue Psych::SyntaxError
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/lib/rets/parser/compact.rb
CHANGED
@@ -1,26 +1,64 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
require 'cgi'
|
1
3
|
module Rets
|
2
4
|
module Parser
|
3
5
|
class Compact
|
4
|
-
|
6
|
+
DEFAULT_DELIMITER = "\t"
|
7
|
+
|
8
|
+
INCLUDE_NULL_FIELDS = -1
|
5
9
|
|
6
10
|
InvalidDelimiter = Class.new(ArgumentError)
|
7
11
|
|
8
12
|
def self.parse_document(xml)
|
9
|
-
doc =
|
13
|
+
doc = SaxParser.new
|
14
|
+
parser = Nokogiri::XML::SAX::Parser.new(doc)
|
15
|
+
io = StringIO.new(xml.to_s)
|
16
|
+
|
17
|
+
parser.parse(io)
|
18
|
+
doc.results.map {|r| parse(doc.columns, r, doc.delimiter) }
|
19
|
+
end
|
10
20
|
|
11
|
-
|
12
|
-
|
21
|
+
class SaxParser < Nokogiri::XML::SAX::Document
|
22
|
+
attr_reader :results, :columns, :delimiter
|
13
23
|
|
14
|
-
|
15
|
-
|
24
|
+
def initialize
|
25
|
+
@results = []
|
26
|
+
@columns = ''
|
27
|
+
@result_index = nil
|
28
|
+
@delimiter = nil
|
29
|
+
@columns_start = false
|
30
|
+
@data_start = false
|
31
|
+
end
|
32
|
+
|
33
|
+
def start_element name, attrs=[]
|
34
|
+
case name
|
35
|
+
when 'DELIMITER'
|
36
|
+
@delimiter = attrs.last.last.to_i.chr
|
37
|
+
when 'COLUMNS'
|
38
|
+
@columns_start = true
|
39
|
+
when 'DATA'
|
40
|
+
@result_index = @results.size
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def end_element name
|
45
|
+
case name
|
46
|
+
when 'COLUMNS'
|
47
|
+
@columns_start = false
|
48
|
+
when 'DATA'
|
49
|
+
@result_index = nil
|
50
|
+
end
|
16
51
|
end
|
17
52
|
|
18
|
-
|
19
|
-
|
53
|
+
def characters string
|
54
|
+
if @columns_start
|
55
|
+
@columns << string
|
56
|
+
end
|
20
57
|
|
21
|
-
|
22
|
-
|
23
|
-
|
58
|
+
if @result_index
|
59
|
+
@results[@result_index] ||= ''
|
60
|
+
@results[@result_index] << string
|
61
|
+
end
|
24
62
|
end
|
25
63
|
end
|
26
64
|
|
@@ -29,10 +67,16 @@ module Rets
|
|
29
67
|
# Delimiter must be a regexp because String#split behaves differently when
|
30
68
|
# given a string pattern. (It removes leading spaces).
|
31
69
|
#
|
32
|
-
def self.
|
33
|
-
|
70
|
+
def self.parse(columns, data, delimiter = nil)
|
71
|
+
delimiter ||= DEFAULT_DELIMITER
|
72
|
+
delimiter = Regexp.new(Regexp.escape(delimiter))
|
73
|
+
|
74
|
+
if delimiter == // || delimiter == /,/
|
75
|
+
raise Rets::Parser::Compact::InvalidDelimiter, "Empty or invalid delimiter found, unable to parse."
|
76
|
+
end
|
34
77
|
|
35
|
-
|
78
|
+
column_names = columns.split(delimiter)
|
79
|
+
data_values = data.split(delimiter, INCLUDE_NULL_FIELDS).map { |x| CGI.unescapeHTML(x) }
|
36
80
|
|
37
81
|
zipped_key_values = column_names.zip(data_values).map { |k, v| [k.freeze, v.to_s] }
|
38
82
|
|
@@ -43,10 +87,9 @@ module Rets
|
|
43
87
|
def self.get_count(xml)
|
44
88
|
doc = Nokogiri.parse(xml.to_s)
|
45
89
|
if node = doc.at("//COUNT")
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
return 0 if node.attr('ReplyCode') == '20201'
|
90
|
+
node.attr('Records').to_i
|
91
|
+
else
|
92
|
+
0
|
50
93
|
end
|
51
94
|
end
|
52
95
|
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Rets
|
2
2
|
module Parser
|
3
3
|
class ErrorChecker
|
4
|
+
INVALID_REQUEST_ERROR_MAPPING = Hash[Rets.constants.map {|c| Rets.const_get(c) }.select { |klass| klass.is_a?(Class) && klass < Rets::InvalidRequest }.map {|klass| [klass.const_get('ERROR_CODE'), klass] }]
|
5
|
+
|
4
6
|
def self.check(response)
|
5
7
|
# some RETS servers returns HTTP code 412 when session cookie expired, yet the response body
|
6
8
|
# passes XML check. We need to special case for this situation.
|
@@ -26,7 +28,12 @@ module Rets
|
|
26
28
|
elsif reply_code == NoObjectFound::ERROR_CODE
|
27
29
|
raise NoObjectFound.new(reply_text)
|
28
30
|
elsif reply_code.nonzero?
|
29
|
-
|
31
|
+
error_class = INVALID_REQUEST_ERROR_MAPPING[reply_code]
|
32
|
+
if error_class
|
33
|
+
raise error_class.new(reply_code, reply_text)
|
34
|
+
else
|
35
|
+
raise InvalidRequest.new(reply_code, reply_text)
|
36
|
+
end
|
30
37
|
else
|
31
38
|
return
|
32
39
|
end
|
data/test/fixtures.rb
CHANGED
@@ -65,6 +65,16 @@ INVALID_DELIMETER = <<-XML
|
|
65
65
|
</METADATA>
|
66
66
|
XML
|
67
67
|
|
68
|
+
CHANGED_DELIMITER = <<-XML
|
69
|
+
<?xml version="1.0"?>
|
70
|
+
<METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
|
71
|
+
<DELIMITER value="45" />
|
72
|
+
<COLUMNS>A-B</COLUMNS>
|
73
|
+
<DATA>1-2</DATA>
|
74
|
+
<DATA>4-5</DATA>
|
75
|
+
</METADATA>
|
76
|
+
XML
|
77
|
+
|
68
78
|
COMPACT = <<-XML
|
69
79
|
<?xml version="1.0"?>
|
70
80
|
<METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
|
@@ -255,3 +265,51 @@ SAMPLE_COMPACT_WITH_SPECIAL_CHARS_2 = <<EOF
|
|
255
265
|
<DATA> text with <tag> 1999 00 </DATA>
|
256
266
|
</RETS>
|
257
267
|
EOF
|
268
|
+
|
269
|
+
SAMPLE_PROPERTY_WITH_LOTS_OF_COLUMNS = <<EOF
|
270
|
+
<RETS ReplyCode=\"0\" ReplyText=\"Operation Success.\">
|
271
|
+
<DELIMITER value=\"09\" />
|
272
|
+
<COLUMNS>\t#{800.times.map { |x| "K%03d" % x }.join("\t") }\t</COLUMNS>
|
273
|
+
<DATA>\t\t</DATA>
|
274
|
+
</RETS>
|
275
|
+
EOF
|
276
|
+
|
277
|
+
EXAMPLE_METADATA_TREE = <<EOF
|
278
|
+
Resource: Properties (Key Field: matrix_unique_key)
|
279
|
+
Class: T100
|
280
|
+
Visible Name: Prop
|
281
|
+
Description : some description
|
282
|
+
Table: L_1
|
283
|
+
Resource: Properties
|
284
|
+
ShortName: Sq
|
285
|
+
LongName: Square Footage
|
286
|
+
StandardName: Sqft
|
287
|
+
Units: Meters
|
288
|
+
Searchable: Y
|
289
|
+
Required: N
|
290
|
+
LookupTable: L_10
|
291
|
+
Resource: Properties
|
292
|
+
Required: N
|
293
|
+
Searchable: Y
|
294
|
+
Units:
|
295
|
+
ShortName: HF
|
296
|
+
LongName: HOA Frequency
|
297
|
+
StandardName: HOA F
|
298
|
+
Types:
|
299
|
+
Quarterly -> Q
|
300
|
+
Annually -> A
|
301
|
+
MultiLookupTable: L_11
|
302
|
+
Resource: Properties
|
303
|
+
Required: N
|
304
|
+
Searchable: Y
|
305
|
+
Units:
|
306
|
+
ShortName: HFs
|
307
|
+
LongName: HOA Frequencies
|
308
|
+
StandardName: HOA Fs
|
309
|
+
Types:
|
310
|
+
Quarterly -> Q
|
311
|
+
Annually -> A
|
312
|
+
Object: Photo
|
313
|
+
MimeType: photo/jpg
|
314
|
+
Description: photo description
|
315
|
+
EOF
|