flexmls_api 0.4.5 → 0.6.4

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 (93) hide show
  1. data/Gemfile +2 -17
  2. data/Gemfile.lock +35 -27
  3. data/README.md +23 -1
  4. data/Rakefile +18 -5
  5. data/VERSION +1 -1
  6. data/bin/flexmls_api +8 -0
  7. data/lib/flexmls_api.rb +2 -0
  8. data/lib/flexmls_api/authentication.rb +5 -6
  9. data/lib/flexmls_api/authentication/api_auth.rb +4 -2
  10. data/lib/flexmls_api/authentication/oauth2.rb +51 -99
  11. data/lib/flexmls_api/authentication/oauth2_impl/grant_type_base.rb +85 -0
  12. data/lib/flexmls_api/authentication/oauth2_impl/grant_type_code.rb +48 -0
  13. data/lib/flexmls_api/authentication/oauth2_impl/grant_type_password.rb +45 -0
  14. data/lib/flexmls_api/authentication/oauth2_impl/grant_type_refresh.rb +36 -0
  15. data/lib/flexmls_api/authentication/oauth2_impl/middleware.rb +39 -0
  16. data/lib/flexmls_api/cli.rb +132 -0
  17. data/lib/flexmls_api/cli/api_auth.rb +8 -0
  18. data/lib/flexmls_api/cli/oauth2.rb +43 -0
  19. data/lib/flexmls_api/cli/setup.rb +44 -0
  20. data/lib/flexmls_api/configuration.rb +6 -6
  21. data/lib/flexmls_api/faraday.rb +11 -21
  22. data/lib/flexmls_api/models.rb +3 -0
  23. data/lib/flexmls_api/models/account.rb +48 -5
  24. data/lib/flexmls_api/models/base.rb +27 -2
  25. data/lib/flexmls_api/models/contact.rb +28 -9
  26. data/lib/flexmls_api/models/listing_cart.rb +72 -0
  27. data/lib/flexmls_api/models/note.rb +0 -2
  28. data/lib/flexmls_api/models/saved_search.rb +16 -0
  29. data/lib/flexmls_api/models/shared_listing.rb +35 -0
  30. data/lib/flexmls_api/multi_client.rb +37 -0
  31. data/lib/flexmls_api/paginate.rb +5 -0
  32. data/lib/flexmls_api/request.rb +7 -3
  33. data/script/console +6 -0
  34. data/script/example.rb +27 -0
  35. data/spec/fixtures/accounts/all.json +160 -0
  36. data/spec/fixtures/accounts/my.json +74 -0
  37. data/spec/fixtures/accounts/my_portal.json +20 -0
  38. data/spec/fixtures/accounts/my_put.json +5 -0
  39. data/spec/fixtures/accounts/my_save.json +5 -0
  40. data/spec/fixtures/accounts/office.json +142 -0
  41. data/spec/fixtures/base.json +13 -0
  42. data/spec/fixtures/contact_my.json +19 -0
  43. data/spec/fixtures/contact_new.json +11 -0
  44. data/spec/fixtures/contact_new_empty.json +8 -0
  45. data/spec/fixtures/contact_new_notify.json +11 -0
  46. data/spec/fixtures/contact_tags.json +11 -0
  47. data/spec/fixtures/contacts.json +6 -3
  48. data/spec/fixtures/contacts_post.json +10 -0
  49. data/spec/fixtures/empty.json +3 -0
  50. data/spec/fixtures/errors/failure.json +5 -0
  51. data/spec/fixtures/listing_cart.json +19 -0
  52. data/spec/fixtures/listing_cart_add_listing.json +13 -0
  53. data/spec/fixtures/listing_cart_add_listing_post.json +5 -0
  54. data/spec/fixtures/listing_cart_empty.json +5 -0
  55. data/spec/fixtures/listing_cart_new.json +12 -0
  56. data/spec/fixtures/listing_cart_post.json +10 -0
  57. data/spec/fixtures/listing_cart_remove_listing.json +13 -0
  58. data/spec/fixtures/note_new.json +5 -0
  59. data/spec/fixtures/{oauth2_access.json → oauth2/access.json} +0 -0
  60. data/spec/fixtures/oauth2/access_with_old_refresh.json +5 -0
  61. data/spec/fixtures/oauth2/access_with_refresh.json +5 -0
  62. data/spec/fixtures/oauth2/authorization_code_body.json +7 -0
  63. data/spec/fixtures/oauth2/error.json +3 -0
  64. data/spec/fixtures/oauth2/password_body.json +7 -0
  65. data/spec/fixtures/oauth2/refresh_body.json +7 -0
  66. data/spec/fixtures/saved_search.json +17 -0
  67. data/spec/fixtures/shared_listing_new.json +9 -0
  68. data/spec/fixtures/shared_listing_post.json +10 -0
  69. data/spec/mock_helper.rb +123 -0
  70. data/spec/oauth2_helper.rb +69 -0
  71. data/spec/spec_helper.rb +1 -57
  72. data/spec/unit/flexmls_api/authentication/api_auth_spec.rb +1 -0
  73. data/spec/unit/flexmls_api/authentication/oauth2_impl/grant_type_base_spec.rb +10 -0
  74. data/spec/unit/flexmls_api/authentication/oauth2_spec.rb +74 -79
  75. data/spec/unit/flexmls_api/configuration_spec.rb +25 -4
  76. data/spec/unit/flexmls_api/models/account_spec.rb +152 -85
  77. data/spec/unit/flexmls_api/models/base_spec.rb +69 -25
  78. data/spec/unit/flexmls_api/models/contact_spec.rb +48 -34
  79. data/spec/unit/flexmls_api/models/document_spec.rb +1 -7
  80. data/spec/unit/flexmls_api/models/listing_cart_spec.rb +114 -0
  81. data/spec/unit/flexmls_api/models/listing_spec.rb +8 -56
  82. data/spec/unit/flexmls_api/models/note_spec.rb +8 -38
  83. data/spec/unit/flexmls_api/models/photo_spec.rb +1 -11
  84. data/spec/unit/flexmls_api/models/saved_search_spec.rb +34 -0
  85. data/spec/unit/flexmls_api/models/shared_listing_spec.rb +30 -0
  86. data/spec/unit/flexmls_api/models/standard_fields_spec.rb +50 -30
  87. data/spec/unit/flexmls_api/models/tour_of_home_spec.rb +1 -7
  88. data/spec/unit/flexmls_api/models/video_spec.rb +1 -10
  89. data/spec/unit/flexmls_api/models/virtual_tour_spec.rb +1 -7
  90. data/spec/unit/flexmls_api/multi_client_spec.rb +48 -0
  91. data/spec/unit/flexmls_api/request_spec.rb +42 -5
  92. metadata +239 -93
  93. data/spec/unit/flexmls_api/standard_fields_spec.rb +0 -86
