rets 0.9.0 → 0.10.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/Manifest.txt +24 -0
  4. data/README.md +73 -1
  5. data/Rakefile +1 -1
  6. data/lib/rets.rb +202 -1
  7. data/lib/rets/client.rb +83 -94
  8. data/lib/rets/http_client.rb +42 -0
  9. data/lib/rets/metadata.rb +15 -3
  10. data/lib/rets/metadata/caching.rb +59 -0
  11. data/lib/rets/metadata/file_cache.rb +29 -0
  12. data/lib/rets/metadata/json_serializer.rb +27 -0
  13. data/lib/rets/metadata/lookup_table.rb +65 -0
  14. data/lib/rets/metadata/lookup_type.rb +3 -4
  15. data/lib/rets/metadata/marshal_serializer.rb +27 -0
  16. data/lib/rets/metadata/multi_lookup_table.rb +70 -0
  17. data/lib/rets/metadata/null_cache.rb +24 -0
  18. data/lib/rets/metadata/resource.rb +39 -29
  19. data/lib/rets/metadata/rets_class.rb +27 -23
  20. data/lib/rets/metadata/rets_object.rb +32 -0
  21. data/lib/rets/metadata/table.rb +9 -101
  22. data/lib/rets/metadata/table_factory.rb +19 -0
  23. data/lib/rets/metadata/yaml_serializer.rb +27 -0
  24. data/lib/rets/parser/compact.rb +61 -18
  25. data/lib/rets/parser/error_checker.rb +8 -1
  26. data/test/fixtures.rb +58 -0
  27. data/test/test_caching.rb +89 -0
  28. data/test/test_client.rb +44 -24
  29. data/test/test_error_checker.rb +18 -0
  30. data/test/test_file_cache.rb +42 -0
  31. data/test/test_http_client.rb +96 -60
  32. data/test/test_json_serializer.rb +26 -0
  33. data/test/test_marshal_serializer.rb +26 -0
  34. data/test/test_metadata.rb +62 -450
  35. data/test/test_metadata_class.rb +50 -0
  36. data/test/test_metadata_lookup_table.rb +21 -0
  37. data/test/test_metadata_lookup_type.rb +12 -0
  38. data/test/test_metadata_multi_lookup_table.rb +60 -0
  39. data/test/test_metadata_object.rb +20 -0
  40. data/test/test_metadata_resource.rb +140 -0
  41. data/test/test_metadata_root.rb +151 -0
  42. data/test/test_metadata_table.rb +21 -0
  43. data/test/test_metadata_table_factory.rb +24 -0
  44. data/test/test_parser_compact.rb +23 -28
  45. data/test/test_yaml_serializer.rb +26 -0
  46. metadata +29 -5
@@ -1,3 +1,6 @@
1
+ require 'http-cookie'
2
+ require 'httpclient'
3
+
1
4
  module Rets
2
5
  class HttpClient
3
6
  attr_reader :http, :options, :logger, :login_url
@@ -10,6 +13,45 @@ module Rets
10
13
  @options.fetch(:ca_certs, []).each {|c| @http.ssl_config.add_trust_ca(c) }
11
14
  end
12
15
 
16
+ def self.from_options(options, logger)
17
+ if options[:http_proxy]
18
+ http = HTTPClient.new(options.fetch(:http_proxy))
19
+
20
+ if options[:proxy_username]
21
+ http.set_proxy_auth(options.fetch(:proxy_username), options.fetch(:proxy_password))
22
+ end
23
+ else
24
+ http = HTTPClient.new
25
+ end
26
+
27
+ if options[:receive_timeout]
28
+ http.receive_timeout = options[:receive_timeout]
29
+ end
30
+
31
+ if options[:cookie_store]
32
+ ensure_cookie_store_exists! options[:cookie_store]
33
+ http.set_cookie_store(options[:cookie_store])
34
+ end
35
+
36
+ http_client = new(http, options, logger, options[:login_url])
37
+
38
+ if options[:http_timing_stats_collector]
39
+ http_client = Rets::MeasuringHttpClient.new(http_client, options.fetch(:http_timing_stats_collector), options.fetch(:http_timing_stats_prefix))
40
+ end
41
+
42
+ if options[:lock_around_http_requests]
43
+ http_client = Rets::LockingHttpClient.new(http_client, options.fetch(:locker), options.fetch(:lock_name), options.fetch(:lock_options))
44
+ end
45
+
46
+ http_client
47
+ end
48
+
49
+ def self.ensure_cookie_store_exists!(cookie_store)
50
+ unless File.exist? cookie_store
51
+ FileUtils.touch(cookie_store)
52
+ end
53
+ end
54
+
13
55
  def http_get(url, params=nil, extra_headers={})
