flexmls_api 0.3.2

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 (61) hide show
  1. data/Gemfile +20 -0
  2. data/Gemfile.lock +60 -0
  3. data/LICENSE +14 -0
  4. data/README.md +128 -0
  5. data/Rakefile +78 -0
  6. data/VERSION +1 -0
  7. data/lib/flexmls_api/authentication.rb +104 -0
  8. data/lib/flexmls_api/client.rb +20 -0
  9. data/lib/flexmls_api/configuration.rb +40 -0
  10. data/lib/flexmls_api/faraday.rb +52 -0
  11. data/lib/flexmls_api/models/base.rb +76 -0
  12. data/lib/flexmls_api/models/connect_prefs.rb +10 -0
  13. data/lib/flexmls_api/models/contact.rb +25 -0
  14. data/lib/flexmls_api/models/custom_fields.rb +12 -0
  15. data/lib/flexmls_api/models/document.rb +11 -0
  16. data/lib/flexmls_api/models/idx_link.rb +45 -0
  17. data/lib/flexmls_api/models/listing.rb +110 -0
  18. data/lib/flexmls_api/models/market_statistics.rb +33 -0
  19. data/lib/flexmls_api/models/photo.rb +15 -0
  20. data/lib/flexmls_api/models/property_types.rb +7 -0
  21. data/lib/flexmls_api/models/standard_fields.rb +7 -0
  22. data/lib/flexmls_api/models/subresource.rb +13 -0
  23. data/lib/flexmls_api/models/system_info.rb +7 -0
  24. data/lib/flexmls_api/models/video.rb +16 -0
  25. data/lib/flexmls_api/models/virtual_tour.rb +18 -0
  26. data/lib/flexmls_api/models.rb +21 -0
  27. data/lib/flexmls_api/paginate.rb +87 -0
  28. data/lib/flexmls_api/request.rb +172 -0
  29. data/lib/flexmls_api/version.rb +4 -0
  30. data/lib/flexmls_api.rb +41 -0
  31. data/spec/fixtures/contacts.json +25 -0
  32. data/spec/fixtures/listing_document_index.json +19 -0
  33. data/spec/fixtures/listing_no_subresources.json +38 -0
  34. data/spec/fixtures/listing_photos_index.json +469 -0
  35. data/spec/fixtures/listing_videos_index.json +18 -0
  36. data/spec/fixtures/listing_virtual_tours_index.json +42 -0
  37. data/spec/fixtures/listing_with_documents.json +52 -0
  38. data/spec/fixtures/listing_with_photos.json +110 -0
  39. data/spec/fixtures/listing_with_supplement.json +39 -0
  40. data/spec/fixtures/listing_with_videos.json +54 -0
  41. data/spec/fixtures/listing_with_vtour.json +48 -0
  42. data/spec/fixtures/session.json +10 -0
  43. data/spec/json_helper.rb +77 -0
  44. data/spec/spec_helper.rb +78 -0
  45. data/spec/unit/flexmls_api/configuration_spec.rb +97 -0
  46. data/spec/unit/flexmls_api/faraday_spec.rb +94 -0
  47. data/spec/unit/flexmls_api/models/base_spec.rb +62 -0
  48. data/spec/unit/flexmls_api/models/connect_prefs_spec.rb +9 -0
  49. data/spec/unit/flexmls_api/models/contact_spec.rb +70 -0
  50. data/spec/unit/flexmls_api/models/document_spec.rb +39 -0
  51. data/spec/unit/flexmls_api/models/listing_spec.rb +174 -0
  52. data/spec/unit/flexmls_api/models/photo_spec.rb +59 -0
  53. data/spec/unit/flexmls_api/models/property_types_spec.rb +20 -0
  54. data/spec/unit/flexmls_api/models/standard_fields_spec.rb +42 -0
  55. data/spec/unit/flexmls_api/models/system_info_spec.rb +37 -0
  56. data/spec/unit/flexmls_api/models/video_spec.rb +43 -0
  57. data/spec/unit/flexmls_api/models/virtual_tour_spec.rb +46 -0
  58. data/spec/unit/flexmls_api/paginate_spec.rb +221 -0
  59. data/spec/unit/flexmls_api/request_spec.rb +288 -0
  60. data/spec/unit/flexmls_api_spec.rb +44 -0
  61. metadata +315 -0
