spark_api 1.4.29 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +1 -1
  3. data/VERSION +1 -1
  4. data/lib/spark_api/authentication/api_auth.rb +1 -1
  5. data/lib/spark_api/authentication/oauth2.rb +1 -1
  6. data/lib/spark_api/authentication/oauth2_impl/grant_type_base.rb +1 -1
  7. data/lib/spark_api/client.rb +2 -2
  8. data/lib/spark_api/models.rb +2 -0
  9. data/lib/spark_api/models/floplan.rb +24 -0
  10. data/lib/spark_api/models/listing.rb +11 -1
  11. data/lib/spark_api/models/media.rb +30 -0
  12. data/lib/spark_api/models/subresource.rb +2 -2
  13. data/lib/spark_api/models/video.rb +108 -0
  14. data/lib/spark_api/models/virtual_tour.rb +16 -0
  15. data/lib/spark_api/request.rb +2 -2
  16. data/script/reso_middleware_example.rb +70 -0
  17. data/spec/fixtures/listings/floplans_index.json +15 -0
  18. data/spec/spec_helper.rb +9 -4
  19. data/spec/unit/spark_api/authentication/api_auth_spec.rb +21 -22
  20. data/spec/unit/spark_api/authentication/base_auth_spec.rb +3 -3
  21. data/spec/unit/spark_api/authentication/oauth2_impl/faraday_middleware_spec.rb +1 -1
  22. data/spec/unit/spark_api/authentication/oauth2_impl/grant_type_base_spec.rb +1 -1
  23. data/spec/unit/spark_api/authentication/oauth2_impl/single_session_provider_spec.rb +2 -2
  24. data/spec/unit/spark_api/authentication/oauth2_spec.rb +40 -40
  25. data/spec/unit/spark_api/authentication_spec.rb +2 -2
  26. data/spec/unit/spark_api/configuration/yaml_spec.rb +44 -44
  27. data/spec/unit/spark_api/configuration_spec.rb +56 -57
  28. data/spec/unit/spark_api/faraday_middleware_spec.rb +12 -12
  29. data/spec/unit/spark_api/models/account_spec.rb +20 -20
  30. data/spec/unit/spark_api/models/activity_spec.rb +5 -5
  31. data/spec/unit/spark_api/models/base_spec.rb +32 -32
  32. data/spec/unit/spark_api/models/concerns/destroyable_spec.rb +2 -2
  33. data/spec/unit/spark_api/models/concerns/savable_spec.rb +19 -19
  34. data/spec/unit/spark_api/models/connect_prefs_spec.rb +1 -1
  35. data/spec/unit/spark_api/models/constraint_spec.rb +1 -1
  36. data/spec/unit/spark_api/models/contact_spec.rb +50 -50
  37. data/spec/unit/spark_api/models/dirty_spec.rb +12 -12
  38. data/spec/unit/spark_api/models/document_spec.rb +3 -3
  39. data/spec/unit/spark_api/models/fields_spec.rb +17 -17
  40. data/spec/unit/spark_api/models/finders_spec.rb +7 -7
  41. data/spec/unit/spark_api/models/floplan_spec.rb +24 -0
  42. data/spec/unit/spark_api/models/listing_cart_spec.rb +46 -46
  43. data/spec/unit/spark_api/models/listing_meta_translations_spec.rb +6 -6
  44. data/spec/unit/spark_api/models/listing_spec.rb +91 -91
  45. data/spec/unit/spark_api/models/message_spec.rb +10 -10
  46. data/spec/unit/spark_api/models/note_spec.rb +10 -10
  47. data/spec/unit/spark_api/models/notification_spec.rb +6 -6
  48. data/spec/unit/spark_api/models/open_house_spec.rb +4 -4
  49. data/spec/unit/spark_api/models/photo_spec.rb +8 -8
  50. data/spec/unit/spark_api/models/portal_spec.rb +4 -4
  51. data/spec/unit/spark_api/models/property_types_spec.rb +5 -5
  52. data/spec/unit/spark_api/models/rental_calendar_spec.rb +13 -11
  53. data/spec/unit/spark_api/models/rule_spec.rb +2 -2
  54. data/spec/unit/spark_api/models/saved_search_spec.rb +33 -33
  55. data/spec/unit/spark_api/models/search_template/quick_search_spec.rb +5 -5
  56. data/spec/unit/spark_api/models/shared_listing_spec.rb +12 -12
  57. data/spec/unit/spark_api/models/sort_spec.rb +3 -3
  58. data/spec/unit/spark_api/models/standard_fields_spec.rb +12 -12
  59. data/spec/unit/spark_api/models/subresource_spec.rb +33 -15
  60. data/spec/unit/spark_api/models/system_info_spec.rb +7 -7
  61. data/spec/unit/spark_api/models/tour_of_home_spec.rb +3 -3
  62. data/spec/unit/spark_api/models/video_spec.rb +9 -9
  63. data/spec/unit/spark_api/models/virtual_tour_spec.rb +7 -7
  64. data/spec/unit/spark_api/models/vow_account_spec.rb +8 -8
  65. data/spec/unit/spark_api/multi_client_spec.rb +14 -14
  66. data/spec/unit/spark_api/options_hash_spec.rb +4 -4
  67. data/spec/unit/spark_api/paginate_spec.rb +71 -71
  68. data/spec/unit/spark_api/primary_array_spec.rb +5 -5
  69. data/spec/unit/spark_api/request_spec.rb +65 -59
  70. data/spec/unit/spark_api_spec.rb +6 -6
  71. metadata +184 -248
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: fe8ad347473ad647fb0da6fd681ebe2c6da50daa
4
- data.tar.gz: 7d17ef58323eb5aad73e09167478d0456a7f1df9
2
+ SHA256:
3
+ metadata.gz: b9c9695944207ba64adf36cd6417e6f534222643a04058581af5451ef4e0361e
4
+ data.tar.gz: 4911595b9144031b4659ef04bbd1ca8689090e23297d686741c26649c359b0ed
5
5
  SHA512:
6
- metadata.gz: 0c2df47ef7d780b071df24b586c35838ef1537b68fbd4aac78d8adc99786c2b7de3e81f5d322e781bd0d06defd27fd4bb9f2058788d169d6e48184e346b629ef
7
- data.tar.gz: a23bbccaab494a0007fc75ba5f7317473bc66bfaa268e221b008ad9eef1accbea77008fdc2ba246ebe5508a4f328164c27876f6404bfa3bf1bbfb049e3bed26e
6
+ metadata.gz: d1e45e5ebb03f2561b2d1a8234510598ced6b3816f961dd84e2200f66359bd22a6360b4bdba06bfcaad46e2a5bbac5c22cb03611dc0b4b249ea001ac5022964b
7
+ data.tar.gz: 5259a965c9131db98ef5c9ca876f59189a43274fa30c780c98b1d56b1d762fd974f9a618a407e0daf0d16c9f1e0b690ffd0a500dfe4f225c9eadc2b81091ec6e
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  Spark API
2
2
  =====================
3
- [![Build Status](https://travis-ci.org/sparkapi/spark_api.png?branch=master)](http://travis-ci.org/sparkapi/spark_api) [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/sparkapi/spark_api)
3
+ ![CI](https://github.com/sparkapi/spark_api/workflows/CI/badge.svg) ![Code Climate](https://codeclimate.com/badge.png)
4
4
 
5
5
  A Ruby wrapper for the Spark REST API. Loosely based on ActiveResource to provide models to interact with remote services.
6
6
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.4.29
1
+ 1.5.1
@@ -62,7 +62,7 @@ module SparkApi
62
62
 
63
63
  # Perform an HTTP request (no data)
64
64
  def request(method, path, body, options)
65
- escaped_path = URI.escape(path)
65
+ escaped_path = Addressable::URI.escape(path)
66
66
  request_opts = {
67
67
  :AuthToken => @session.auth_token
68
68
  }
@@ -40,7 +40,7 @@ module SparkApi
40
40
 
41
41
  # Perform an HTTP request (no data)
42
42
  def request(method, path, body, options={})
43
- escaped_path = URI.escape(path)
43
+ escaped_path = Addressable::URI.escape(path)
44
44
  connection = @client.connection(true) # SSL Only!
45
45
  connection.headers.merge!(self.auth_header)
46
46
 
@@ -45,7 +45,7 @@ module SparkApi
45
45
  response.expires_in = provider.session_timeout if response.expires_in.nil?
46
46
  SparkApi.logger.debug { "[oauth2] New session created #{response}" }
47
47
  response
48
- rescue Faraday::Error::ConnectionFailed => e
48
+ rescue Faraday::ConnectionFailed => e
49
49
  if @client.ssl_verify && e.message =~ /certificate verify failed/
50
50
  SparkApi.logger.error { SparkApi::Errors.ssl_verification_error }
51
51
  end
@@ -2,7 +2,7 @@ module SparkApi
2
2
  # =API Client
3
3
  # Main class to setup and run requests on the API. A default client is accessible globally as
4
4
  # SparkApi::client if the global configuration has been set as well. Otherwise, this class may
5
- # be instanciated separately with the configuration information.
5
+ # be instantiated separately with the configuration information.
6
6
  class Client
7
7
  include Connection
8
8
  include Authentication
@@ -21,7 +21,7 @@ module SparkApi
21
21
  Configuration::VALID_OPTION_KEYS.each do |key|
22
22
  send("#{key}=", options[key])
23
23
  end
24
- # Instanciate the authenication class passed in.
24
+ # Instantiate the authentication class passed in.
25
25
  @authenticator = authentication_mode.send("new", self)
26
26
  end
27
27
 
@@ -20,10 +20,12 @@ require 'spark_api/models/fields'
20
20
  require 'spark_api/models/idx'
21
21
  require 'spark_api/models/idx_link'
22
22
  require 'spark_api/models/incomplete_listing'
23
+ require 'spark_api/models/floplan'
23
24
  require 'spark_api/models/listing'
24
25
  require 'spark_api/models/listing_cart'
25
26
  require 'spark_api/models/listing_meta_translations'
26
27
  require 'spark_api/models/market_statistics'
28
+ require 'spark_api/models/media'
27
29
  require 'spark_api/models/message'
28
30
  require 'spark_api/models/news_feed_meta'
29
31
  require 'spark_api/models/newsfeed'
@@ -0,0 +1,24 @@
1
+ module SparkApi
2
+ module Models
3
+ class FloPlan < Base
4
+ extend Subresource
5
+ self.element_name = 'floplans'
6
+
7
+ attr_accessor :images, :thumbnails
8
+
9
+ def initialize(attributes={})
10
+ @images = []
11
+ @thumbnails = []
12
+
13
+ attributes['Images'].each do |img|
14
+ if img["Type"].include?('thumbnail')
15
+ @thumbnails << img
16
+ else
17
+ @images << img
18
+ end
19
+ end
20
+ super(attributes)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -2,7 +2,7 @@ module SparkApi
2
2
  module Models
3
3
  class Listing < Base
4
4
  extend Finders
5
- attr_accessor :photos, :videos, :virtual_tours, :documents, :open_houses, :tour_of_homes, :rental_calendars
5
+ attr_accessor :photos, :videos, :virtual_tours, :documents, :open_houses, :tour_of_homes, :rental_calendars, :floplans
6
6
  attr_accessor :constraints
7
7
  self.element_name="listings"
8
8
  DATA_MASK = "********"
@@ -17,6 +17,7 @@ module SparkApi
17
17
  @constraints = []
18
18
  @tour_of_homes = []
19
19
  @open_houses = []
20
+ @floplans = []
20
21
 
21
22
  if attributes.has_key?('StandardFields')
22
23
  pics, vids, tours, docs, ohouses, tourhomes = attributes['StandardFields'].values_at('Photos','Videos', 'VirtualTours', 'Documents', 'OpenHouses', 'TourOfHomes')
@@ -26,6 +27,10 @@ module SparkApi
26
27
  rentalcalendars = attributes['RentalCalendar']
27
28
  end
28
29
 
30
+ if attributes.has_key?('FloPlans')
31
+ floplans = attributes['FloPlans']
32
+ end
33
+
29
34
  if pics != nil
30
35
  setup_attribute(@photos, pics, Photo)
31
36
  attributes['StandardFields'].delete('Photos')
@@ -61,6 +66,11 @@ module SparkApi
61
66
  attributes.delete('RentalCalendar')
62
67
  end
63
68
 
69
+ if floplans != nil
70
+ setup_attribute(@floplans, floplans, FloPlan)
71
+ attributes.delete('FloPlans')
72
+ end
73
+
64
74
  super(attributes)
65
75
  end
66
76
 
@@ -0,0 +1,30 @@
1
+ module SparkApi
2
+ module Models
3
+ module Media
4
+ # This module is effectively an interface and helper to combine common media
5
+ # actions and information. Media types (videos, virtual tours, etc)
6
+ # should include this module and implement the methods contained
7
+
8
+ def url
9
+ raise "Not Implemented"
10
+ end
11
+
12
+ def description
13
+ raise "Not Implemented"
14
+ end
15
+
16
+ def private?
17
+ attributes['Privacy'] == 'Private'
18
+ end
19
+
20
+ def public?
21
+ attributes['Privacy'] == 'Public'
22
+ end
23
+
24
+ def automatic?
25
+ attributes['Privacy'] == 'Automatic'
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -16,10 +16,10 @@ module SparkApi
16
16
 
17
17
  def parse_date_start_and_end_times(attributes)
18
18
  # Transform the date strings
19
- unless attributes['Date'].nil?
19
+ unless attributes['Date'].nil? || attributes['Date'].empty?
20
20
  date = Date.strptime attributes['Date'], '%m/%d/%Y'
21
21
  ['StartTime','EndTime'].each do |time|
22
- next if attributes[time].nil?
22
+ next if attributes[time].nil? || attributes[time].empty?
23
23
  formatted_date = "#{attributes['Date']}T#{attributes[time]}"
24
24
  datetime = nil
25
25
 
@@ -1,7 +1,12 @@
1
+ require 'net/http'
1
2
  module SparkApi
2
3
  module Models
3
4
  class Video < Base
4
5
  extend Subresource
6
+ include Media
7
+ include Concerns::Savable,
8
+ Concerns::Destroyable
9
+
5
10
  self.element_name = 'videos'
6
11
 
7
12
  def branded?
@@ -11,6 +16,109 @@ module SparkApi
11
16
  def unbranded?
12
17
  attributes['Type'] == 'unbranded'
13
18
  end
19
+
20
+ def url
21
+ attributes['ObjectHtml']
22
+ end
23
+
24
+ def description
25
+ attributes['Name']
26
+ end
27
+
28
+ # Some youtube URLS are youtu.be instead of youtube
29
+ SUPPORTED_VIDEO_TYPES = %w[vimeo youtu].freeze
30
+
31
+ def is_supported_type?
32
+ # Unfortunately there are so many formats of vimeo videos that we canot support all vimeo videos
33
+ # Therefore, we need to do a little more checking here and validate that we can get video codes out of the urls
34
+ (self.ObjectHtml.include?('youtu') && youtube_video_code.present?) || (self.ObjectHtml.include?('vimeo') && vimeo_video_code.present?)
35
+ end
36
+
37
+ def is_valid_iframe?
38
+ self.ObjectHtml.include?('<iframe') && self.ObjectHtml.include?('</iframe>')
39
+ end
40
+
41
+ # gets the thumbnail to be shown on supported (Vimeo and Youtube) videos
42
+ # YouTube provides a predictable url for each video's images
43
+ # for Vimeo, a get request is necessary
44
+ def display_image
45
+ url = self.video_link
46
+ if url
47
+ if(url.include?('youtube'))
48
+ youtube_thumbnail_url
49
+ else
50
+ vimeo_thumbnail_url
51
+ end
52
+ end
53
+ end
54
+
55
+ def video_link
56
+ return nil unless is_supported_type?
57
+
58
+ if self.ObjectHtml.include?('youtu')
59
+ youtube_link
60
+ elsif self.ObjectHtml.include?('vimeo')
61
+ vimeo_link
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def vimeo_video_code
68
+ html = self.ObjectHtml
69
+ if html.match(/(src=)('|")((https:)?\/\/player\.vimeo\.com\/video\/)/)
70
+ new_url = html.split(/(src=')|(src=")/)
71
+ if new_url[2]
72
+ html = new_url[2].split(/("|')/)[0]
73
+ end
74
+ end
75
+ if html.match(/(?:.+?)?(player\.vimeo\.com|vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|)(\d+)(?:$|\/|\?))/)
76
+ code = html.split('/').last.split('?').first
77
+ # Vimeo Ids are always numerical
78
+ code.to_i.to_s === code ? code : nil
79
+ else
80
+ nil
81
+ end
82
+ end
83
+
84
+ # This if correctly embedded by the user is an embed
85
+ # If not, it could be pretty much anything
86
+ def youtube_video_code
87
+ html = self.ObjectHtml
88
+ if html.match(/(?:.+?)?(?:\/v\/|watch\/|\?v=|\&v=|youtu\.be\/|\/v=|^youtu\.be\/|embed\/|watch\%3Fv\%3D)([a-zA-Z0-9_-]{11})/) || html.match(/(iframe)(.*)(src=)('|")(https:\/\/www\.youtube\.com\/embed)/)
89
+ html.split(/([a-zA-Z0-9_-]{11})/)[1]
90
+ else
91
+ nil
92
+ end
93
+ end
94
+
95
+ def youtube_link
96
+ normalize_youtube_url
97
+ code = youtube_video_code
98
+ code ? "https://www.youtube.com/watch?v=#{code}" : nil
99
+ end
100
+
101
+ def vimeo_link
102
+ code = vimeo_video_code
103
+ code ? "https://vimeo.com/#{code}" : nil
104
+ end
105
+
106
+ def youtube_thumbnail_url
107
+ code = youtube_video_code
108
+ code ? "https://i1.ytimg.com/vi/#{code}/hqdefault.jpg" : nil
109
+ end
110
+
111
+ def vimeo_thumbnail_url
112
+ # due to the rate limiting issue that surfaced shortly before launch,
113
+ # we will temporarily not return vimeo thumbnails until
114
+ # there is bandwidth to implement the solution in FLEX-9959
115
+ return nil
116
+ end
117
+
118
+ def normalize_youtube_url
119
+ self.ObjectHtml.sub!('-nocookie', '')
120
+ end
121
+
14
122
  end
15
123
  end
16
124
  end
@@ -2,6 +2,10 @@ module SparkApi
2
2
  module Models
3
3
  class VirtualTour < Base
4
4
  extend Subresource
5
+ include Media
6
+ include Concerns::Savable,
7
+ Concerns::Destroyable
8
+
5
9
  self.element_name="virtualtours"
6
10
 
7
11
 
@@ -13,6 +17,18 @@ module SparkApi
13
17
  attributes["Type"] == "unbranded"
14
18
  end
15
19
 
20
+ def url
21
+ attributes['Uri']
22
+ end
23
+
24
+ def description
25
+ attributes['Name']
26
+ end
27
+
28
+ def display_image
29
+ # Currently we have no universally good mechanism to get images for virtual tours
30
+ return nil
31
+ end
16
32
  end
17
33
  end
18
34
  end
@@ -99,7 +99,7 @@ module SparkApi
99
99
  else
100
100
  return response.body
101
101
  end
102
- rescue Faraday::Error::ConnectionFailed => e
102
+ rescue Faraday::ConnectionFailed => e
103
103
  if self.ssl_verify && e.message =~ /certificate verify failed/
104
104
  SparkApi.logger.error { SparkApi::Errors.ssl_verification_error }
105
105
  end
@@ -107,7 +107,7 @@ module SparkApi
107
107
  end
108
108
 
109
109
  def process_request_body(body)
110
- if body.is_a?(Hash)
110
+ if body.is_a?(Hash) || body.is_a?(Array)
111
111
  body.empty? ? nil : {"D" => body }.to_json
112
112
  else
113
113
  body
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script demonstrates how to use the RESO Web API with the Ruby client by pulling listings
4
+ # and replacing encoded field values with their corresponding human-readable values, which are
5
+ # pulled from the XML returned by the RESO metadata endpoint.
6
+
7
+ require "spark_api"
8
+ require "nokogiri"
9
+
10
+ # set up session and RESO Web API middleware
11
+ SparkApi.configure do |config|
12
+ config.authentication_mode = SparkApi::Authentication::OAuth2
13
+ config.middleware = :reso_api
14
+ end
15
+
16
+ SparkApi.client.session = SparkApi::Authentication::OAuthSession.new({ :access_token => "OAUTH2_ACCESS_TOKEN" })
17
+
18
+ # pull metadata from RESO Web API
19
+ metadata_res = (SparkApi.client.get("/$metadata", {:$ApiUser => "FLEXMLS_TECH_ID"}) )
20
+ metadata_xml = Nokogiri::XML(metadata_res).remove_namespaces!
21
+
22
+ # make an array of fields which need to be checked for readable values
23
+ fields_to_lookup = []
24
+ metadata_xml.xpath('//Schema/EnumType/@Name').each do |el|
25
+ fields_to_lookup << el.to_str
26
+ end
27
+
28
+ # get 25 listings
29
+ listings = (SparkApi.client.get("/Property", {:$top => 25, :$ApiUser => "FLEXMLS_TECH_ID"} ))
30
+
31
+ listings['value'].each do |listing| # for each listing,
32
+ fields_to_lookup.each do |field| # go through the array of fields to be checked.
33
+ if !!listing[field] # when one of the fields that needs to be checked exists in a listing,
34
+ if listing[field].is_a? String
35
+ readable = metadata_xml.xpath( # check for readable value to be swapped in
36
+ "//Schema/
37
+ EnumType[@Name=\"#{field}\"]/
38
+ Member[@Name=\"#{listing[field]}\"]/
39
+ Annotation"
40
+ ).attr("String")
41
+
42
+ # if there is a readable value, swap it in
43
+ if !!readable
44
+ listing[field] = readable.to_str
45
+ end
46
+
47
+ elsif listing[field].is_a? Array
48
+ readable_arr = []
49
+ listing[field].each do |el|
50
+ readable = metadata_xml.xpath( # check for readable value to be swapped in
51
+ "//Schema/
52
+ EnumType[@Name=\"#{field}\"]/
53
+ Member[@Name=\"#{el}\"]/
54
+ Annotation"
55
+ ).attr("String")
56
+
57
+ # assemble a new array with readable values and swap it in
58
+ if !!readable
59
+ readable_arr << readable.to_str
60
+ else
61
+ readable_arr << el
62
+ end
63
+ listing[field] = readable_arr
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ puts listings
@@ -0,0 +1,15 @@
1
+ {
2
+ "D":{
3
+ "Results":[{
4
+ "Id": 4300,
5
+ "Name": "Feb 13",
6
+ "Images": [
7
+ {"Uri": "https://foo.bar",
8
+ "Type":"all_in_one_png"},
9
+ {"Uri": "https://foo.bar",
10
+ "Type": "all_in_one_thumbnail_png"}
11
+ ],
12
+ "Success":true
13
+ }]
14
+ }
15
+ }