@@ -0,0 +1,44 @@
1
+ require "rubygems"
2
+ require 'pp'
3
+
4
+ if ENV["FLEXMLS_API_CONSOLE"].nil?
5
+ require 'flexmls_api'
6
+ else
7
+ puts "Enabling console mode for local gem"
8
+ Bundler.require(:default, "development") if defined?(Bundler)
9
+ path = File.expand_path(File.dirname(__FILE__) + "/../../../lib/")
10
+ $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
11
+ require path + '/flexmls_api'
12
+ end
13
+
14
+ IRB.conf[:AUTO_INDENT]=true
15
+ IRB.conf[:PROMPT][:FLEXMLS]= {
16
+ :PROMPT_I => "flexmlsApi:%03n:%i> ",
17
+ :PROMPT_S => "flexmlsApi:%03n:%i%l ",
18
+ :PROMPT_C => "flexmlsApi:%03n:%i* ",
19
+ :RETURN => "%s\n"
20
+ }
21
+
22
+ IRB.conf[:PROMPT_MODE] = :FLEXMLS
23
+
24
+ path = File.expand_path(File.dirname(__FILE__) + "/../../../lib/")
25
+ $LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
26
+ require path + '/flexmls_api'
27
+
28
+ module FlexmlsApi
29
+ def self.logger
30
+ if @logger.nil?
31
+ @logger = Logger.new(STDOUT)
32
+ @logger.level = ENV["VERBOSE"].nil? ? Logger::WARN : Logger::DEBUG
33
+ end
34
+ @logger
35
+ end
36
+ end
37
+
38
+ FlexmlsApi.logger.info("Client configured!")
39
+
40
+ include FlexmlsApi::Models
41
+
42
+ def c
43
+ FlexmlsApi.client
44
+ end
@@ -2,7 +2,7 @@ module FlexmlsApi
2
2
  module Configuration
