rets 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ require 'rets/metadata/containers'
2
+ require 'rets/metadata/lookup_type'
3
+ require 'rets/metadata/resource'
4
+ require 'rets/metadata/rets_class'
5
+ require 'rets/metadata/root'
6
+ require 'rets/metadata/table'
@@ -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