Class: RESO::API::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/reso_api/app/models/reso/api/client.rb

Constant Summary collapse

RESOURCE_KEYS =
{
  media: "MediaKey",
  members: "MemberKey",
  offices: "OfficeKey",
  open_houses: "OpenHouseKey",
  properties: "ListingKey"
}
DETAIL_ENDPOINTS =
{
  medium: "/Media",
  member: "/Member",
  office: "/Office",
  open_house: "/OpenHouse",
  property: "/Property"
}
FILTERABLE_ENDPOINTS =
{
  media: "/Media",
  members: "/Member",
  offices: "/Office",
  open_houses: "/OpenHouse",
  properties: "/Property"
}
PASSTHROUGH_ENDPOINTS =
{
  metadata: "/$metadata"
}

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**opts) ⇒ Client

Returns a new instance of Client.



12
13
14
15
# File 'lib/reso_api/app/models/reso/api/client.rb', line 12

def initialize(**opts)
  @access_token, @client_id, @client_secret, @auth_url, @base_url, @scope, @osn = opts.values_at(:access_token, :client_id, :client_secret, :auth_url, :base_url, :scope, :osn)
  validate!
end

Instance Attribute Details

#access_tokenObject

Returns the value of attribute access_token.



10
11
12
# File 'lib/reso_api/app/models/reso/api/client.rb', line 10

def access_token
  @access_token
end

#auth_urlObject

Returns the value of attribute auth_url.



10
11
12
# File 'lib/reso_api/app/models/reso/api/client.rb', line 10

def auth_url
  @auth_url
end

#base_urlObject

Returns the value of attribute base_url.



10
11
12
# File 'lib/reso_api/app/models/reso/api/client.rb', line 10

def base_url
  @base_url
end

#client_idObject

Returns the value of attribute client_id.



10
11
12
# File 'lib/reso_api/app/models/reso/api/client.rb', line 10

def client_id
  @client_id
end

#client_secretObject

Returns the value of attribute client_secret.



10
11
12
# File 'lib/reso_api/app/models/reso/api/client.rb', line 10

def client_secret
  @client_secret
end

#osnObject

Returns the value of attribute osn.



10
11
12
# File 'lib/reso_api/app/models/reso/api/client.rb', line 10

def osn
  @osn
end

#scopeObject

Returns the value of attribute scope.



10
11
12
# File 'lib/reso_api/app/models/reso/api/client.rb', line 10

def scope
  @scope
end

Instance Method Details

#auth_tokenObject



111
112
113
# File 'lib/reso_api/app/models/reso/api/client.rb', line 111

def auth_token
  access_token.presence ? access_token : oauth2_token
end

#entity_namesObject



210
211
212
213
214
# File 'lib/reso_api/app/models/reso/api/client.rb', line 210

def entity_names
  doc = Nokogiri::XML()
  namespace = { 'edm' => 'http://docs.oasis-open.org/odata/ns/edm' }
  doc.xpath('//edm:EntityType', namespace).map { |node| node['Name'] }
end

#fresh_oauth2_payloadObject



138
139
140
141
142
# File 'lib/reso_api/app/models/reso/api/client.rb', line 138

def fresh_oauth2_payload
  @oauth2_payload = oauth2_client.client_credentials.get_token('client_id' => client_id, 'client_secret' => client_secret, 'scope' => scope.presence)
  File.write(oauth2_token_path, @oauth2_payload.to_hash.to_json)
  return @oauth2_payload
end

#get_oauth2_payloadObject



152
153
154
155
156
157
158
159
160
161
# File 'lib/reso_api/app/models/reso/api/client.rb', line 152

def get_oauth2_payload
  if File.exist?(oauth2_token_path)
    persisted = File.read(oauth2_token_path)
    payload = OAuth2::AccessToken.from_hash(oauth2_client, JSON.parse(persisted))
  else
    payload = oauth2_client.client_credentials.get_token('client_id' => client_id, 'client_secret' => client_secret, 'scope' => scope.presence)
    File.write(oauth2_token_path, payload.to_hash.to_json)
  end
  return payload
end

#oauth2_clientObject



115
116
117
118
119
120
121
122
123
# File 'lib/reso_api/app/models/reso/api/client.rb', line 115

def oauth2_client
  OAuth2::Client.new(
    client_id,
    client_secret,
    token_url: auth_url,
    scope: scope.presence,
    grant_type: "client_credentials"
  )