3
3
  # valid configuration options
4
4
  VALID_OPTION_KEYS = [:api_key, :api_secret, :api_user, :endpoint, :user_agent, :version, :ssl, :oauth2_provider, :authentication_mode].freeze
5
-
5
+
6
6
  DEFAULT_API_KEY = nil
7
7
  DEFAULT_API_SECRET = nil
8
8
  DEFAULT_API_USER = nil
@@ -11,6 +11,8 @@ module FlexmlsApi
11
11
  DEFAULT_USER_AGENT = "flexmls API Ruby Gem #{VERSION}"
12
12
  DEFAULT_SSL = false
13
13
  DEFAULT_OAUTH2 = nil
14
+
15
+ X_FLEXMLS_API_USER_AGENT = "X-flexmlsApi-User-Agent"
14
16
 
15
17
  attr_accessor *VALID_OPTION_KEYS
16
18
  def configure
@@ -21,24 +23,22 @@ module FlexmlsApi
21
23
  base.reset_configuration
22
24
  end
23
25
 
24
-
25
26
  def options
26
27
  VALID_OPTION_KEYS.inject({}) do |opt,key|
27
28
  opt.merge(key => send(key))
28
29
  end
29
30
  end
30
31
 
31
-
32
32
  def reset_configuration
33
33
  self.api_key = DEFAULT_API_KEY
34
34
  self.api_secret = DEFAULT_API_SECRET
35
35
  self.api_user = DEFAULT_API_USER
36
+ self.authentication_mode = FlexmlsApi::Authentication::ApiAuth
36
37
  self.endpoint = DEFAULT_ENDPOINT
37
- self.version = DEFAULT_VERSION
38
+ self.oauth2_provider = DEFAULT_OAUTH2
38
39
  self.user_agent = DEFAULT_USER_AGENT
39
40
  self.ssl = DEFAULT_SSL
40
- self.oauth2_provider = DEFAULT_OAUTH2
41
- self.authentication_mode = FlexmlsApi::Authentication::ApiAuth
41
+ self.version = DEFAULT_VERSION
42
42
  self
43
43
  end
44
44
  end
@@ -2,21 +2,12 @@ module FlexmlsApi
2
2
  module FaradayExt
3
3
  #=Flexmls API Faraday middleware
4
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
- validate_and_build_response(finished_env)
10
- end
11
- end
12
- rescue LoadError, NameError => e
13
- self.load_error = e
14
- end
5
+ class FlexmlsMiddleware < Faraday::Response::ParseJson
15
6
 
16
7
  # Handles pretty much all the api response parsing and error handling. All responses that
17
8
  # indicate a failure will raise a FlexmlsApi::ClientError exception
18
- def self.validate_and_build_response(finished_env)
19
- body = finished_env[:body]
9
+ def on_complete(finished_env)
10
+ body = parse(finished_env[:body])
20
11
  FlexmlsApi.logger.debug("Response Body: #{body.inspect}")
21
12
  unless body.is_a?(Hash) && body.key?("D")
22
13
  raise InvalidResponse, "The server response could not be understood"
@@ -24,30 +15,29 @@ module FlexmlsApi
24
15
  response = ApiResponse.new body
25
16
  case finished_env[:status]
26
17
  when 400, 409
