Class: RESO::API::Client
- Inherits:
-
Object
- Object
- RESO::API::Client
- 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
-
#access_token ⇒ Object
Returns the value of attribute access_token.
-
#auth_url ⇒ Object
Returns the value of attribute auth_url.
-
#base_url ⇒ Object
Returns the value of attribute base_url.
-
#client_id ⇒ Object
Returns the value of attribute client_id.
-
#client_secret ⇒ Object
Returns the value of attribute client_secret.
-
#last_request_url ⇒ Object
readonly
Returns the value of attribute last_request_url.
-
#osn ⇒ Object
Returns the value of attribute osn.
-
#scope ⇒ Object
Returns the value of attribute scope.
Instance Method Summary collapse
- #auth_token ⇒ Object
- #can_use_oauth? ⇒ Boolean
- #entity_names ⇒ Object
- #fresh_oauth2_payload ⇒ Object
- #get_oauth2_payload ⇒ Object
-
#initialize(**opts) ⇒ Client
constructor
A new instance of Client.
- #oauth2_client ⇒ Object
- #oauth2_payload ⇒ Object
- #oauth2_token ⇒ Object
- #oauth2_token_path ⇒ Object
- #perform_call(endpoint, params, max_retries = 5, debug = false) ⇒ Object
- #supported_expandables ⇒ Object
- #try_expand(entity_name) ⇒ Object
- #uri_for_endpoint(endpoint) ⇒ Object
- #validate! ⇒ Object
Constructor Details
#initialize(**opts) ⇒ Client
Returns a new instance of Client.
14 15 16 17 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 14 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_token ⇒ Object
Returns the value of attribute access_token.
11 12 13 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 11 def access_token @access_token end |
#auth_url ⇒ Object
Returns the value of attribute auth_url.
11 12 13 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 11 def auth_url @auth_url end |
#base_url ⇒ Object
Returns the value of attribute base_url.
11 12 13 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 11 def base_url @base_url end |
#client_id ⇒ Object
Returns the value of attribute client_id.
11 12 13 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 11 def client_id @client_id end |
#client_secret ⇒ Object
Returns the value of attribute client_secret.
11 12 13 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 11 def client_secret @client_secret end |
#last_request_url ⇒ Object (readonly)
Returns the value of attribute last_request_url.
12 13 14 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 12 def last_request_url @last_request_url end |
#osn ⇒ Object
Returns the value of attribute osn.
11 12 13 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 11 def osn @osn end |
#scope ⇒ Object
Returns the value of attribute scope.
11 12 13 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 11 def scope @scope end |
Instance Method Details
#auth_token ⇒ Object
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 113 def auth_token # If access_token is provided, check if it's expired if access_token.present? # Try to decode as JWT to check expiration begin token = JWT.decode(access_token, nil, false) = Hash(token.try(:first))["exp"].to_s expiration = DateTime.strptime(, '%s').utc rescue nil # If token is expired and we have OAuth credentials, get a fresh token if expiration && expiration <= DateTime.now.utc && can_use_oauth? return oauth2_token end rescue JWT::DecodeError # Not a JWT token, just use it as-is end return access_token end # No access_token provided, use OAuth flow oauth2_token end |
#can_use_oauth? ⇒ Boolean
137 138 139 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 137 def can_use_oauth? client_id.present? && client_secret.present? && auth_url.present? end |
#entity_names ⇒ Object
299 300 301 302 303 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 299 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_payload ⇒ Object
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 164 def fresh_oauth2_payload begin @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 rescue OAuth2::Error => e # Provide detailed error message for OAuth failures error_details = "OAuth token refresh failed for #{base_url}" error_details += "\n Scope attempted: #{scope.inspect}" error_details += "\n OAuth error: #{e.message}" raise StandardError, error_details rescue => e raise StandardError, "Failed to refresh OAuth token: #{e.message}" end end |
#get_oauth2_payload ⇒ Object
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 188 def get_oauth2_payload if File.exist?(oauth2_token_path) begin persisted = File.read(oauth2_token_path) parsed = JSON.parse(persisted) # Check if the persisted data is a valid token (has access_token or token field) if parsed['access_token'].present? || parsed['token'].present? payload = OAuth2::AccessToken.from_hash(oauth2_client, parsed) # Verify the payload actually has a token if payload.token.present? return payload end end # If we get here, the cached token is invalid - delete it and get fresh File.delete(oauth2_token_path) rescue JSON::ParserError, StandardError => e # If there's any error reading/parsing the cached token, delete it File.delete(oauth2_token_path) if File.exist?(oauth2_token_path) end end # Get fresh token begin 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) return payload rescue OAuth2::Error => e # Clean up any bad cached token File.delete(oauth2_token_path) if File.exist?(oauth2_token_path) # Provide detailed error message error_details = "OAuth token request failed for #{base_url}" error_details += "\n Scope attempted: #{scope.inspect}" error_details += "\n OAuth error: #{e.message}" raise StandardError, error_details end end |
#oauth2_client ⇒ Object
141 142 143 144 145 146 147 148 149 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 141 def oauth2_client OAuth2::Client.new( client_id, client_secret, token_url: auth_url, scope: scope.presence, grant_type: "client_credentials" ) end |
#oauth2_payload ⇒ Object
184 185 186 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 184 def oauth2_payload @oauth2_payload ||= get_oauth2_payload end |
#oauth2_token ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 151 def oauth2_token payload = oauth2_payload token = JWT.decode(payload.token, nil, false) = Hash(token.try(:first))["exp"].to_s expiration = DateTime.strptime(, '%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_path ⇒ Object
180 181 182 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 180 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
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 233 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 # Store the full request URL for debugging @last_request_url = uri.to_s begin req = Net::HTTP::Get.new(uri.request_uri) req['Authorization'] = "Bearer #{auth_token}" # Configure HTTP object before starting connection http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = (uri.scheme == 'https') if http.use_ssl? # Configure certificate store to avoid CRL checking issues with OpenSSL 3.5+ # This maintains certificate chain verification while preventing CRL-related failures http.verify_mode = OpenSSL::SSL::VERIFY_PEER store = OpenSSL::X509::Store.new store.set_default_paths http.cert_store = store end res = http.request(req) 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") error_msg = response.inspect puts "Error: #{error_msg}" if debug raise StandardError, error_msg 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 # Add metadata to response hash (if response is a hash) if response.is_a?(Hash) response['@reso_request_url'] = @last_request_url response['@reso_auth_scope'] = scope end return response end |
#supported_expandables ⇒ Object
305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 305 def = [] omit_arr = %w[HistoryTransactional] entity_names.each do |entity_name| success = (entity_name) << entity_name if success end -= omit_arr .join(',').presence end |
#try_expand(entity_name) ⇒ Object
318 319 320 321 322 323 324 325 326 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 318 def (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
229 230 231 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 229 def uri_for_endpoint endpoint return URI(endpoint).host ? URI(endpoint) : URI([base_url, endpoint].join) end |
#validate! ⇒ Object
19 20 21 22 23 24 25 26 27 28 |
# File 'lib/reso_api/app/models/reso/api/client.rb', line 19 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 |