flexmls_api 0.3.2

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