27
- raise BadResourceRequest.new(response.code, finished_env[:status]), response.message
18
+ raise BadResourceRequest, {:message => response.message, :code => response.code, :status => finished_env[:status]}
28
19
  when 401
29
20
  # Handle the WWW-Authenticate Response Header Field if present. This can be returned by
30
21
  # OAuth2 implementations and wouldn't hurt to log.
31
22
  auth_header_error = finished_env[:request_headers]["WWW-Authenticate"]
32
23
  FlexmlsApi.logger.warn("Authentication error #{auth_header_error}") unless auth_header_error.nil?
33
- raise PermissionDenied.new(response.code, finished_env[:status]), response.message
24
+ raise PermissionDenied, {:message => response.message, :code => response.code, :status => finished_env[:status]}
34
25
  when 404
35
- raise NotFound.new(response.code, finished_env[:status]), response.message
26
+ raise NotFound, {:message => response.message, :code => response.code, :status => finished_env[:status]}
36
27
  when 405
37
- raise NotAllowed.new(response.code, finished_env[:status]), response.message
28
+ raise NotAllowed, {:message => response.message, :code => response.code, :status => finished_env[:status]}
38
29
  when 500
39
- raise ClientError.new(response.code, finished_env[:status]), response.message
30
+ raise ClientError, {:message => response.message, :code => response.code, :status => finished_env[:status]}
40
31
  when 200..299
41
32
  FlexmlsApi.logger.debug("Success!")
42
33
  else
43
- raise ClientError.new(response.code, finished_env[:status]), response.message
34
+ raise ClientError, {:message => response.message, :code => response.code, :status => finished_env[:status]}
44
35
  end
45
36
  finished_env[:body] = response
46
37
  end
47
-
38
+
48
39
  def initialize(app)
49
- super
50
- @parser = nil
40
+ super(app)
51
41
  end
52
42
 
53
43
  end
@@ -17,6 +17,9 @@ require File.expand_path('../models/tour_of_home', __FILE__)
17
17
  require File.expand_path('../models/virtual_tour', __FILE__)
18
18
  require File.expand_path('../models/document', __FILE__)
19
19
  require File.expand_path('../models/note', __FILE__)
20
+ require File.expand_path('../models/listing_cart.rb', __FILE__)
21
+ require File.expand_path('../models/shared_listing.rb', __FILE__)
22
+ require File.expand_path('../models/saved_search.rb', __FILE__)
20
23
 
21
24
  module FlexmlsApi
22
25
  module Models
@@ -1,22 +1,65 @@
1
1
  module FlexmlsApi
2
2
  module Models
3
3
  class Account < Base
4
+ extend Finders
4
5
  self.element_name="accounts"
5
6
 
6
7
  SUBELEMENTS = [:emails, :phones, :websites, :addresses, :images]
7
- attr_accessor *SUBELEMENTS
8
+ attr_accessor :my_account, *SUBELEMENTS
8
9
 
9
10
  def initialize(attributes={})
10
11
  @emails = subresource(Email, "Emails", attributes)
11
12
  @phones = subresource(Phone, "Phones", attributes)
12
13
  @websites = subresource(Website, "Websites", attributes)
13
14
  @addresses = subresource(Address, "Addresses", attributes)
14
- @images = subresource(Image, "Images", attributes)
15
+ if attributes["Images"]
16
+ @images = []
17
+ attributes["Images"].each { |i| @images << Image.new(i) }
18
+ else
19
+ @images = nil
20
+ end
21
+ @my_account = false
15
22
  super(attributes)
16
23
  end
17
24
 
18
25
  def self.my(arguments={})
