rets-sarmiena 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.
@@ -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