14
56
  http.set_auth(url, options[:username], options[:password])
15
57
  headers = extra_headers.merge(rets_extra_headers)
@@ -1,6 +1,18 @@
1
+ require 'rets/metadata/caching'
2
+ require 'rets/metadata/null_cache'
3
+ require 'rets/metadata/file_cache'
4
+ require 'rets/metadata/json_serializer'
5
+ require 'rets/metadata/yaml_serializer'
6
+ require 'rets/metadata/marshal_serializer'
7
+
1
8
  require 'rets/metadata/containers'
2
- require 'rets/metadata/lookup_type'
3
- require 'rets/metadata/resource'
4
- require 'rets/metadata/rets_class'
9
+
5
10
  require 'rets/metadata/root'
11
+ require 'rets/metadata/resource'
12
+ require 'rets/metadata/lookup_type'
13
+ require 'rets/metadata/table_factory'
6
14
  require 'rets/metadata/table'
15
+ require 'rets/metadata/lookup_table'
16
+ require 'rets/metadata/multi_lookup_table'
17
+ require 'rets/metadata/rets_class'
18
+ require 'rets/metadata/rets_object'
@@ -0,0 +1,59 @@
1
+ module Rets
2
+ module Metadata
3
+
4
+ # Metadata caching.
5
+ # @api internal
6
+ class Caching
7
+
8
+ # Given the options passed to Client#initialize, make an
9
+ # instance. Options:
10
+ #
11
+ # * :metadata_cache - Persistence mechanism. Defaults to
12
+ # NullCache.
13
+ #
14
+ # * "metadata_serializer - Serialization mechanism. Defaults to
15
+ # MarshalSerializer.
16
+ def self.make(options)
17
+ cache = options.fetch(:metadata_cache) { Metadata::NullCache.new }
18
+ serializer = options.fetch(:metadata_serializer) do
19
+ Metadata::MarshalSerializer.new
20
+ end
21
+ new(cache, serializer)
22
+ end
23
+
24
+ attr_reader :cache
25
+ attr_reader :serializer
26
+
27
+ # The cache is responsible for reading and writing the
28
+ # serialized metadata. The cache should quack like a
29
+ # Rets::Metadata::FileCache.
30
+ #
31
+ # The serializer is responsible for serializing/deserializing
32
+ # the metadata. The serializer should quack like a
33
+ # Rets::Metadata::MarshalSerializer.
34
+ def initialize(cache, serializer)
35
+ @cache = cache
36
+ @serializer = serializer
37
+ end
38
+
39
+ # Load metadata. Returns a Metadata::Root if successful, or nil
40
+ # if it could be loaded for any reason.
41
+ def load(logger)
42
+ sources = @cache.load do |file|
43
+ @serializer.load(file)
44
+ end
45
+ return nil unless sources.is_a?(Hash)
46
+ Metadata::Root.new(logger, sources)
47
+ end
48
+
49
+ # Save metadata.
50
+ def save(metadata)
51
+ @cache.save do |file|
52
+ @serializer.save(file, metadata.sources)
53
+ end
54
+ end
55
+
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,29 @@
1
+ module Rets
2
+ module Metadata
3
+
4
+ # This metadata cache persists the metadata to a file.
5
+ class FileCache
6
+
7
+ def initialize(path)
8
+ @path = path
9
+ end
10
+
11
+ # Save the metadata. Should yield an IO-like object to a block;
12
+ # that block will serialize the metadata to that object.
13
+ def save(&block)
14
+ File.open(@path, "wb", &block)
15
+ end
16
+
17
+ # Load the metadata. Should yield an IO-like object to a block;
18
+ # that block will deserialize the metadata from that object and
19
+ # return the metadata. Returns the metadata, or nil if it could
20
+ # not be loaded.
21
+ def load(&block)
22
+ File.open(@path, "rb", &block)
23
+ rescue IOError, SystemCallError
24
+ nil
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ require 'json'
2
+
3
+ module Rets
4
+ module Metadata
5
+
6
+ # Serialize/Deserialize metadata using JSON.
7
+ class JsonSerializer
8
+
9
+ # Serialize to a file. The library reserves the right to change
10
+ # the type or contents of o, so don't depend on it being
11
+ # anything in particular.
12
+ def save(file, o)
13
+ file.write o.to_json
14
+ end
15
+
16
+ # Deserialize from a file. If the metadata cannot be
17
+ # deserialized, return nil.
18
+ def load(file)
19
+ JSON.load(file)
20
+ rescue JSON::ParserError
21
+ nil
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,65 @@
1
+ module Rets
2
+ module Metadata
3
+ class LookupTable
4
+ attr_reader :resource_id, :lookup_types, :table_fragment, :name, :long_name
5
+
6
+ def initialize(resource_id, lookup_types, table_fragment)
7
+ @resource_id = resource_id
8
+ @lookup_types = lookup_types
9
+
10
+ @table_fragment = table_fragment
11
+ @name = table_fragment["SystemName"]
12
+ @long_name = table_fragment["LongName"]
13
+ end
14
+
15
+ def self.build(table_fragment, resource_id, lookup_types)
16
+ lookup_name = table_fragment["LookupName"]
17
+ lookup_types = lookup_types[lookup_name]
18
+ new(resource_id, lookup_types, table_fragment)
19
+ end
20
+
21
+ # Print the tree to a file
22
+ #
23
+ # [out] The file to print to. Defaults to $stdout.
24
+ def print_tree(out = $stdout)
25
+ out.puts " LookupTable: #{name}"
26
+ out.puts " Resource: #{resource_id}"
27
+ out.puts " Required: #{table_fragment['Required']}"
28
+ out.puts " Searchable: #{ table_fragment["Searchable"] }"
29
+ out.puts " Units: #{ table_fragment["Units"] }"
30
+ out.puts " ShortName: #{ table_fragment["ShortName"] }"
31
+ out.puts " LongName: #{ long_name }"
32
+ out.puts " StandardName: #{ table_fragment["StandardName"] }"
33
+ out.puts " Types:"
34
+
35
+ lookup_types.each do |lookup_type|
36
+ lookup_type.print_tree(out)
37
+ end
38
+ end
39
+
40
+ def lookup_type(value)
41
+ lookup_types.detect {|lt| lt.value == value }
42
+ end
43
+
44
+ def resolve(value)
45
+ if value.empty?
46
+ return value.to_s.strip
47
+ end
48
+
49
+ #Remove surrounding quotes
50
+ clean_value = value.scan(/^["']?(.*?)["']?$/).join
51
+
52
+
53
+ lookup_type = lookup_type(clean_value)
54
+
55
+ resolved_value = lookup_type ? lookup_type.long_value : nil
56
+
57
+ if resolved_value.nil? && $VERBOSE
58
+ warn("Discarding unmappable value of #{clean_value.inspect}")
59
+ end
60
+
61
+ resolved_value.to_s.strip
62
+ end
63
+ end
64
+ end
65
+ end
@@ -1,12 +1,11 @@
1
1
  module Rets
2
2
  module Metadata
3
3
  class LookupType
4
- attr_accessor :long_value, :short_value, :value
4
+ attr_reader :long_value, :value
5
5
 
6
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"]
7
+ @value = lookup_type_fragment["Value"]
8
+ @long_value = lookup_type_fragment["LongValue"]
10
9
  end
11
10
 
12
11
  # Print the tree to a file
@@ -0,0 +1,27 @@
1
+ require 'json'
2
+
3
+ module Rets
4
+ module Metadata
5
+
6
+ # Serialize/Deserialize metadata using Marshal.
7
+ class MarshalSerializer
8
+
9
+ # Serialize to a file. The library reserves the right to change
10
+ # the type or contents of o, so don't depend on it being
11
+ # anything in particular.
12
+ def save(file, o)
13
+ Marshal.dump(o, file)
14
+ end
15
+
16
+ # Deserialize from a file. If the metadata cannot be
17
+ # deserialized, return nil.
18
+ def load(file)
19
+ Marshal.load(file)
20
+ rescue TypeError
21
+ nil
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,70 @@
1
+ module Rets
2
+ module Metadata
3
+ class MultiLookupTable
4
+ attr_reader :resource_id, :lookup_types, :table_fragment, :name, :long_name
5
+
6
+ def initialize(resource_id, lookup_types, table_fragment)
7
+ @resource_id = resource_id
8
+ @lookup_types = lookup_types
9
+
10
+ @table_fragment = table_fragment
11
+ @name = table_fragment["SystemName"]
12
+ @long_name = table_fragment["LongName"]
13
+ end
14
+
15
+ def self.build(table_fragment, resource_id, lookup_types)
16
+ lookup_name = table_fragment["LookupName"]
17
+ lookup_types = lookup_types[lookup_name]
18
+ new(resource_id, lookup_types, table_fragment)
19
+ end
20
+
21
+ # Print the tree to a file
22
+ #
23
+ # [out] The file to print to. Defaults to $stdout.
24
+ def print_tree(out = $stdout)
25
+ out.puts " MultiLookupTable: #{name}"
26
+ out.puts " Resource: #{resource_id}"
27
+ out.puts " Required: #{table_fragment['Required']}"
28
+ out.puts " Searchable: #{ table_fragment["Searchable"] }"
29
+ out.puts " Units: #{ table_fragment["Units"] }"
30
+ out.puts " ShortName: #{ table_fragment["ShortName"] }"
31
+ out.puts " LongName: #{ long_name }"
32
+ out.puts " StandardName: #{ table_fragment["StandardName"] }"
33
+ out.puts " Types:"
34
+
35
+ lookup_types.each do |lookup_type|
36
+ lookup_type.print_tree(out)
37
+ end
38
+ end
39
+
40
+ def lookup_type(value)
41
+ lookup_types.detect {|lt| lt.value == value }
42
+ end
43
+
44
+ def resolve(value)
45
+ if value.empty?
46
+ return []
47
+ end
48
+
49
+ values = value.split(",")
50
+
51
+ values = values.map do |v|
52
+
53
+ #Remove surrounding quotes
54
+ clean_value = v.scan(/^["']?(.*?)["']?$/).join
55
+
56
+
57
+ lookup_type = lookup_type(clean_value)
58
+
59
+ resolved_value = lookup_type ? lookup_type.long_value : nil
60
+
61
+ if resolved_value.nil? && $VERBOSE
62
+ warn("Discarding unmappable value of #{clean_value.inspect}")
63
+ end
64
+
65
+ resolved_value.to_s.strip
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,24 @@
1
+ module Rets
2
+ module Metadata
3
+
4
+ # This type of metadata cache, which is the default, neither saves
5
+ # nor restores.
6
+ class NullCache
7
+
8
+ # Save the metadata. Should yield an IO-like object to a block;
9
+ # that block will serialize the metadata to that object.
10
+ def save(&_block)
11
+ end
12
+
13
+ # Load the metadata. Should yield an IO-like object to a block;
14
+ # that block will deserialize the metadata from that object and
15
+ # return the metadata. Returns the metadata, or nil if it could
16
+ # not be loaded.
17
+ def load(&_block)
18
+ nil
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
@@ -2,46 +2,44 @@ module Rets
2
2
  module Metadata
3
3
  class Resource
4
4
  class MissingRetsClass < RuntimeError; end
5
- attr_accessor :rets_classes
6
- attr_accessor :lookup_types
7
- attr_accessor :key_field
5
+ attr_reader :id, :key_field, :rets_classes, :rets_objects
8
6
 
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"]
7
+ def initialize(id, key_field, rets_classes, rets_objects)
8
+ @id = id
9
+ @key_field = key_field
10
+ @rets_classes = rets_classes
11
+ @rets_objects = rets_objects
17
12
  end
18
13
 
19
- def self.find_lookup_containers(metadata, resource)
20
- metadata[:lookup].select { |lc| lc.resource == resource.id }
14
+ def self.find_lookup_containers(metadata, resource_id)
15
+ metadata[:lookup].select { |lc| lc.resource == resource_id }
21
16
  end
22
17
 
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 }
18
+ def self.find_lookup_type_containers(metadata, resource_id, lookup_name)
19
+ metadata[:lookup_type].select { |ltc| ltc.resource == resource_id && ltc.lookup == lookup_name }
25
20
  end
26
21
 
27
- def self.find_rets_classes(metadata, resource)
28
- class_container = metadata[:class].detect { |c| c.resource == resource.id }
22
+ def self.find_rets_classes(metadata, resource_id)
23
+ class_container = metadata[:class].detect { |c| c.resource == resource_id }
29
24
  if class_container.nil?
30
- raise MissingRetsClass.new("No Metadata classes for #{resource.id}")
25
+ raise MissingRetsClass.new("No Metadata classes for #{resource_id}")
31
26
  else
32
27
  class_container.classes
33
28
  end
34
29
  end
35
30
 
36
- def self.build_lookup_tree(resource, metadata)
31
+ def self.find_rets_objects(metadata, resource_id)
32
+ metadata[:object].select { |object| object.resource == resource_id }.map(&:objects).flatten
33
+ end
34
+
35
+ def self.build_lookup_tree(resource_id, metadata)
37
36
  lookup_types = Hash.new {|h, k| h[k] = Array.new }
38
37
 
39
- find_lookup_containers(metadata, resource).each do |lookup_container|
38
+ find_lookup_containers(metadata, resource_id).each do |lookup_container|
40
39
  lookup_container.lookups.each do |lookup_fragment|
41
40
  lookup_name = lookup_fragment["LookupName"]
42
41
 
43
- find_lookup_type_containers(metadata, resource, lookup_name).each do |lookup_type_container|
44
-
42
+ find_lookup_type_containers(metadata, resource_id, lookup_name).each do |lookup_type_container|
45
43
  lookup_type_container.lookup_types.each do |lookup_type_fragment|
46
44
  lookup_types[lookup_name] << LookupType.new(lookup_type_fragment)
47
45
  end
@@ -52,18 +50,27 @@ module Rets
52
50
  lookup_types
53
51
  end
54
52
 
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)
53
+ def self.build_classes(resource_id, lookup_types, metadata)
54
+ find_rets_classes(metadata, resource_id).map do |rets_class_fragment|
55
+ RetsClass.build(rets_class_fragment, resource_id, lookup_types, metadata)
56
+ end
57
+ end
58
+
59
+ def self.build_objects(resource_id, metadata)
60
+ find_rets_objects(metadata, resource_id).map do |rets_object_fragment|
61
+ RetsObject.build(rets_object_fragment)
58
62
  end
59
63
  end
60
64
 
61
65
  def self.build(resource_fragment, metadata, logger)
62
- resource = new(resource_fragment)
66
+ resource_id = resource_fragment["ResourceID"]
67
+ key_field = resource_fragment["KeyField"]
63
68
 
64
- resource.lookup_types = build_lookup_tree(resource, metadata)
65
- resource.rets_classes = build_classes(resource, metadata)
66
- resource
69
+ lookup_types = build_lookup_tree(resource_id, metadata)
70
+ rets_classes = build_classes(resource_id, lookup_types, metadata)
71
+ rets_objects = build_objects(resource_id, metadata)
72
+
73
+ new(resource_id, key_field, rets_classes, rets_objects)
67
74
  rescue MissingRetsClass => e
68
75
  logger.warn(e.message)
69
76
  nil
@@ -77,6 +84,9 @@ module Rets
77
84
  rets_classes.each do |rets_class|
78
85
  rets_class.print_tree(out)
79
86
  end
87
+ rets_objects.each do |rets_object|
88
+ rets_object.print_tree(out)
89
+ end
80
90
  end
81
91
 
82
92
  def find_rets_class(rets_class_name)