19
- collect(connection.get("/my/account", arguments)).first
26
+ account = collect(connection.get("/my/account", arguments)).first
27
+ account.my_account = true
28
+ account
29
+ end
30
+
31
+ def my_account?
32
+ @my_account
33
+ end
34
+
35
+ def self.by_office(office_id, arguments={})
36
+ collect(connection.get("#{self.path()}/by/office/#{office_id}", arguments))
37
+ end
38
+
39
+ def primary_img(typ)
40
+ if @images.is_a?(Array)
41
+ matches = @images.select {|i| i.Type == typ}
42
+ matches.sort {|a,b| a.Name <=> b.Name }.first
43
+ else
44
+ nil
45
+ end
46
+ end
47
+
48
+ def save(arguments={})
49
+ begin
50
+ return save!(arguments)
51
+ rescue NotFound, BadResourceRequest => e
52
+ FlexmlsApi.logger.error("Failed to save resource #{self}: #{e.message}")
53
+ end
54
+ false
55
+ end
56
+ def save!(arguments={})
57
+ # The long-term idea is that any setting in the user's account could be updated by including
58
+ # an attribute and calling PUT /my/account, but for now only the GetEmailUpdates attribute
59
+ # is supported
60
+ save_path = my_account? ? "/my/account" : self.class.path
61
+ results = connection.put save_path, {"GetEmailUpdates" => self.GetEmailUpdates }, arguments
62
+ true
20
63
  end
21
64
 
22
65
  private
@@ -46,10 +89,10 @@ module FlexmlsApi
46
89
  class Address < Base
47
90
  include Primary
48
91
  end
92
+
49
93
  class Image < Base
50
- include Primary
51
94
  end
52
95
  end
53
96
  end
54
97
 
55
- end
98
+ end
@@ -68,12 +68,37 @@ module FlexmlsApi
68
68
  attributes[$`] = arguments.first
69
69
  # TODO figure out a nice way to present setters for the standard fields
70
70
  when "?"
71
- attributes[$`]
71
+ if attributes.include?($`)
72
+ attributes[$`] ? true : false
73
+ else
74
+ raise NoMethodError
75
+ end
72
76
  end
73
77
  else
74
78
  return attributes[method_name] if attributes.include?(method_name)
75
79
  super # GTFO
