rets-sarmiena 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,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