sean-rets 0.6.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,34 @@
1
+ module Rets
2
+ class LockingHttpClient
3
+ def initialize(http_client, locker, lock_name, options={})
4
+ @http_client = http_client
5
+ @locker = locker
6
+ @lock_name = lock_name
7
+ @options = options
8
+ end
9
+
10
+ def http_get(url, params=nil, extra_headers={})
11
+ lock_around do
12
+ @http_client.http_get(url, params, extra_headers)
13
+ end
14
+ end
15
+
16
+ def http_post(url, params, extra_headers = {})
17
+ lock_around do
18
+ @http_client.http_post(url, params, extra_headers)
19
+ end
20
+ end
21
+
22
+ def save_cookie_store(force=nil)
23
+ @http_client.save_cookie_store(force)
24
+ end
25
+
26
+ def lock_around(&block)
27
+ result = nil
28
+ @locker.lock(@lock_name, @options) do
29
+ result = block.call
30
+ end
31
+ result
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,27 @@
1
+ module Rets
2
+ class MeasuringHttpClient
3
+ def initialize(http_client, stats, prefix)
4
+ @http_client = http_client
5
+ @stats = stats
6
+ @prefix = prefix
7
+ end
8
+
9
+ def http_get(url, params=nil, extra_headers={})
10
+ @stats.count("#{@prefix}.http_get_rate")
11
+ @stats.time("#{@prefix}.http_get") do
12
+ @http_client.http_get(url, params, extra_headers)
13
+ end
14
+ end
15
+
16
+ def http_post(url, params, extra_headers = {})
17
+ @stats.count("#{@prefix}.http_post_rate")
18
+ @stats.time("#{@prefix}.http_post") do
19
+ @http_client.http_post(url, params, extra_headers)
20
+ end
21
+ end
22
+
23
+ def save_cookie_store(force=nil)
24
+ @http_client.save_cookie_store(force)
25
+ end
26
+ end
27
+ end
@@ -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,84 @@
1
+ module Rets
2
+ module Metadata
3
+ class Resource
4
+ class MissingRetsClass < RuntimeError; end
5
+ attr_accessor :rets_classes
6
+ attr_accessor :lookup_types
7
+ attr_accessor :key_field
8
+
9
+ attr_accessor :id
10
+
11
+ def initialize(resource)
12
+ self.rets_classes = []
13
+ self.lookup_types = {}
14
+
15
+ self.id = resource["ResourceID"]
16
+ self.key_field = resource["KeyField"]
17
+ end
18
+
19
+ def self.find_lookup_containers(metadata, resource)
20
+ metadata[:lookup].select { |lc| lc.resource == resource.id }
21
+ end
22
+
23
+ def self.find_lookup_type_containers(metadata, resource, lookup_name)
24
+ metadata[:lookup_type].select { |ltc| ltc.resource == resource.id && ltc.lookup == lookup_name }
25
+ end
26
+
27
+ def self.find_rets_classes(metadata, resource)
28
+ class_container = metadata[:class].detect { |c| c.resource == resource.id }
29
+ if class_container.nil?
30
+ raise MissingRetsClass.new("No Metadata classes for #{resource.id}")
31
+ else
32
+ class_container.classes
33
+ end
34
+ end
35
+
36
+ def self.build_lookup_tree(resource, metadata)
37
+ lookup_types = Hash.new {|h, k| h[k] = Array.new }
38
+
39
+ find_lookup_containers(metadata, resource).each do |lookup_container|
40
+ lookup_container.lookups.each do |lookup_fragment|
41
+ lookup_name = lookup_fragment["LookupName"]
42
+
43
+ find_lookup_type_containers(metadata, resource, lookup_name).each do |lookup_type_container|
44
+
45
+ lookup_type_container.lookup_types.each do |lookup_type_fragment|
46
+ lookup_types[lookup_name] << LookupType.new(lookup_type_fragment)
47
+ end
48
+ end
49
+ end
50
+ end
51
+
52
+ lookup_types
53
+ end
54
+
55
+ def self.build_classes(resource, metadata)
56
+ find_rets_classes(metadata, resource).map do |rets_class_fragment|
57
+ RetsClass.build(rets_class_fragment, resource, metadata)
58
+ end
59
+ end
60
+
61
+ def self.build(resource_fragment, metadata, logger)
62
+ resource = new(resource_fragment)
63
+
64
+ resource.lookup_types = build_lookup_tree(resource, metadata)
65
+ resource.rets_classes = build_classes(resource, metadata)
66
+ resource
67
+ rescue MissingRetsClass => e
68
+ logger.warn(e.message)
69
+ nil
70
+ end
71
+
72
+ def print_tree
73
+ puts "Resource: #{id} (Key Field: #{key_field})"
74
+
75
+ rets_classes.each(&:print_tree)
76
+ end
77
+
78
+ def find_rets_class(rets_class_name)
79
+ rets_classes.detect {|rc| rc.name == rets_class_name }
80
+ end
81
+ end
82
+ end
83
+ end
84
+
@@ -0,0 +1,48 @@
1
+ module Rets
2
+ module Metadata
3
+ class RetsClass
4
+ attr_accessor :tables
5
+ attr_accessor :name
6
+ attr_accessor :visible_name
7
+ attr_accessor :description
8
+ attr_accessor :resource
9
+
10
+ def initialize(rets_class_fragment, resource)
11
+ self.resource = resource
12
+ self.tables = []
13
+ self.name = rets_class_fragment["ClassName"]
14
+ self.visible_name = rets_class_fragment["VisibleName"]
15
+ self.description = rets_class_fragment["Description"]
16
+ end
17
+
18
+ def self.find_table_container(metadata, resource, rets_class)
19
+ metadata[:table].detect { |t| t.resource == resource.id && t.class == rets_class.name }
20
+ end
21
+
22
+ def self.build(rets_class_fragment, resource, metadata)
23
+ rets_class = new(rets_class_fragment, resource)
24
+
25
+ table_container = find_table_container(metadata, resource, rets_class)
26
+
27
+ if table_container
28
+ table_container.tables.each do |table_fragment|
29
+ rets_class.tables << TableFactory.build(table_fragment, resource)
30
+ end
31
+ end
32
+
33
+ rets_class
34
+ end
35
+
36
+ def print_tree
37
+ puts " Class: #{name}"
38
+ puts " Visible Name: #{visible_name}"
39
+ puts " Description : #{description}"
40
+ tables.each(&:print_tree)
41
+ end
42
+
43
+ def find_table(name)
44
+ tables.detect { |value| value.name == name }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,152 @@
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_reader :sources
51
+
52
+ # Metadata can be unmarshalled from cache. @logger is not set during that process, constructor is not called.
53
+ # Client code must set it after unmarshalling.
54
+ attr_reader :logger
55
+
56
+ # fetcher is a proc that inverts control to the client to retrieve metadata
57
+ # types
58
+ def initialize(logger, sources)
59
+ @logger = logger
60
+ @tree = nil
61
+ @metadata_types = nil # TODO think up a better name ... containers?
62
+ @sources = sources
63
+ end
64
+
65
+ def marshal_dump
66
+ sources
67
+ end
68
+
69
+ def version
70
+ metadata_types[:system].first.version
71
+ end
72
+
73
+ def date
74
+ metadata_types[:system].first.date
75
+ end
76
+
77
+ # Wether there exists a more up to date version of the metadata to fetch
78
+ # is dependant on either a timestamp indicating when the most recent
79
+ # version was published, or a version number. These values may or may
80
+ # not exist on any given rets server.
81
+ def current?(current_timestamp, current_version)
82
+ if !current_version.to_s.empty? && !version.to_s.empty?
83
+ current_version == version
84
+ else
85
+ current_timestamp ? current_timestamp == date : true
86
+ end
87
+ end
88
+
89
+ def build_tree
90
+ tree = Hash.new { |h, k| h.key?(k.downcase) ? h[k.downcase] : nil }
91
+
92
+ resource_containers = metadata_types[:resource]
93
+
94
+ resource_containers.each do |resource_container|
95
+ resource_container.rows.each do |resource_fragment|
96
+ resource = Resource.build(resource_fragment, metadata_types, @logger)
97
+ #some mlses list resource types without an associated data, throw those away
98
+ tree[resource.id.downcase] = resource if resource
99
+ end
100
+ end
101
+
102
+ tree
103
+ end
104
+
105
+ def tree
106
+ @tree ||= build_tree
107
+ end
108
+
109
+ def print_tree
110
+ tree.each do |name, value|
111
+ value.print_tree
112
+ end
113
+ end
114
+
115
+ def metadata_types
116
+ return @metadata_types if @metadata_types
117
+
118
+ h = {}
119
+
120
+ sources.each do |name, source|
121
+ h[name.downcase.to_sym] = build_containers(Nokogiri.parse(source))
122
+ end
123
+
124
+ @metadata_types = h
125
+ end
126
+
127
+ # Returns an array of container classes that represents
128
+ # the metadata stored in the document provided.
129
+ def build_containers(doc)
130
+ # find all tags that match /RETS/METADATA-*
131
+ fragments = doc.xpath("/RETS/*[starts-with(name(), 'METADATA-')]")
132
+
133
+ fragments.map { |fragment| build_container(fragment) }
134
+ end
135
+
136
+ def build_container(fragment)
137
+ tag = fragment.name # METADATA-RESOURCE
138
+ type = tag.sub(/^METADATA-/, "") # RESOURCE
139
+
140
+ class_name = type.capitalize.gsub(/_(\w)/) { $1.upcase }
141
+ container_name = "#{class_name}Container"
142
+
143
+ if ::RUBY_VERSION < '1.9'
144
+ container_class = Containers.const_defined?(container_name) ? Containers.const_get(container_name) : Containers::Container
145
+ else
146
+ container_class = Containers.const_defined?(container_name, true) ? Containers.const_get(container_name, true) : Containers::Container
147
+ end
148
+ container_class.new(fragment)
149
+ end
150
+ end
151
+ end
152
+ end