flexmls_api 0.4.5 → 0.6.4

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