@@ -0,0 +1,52 @@
1
+ module FlexmlsApi
2
+ module FaradayExt
3
+ #=Flexmls API Faraday middle way
4
+ # HTTP Response after filter to package api responses and bubble up basic api errors.
5
+ class FlexmlsMiddleware < Faraday::Response::Middleware
6
+ begin
7
+ def self.register_on_complete(env)
8
+ env[:response].on_complete do |finished_env|
9
+ FlexmlsApi.logger.debug("hashed response: #{finished_env[:body].inspect}")
10
+ validate_and_build_response(finished_env)
11
+ end
12
+ end
13
+ rescue LoadError, NameError => e
14
+ self.load_error = e
15
+ end
16
+
17
+ # Handles pretty much all the api response parsing and error handling. All responses that
18
+ # indicate a failure will raise a FlexmlsApi::ClientError exception
19
+ def self.validate_and_build_response(finished_env)
20
+ body = finished_env[:body]
21
+ FlexmlsApi.logger.debug("Response Body: #{body.inspect}")
22
+ unless body.is_a?(Hash) && body.key?("D")
23
+ raise InvalidResponse, "The server response could not be understood"
24
+ end
25
+ response = ApiResponse.new body
26
+ case finished_env[:status]
27
+ when 400, 409
28
+ raise BadResourceRequest.new(response.code, finished_env[:status]), response.message
29
+ when 401
30
+ raise PermissionDenied.new(response.code, finished_env[:status]), response.message
31
+ when 404
32
+ raise NotFound.new(response.code, finished_env[:status]), response.message
33
+ when 405
34
+ raise NotAllowed.new(response.code, finished_env[:status]), response.message
35
+ when 500
36
+ raise ClientError.new(response.code, finished_env[:status]), response.message
37
+ when 200..299
38
+ FlexmlsApi.logger.debug("Success!")
39
+ else
40
+ raise ClientError.new(response.code, finished_env[:status]), response.message
41
+ end
42
+ finished_env[:body] = response
43
+ end
44
+
45
+ def initialize(app)
46
+ super
47
+ @parser = nil
48
+ end
49
+
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,76 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ # =API Model Base class
4
+ # Intended to be a lot like working with ActiveResource, this class adds most of the basic
5
+ # active model type niceties.
6
+ class Base
7
+ extend Paginate
8
+
9
+ attr_accessor :attributes
10
+
11
+ # Name of the resource as related to the path name
12
+ def self.element_name
13
+ # TODO I'd love to pull in active model at this point to provide default naming
14
+ @element_name ||= "resource"
15
+ end
16
+
17
+ def self.element_name=(name)
18
+ @element_name = name
19
+ end
20
+
21
+ # Resource path prefix, prepended to the url
22
+ def self.prefix
23
+ @prefix ||= "/"
24
+ end
25
+ def self.prefix=(prefix)
26
+ @prefix = prefix
27
+ end
28
+ def self.path
29
+ "#{prefix}#{element_name}"
30
+ end
31
+
32
+ def self.connection
33
+ FlexmlsApi.client
34
+ end
35
+ def connection
36
+ self.class.connection
37
+ end
38
+
39
+ def initialize(attributes={})
40
+ @attributes = {}
41
+ load(attributes)
42
+ end
43
+
44
+ def load(attributes)
45
+ attributes.each do |key,val|
46
+ @attributes[key.to_s] = val
47
+ end
48
+ end
49
+
50
+ def self.get(options={})
51
+ collect(connection.get(path, options))
52
+ end
53
+
54
+ def self.first(options={})
55
+ get(options)[0]
56
+ end
57
+
58
+ def method_missing(method_symbol, *arguments)
59
+ method_name = method_symbol.to_s
60
+
61
+ if method_name =~ /(=|\?)$/
62
+ case $1
63
+ when "="
64
+ attributes[$`] = arguments.first
65
+ # TODO figure out a nice way to present setters for the standard fields
66
+ when "?"
67
+ attributes[$`]
68
+ end
69
+ else
70
+ return attributes[method_name] if attributes.include?(method_name)
71
+ super # GTFO
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,10 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class Connect < Base
4
+ self.element_name="connect"
5
+ def self.prefs
6
+ connection.get("#{path}/prefs")
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,25 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class Contact < Base
4
+ self.element_name="contacts"
5
+
6
+ def save
7
+ begin
8
+ return save!
9
+ rescue BadResourceRequest => e
10
+ rescue NotFound => e
11
+ # log and leave
12
+ FlexmlsApi.logger.error("Failed to save contact #{self}: #{e.message}")
13
+ end
14
+ false
15
+ end
16
+ def save!
17
+ results = connection.post self.class.path, "Contacts" => [ attributes ]
18
+ result = results.first
19
+ attributes['ResourceUri'] = result['ResourceUri']
20
+ attributes['Id'] = result['ResourceUri'][/\/.*\/(.+)$/, 1]
21
+ true
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class CustomFields < Base
4
+ self.element_name="customfields"
5
+
6
+
7
+ def self.find_by_property_type(card_fmt, user)
8
+ collect(connection.get("#{self.path}/#{card_fmt}", :ApiUser => user))
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class Document < Base
4
+ extend Subresource
5
+
6
+ self.element_name="documents"
7
+
8
+
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class IdxLink < Base
4
+ self.element_name="idxlinks"
5
+
6
+ #TODO Work all below into common base class
7
+ def self.find(*arguments)
8
+ scope = arguments.slice!(0)
9
+ options = arguments.slice!(0) || {}
10
+
11
+ case scope
12
+ when :all then find_every(options)
13
+ when :first then find_every(options).first
14
+ when :last then find_every(options).last
15
+ when :one then find_one(options)
16
+ else find_single(scope, options)
17
+ end
18
+ end
19
+
20
+ def self.first(*arguments)
21
+ find(:first, *arguments)
22
+ end
23
+
24
+ def self.last(*arguments)
25
+ find(:last, *arguments)
26
+ end
27
+
28
+ private
29
+
30
+ def self.find_every(options)
31
+ raise NotImplementedError # TODO
32
+ end
33
+
34
+ def self.find_one(options)
35
+ raise NotImplementedError # TODO
36
+ end
37
+
38
+ def self.find_single(scope, options)
39
+ resp = FlexmlsApi.client.get("/idxlinks/#{scope}", options)
40
+ new(resp[0])
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,110 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class Listing < Base
4
+ attr_accessor :photos, :videos, :virtual_tours, :documents
5
+ self.element_name="listings"
6
+
7
+ def initialize(attributes={})
8
+ @photos = []
9
+ @videos = []
10
+ @virtual_tours = []
11
+ @documents = []
12
+
13
+
14
+ if attributes.has_key?('StandardFields')
15
+ pics, vids, tours, docs = attributes['StandardFields'].values_at('Photos','Videos', 'VirtualTours', 'Documents')
16
+ end
17
+
18
+ if pics != nil
19
+ pics.collect { |pic| @photos.push(Photo.new(pic)) }
20
+ attributes['StandardFields'].delete('Photos')
21
+ end
22
+
23
+ if vids != nil
24
+ vids.collect { |vid| @videos.push(Video.new(vid)) }
25
+ attributes['StandardFields'].delete('Videos')
26
+ end
27
+
28
+ if tours != nil
29
+ tours.collect { |tour| @virtual_tours.push(VirtualTour.new(tour)) }
30
+ attributes['StandardFields'].delete('VirtualTours')
31
+ end
32
+
33
+ if docs != nil
34
+ docs.collect { |doc| @documents.push(Document.new(doc)) }
35
+ attributes['StandardFields'].delete('Documents')
36
+ end
37
+
38
+
39
+ super(attributes)
40
+ end
41
+
42
+
43
+ def self.find(*arguments)
44
+ scope = arguments.slice!(0)
45
+ options = arguments.slice!(0) || {}
46
+
47
+ case scope
48
+ when :all then find_every(options)
49
+ when :first then find_every(options).first
50
+ when :last then find_every(options).last
51
+ when :one then find_one(options)
52
+ else find_single(scope, options)
53
+ end
54
+ end
55
+
56
+ def self.find_by_cart_id(cart_id, owner, options={})
57
+ options.merge!({ :ApiUser => owner, :_filter => "ListingCart Eq '#{cart_id}'" })
58
+ find(:all, options)
59
+ end
60
+
61
+ def self.first(*arguments)
62
+ find(:first, *arguments)
63
+ end
64
+
65
+ def self.last(*arguments)
66
+ find(:last, *arguments)
67
+ end
68
+
69
+ def self.my(arguments={})
70
+ collect(connection.get("/my/listings", arguments))
71
+ end
72
+
73
+
74
+ private
75
+
76
+ def self.find_every(options)
77
+ collect(connection.get('/listings', options))
78
+ end
79
+
80
+ def self.find_one(options)
81
+ raise NotImplementedError # TODO
82
+ end
83
+
84
+ def self.find_single(scope, options)
85
+ resp = FlexmlsApi.client.get("/listings/#{scope}", options)
86
+ new(resp[0])
87
+ end
88
+
89
+
90
+ # TODO trim this down so we're only overriding the StandardFields access
91
+ def method_missing(method_symbol, *arguments)
92
+ method_name = method_symbol.to_s
93
+
94
+ if method_name =~ /(=|\?)$/
95
+ case $1
96
+ when "="
97
+ attributes[$`] = arguments.first
98
+ # TODO figure out a nice way to present setters for the standard fields
99
+ when "?"
100
+ attributes[$`]
101
+ end
102
+ else
103
+ return attributes[method_name] if attributes.include?(method_name)
104
+ return @attributes['StandardFields'][method_name] if attributes['StandardFields'].include?(method_name)
105
+ super # GTFO
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,33 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class MarketStatistics < Base
4
+ self.element_name="marketstatistics"
5
+
6
+ def self.absorption(parameters={})
7
+ self.stat('absorption',parameters)
8
+ end
9
+ def self.inventory(parameters={})
10
+ self.stat('inventory',parameters)
11
+ end
12
+ def self.price(parameters={})
13
+ self.stat('price',parameters)
14
+ end
15
+ def self.ratio(parameters={})
16
+ self.stat('ratio',parameters)
17
+ end
18
+ def self.dom(parameters={})
19
+ self.stat('dom',parameters)
20
+ end
21
+ def self.volume(parameters={})
22
+ self.stat('volume',parameters)
23
+ end
24
+
25
+ private
26
+ def self.stat(stat_name, parameters={})
27
+ resp = connection.get("#{path}/#{stat_name}", parameters)
28
+ new(resp[0])
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,15 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class Photo < Base
4
+ extend Subresource
5
+
6
+ self.element_name = "photos"
7
+
8
+ def primary?
9
+ @attributes["Primary"] == true
10
+ end
11
+
12
+
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class PropertyTypes < Base
4
+ self.element_name="propertytypes"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class StandardFields < Base
4
+ self.element_name="standardfields"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ module Subresource
4
+
5
+ def find_by_listing_key(key, user)
6
+ collect(connection.get("/listings/#{key}#{self.path}", :ApiUser => user))
7
+ end
8
+
9
+
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class SystemInfo < Base
4
+ self.element_name="system"
5
+ end
6
+ end
7
+ end