rets-sarmiena 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +4 -0
- data/Manifest.txt +23 -0
- data/README.md +42 -0
- data/Rakefile +1 -0
- data/bin/rets +194 -0
- data/lib/rets-sarmiena.rb +2 -0
- data/lib/rets-sarmiena/version.rb +5 -0
- data/lib/rets.rb +24 -0
- data/lib/rets/authentication.rb +59 -0
- data/lib/rets/client.rb +483 -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 +169 -0
- data/lib/rets/metadata/table.rb +98 -0
- data/lib/rets/parser/compact.rb +54 -0
- data/lib/rets/parser/multipart.rb +36 -0
- data/rets-sarmiena.gemspec +26 -0
- data/test/fixtures.rb +149 -0
- data/test/helper.rb +6 -0
- data/test/test_client.rb +582 -0
- data/test/test_helper.rb +6 -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 +123 -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,169 @@
|
|
1
|
+
module Rets
|
2
|
+
module Metadata
|
3
|
+
METADATA_MAP = {:system => "SYSTEM",
|
4
|
+
:resource => "RESOURCE",
|
5
|
+
:class => "CLASS",
|
6
|
+
:table => "TABLE",
|
7
|
+
:lookup => "LOOKUP",
|
8
|
+
:lookup_type => "LOOKUP_TYPE",
|
9
|
+
:object => "OBJECT"}
|
10
|
+
METADATA_TYPES = METADATA_MAP.values
|
11
|
+
|
12
|
+
# It's useful when dealing with the Rets standard to represent their
|
13
|
+
# relatively flat namespace of interweived components as a Tree. With
|
14
|
+
# a collection of resources at the top, and their various, classes,
|
15
|
+
# tables, lookups, and lookup types underneath.
|
16
|
+
#
|
17
|
+
# It looks something like ...
|
18
|
+
#
|
19
|
+
# Resource
|
20
|
+
# |
|
21
|
+
# Class
|
22
|
+
# |
|
23
|
+
# `-- Table
|
24
|
+
# |
|
25
|
+
# `-- Lookups
|
26
|
+
# |
|
27
|
+
# `-- LookupType
|
28
|
+
#
|
29
|
+
# For our purposes it was helpful to denormalize some of the more deeply
|
30
|
+
# nested branches. In particular by relating Lookups to LookupTypes, and
|
31
|
+
# Tables to lookups with can simplify this diagram.
|
32
|
+
#
|
33
|
+
#
|
34
|
+
# Resource
|
35
|
+
# |
|
36
|
+
# Class
|
37
|
+
# |
|
38
|
+
# `-- Table
|
39
|
+
# |
|
40
|
+
# `-- Lookups
|
41
|
+
#
|
42
|
+
# By associating Tables and lookups when we parse this structure. It allows
|
43
|
+
# us to seemlessly map Lookup values to their Long or Short value forms.
|
44
|
+
class Root
|
45
|
+
# Metadata_types is the low level parsed representation of the raw xml
|
46
|
+
# sources. Just one level up, they contain Containers, consisting of
|
47
|
+
# SystemContainers or RowContainers
|
48
|
+
attr_writer :metadata_types
|
49
|
+
|
50
|
+
# the tree is the high level represenation of the metadata heiarchy
|
51
|
+
# it begins with root. Stored as a list of Metadata::Resources
|
52
|
+
attr_writer :tree
|
53
|
+
|
54
|
+
# Sources are the raw xml documents fetched for each metadata type
|
55
|
+
# they are stored as a hash with the type names as their keys
|
56
|
+
# and the raw xml as the values
|
57
|
+
attr_accessor :sources
|
58
|
+
|
59
|
+
def initialize(client)
|
60
|
+
@tree = nil
|
61
|
+
@metadata_types = {} # TODO think up a better name ... containers?
|
62
|
+
@sources = {}
|
63
|
+
@client = client
|
64
|
+
end
|
65
|
+
|
66
|
+
def sources
|
67
|
+
@sources = fetch_sources
|
68
|
+
end
|
69
|
+
|
70
|
+
def fetch_sources
|
71
|
+
@fetch_sources ||= Hash[*METADATA_TYPES.map {|type| [type, @client.retrieve_metadata_type(type)] }.flatten]
|
72
|
+
end
|
73
|
+
|
74
|
+
def fetch_source_by_type(type)
|
75
|
+
self.sources[type] ||= @client.retrieve_metadata_type(type)
|
76
|
+
end
|
77
|
+
|
78
|
+
def marshal_dump
|
79
|
+
sources
|
80
|
+
end
|
81
|
+
|
82
|
+
def marshal_load(sources)
|
83
|
+
self.sources = sources
|
84
|
+
end
|
85
|
+
|
86
|
+
def version
|
87
|
+
metadata_types[:system].first.version
|
88
|
+
end
|
89
|
+
|
90
|
+
def date
|
91
|
+
metadata_types[:system].first.date
|
92
|
+
end
|
93
|
+
|
94
|
+
# Wether there exists a more up to date version of the metadata to fetch
|
95
|
+
# is dependant on either a timestamp indicating when the most recent
|
96
|
+
# version was published, or a version number. These values may or may
|
97
|
+
# not exist on any given rets server.
|
98
|
+
def current?(current_timestamp, current_version)
|
99
|
+
if !current_version.to_s.empty? && !version.to_s.empty?
|
100
|
+
current_version == version
|
101
|
+
else
|
102
|
+
current_timestamp ? current_timestamp == date : true
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def build_tree
|
107
|
+
tree = Hash.new { |h, k| h.key?(k.downcase) ? h[k.downcase] : nil }
|
108
|
+
|
109
|
+
resource_containers = metadata_types
|
110
|
+
|
111
|
+
resource_containers.each do |resource_container|
|
112
|
+
resource_container.rows.each do |resource_fragment|
|
113
|
+
resource = Resource.build(resource_fragment, metadata_types)
|
114
|
+
tree[resource.id.downcase] = resource
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
tree
|
119
|
+
end
|
120
|
+
|
121
|
+
def tree
|
122
|
+
@tree ||= build_tree
|
123
|
+
end
|
124
|
+
|
125
|
+
def print_tree
|
126
|
+
tree.each do |name, value|
|
127
|
+
value.print_tree
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def for(metadata_key)
|
132
|
+
raise "Unknown metatadata key '#{metadata_key}'" unless key = METADATA_MAP[metadata_key]
|
133
|
+
@metadata_types[key] ||= metadata_type(fetch_source_by_type(key))
|
134
|
+
end
|
135
|
+
|
136
|
+
def metadata_types
|
137
|
+
sources.each do |name, source|
|
138
|
+
@metadata_types[name.downcase.to_sym] ||= metadata_type(source)
|
139
|
+
end
|
140
|
+
|
141
|
+
@metadata_types
|
142
|
+
end
|
143
|
+
|
144
|
+
def metadata_type(source)
|
145
|
+
build_containers(Nokogiri.parse(source))
|
146
|
+
end
|
147
|
+
|
148
|
+
# Returns an array of container classes that represents
|
149
|
+
# the metadata stored in the document provided.
|
150
|
+
def build_containers(doc)
|
151
|
+
# find all tags that match /RETS/METADATA-*
|
152
|
+
fragments = doc.xpath("/RETS/*[starts-with(name(), 'METADATA-')]")
|
153
|
+
|
154
|
+
fragments.map { |fragment| build_container(fragment) }
|
155
|
+
end
|
156
|
+
|
157
|
+
def build_container(fragment)
|
158
|
+
tag = fragment.name # METADATA-RESOURCE
|
159
|
+
type = tag.sub(/^METADATA-/, "") # RESOURCE
|
160
|
+
|
161
|
+
class_name = type.capitalize.gsub(/_(\w)/) { $1.upcase }
|
162
|
+
container_name = "#{class_name}Container"
|
163
|
+
|
164
|
+
container_class = Containers.constants.include?(container_name.to_sym) ? Containers.const_get(container_name) : Containers::Container
|
165
|
+
container_class.new(fragment)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
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
|