76
- end
80
+ end
81
+ end
82
+
83
+ def respond_to?(method_symbol, include_private=false)
84
+ if super
85
+ return true
86
+ else
87
+ method_name = method_symbol.to_s
88
+
89
+ if method_name =~ /=$/
90
+ true
91
+ elsif method_name =~ /(\?)$/
92
+ attributes.include?($`)
93
+ else
94
+ attributes.include?(method_name)
95
+ end
96
+
97
+ end
98
+ end
99
+
100
+ def parse_id(uri)
101
+ uri[/\/.*\/(.+)$/, 1]
77
102
  end
78
103
  end
79
104
  end
@@ -4,23 +4,42 @@ module FlexmlsApi
4
4
  extend Finders
5
5
  self.element_name="contacts"
6
6
 
7
- def save
7
+ def save(arguments={})
8
8
  begin
9
- return save!
10
- rescue BadResourceRequest => e
11
- rescue NotFound => e
12
- # log and leave
13
- FlexmlsApi.logger.error("Failed to save contact #{self}: #{e.message}")
9
+ return save!(arguments)
10
+ rescue NotFound, BadResourceRequest => e
11
+ FlexmlsApi.logger.error("Failed to save resource #{self}: #{e.message}")
14
12
  end
15
13
  false
16
14
  end
17
- def save!
18
- results = connection.post self.class.path, "Contacts" => [ attributes ]
15
+ def save!(arguments={})
16
+ results = connection.post self.class.path, {"Contacts" => [ attributes ], "Notify" => notify? }, arguments
19
17
  result = results.first
20
18
  attributes['ResourceUri'] = result['ResourceUri']
21
- attributes['Id'] = result['ResourceUri'][/\/.*\/(.+)$/, 1]
19
+ attributes['Id'] = parse_id(result['ResourceUri'])
22
20
  true
23
21
  end
22
+
23
+ def self.by_tag(tag_name, arguments={})
24
+ collect(connection.get("#{path}/tags/#{tag_name}", arguments))
25
+ end
26
+
27
+ def self.tags(arguments={})
28
+ connection.get("#{path}/tags", arguments)
29
+ end
30
+
31
+ def self.my(arguments={})
32
+ new(connection.get('/my/contact', arguments).first)
33
+ end
34
+
35
+ # Notify the agent of contact creation via a flexmls message.
36
+ def notify?
37
+ @notify == true
38
+ end
39
+ def notify=(notify_me=true)
40
+ @notify = notify_me
41
+ end
42
+
24
43
  end
25
44
  end
26
45
  end
@@ -0,0 +1,72 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class ListingCart < Base
4
+ extend Finders
5
+ self.element_name="listingcarts"
6
+
7
+ def ListingIds=(listing_ids)
8
+ attributes["ListingIds"] = Array(listing_ids)
9
+ end
10
+ def Name=(name)
11
+ attributes["Name"] = name
12
+ end
13
+
14
+ def add_listing(listing)
15
+ id = listing.respond_to?(:Id) ? listing.Id : listing.to_s
16
+ results = connection.post("#{self.class.path}/#{self.Id}", {"ListingIds" => [ listing ]})
17
+ self.ListingCount = results.first["ListingCount"]
18
+ end
19
+
20
+ def remove_listing(listing)
21
+ id = listing.respond_to?(:Id) ? listing.Id : listing.to_s
22
+ results = connection.delete("#{self.class.path}/#{self.Id}/listings/#{id}")
23
+ self.ListingCount = results.first["ListingCount"]
24
+ end
25
+
26
+ def self.for(listings,arguments={})
27
+ keys = Array(listings).map { |l| l.respond_to?(:Id) ? l.Id : l.to_s }
28
+ collect(connection.get("/#{self.element_name}/for/#{keys.join(",")}", arguments))
29
+ end
30
+
31
+ def self.my(arguments={})
32
+ collect(connection.get("/my/#{self.element_name}", arguments))
33
+ end
34
+
35
+ def self.portal(arguments={})
36
+ collect(connection.get("/#{self.element_name}/portal", arguments))
37
+ end
38
+
39
+ def save(arguments={})
40
+ begin
41
+ return save!(arguments)
42
+ rescue BadResourceRequest => e
43
+ rescue NotFound => e
44
+ # log and leave
45
+ FlexmlsApi.logger.error("Failed to save contact #{self}: #{e.message}")
46
+ end
47
+ false
48
+ end
49
+ def save!(arguments={})
50
+ attributes['Id'].nil? ? create!(arguments) : update!(arguments)
51
+ end
52
+
53
+ def delete(args={})
54
+ connection.delete("#{self.class.path}/#{self.Id}", args)
55
+ end
56
+
57
+ private
58
+ def create!(arguments={})
59
+ results = connection.post self.class.path, {"ListingCarts" => [ attributes ]}, arguments
60
+ result = results.first
61
+ attributes['ResourceUri'] = result['ResourceUri']
62
+ attributes['Id'] = parse_id(result['ResourceUri'])
63
+ true
64
+ end
65
+ def update!(arguments={})
66
+ results = connection.put "#{self.class.path}/#{self.Id}", {"ListingCarts" => [ {"Name" => attributes["Name"], "ListingIds" => attributes["ListingIds"]} ] }, arguments
67
+ true
68
+ end
69
+
70
+ end
71
+ end
72
+ end
@@ -13,7 +13,6 @@ module FlexmlsApi
13
13
  end
14
14
  end
15
15
 
16
-
17
16
  def save(arguments={})
18
17
  begin
19
18
  return save!(arguments)
@@ -37,7 +36,6 @@ module FlexmlsApi
37
36
  connection.delete(self.class.path, args)
38
37
  end
39
38
 
40
-
41
39
  end
42
40
  end
43
41
  end
@@ -0,0 +1,16 @@
1
+ module FlexmlsApi
2
+ module Models
3
+ class SavedSearch < Base
4
+ extend Finders
5
+ self.element_name="savedsearches"
6
+
7
+ def self.provided()
8
+ Class.new(self).tap do |provided|
9
+ provided.element_name = '/savedsearches'
10
+ provided.prefix = '/provided'
11
+ FlexmlsApi.logger.info("#{self.name}.path: #{provided.path}")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end