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,16 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class Video < Base
4
+ extend Subresource
5
+ self.element_name = 'videos'
6
+
7
+ def branded?
8
+ attributes['Type'] == 'branded'
9
+ end
10
+
11
+ def unbranded?
12
+ attributes['Type'] == 'unbranded'
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class VirtualTour < Base
4
+ extend Subresource
5
+ self.element_name="virtualtours"
6
+
7
+
8
+ def branded?
9
+ attributes["Type"] == "branded"
10
+ end
11
+
12
+ def unbranded?
13
+ attributes["Type"] == "unbranded"
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ require File.expand_path('../models/base', __FILE__)
2
+ require File.expand_path('../models/listing', __FILE__)
3
+ require File.expand_path('../models/subresource', __FILE__)
4
+ require File.expand_path('../models/photo', __FILE__)
5
+ require File.expand_path('../models/system_info', __FILE__)
6
+ require File.expand_path('../models/standard_fields', __FILE__)
7
+ require File.expand_path('../models/custom_fields', __FILE__)
8
+ require File.expand_path('../models/property_types', __FILE__)
9
+ require File.expand_path('../models/connect_prefs', __FILE__)
10
+ require File.expand_path('../models/contact', __FILE__)
11
+ require File.expand_path('../models/idx_link', __FILE__)
12
+ require File.expand_path('../models/market_statistics', __FILE__)
13
+ require File.expand_path('../models/video', __FILE__)
14
+ require File.expand_path('../models/virtual_tour', __FILE__)
15
+ require File.expand_path('../models/document', __FILE__)
16
+
17
+ module FlexmlsApi
18
+ module Models
19
+
20
+ end
21
+ end
@@ -0,0 +1,87 @@
1
+ require 'will_paginate/collection'
2
+
3
+ # =Pagination for api resource collections
4
+ # Will paginate adapter for the api client. Utilizes the same interface as will paginate and returns the
5
+ # same WillPaginate::Collection for finder results.
6
+ module FlexmlsApi
7
+ module Paginate
8
+
9
+ DEFAULT_PAGE_SIZE = 25
10
+
11
+ # == Replacement hook for will_paginate's class method
12
+ # Does a best effort to mimic the will_paginate method of same name. All arguments are
13
+ # passed on to the finder method except the special keys for the options hash listed below.
14
+ #
15
+ # == Special parameters for paginating finders
16
+ # * <tt>:page</tt> -- REQUIRED, but defaults to 1 if false or nil
17
+ # * <tt>:per_page</tt> -- defaults to <tt>CurrentModel.per_page</tt> (which is 25 if not overridden)
18
+ # * <tt>:finder</tt> -- name of the finder used (default: "get"). This needs to be a class finder method on the class
19
+ def paginate(*args)
20
+ options = args.last.is_a?(::Hash) ? args.pop : {}
21
+ page = options.delete(:page) || 1
22
+ items_per_page = options.delete(:per_page) || self.per_page
23
+ finder = (options.delete(:finder) || 'get').to_s
24
+ page_options = {
25
+ "_pagination" => 1,
26
+ "_limit" => items_per_page,
27
+ "_page" => page
28
+ }
29
+ options.merge!(page_options)
30
+ args << options
31
+ collection = send(finder,*args)
32
+ end
33
+
34
+ # == Instanciate class instances from array of hash representations.
35
+ # Needs to be called by all finders that would like to support paging. Takes the hash result
36
+ # set from the request layer and instanciates instances of the class called for the finder.
37
+ #
38
+ # * result_array -- the results object returned from the api request layer. An array of hashes.
39
+ #
40
+ # :returns:
41
+ # An array of class instances for the Class of the calling finder
42
+ def collect(result_array)
43
+ collection = result_array.collect { |item| new(item)}
44
+ result_array.replace(collection)
45
+ result_array
46
+ end
47
+
48
+ # Default per_page limit set on all models. Override this method in the model such ala the
49
+ # will_paginate gem to change
50
+ def per_page
51
+ DEFAULT_PAGE_SIZE
52
+ end
53
+ end
54
+
55
+ # ==Paginate Api Responses
56
+ # Module use by the request layer to decorate the response's results array with paging support.
57
+ # Pagination only happens if the response includes the pagination information as specified by the
58
+ # API.
59
+ module PaginateResponse
60
+ # ==Enable pagination
61
+ # * results -- array of hashes representing api resources
62
+ # * paging_hash -- the pagination response information from the api representing paging state.
63
+ #
64
+ # :returns:
65
+ # The result set decorated as a WillPaginate::Collection
66
+ def paginate_response(results, paging_hash)
67
+ pager = Pagination.new(paging_hash)
68
+ paged_results = WillPaginate::Collection.create(pager.current_page, pager.page_size, pager.total_rows) do |p|
69
+ p.replace(results)
70
+ end
71
+ paged_results
72
+ end
73
+ end
74
+
75
+ # ==Pagination
76
+ # Simple class representing the API's pagination response object
77
+ class Pagination
78
+ attr_accessor :total_rows, :page_size, :total_pages, :current_page
79
+ def initialize(hash)
80
+ @total_rows = hash["TotalRows"]
81
+ @page_size = hash["PageSize"]
82
+ @total_pages = hash["TotalPages"]
83
+ @current_page = hash["CurrentPage"]
84
+ end
85
+ end
86
+
87
+ end
@@ -0,0 +1,172 @@
1
+
2
+ module FlexmlsApi
3
+ # HTTP request wrapper. Performs all the api session mumbo jumbo so that the models don't have to.
4
+ module Request
5
+ include PaginateResponse
6
+ # Perform an HTTP GET request
7
+ #
8
+ # * path - Path of an api resource, excluding version and endpoint (domain) information
9
+ # * options - Resource request options as specified being supported via and api resource
10
+ # :returns:
11
+ # Hash of the json results as documented in the api.
12
+ # :raises:
13
+ # FlexmlsApi::ClientError or subclass if the request failed.
14
+ def get(path, options={})
15
+ request(:get, path, nil, options)
16
+ end
17
+
18
+ # Perform an HTTP POST request
19
+ #
20
+ # * path - Path of an api resource, excluding version and endpoint (domain) information
21
+ # * body - Hash for post body data
22
+ # * options - Resource request options as specified being supported via and api resource
23
+ # :returns:
24
+ # Hash of the json results as documented in the api.
25
+ # :raises:
26
+ # FlexmlsApi::ClientError or subclass if the request failed.
27
+ def post(path, body={}, options={})
28
+ request(:post, path, body, options)
29
+ end
30
+
31
+ # Perform an HTTP PUT request
32
+ #
33
+ # * path - Path of an api resource, excluding version and endpoint (domain) information
34
+ # * body - Hash for post body data
35
+ # * options - Resource request options as specified being supported via and api resource
36
+ # :returns:
37
+ # Hash of the json results as documented in the api.
38
+ # :raises:
39
+ # FlexmlsApi::ClientError or subclass if the request failed.
40
+ def put(path, body={}, options={})
41
+ request(:put, path, body, options)
42
+ end
43
+
44
+ # Perform an HTTP DELETE request
45
+ #
46
+ # * path - Path of an api resource, excluding version and endpoint (domain) information
47
+ # * options - Resource request options as specified being supported via and api resource
48
+ # :returns:
49
+ # Hash of the json results as documented in the api.
50
+ # :raises:
51
+ # FlexmlsApi::ClientError or subclass if the request failed.
52
+ def delete(path, options={})
53
+ request(:delete, path, nil, options)
54
+ end
55
+
56
+ private
57
+
58
+ # Perform an HTTP request (no data)
59
+ def request(method, path, body, options)
60
+ if @session.nil? || @session.expired?
61
+ authenticate
62
+ end
63
+ attempts = 0
64
+ begin
65
+ request_opts = {
66
+ "AuthToken" => @session.auth_token
67
+ }
68
+ request_opts.merge!(options)
69
+ post_data = body.nil? ? nil : {"D" => body }.to_json
70
+ sig = sign_token(path, request_opts, post_data)
71
+ request_path = "/#{version}#{path}?ApiSig=#{sig}#{build_url_parameters(request_opts)}"
72
+ FlexmlsApi.logger.debug("Request: #{request_path}")
73
+ start_time = Time.now
74
+ if post_data.nil?
75
+ response = connection.send(method, request_path)
76
+ else
77
+ FlexmlsApi.logger.debug("Data: #{post_data}")
78
+ response = connection.send(method, request_path, post_data)
79
+ end
80
+ request_time = Time.now - start_time
81
+ FlexmlsApi.logger.info("[#{request_time}s] Api: #{method.to_s.upcase} #{request_path}")
82
+ rescue PermissionDenied => e
83
+ if(ResponseCodes::SESSION_TOKEN_EXPIRED == e.code)
84
+ unless (attempts +=1) > 1
85
+ FlexmlsApi.logger.debug("Retrying authentication")
86
+ authenticate
87
+ retry
88
+ end
89
+ end
90
+ # No luck authenticating... KABOOM!
91
+ FlexmlsApi.logger.error("Authentication failed or server is sending us expired tokens, nothing we can do here.")
92
+ raise
93
+ end
94
+ results = response.body.results
95
+ paging = response.body.pagination
96
+ unless paging.nil?
97
+ results = paginate_response(results, paging)
98
+ end
99
+ results
100
+ end
101
+
102
+ # Format a hash as request parameters
103
+ #
104
+ # :returns:
105
+ # Stringized form of the parameters as needed for the http request
106
+ def build_url_parameters(parameters={})
107
+ str = ""
108
+ parameters.map do |key,value|
109
+ str << "&#{key}=#{value}"
110
+ end
111
+ str
112
+ end
113
+ end
114
+
115
+ # All known response codes listed in the API
116
+ module ResponseCodes
117
+ NOT_FOUND = 404
118
+ METHOD_NOT_ALLOWED = 405
119
+ INVALID_KEY = 1000
120
+ DISABLED_KEY = 1010
121
+ API_USER_REQUIRED = 1015
122
+ SESSION_TOKEN_EXPIRED = 1020
123
+ SSL_REQUIRED = 1030
124
+ INVALID_JSON = 1035
125
+ INVALID_FIELD = 1040
126
+ MISSING_PARAMETER = 1050
127
+ INVALID_PARAMETER = 1053
128
+ CONFLICTING_DATA = 1055
129
+ NOT_AVAILABLE= 1500
130
+ RATE_LIMIT_EXCEEDED = 1550
131
+ end
132
+
133
+ # Errors built from API responses
134
+ class InvalidResponse < StandardError; end
135
+ class ClientError < StandardError
136
+ attr_reader :code, :status
137
+ def initialize (code, status)
138
+ @code = code
139
+ @status = status
140
+ end
141
+ end
142
+ class NotFound < ClientError; end
143
+ class PermissionDenied < ClientError; end
144
+ class NotAllowed < ClientError; end
145
+ class BadResourceRequest < ClientError; end
146
+
147
+
148
+ # Nice and handy class wrapper for the api response hash
149
+ class ApiResponse
150
+ attr_accessor :code, :message, :results, :success, :pagination
151
+ def initialize(d)
152
+ begin
153
+ hash = d["D"]
154
+ if hash.nil? || hash.empty?
155
+ raise InvalidResponse, "The server response could not be understood"
156
+ end
157
+ self.message = hash["Message"]
158
+ self.code = hash["Code"]
159
+ self.results = hash["Results"]
160
+ self.success = hash["Success"]
161
+ self.pagination = hash["Pagination"]
162
+ rescue Exception => e
163
+ FlexmlsApi.logger.error "Unable to understand the response! #{d}"
164
+ raise
165
+ end
166
+ end
167
+ def success?
168
+ @success
169
+ end
170
+ end
171
+
172
+ end
@@ -0,0 +1,4 @@
1
+ # Gem version information
2
+ module FlexmlsApi
3
+ VERSION = File.read(File.dirname(__FILE__) + "/../../VERSION").chomp
4
+ end
@@ -0,0 +1,41 @@
1
+ # Flexmlsapi
2
+ require 'rubygems'
3
+ require 'curb'
4
+ require 'json'
5
+ require 'logger'
6
+
7
+ require File.expand_path('../flexmls_api/version', __FILE__)
8
+ require File.expand_path('../flexmls_api/configuration', __FILE__)
9
+ require File.expand_path('../flexmls_api/authentication', __FILE__)
10
+ require File.expand_path('../flexmls_api/paginate', __FILE__)
11
+ require File.expand_path('../flexmls_api/request', __FILE__)
12
+ require File.expand_path('../flexmls_api/client', __FILE__)
13
+ require File.expand_path('../flexmls_api/faraday', __FILE__)
14
+ require File.expand_path('../flexmls_api/models', __FILE__)
15
+
16
+ module FlexmlsApi
17
+ extend Configuration
18
+
19
+ def self.logger
20
+ if @logger.nil?
21
+ @logger = Logger.new(STDOUT)
22
+ @logger.level = Logger::INFO
23
+ end
24
+ @logger
25
+ end
26
+
27
+ def self.client(opts={})
28
+ Thread.current[:flexmls_api_client] ||= FlexmlsApi::Client.new(opts)
29
+ end
30
+
31
+ def self.method_missing(method, *args, &block)
32
+ return super unless (client.respond_to?(method))
33
+ client.send(method, *args, &block)
34
+ end
35
+
36
+ def self.reset
37
+ reset_configuration
38
+ Thread.current[:flexmls_api_client] = nil
39
+ end
40
+
41
+ end
@@ -0,0 +1,25 @@
1
+ {
2
+ "D": {
3
+ "Success": true,
4
+ "Results": [
5
+ {
6
+ "ResourceUri":"/v1/contacts/20101230223226074201000000",
7
+ "DisplayName":"Contact One",
8
+ "Id":"20101230223226074201000000",
9
+ "PrimaryEmail":"contact1@fbsdata.com"
10
+ },
11
+ {
12
+ "ResourceUri":"/v1/contacts/20101230223226074202000000",
13
+ "DisplayName":"Contact Two",
14
+ "Id":"20101230223226074202000000",
15
+ "PrimaryEmail":"contact2fbsdata.com"
16
+ },
17
+ {
18
+ "ResourceUri":"/v1/contacts/20101230223226074203000000",
19
+ "DisplayName":"Contact Three",
20
+ "Id":"20101230223226074203000000",
21
+ "PrimaryEmail":"contact3@fbsdata.com"
22
+ }
23
+ ]
24
+ }
25
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "D": {
3
+ "Results": [
4
+ {
5
+ "Uri": "http://images.dev.fbsdata.com/documents/cda/20060725224801143085000000.pdf",
6
+ "ResourceUri": "/v1/listings/20060725224713296297000000/documents/20060725224801143085000000",
7
+ "Name": "Disclosure",
8
+ "Id": "20060725224801143085000000"
9
+ },
10
+ {
11
+ "Uri": "http://images.dev.fbsdata.com/documents/cda/20060725224818080340000000.pdf",
12
+ "ResourceUri": "/v1/listings/20060725224713296297000000/documents/20060725224818080340000000",
13
+ "Name": "Plat Map",
14
+ "Id": "20060725224818080340000000"
15
+ }
16
+ ],
17
+ "Success": true
18
+ }
19
+ }
@@ -0,0 +1,38 @@
1
+ {
2
+ "D": {
3
+ "Results": [
4
+ {
5
+ "ResourceUri": "/v1/listings/20060725224713296297000000",
6
+ "StandardFields": {
7
+ "StreetNumber": "7298",
8
+ "Longitude": "-116.3237",
9
+ "City": "Bonners Ferry",
10
+ "ListingId": "06-9395",
11
+ "PublicRemarks": "Afforadable home in town close to hospital,yet quiet country like setting. Good views. Must see",
12
+ "BuildingAreaTotal": "924.0",
13
+ "YearBuilt": 1977,
14
+ "StreetName": "BIRCH",
15
+ "ListPrice": "50000.0",
16
+ "PostalCode": "83805",
17
+ "Latitude": "48.7001",
18
+ "BathsThreeQuarter": null,
19
+ "BathsFull": null,
20
+ "BathsTotal": "1.0",
21
+ "StateOrProvince": "ID",
22
+ "PropertyType": "A",
23
+ "StreetAdditionalInfo": null,
24
+ "StreetDirPrefix": null,
25
+ "BedsTotal": 3,
26
+ "StreetDirSuffix": null,
27
+ "ListingKey": "20060725224713296297000000",
28
+ "ListOfficeName": "Century 21 On The Lake",
29
+ "BathsHalf": null,
30
+ "ModificationTimestamp": "2010-11-22T20:47:21Z",
31
+ "CountyOrParish": "Boundary"
32
+ },
33
+ "Id": "20060725224713296297000000"
34
+ }
35
+ ],
36
+ "Success": true
37
+ }
38
+ }