rets-hack 0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +142 -0
  3. data/Manifest.txt +58 -0
  4. data/README.md +129 -0
  5. data/Rakefile +28 -0
  6. data/bin/rets +202 -0
  7. data/example/connect.rb +19 -0
  8. data/example/get-photos.rb +20 -0
  9. data/example/get-property.rb +16 -0
  10. data/lib/rets/client.rb +373 -0
  11. data/lib/rets/client_progress_reporter.rb +48 -0
  12. data/lib/rets/http_client.rb +133 -0
  13. data/lib/rets/locking_http_client.rb +34 -0
  14. data/lib/rets/measuring_http_client.rb +27 -0
  15. data/lib/rets/metadata/caching.rb +59 -0
  16. data/lib/rets/metadata/containers.rb +89 -0
  17. data/lib/rets/metadata/file_cache.rb +29 -0
  18. data/lib/rets/metadata/json_serializer.rb +27 -0
  19. data/lib/rets/metadata/lookup_table.rb +65 -0
  20. data/lib/rets/metadata/lookup_type.rb +19 -0
  21. data/lib/rets/metadata/marshal_serializer.rb +27 -0
  22. data/lib/rets/metadata/multi_lookup_table.rb +70 -0
  23. data/lib/rets/metadata/null_cache.rb +24 -0
  24. data/lib/rets/metadata/resource.rb +103 -0
  25. data/lib/rets/metadata/rets_class.rb +57 -0
  26. data/lib/rets/metadata/rets_object.rb +41 -0
  27. data/lib/rets/metadata/root.rb +155 -0
  28. data/lib/rets/metadata/table.rb +33 -0
  29. data/lib/rets/metadata/table_factory.rb +19 -0
  30. data/lib/rets/metadata/yaml_serializer.rb +27 -0
  31. data/lib/rets/metadata.rb +18 -0
  32. data/lib/rets/parser/compact.rb +117 -0
  33. data/lib/rets/parser/error_checker.rb +56 -0
  34. data/lib/rets/parser/multipart.rb +39 -0
  35. data/lib/rets.rb +269 -0
  36. data/test/fixtures.rb +324 -0
  37. data/test/helper.rb +14 -0
  38. data/test/test_caching.rb +89 -0
  39. data/test/test_client.rb +307 -0
  40. data/test/test_error_checker.rb +87 -0
  41. data/test/test_file_cache.rb +42 -0
  42. data/test/test_http_client.rb +132 -0
  43. data/test/test_json_serializer.rb +26 -0
  44. data/test/test_locking_http_client.rb +29 -0
  45. data/test/test_marshal_serializer.rb +26 -0
  46. data/test/test_metadata.rb +71 -0
  47. data/test/test_metadata_class.rb +50 -0
  48. data/test/test_metadata_lookup_table.rb +21 -0
  49. data/test/test_metadata_lookup_type.rb +21 -0
  50. data/test/test_metadata_multi_lookup_table.rb +60 -0
  51. data/test/test_metadata_object.rb +33 -0
  52. data/test/test_metadata_resource.rb +148 -0
  53. data/test/test_metadata_root.rb +151 -0
  54. data/test/test_metadata_table.rb +21 -0
  55. data/test/test_metadata_table_factory.rb +24 -0
  56. data/test/test_parser_compact.rb +115 -0
  57. data/test/test_parser_multipart.rb +39 -0
  58. data/test/test_yaml_serializer.rb +26 -0
  59. data/test/vcr_cassettes/unauthorized_response.yml +262 -0
  60. metadata +227 -0
