rets 0.1.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.
- data/.gemtest +0 -0
- data/CHANGELOG.md +9 -0
- data/Manifest.txt +23 -0
- data/README.md +42 -0
- data/Rakefile +18 -0
- data/bin/rets +194 -0
- data/lib/rets.rb +24 -0
- data/lib/rets/authentication.rb +59 -0
- data/lib/rets/client.rb +473 -0
- data/lib/rets/metadata.rb +6 -0
- data/lib/rets/metadata/containers.rb +84 -0
- data/lib/rets/metadata/lookup_type.rb +17 -0
- data/lib/rets/metadata/resource.rb +73 -0
- data/lib/rets/metadata/rets_class.rb +42 -0
- data/lib/rets/metadata/root.rb +155 -0
- data/lib/rets/metadata/table.rb +98 -0
- data/lib/rets/parser/compact.rb +46 -0
- data/lib/rets/parser/multipart.rb +36 -0
- data/test/fixtures.rb +142 -0
- data/test/helper.rb +6 -0
- data/test/test_client.rb +571 -0
- data/test/test_metadata.rb +452 -0
- data/test/test_parser_compact.rb +71 -0
- data/test/test_parser_multipart.rb +21 -0
- metadata +162 -0
@@ -0,0 +1,84 @@
|
|
1
|
+
module Rets
|
2
|
+
module Metadata
|
3
|
+
#########################
|
4
|
+
# Basic representation of the underlying metadata. This models
|
5
|
+
# the structure of RETS metadata closely. The OO-representation
|
6
|
+
# uses this structure for its construction. External usage of
|
7
|
+
# this API should be discouraged in favor of the richer OO
|
8
|
+
# representation.
|
9
|
+
#
|
10
|
+
module Containers
|
11
|
+
class Container
|
12
|
+
attr_accessor :fragment
|
13
|
+
|
14
|
+
def self.uses(*fields)
|
15
|
+
fields.each do |field|
|
16
|
+
define_method(field) do
|
17
|
+
instance_variable_get("@#{field}") ||
|
18
|
+
instance_variable_set("@#{field}", extract(fragment, field.to_s.capitalize))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
uses :date, :version
|
24
|
+
|
25
|
+
def initialize(fragment)
|
26
|
+
self.fragment = fragment
|
27
|
+
end
|
28
|
+
|
29
|
+
def extract(fragment, attr)
|
30
|
+
fragment.attr(attr)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class RowContainer < Container
|
36
|
+
|
37
|
+
attr_accessor :rows
|
38
|
+
|
39
|
+
def initialize(doc)
|
40
|
+
super
|
41
|
+
self.rows = Parser::Compact.parse_document(doc)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
class ResourceContainer < RowContainer
|
47
|
+
alias resources rows
|
48
|
+
end
|
49
|
+
|
50
|
+
class ClassContainer < RowContainer
|
51
|
+
uses :resource
|
52
|
+
|
53
|
+
alias classes rows
|
54
|
+
end
|
55
|
+
|
56
|
+
class TableContainer < RowContainer
|
57
|
+
uses :resource, :class
|
58
|
+
|
59
|
+
alias tables rows
|
60
|
+
end
|
61
|
+
|
62
|
+
class LookupContainer < RowContainer
|
63
|
+
uses :resource
|
64
|
+
|
65
|
+
alias lookups rows
|
66
|
+
end
|
67
|
+
|
68
|
+
class LookupTypeContainer < RowContainer
|
69
|
+
uses :resource, :lookup
|
70
|
+
|
71
|
+
alias lookup_types rows
|
72
|
+
end
|
73
|
+
|
74
|
+
class ObjectContainer < RowContainer
|
75
|
+
uses :resource
|
76
|
+
|
77
|
+
alias objects rows
|
78
|
+
end
|
79
|
+
|
80
|
+
class SystemContainer < Container
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Rets
|
2
|
+
module Metadata
|
3
|
+
class LookupType
|
4
|
+
attr_accessor :long_value, :short_value, :value
|
5
|
+
|
6
|
+
def initialize(lookup_type_fragment)
|
7
|
+
self.value = lookup_type_fragment["Value"]
|
8
|
+
self.short_value = lookup_type_fragment["ShortValue"]
|
9
|
+
self.long_value = lookup_type_fragment["LongValue"]
|
10
|
+
end
|
11
|
+
|
12
|
+
def print_tree
|
13
|
+
puts " #{long_value} -> #{value}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Rets
|
2
|
+
module Metadata
|
3
|
+
class Resource
|
4
|
+
attr_accessor :rets_classes
|
5
|
+
attr_accessor :lookup_types
|
6
|
+
|
7
|
+
attr_accessor :id
|
8
|
+
|
9
|
+
def initialize(resource)
|
10
|
+
self.rets_classes = []
|
11
|
+
self.lookup_types = {}
|
12
|
+
|
13
|
+
self.id = resource["ResourceID"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.find_lookup_containers(metadata, resource)
|
17
|
+
metadata[:lookup].select { |lc| lc.resource == resource.id }
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.find_lookup_type_containers(metadata, resource, lookup_name)
|
21
|
+
metadata[:lookup_type].select { |ltc| ltc.resource == resource.id && ltc.lookup == lookup_name }
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find_rets_classes(metadata, resource)
|
25
|
+
metadata[:class].detect { |c| c.resource == resource.id }.classes
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.build_lookup_tree(resource, metadata)
|
29
|
+
lookup_types = Hash.new {|h, k| h[k] = Array.new }
|
30
|
+
|
31
|
+
find_lookup_containers(metadata, resource).each do |lookup_container|
|
32
|
+
lookup_container.lookups.each do |lookup_fragment|
|
33
|
+
lookup_name = lookup_fragment["LookupName"]
|
34
|
+
|
35
|
+
find_lookup_type_containers(metadata, resource, lookup_name).each do |lookup_type_container|
|
36
|
+
|
37
|
+
lookup_type_container.lookup_types.each do |lookup_type_fragment|
|
38
|
+
lookup_types[lookup_name] << LookupType.new(lookup_type_fragment)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
lookup_types
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.build_classes(resource, metadata)
|
48
|
+
find_rets_classes(metadata, resource).map do |rets_class_fragment|
|
49
|
+
RetsClass.build(rets_class_fragment, resource, metadata)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.build(resource_fragment, metadata)
|
54
|
+
resource = new(resource_fragment)
|
55
|
+
|
56
|
+
resource.lookup_types = build_lookup_tree(resource, metadata)
|
57
|
+
resource.rets_classes = build_classes(resource, metadata)
|
58
|
+
resource
|
59
|
+
end
|
60
|
+
|
61
|
+
def print_tree
|
62
|
+
puts "Resource: #{id}"
|
63
|
+
|
64
|
+
rets_classes.each(&:print_tree)
|
65
|
+
end
|
66
|
+
|
67
|
+
def find_rets_class(rets_class_name)
|
68
|
+
rets_classes.detect {|rc| rc.name == rets_class_name }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Rets
|
2
|
+
module Metadata
|
3
|
+
class RetsClass
|
4
|
+
attr_accessor :tables
|
5
|
+
attr_accessor :name
|
6
|
+
attr_accessor :resource
|
7
|
+
|
8
|
+
def initialize(rets_class_fragment, resource)
|
9
|
+
self.resource = resource
|
10
|
+
self.tables = []
|
11
|
+
self.name = rets_class_fragment["ClassName"]
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.find_table_container(metadata, resource, rets_class)
|
15
|
+
metadata[:table].detect { |t| t.resource == resource.id && t.class == rets_class.name }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.build(rets_class_fragment, resource, metadata)
|
19
|
+
rets_class = new(rets_class_fragment, resource)
|
20
|
+
|
21
|
+
table_container = find_table_container(metadata, resource, rets_class)
|
22
|
+
|
23
|
+
if table_container
|
24
|
+
table_container.tables.each do |table_fragment|
|
25
|
+
rets_class.tables << TableFactory.build(table_fragment, resource)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
rets_class
|
30
|
+
end
|
31
|
+
|
32
|
+
def print_tree
|
33
|
+
puts " Class: #{name}"
|
34
|
+
tables.each(&:print_tree)
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_table(name)
|
38
|
+
tables.detect { |value| value.name == name }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Rets
|
2
|
+
module Metadata
|
3
|
+
METADATA_TYPES = %w(SYSTEM RESOURCE CLASS TABLE LOOKUP LOOKUP_TYPE OBJECT)
|
4
|
+
|
5
|
+
# It's useful when dealing with the Rets standard to represent their
|
6
|
+
# relatively flat namespace of interweived components as a Tree. With
|
7
|
+
# a collection of resources at the top, and their various, classes,
|
8
|
+
# tables, lookups, and lookup types underneath.
|
9
|
+
#
|
10
|
+
# It looks something like ...
|
11
|
+
#
|
12
|
+
# Resource
|
13
|
+
# |
|
14
|
+
# Class
|
15
|
+
# |
|
16
|
+
# `-- Table
|
17
|
+
# |
|
18
|
+
# `-- Lookups
|
19
|
+
# |
|
20
|
+
# `-- LookupType
|
21
|
+
#
|
22
|
+
# For our purposes it was helpful to denormalize some of the more deeply
|
23
|
+
# nested branches. In particular by relating Lookups to LookupTypes, and
|
24
|
+
# Tables to lookups with can simplify this diagram.
|
25
|
+
#
|
26
|
+
#
|
27
|
+
# Resource
|
28
|
+
# |
|
29
|
+
# Class
|
30
|
+
# |
|
31
|
+
# `-- Table
|
32
|
+
# |
|
33
|
+
# `-- Lookups
|
34
|
+
#
|
35
|
+
# By associating Tables and lookups when we parse this structure. It allows
|
36
|
+
# us to seemlessly map Lookup values to their Long or Short value forms.
|
37
|
+
class Root
|
38
|
+
# Metadata_types is the low level parsed representation of the raw xml
|
39
|
+
# sources. Just one level up, they contain Containers, consisting of
|
40
|
+
# SystemContainers or RowContainers
|
41
|
+
attr_writer :metadata_types
|
42
|
+
|
43
|
+
# the tree is the high level represenation of the metadata heiarchy
|
44
|
+
# it begins with root. Stored as a list of Metadata::Resources
|
45
|
+
attr_writer :tree
|
46
|
+
|
47
|
+
# Sources are the raw xml documents fetched for each metadata type
|
48
|
+
# they are stored as a hash with the type names as their keys
|
49
|
+
# and the raw xml as the values
|
50
|
+
attr_accessor :sources
|
51
|
+
|
52
|
+
# fetcher is a proc that inverts control to the client to retrieve metadata
|
53
|
+
# types
|
54
|
+
def initialize(&fetcher)
|
55
|
+
@tree = nil
|
56
|
+
@metadata_types = nil # TODO think up a better name ... containers?
|
57
|
+
@sources = {}
|
58
|
+
|
59
|
+
# allow Root's to be built with no fetcher. Makes for easy testing
|
60
|
+
return unless block_given?
|
61
|
+
|
62
|
+
fetch_sources(&fetcher)
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch_sources(&fetcher)
|
66
|
+
self.sources = Hash[*METADATA_TYPES.map {|type| [type, fetcher.call(type)] }.flatten]
|
67
|
+
end
|
68
|
+
|
69
|
+
def marshal_dump
|
70
|
+
sources
|
71
|
+
end
|
72
|
+
|
73
|
+
def marshal_load(sources)
|
74
|
+
self.sources = sources
|
75
|
+
end
|
76
|
+
|
77
|
+
def version
|
78
|
+
metadata_types[:system].first.version
|
79
|
+
end
|
80
|
+
|
81
|
+
def date
|
82
|
+
metadata_types[:system].first.date
|
83
|
+
end
|
84
|
+
|
85
|
+
# Wether there exists a more up to date version of the metadata to fetch
|
86
|
+
# is dependant on either a timestamp indicating when the most recent
|
87
|
+
# version was published, or a version number. These values may or may
|
88
|
+
# not exist on any given rets server.
|
89
|
+
def current?(current_timestamp, current_version)
|
90
|
+
if !current_version.to_s.empty? && !version.to_s.empty?
|
91
|
+
current_version == version
|
92
|
+
else
|
93
|
+
current_timestamp ? current_timestamp == date : true
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def build_tree
|
98
|
+
tree = Hash.new { |h, k| h.key?(k.downcase) ? h[k.downcase] : nil }
|
99
|
+
|
100
|
+
resource_containers = metadata_types[:resource]
|
101
|
+
|
102
|
+
resource_containers.each do |resource_container|
|
103
|
+
resource_container.rows.each do |resource_fragment|
|
104
|
+
resource = Resource.build(resource_fragment, metadata_types)
|
105
|
+
tree[resource.id.downcase] = resource
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
tree
|
110
|
+
end
|
111
|
+
|
112
|
+
def tree
|
113
|
+
@tree ||= build_tree
|
114
|
+
end
|
115
|
+
|
116
|
+
def print_tree
|
117
|
+
tree.each do |name, value|
|
118
|
+
value.print_tree
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def metadata_types
|
123
|
+
return @metadata_types if @metadata_types
|
124
|
+
|
125
|
+
h = {}
|
126
|
+
|
127
|
+
sources.each do |name, source|
|
128
|
+
h[name.downcase.to_sym] = build_containers(Nokogiri.parse(source))
|
129
|
+
end
|
130
|
+
|
131
|
+
@metadata_types = h
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns an array of container classes that represents
|
135
|
+
# the metadata stored in the document provided.
|
136
|
+
def build_containers(doc)
|
137
|
+
# find all tags that match /RETS/METADATA-*
|
138
|
+
fragments = doc.xpath("/RETS/*[starts-with(name(), 'METADATA-')]")
|
139
|
+
|
140
|
+
fragments.map { |fragment| build_container(fragment) }
|
141
|
+
end
|
142
|
+
|
143
|
+
def build_container(fragment)
|
144
|
+
tag = fragment.name # METADATA-RESOURCE
|
145
|
+
type = tag.sub(/^METADATA-/, "") # RESOURCE
|
146
|
+
|
147
|
+
class_name = type.capitalize.gsub(/_(\w)/) { $1.upcase }
|
148
|
+
container_name = "#{class_name}Container"
|
149
|
+
|
150
|
+
container_class = Containers.constants.include?(container_name) ? Containers.const_get(container_name) : Containers::Container
|
151
|
+
container_class.new(fragment)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module Rets
|
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)
|
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
|
+
class Table
|
17
|
+
attr_accessor :type
|
18
|
+
attr_accessor :name
|
19
|
+
attr_accessor :table_fragment
|
20
|
+
|
21
|
+
def initialize(table_fragment)
|
22
|
+
self.table_fragment = table_fragment
|
23
|
+
self.type = table_fragment["DataType"]
|
24
|
+
self.name = table_fragment["SystemName"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def print_tree
|
28
|
+
puts " Table: #{name}"
|
29
|
+
puts " ShortName: #{ table_fragment["ShortName"] }"
|
30
|
+
puts " LongName: #{ table_fragment["LongName"] }"
|
31
|
+
puts " StandardName: #{ table_fragment["StandardName"] }"
|
32
|
+
puts " Units: #{ table_fragment["Units"] }"
|
33
|
+
puts " Searchable: #{ table_fragment["Searchable"] }"
|
34
|
+
end
|
35
|
+
|
36
|
+
def resolve(value)
|
37
|
+
value.to_s.strip
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class LookupTable
|
42
|
+
attr_accessor :resource
|
43
|
+
attr_accessor :lookup_name
|
44
|
+
attr_accessor :name
|
45
|
+
attr_accessor :interpretation
|
46
|
+
|
47
|
+
def initialize(table_fragment, resource)
|
48
|
+
self.resource = resource
|
49
|
+
self.name = table_fragment["SystemName"]
|
50
|
+
self.interpretation = table_fragment["Interpretation"]
|
51
|
+
self.lookup_name = table_fragment["LookupName"]
|
52
|
+
end
|
53
|
+
|
54
|
+
def multi?
|
55
|
+
interpretation == "LookupMulti"
|
56
|
+
end
|
57
|
+
|
58
|
+
def lookup_types
|
59
|
+
resource.lookup_types[lookup_name]
|
60
|
+
end
|
61
|
+
|
62
|
+
def print_tree
|
63
|
+
puts " LookupTable: #{name}"
|
64
|
+
|
65
|
+
lookup_types.each(&:print_tree)
|
66
|
+
end
|
67
|
+
|
68
|
+
def lookup_type(value)
|
69
|
+
lookup_types.detect {|lt| lt.value == value }
|
70
|
+
end
|
71
|
+
|
72
|
+
def resolve(value)
|
73
|
+
if value.empty?
|
74
|
+
return [] if multi?
|
75
|
+
return value.to_s.strip
|
76
|
+
end
|
77
|
+
|
78
|
+
values = multi? ? value.split(","): [value]
|
79
|
+
|
80
|
+
values = values.map do |value|
|
81
|
+
|
82
|
+
#Remove surrounding quotes
|
83
|
+
value = value.scan(/^["']?(.*?)["']?$/).to_s
|
84
|
+
|
85
|
+
lookup_type = lookup_type(value)
|
86
|
+
|
87
|
+
resolved_value = lookup_type ? lookup_type.long_value : nil
|
88
|
+
|
89
|
+
warn("Discarding unmappable value of #{value.inspect}") if resolved_value.nil? && $VERBOSE
|
90
|
+
|
91
|
+
resolved_value
|
92
|
+
end
|
93
|
+
|
94
|
+
multi? ? values.map {|value| value.to_s.strip } : values.first.to_s.strip
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|