require 'uri'
require 'cgi'
require 'logger'
require 'net/https'
require 'singleton'
require 'yajl'
require 'bigdecimal'
require 'bigdecimal/util'
require 'active_support/core_ext'
require 'date'
require 'time'
class Decimal #:nodoc:
end
class Boolean #:nodoc:
end
# _MLS_ is a low-level API. It provides basic HTTP #get, #post, #put, and #delete
# calls to the MLS. It can also provides basic error checking of responses.
class MLS
include Singleton
API_VERSION = '0.1.0'
attr_reader :url, :user_agent
attr_writer :asset_host, :image_host, :listing_amenities, :address_amenities
attr_accessor :api_key, :auth_key, :logger
# Sets the API Token and Host of the MLS Server
#
# #!ruby
# MLS.url = "https://mls.42floors.com/API_KEY"
def url=(uri) # TODO: testme
@url = URI.parse(uri)
@api_key = CGI.unescape(@url.user)
@host, @port = @url.host, @url.port
end
# Sets the user agent so that MLS can distinguish between multiple users
# with the same auth
def user_agent=(user_agent)
@user_agent = user_agent
end
def logger # TODO: testme
@logger ||= default_logger
end
# Returns the current connection to the MLS or if connection has been made
# it returns a new connection
def connection # TODO: testme
@connection ||= Net::HTTP.new(@host, @port)
end
# provides the asset host, if asset_host is set then it is returned,
# otherwise it queries the MLS for this configuration.
def asset_host # TODO: testme
@asset_host ||= get('/asset_host').body
end
def image_host # TODO: testme
raw_image_host % (rand(4))
end
def raw_image_host
@image_host ||= get('/image_host').body
end
def listing_amenities
@listing_amenities ||= Yajl::Parser.new(:symbolize_keys => true)
.parse(MLS.get('/listings/amenities').body)
end
def address_amenities
@address_amenities ||= Yajl::Parser.new(:symbolize_keys => true)
.parse(MLS.get('/addresses/amenities').body)
end
def headers # TODO: testme
h = {
'Content-Type' => 'application/json',
'User-Agent' => @user_agent,
'X-42Floors-API-Version' => API_VERSION,
'X-42Floors-API-Key' => api_key
}
h['X-42Floors-API-Auth-Key'] = auth_key if auth_key
h
end
def add_headers(req) # TODO: testme
headers.each { |k, v| req[k] = v }
end
# Gets to +url+ on the MLS Server. Automatically includes any headers returned
# by the MLS#headers function.
#
# Paramaters::
#
# * +url+ - The +url+ on the server to Get to. This url will automatically
# be prefixed with "/api". To get to "/api/accounts"
# pass "/accounts" as +url+
# * +params+ - A Hash or Ruby Object that responds to #to_param. The result
# of this method is appended on the URL as query params
# * +valid_response_codes+ - An Array of HTTP response codes that should be
# considered accepable and not raise exceptions. For example If you don't
# want a MLS::Exception::NotFound to be raised when a GET request returns
# a 404 pass in 404, and the response body will be returned if the status
# code is a 404 as it does if the status code is in the 200..299 rage. Status
# codes in the 200..299 range are *always* considred acceptable
#
# Return Value::
#
# Returns the return value of the &block if given, otherwise the response
# object
#
# Examples:
#
# #!ruby
# MLS.get('/example') # => #
#
# MLS.get('/example', {:body => 'stuff'}) # => #
#
# MLS.get('/404') # => raises MLS::Exception::NotFound
#
# MLS.get('/404', nil, 404, 450..499) # => #
#
# MLS.get('/404', nil, [404, 450..499]) # => #
#
# MLS.get('/404', nil, 404) # => #
#
# # this will still raise an exception if the response_code is not valid
# # and the block will not be called
# MLS.get('/act') do |response, response_code|
# # ...
# end
def get(url, params={}, *valid_response_codes, &block)
params ||= {}
req = Net::HTTP::Get.new("/api#{url}?" + params.to_param)
add_headers(req)
response = connection.request(req)
handle_response(response, valid_response_codes)
response.body.force_encoding(Encoding::UTF_8)
if block_given?
yield(response, response.code.to_i)
else
response
end
end
# Puts to +url+ on the MLS Server. Automatically includes any headers returned
# by the MLS#headers function.
#
# Paramaters::
#
# * +url+ - The +url+ on the server to Put to. This url will automatically
# be prefixed with "/api". To put to "/api/accounts"
# pass "/accounts" as +url+
# * +body+ - A Ruby object which is converted into JSON and added in the request
# Body.
# * +valid_response_codes+ - An Array of HTTP response codes that should be
# considered accepable and not raise exceptions. For example If you don't
# want a MLS::Exception::NotFound to be raised when a PUT request returns
# a 404 pass in 404, and the response body will be returned if the status
# code is a 404 as it does if the status code is in the 200..299 rage. Status
# codes in the 200..299 range are *always* considred acceptable
#
# Return Value::
#
# Returns the return value of the &block if given, otherwise the response
# object
#
# Examples:
#
# #!ruby
# MLS.put('/example') # => #
#
# MLS.put('/example', {:body => 'stuff'}) # => #
#
# MLS.put('/404') # => raises MLS::Exception::NotFound
#
# MLS.put('/404', nil, 404, 450..499) # => #
#
# MLS.put('/404', nil, [404, 450..499]) # => #
#
# MLS.put('/404', nil, 404) # => #
#
# # this will still raise an exception if the response_code is not valid
# # and the block will not be called
# MLS.put('/act') do |response, response_code|
# # ...
# end
def put(url, body={}, *valid_response_codes, &block)
body ||= {}
req = Net::HTTP::Put.new("/api#{url}")
req.body = Yajl::Encoder.encode(body)
add_headers(req)
response = connection.request(req)
handle_response(response, valid_response_codes)
if block_given?
yield(response, response.code.to_i)
else
response
end
end
# Posts to +url+ on the MLS Server. Automatically includes any headers returned
# by the MLS#headers function.
#
# Paramaters::
#
# * +url+ - The +url+ on the server to Post to. This url will automatically
# be prefixed with "/api". To post to "/api/accounts"
# pass "/accounts" as +url+
# * +body+ - A Ruby object which is converted into JSON and added in the request
# Body.
# * +valid_response_codes+ - An Array of HTTP response codes that should be
# considered accepable and not raise exceptions. For example If you don't
# want a MLS::Exception::NotFound to be raised when a POST request returns
# a 404 pass in 404, and the response body will be returned if the status
# code is a 404 as it does if the status code is in the 200..299 rage. Status
# codes in the 200..299 range are *always* considred acceptable
#
# Return Value::
#
# Returns the return value of the &block if given, otherwise the response
# object
#
# Examples:
#
# #!ruby
# MLS.post('/example') # => #
#
# MLS.post('/example', {:body => 'stuff'}) # => #
#
# MLS.post('/404') # => raises MLS::Exception::NotFound
#
# MLS.post('/404', nil, 404, 450..499) # => #
#
# MLS.post('/404', nil, [404, 450..499]) # => #
#
# MLS.post('/404', nil, 404) # => #
#
# # this will still raise an exception if the response_code is not valid
# # and the block will not be called
# MLS.post('/act') do |response, response_code|
# # ...
# end
def post(url, body={}, *valid_response_codes, &block)
body ||= {}
req = Net::HTTP::Post.new("/api#{url}")
req.body = Yajl::Encoder.encode(body)
add_headers(req)
response = connection.request(req)
handle_response(response, valid_response_codes)
if block_given?
yield(response, response.code.to_i)
else
response
end
end
# Deletes to +url+ on the MLS Server. Automatically includes any headers returned
# by the MLS#headers function.
#
# Paramaters::
#
# * +url+ - The +url+ on the server to Post to. This url will automatically
# be prefixed with "/api". To delete to "/api/accounts"
# pass "/accounts" as +url+
# * +body+ - A Ruby object which is converted into JSON and added in the request
# Body.
# * +valid_response_codes+ - An Array of HTTP response codes that should be
# considered accepable and not raise exceptions. For example If you don't
# want a MLS::Exception::NotFound to be raised when a POST request returns
# a 404 pass in 404, and the response body will be returned if the status
# code is a 404 as it does if the status code is in the 200..299 rage. Status
# codes in the 200..299 range are *always* considred acceptable
#
# Return Value::
#
# Returns the return value of the &block if given, otherwise the
# response object
#
# Examples:
#
# #!ruby
# MLS.delete('/example') # => #
#
# MLS.delete('/example', {:body => 'stuff'}) # => #
#
# MLS.delete('/404') # => raises MLS::Exception::NotFound
#
# MLS.delete('/404', nil, 404, 450..499) # => #
#
# MLS.delete('/404', nil, [404, 450..499]) # => #
#
# MLS.delete('/404', nil, 404) # => #
#
# # this will still raise an exception if the response_code is not valid
# # and the block will not be called
# MLS.delete('/act') do |response, response_code|
# # ...
# end
def delete(url, body={}, *valid_response_codes, &block)
body ||= {}
req = Net::HTTP::Delete.new("/api#{url}")
req.body = Yajl::Encoder.encode(body)
add_headers(req)
response = connection.request(req)
handle_response(response, valid_response_codes)
if block_given?
yield(response, response.code.to_i)
else
response
end
end
# Raise an MLS::Exception based on the response_code, unless the response_code
# is include in the valid_response_codes Array
#
# Paramaters::
#
# * +response+ - The Net::HTTP::Response object
# * +valid_response_codes+ - An Array, Integer, or Range. If it's Array the
# Array can include both Integers or Ranges.
#
# Return Value::
#
# If an exception is not raised the +response+ is returned
#
# Examples:
#
# #!ruby
# MLS.handle_response() # =>
#
# MLS.handle_response() # => raises MLS::Exception::NotFound
#
# MLS.handle_response() # => raises MLS::Exception
#
# MLS.handle_response(, 404) # =>
#
# MLS.handle_response(, 404, 500) # =>
#
# MLS.handle_response(, 300, 400..499) # =>
#
# MLS.handle_response(, [300, 400..499]) # =>
def handle_response(response, *valid_response_codes)
if response['X-42Floors-API-Version-Deprecated']
logger.warn("DEPRECATION WARNING: API v#{API_VERSION} is being phased out")
end
code = response.code.to_i
valid_response_codes.flatten!
valid_response_codes << (200..299)
if !valid_response_codes.detect{|i| i.is_a?(Range) ? i.include?(code) : i == code}
case code
when 400
raise MLS::Exception::BadRequest, response.body
when 401
raise MLS::Exception::Unauthorized, response.body
when 404, 410
raise MLS::Exception::NotFound
when 422
raise MLS::Exception::ApiVersionUnsupported, response.body
when 503
raise MLS::Exception::ServiceUnavailable, response.body
when 300..599
raise MLS::Exception, code
end
end
response
end
# Ping the MLS. If everything is configured and operating correctly "pong"
# will be returned. Otherwise and MLS::Exception should be thrown.
#
# #!ruby
# MLS.ping # => "pong"
#
# MLS.ping # raises MLS::Exception::ServiceUnavailable if a 503 is returned
def ping # TODO: testme
get('/ping').body
end
def auth_ping # TODO: testme
post('/ping').body
end
def default_logger # TODO: testme
logger = Logger.new(STDOUT)
logger.level = Logger::INFO
logger
end
# Delegates all uncauge class method calls to the singleton
def self.method_missing(method, *args, &block) #:nodoc: # TODO: testme
instance.__send__(method, *args, &block)
end
def self.parse(json) # TODO: testme
Yajl::Parser.new(:symbolize_keys => true).parse(json)
end
end
require 'mls/errors'
require 'mls/resource'
require 'mls/parser'
# Properties
require 'mls/property'
require 'mls/properties/fixnum'
require 'mls/properties/boolean'
require 'mls/properties/decimal'
require 'mls/properties/datetime'
require 'mls/properties/string'
require 'mls/properties/hash'
require 'mls/properties/array'
# Models
require 'mls/model'
require 'mls/models/account'
require 'mls/models/listing'
require 'mls/models/address'
require 'mls/models/photo'
require 'mls/models/video'
require 'mls/models/pdf'
require 'mls/models/tour'
require 'mls/models/flyer'
require 'mls/models/floorplan'
require 'mls/models/region'