sean-rets 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/CHANGELOG.md +89 -0
- data/Manifest.txt +28 -0
- data/README.md +47 -0
- data/Rakefile +27 -0
- data/bin/rets +202 -0
- data/lib/rets.rb +45 -0
- data/lib/rets/client.rb +391 -0
- data/lib/rets/client_progress_reporter.rb +44 -0
- data/lib/rets/http_client.rb +91 -0
- data/lib/rets/locking_http_client.rb +34 -0
- data/lib/rets/measuring_http_client.rb +27 -0
- data/lib/rets/metadata.rb +6 -0
- data/lib/rets/metadata/containers.rb +84 -0
- data/lib/rets/metadata/lookup_type.rb +17 -0
- data/lib/rets/metadata/resource.rb +84 -0
- data/lib/rets/metadata/rets_class.rb +48 -0
- data/lib/rets/metadata/root.rb +152 -0
- data/lib/rets/metadata/table.rb +113 -0
- data/lib/rets/parser/compact.rb +62 -0
- data/lib/rets/parser/multipart.rb +40 -0
- data/test/fixtures.rb +212 -0
- data/test/helper.rb +14 -0
- data/test/test_client.rb +238 -0
- data/test/test_locking_http_client.rb +29 -0
- data/test/test_metadata.rb +459 -0
- data/test/test_parser_compact.rb +86 -0
- data/test/test_parser_multipart.rb +39 -0
- data/test/vcr_cassettes/unauthorized_response.yml +262 -0
- metadata +186 -0
@@ -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,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
|