rets-hack 0.11

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 (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