rets 0.9.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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)