@@ -0,0 +1,89 @@
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
+ fields_hash[field] || fields_hash[field] = extract(fragment, field.to_s.capitalize)
18
+ end
19
+ end
20
+ end
21
+
22
+ uses :date, :version
23
+
24
+ def initialize(fragment)
25
+ self.fragment = fragment
26
+ end
27
+
28
+ def extract(fragment, attr)
29
+ fragment.attr(attr)
30
+ end
31
+
32
+ private
33
+
34
+ def fields_hash
35
+ @fields ||= {}
36
+ end
37
+
38
+ end
39
+
40
+ class RowContainer < Container
41
+
42
+ attr_accessor :rows
43
+
44
+ def initialize(doc)
45
+ super
46
+ self.rows = Parser::Compact.parse_document(doc)
47
+ end
48
+
49
+ end
50
+
51
+ class ResourceContainer < RowContainer
52
+ alias resources rows
53
+ end
54
+
55
+ class ClassContainer < RowContainer
56
+ uses :resource
57
+
58
+ alias classes rows
59
+ end
60
+
61
+ class TableContainer < RowContainer
62
+ uses :resource, :class
63
+
64
+ alias tables rows
65
+ end
66
+
67
+ class LookupContainer < RowContainer
68
+ uses :resource
69
+
70
+ alias lookups rows
71
+ end
72
+
73
+ class LookupTypeContainer < RowContainer
74
+ uses :resource, :lookup
75
+
76
+ alias lookup_types rows
77
+ end
78
+
79
+ class ObjectContainer < RowContainer
80
+ uses :resource
81
+
82
+ alias objects rows
83
+ end
84
+
85
+ class SystemContainer < Container
86
+ end
87
+ end
88
+ end
89
+ 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
@@ -0,0 +1,19 @@
1
+ module Rets
2
+ module Metadata
3
+ class LookupType
4
+ attr_reader :long_value, :value
5
+
6
+ def initialize(lookup_type_fragment)
7
+ @value = lookup_type_fragment["Value"].strip
8
+ @long_value = lookup_type_fragment["LongValue"].strip
9
+ end
10
+
11
+ # Print the tree to a file
12
+ #
13
+ # [out] The file to print to. Defaults to $stdout.
14
+ def print_tree(out = $stdout)
15
+ out.puts " #{long_value} -> #{value}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -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
@@ -0,0 +1,103 @@
1
+ module Rets
2
+ module Metadata
3
+ class Resource
4
+ class MissingRetsClass < RuntimeError; end
5
+ attr_reader :id, :key_field, :rets_classes, :rets_objects
6
+
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
12
+ end
13
+
14
+ def self.find_lookup_containers(metadata, resource_id)
15
+ metadata[:lookup].select { |lc| lc.resource == resource_id }
16
+ end
17
+
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 }
20
+ end
21
+
22
+ def self.find_rets_classes(metadata, resource_id)
23
+ class_container = metadata[:class].detect { |c| c.resource == resource_id }
24
+ if class_container.nil?
25
+ raise MissingRetsClass.new("No Metadata classes for #{resource_id}")
26
+ else
27
+ class_container.classes
28
+ end
29
+ end
30
+
31
+ def self.find_rets_objects(metadata, resource_id)
32
+ objects = metadata[:object]
33
+ if objects
34
+ objects.select { |object| object.resource == resource_id }.map(&:objects).flatten
35
+ else
36
+ []
37
+ end
38
+ end
39
+
40
+ def self.build_lookup_tree(resource_id, metadata)
41
+ lookup_types = Hash.new {|h, k| h[k] = Array.new }
42
+
43
+ find_lookup_containers(metadata, resource_id).each do |lookup_container|
44
+ lookup_container.lookups.each do |lookup_fragment|
45
+ lookup_name = lookup_fragment["LookupName"]
46
+
47
+ find_lookup_type_containers(metadata, resource_id, lookup_name).each do |lookup_type_container|
48
+ lookup_type_container.lookup_types.each do |lookup_type_fragment|
49
+ lookup_types[lookup_name] << LookupType.new(lookup_type_fragment)
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ lookup_types
56
+ end
57
+
58
+ def self.build_classes(resource_id, lookup_types, metadata)
59
+ find_rets_classes(metadata, resource_id).map do |rets_class_fragment|
60
+ RetsClass.build(rets_class_fragment, resource_id, lookup_types, metadata)
61
+ end
62
+ end
63
+
64
+ def self.build_objects(resource_id, metadata)
65
+ find_rets_objects(metadata, resource_id).map do |rets_object_fragment|
66
+ RetsObject.build(rets_object_fragment)
67
+ end
68
+ end
69
+
70
+ def self.build(resource_fragment, metadata, logger)
71
+ resource_id = resource_fragment["ResourceID"]
72
+ key_field = resource_fragment["KeyField"]
73
+
74
+ lookup_types = build_lookup_tree(resource_id, metadata)
75
+ rets_classes = build_classes(resource_id, lookup_types, metadata)
76
+ rets_objects = build_objects(resource_id, metadata)
77
+
78
+ new(resource_id, key_field, rets_classes, rets_objects)
79
+ rescue MissingRetsClass => e
80
+ logger.warn(e.message)
81
+ nil
82
+ end
83
+
84
+ # Print the tree to a file
85
+ #
86
+ # [out] The file to print to. Defaults to $stdout.
87
+ def print_tree(out = $stdout)
88
+ out.puts "# Resource: #{id} (Key Field: #{key_field})"
89
+ rets_classes.each do |rets_class|
90
+ rets_class.print_tree(out)
91
+ end
92
+ rets_objects.each do |rets_object|
93
+ rets_object.print_tree(out)
94
+ end
95
+ end
96
+
97
+ def find_rets_class(rets_class_name)
98
+ rets_classes.detect {|rc| rc.name == rets_class_name }
99
+ end
100
+ end
101
+ end
102
+ end
103
+
@@ -0,0 +1,57 @@
1
+ module Rets
2
+ module Metadata
3
+ class RetsClass
4
+ attr_reader :name, :visible_name, :standard_name, :description, :tables
5
+
6
+ def initialize(name, visible_name, standard_name, description, tables)
7
+ @name = name
8
+ @visible_name = visible_name
9
+ @description = description
10
+ @standard_name = standard_name
11
+
12
+ @tables = tables
13
+ end
14
+
15
+ def self.find_table_container(metadata, resource_id, class_name)
16
+ metadata[:table].detect { |t| t.resource == resource_id && t.class == class_name }
17
+ end
18
+
19
+ def self.builds_tables(table_container, resource_id, lookup_types)
20
+ if table_container
21
+ table_container.tables.map do |table_fragment|
22
+ TableFactory.build(table_fragment, resource_id, lookup_types)
23
+ end
24
+ else
25
+ []
26
+ end
27
+ end
28
+
29
+ def self.build(rets_class_fragment, resource_id, lookup_types, metadata)
30
+ class_name = rets_class_fragment["ClassName"]
31
+ visible_name = rets_class_fragment["VisibleName"]
32
+ standard_name = rets_class_fragment["StandardName"]
33
+ description = rets_class_fragment["Description"]
34
+
35
+ table_container = find_table_container(metadata, resource_id, class_name)
36
+ tables = builds_tables(table_container, resource_id, lookup_types)
37
+ new(class_name, visible_name, standard_name, description, tables)
38
+ end
39
+
40
+ # Print the tree to a file
41
+ #
42
+ # [out] The file to print to. Defaults to $stdout.
43
+ def print_tree(out = $stdout)
44
+ out.puts "## Class: #{name}"
45
+ out.puts " Visible Name: #{visible_name}"
46
+ out.puts " Description : #{description}"
47
+ tables.each do |table|
48
+ table.print_tree(out)
49
+ end
50
+ end
51
+
52
+ def find_table(name)
53
+ tables.detect { |value| value.name.downcase == name.downcase }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,41 @@
1
+ module Rets
2
+ module Metadata
3
+ class RetsObject
4
+ attr_reader :name, :mime_type, :description, :type
5
+
6
+ def initialize(type, name, mime_type, description)
7
+ @name = name
8
+ @mime_type = mime_type
9
+ @description = description
10
+ @type = type
11
+ end
12
+
13
+ def self.build(rets_object_fragment)
14
+ rets_object_fragment = downcase_hash_keys(rets_object_fragment)
15
+ name = rets_object_fragment["visiblename"]
16
+ mime_type = rets_object_fragment["mimetype"]
17
+ description = rets_object_fragment["description"]
18
+ type = rets_object_fragment['objecttype']
19
+ new(type, name, mime_type, description)
20
+ end
21
+
22
+ def print_tree(out = $stdout)
23
+ out.puts " Object: #{type}"
24
+ out.puts " Visible Name: #{name}"
25
+ out.puts " Mime Type: #{mime_type}"
26
+ out.puts " Description: #{description}"
27
+ end
28
+
29
+ def ==(other)
30
+ name == other.name &&
31
+ mime_type == other.mime_type &&
32
+ description == other.description
33
+ end
34
+
35
+ private
36
+ def self.downcase_hash_keys(hash)
37
+ Hash[hash.map { |k, v| [k.downcase, v] }]
38
+ end
39
+ end
40
+ end
41
+ end