end

#oauth2_payloadObject



148
149
150
# File 'lib/reso_api/app/models/reso/api/client.rb', line 148

def oauth2_payload
  @oauth2_payload ||= get_oauth2_payload
end

#oauth2_tokenObject



125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/reso_api/app/models/reso/api/client.rb', line 125

def oauth2_token
  payload = oauth2_payload
  token = JWT.decode(payload.token, nil, false)
  exp_timestamp = Hash(token.try(:first))["exp"].to_s
  expiration = DateTime.strptime(exp_timestamp, '%s').utc rescue DateTime.now.utc
  if expiration > DateTime.now.utc
    return payload.token
  else
    @oauth2_payload = fresh_oauth2_payload
    return @oauth2_payload.token
  end
end

#oauth2_token_pathObject



144
145
146
# File 'lib/reso_api/app/models/reso/api/client.rb', line 144

def oauth2_token_path
  File.join(Dir.tmpdir, [base_url.parameterize, client_id, "oauth-token.json"].join("-"))
end

#perform_call(endpoint, params, max_retries = 5, debug = false) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/reso_api/app/models/reso/api/client.rb', line 167

def perform_call(endpoint, params, max_retries = 5, debug = false)
  uri = uri_for_endpoint(endpoint)
  retries = 0

  if params.present?
    query = params.present? ? URI.encode_www_form(params).gsub("+", " ") : ""
    uri.query && uri.query.length > 0 ? uri.query += '&' + query : uri.query = query
    return URI::decode(uri.request_uri) if params.dig(:$debug).present?
  end

  begin
    req = Net::HTTP::Get.new(uri.request_uri)
    req['Authorization'] = "Bearer #{auth_token}"
    res = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
      http.request(req)
    end
    response = JSON(res.body) rescue res.body
    if response.is_a?(String) && response.include?('Bad Gateway')
      puts "Error: Bad Gateway." if debug
      raise StandardError
    elsif response.is_a?(String) && response.include?('Unauthorized')
      puts "Error: Unauthorized." if debug
      fresh_oauth2_payload
      raise StandardError
    elsif response.is_a?(Hash) && response.has_key?("error")
      puts "Error: #{response.inspect}" if debug
      raise StandardError
    elsif response.is_a?(Hash) && response.has_key?("retry-after")
      puts "Error: Retrying in #{response["retry-after"].to_i}} seconds." if debug
      sleep response["retry-after"].to_i
      raise StandardError
    end
  rescue Net::ReadTimeout, StandardError
    if (retries += 1) <= max_retries
      sleep 5
      retry
    else
      raise
    end
  end
  return response
end

#supported_expandablesObject



216
217
218
219
220
221
222
223
224
225
# File 'lib/reso_api/app/models/reso/api/client.rb', line 216

def supported_expandables
  expandables_arr = []

  entity_names.each do |entity_name|
    success = try_expand(entity_name)
    expandables_arr << entity_name if success
  end

  expandables_arr.join(',').presence
end

#try_expand(entity_name) ⇒ Object



227
228
229
230
231
232
233
234
235
# File 'lib/reso_api/app/models/reso/api/client.rb', line 227

def try_expand(entity_name)
  endpoint = '/Property'
  params = { '$expand' => entity_name }

  response = perform_call(endpoint, params, max_retries = 0)
  (!response.is_a?(Hash) || !response.key?('error')) && response['statusCode'].blank? && response['status'].blank?
rescue StandardError
  false
end

#uri_for_endpoint(endpoint) ⇒ Object



163
164
165
# File 'lib/reso_api/app/models/reso/api/client.rb', line 163

def uri_for_endpoint endpoint
  return URI(endpoint).host ? URI(endpoint) : URI([base_url, endpoint].join)
end

#validate!Object



17
18
19
20
21
22
23
24
25
26
# File 'lib/reso_api/app/models/reso/api/client.rb', line 17

def validate!
  if access_token.nil?
    raise 'Missing Client ID `client_id`' if client_id.nil?
    raise 'Missing Client Secret `client_secret`' if client_secret.nil?
    raise 'Missing Authentication URL `auth_url`' if auth_url.nil?
    raise 'Missing API Base URL `base_url`' if base_url.nil?
  else
    raise 'Missing API Base URL `base_url`' if base_url.nil?
  end
end