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