Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement client to handle http requests #6

Merged
merged 3 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ jobs:
- name: Run tests
run: bundle exec rspec

- name: Upload coverage report
if: matrix.os == 'ubuntu-latest' # Only upload from Ubuntu
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
# Temporarily disable uploading of coverage report as it is failing with error as:
# Error: Failed to CreateArtifact: Received non-retryable error: Failed request: (409) Conflict: an artifact with this name already exists on the workflow run
# - name: Upload coverage report
# if: matrix.os == 'ubuntu-latest' # Only upload from Ubuntu
# uses: actions/upload-artifact@v4
# with:
# name: coverage-report
# path: coverage/
26 changes: 26 additions & 0 deletions lib/api_wrapper/http_client/base_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

module ApiWrapper
module HttpClient
# Base class for HTTP clients
class BaseClient
# Initializes a new instance of the BaseClient class.
#
# @param base_url [String] The base URL for the HTTP client.
def initialize(base_url, cache_policy)
@base_url = base_url
@cache_policy = cache_policy
end

# Sends a GET request to the specified endpoint.
#
# @param endpoint [String] The endpoint to send the GET request to.
# @raise [NotImplementedError] If the method is not implemented by subclasses.
def get(endpoint)
raise NotImplementedError, 'Subclasses must implement the get method'
end

# TODO: Other HTTP methods like post, put, delete can be added here if needed
end
end
end
81 changes: 81 additions & 0 deletions lib/api_wrapper/http_client/faraday_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# frozen_string_literal: true

require 'faraday'
require 'faraday-http-cache'
require 'json'
require_relative 'base_client'

module ApiWrapper
module HttpClient
# FaradayClient class is responsible for making HTTP requests using Faraday.
class FaradayClient < BaseClient
# Sends a GET request to the specified endpoint.
#
# @param endpoint [String] The endpoint to send the request to.
# @param force_refresh [Boolean] Whether to force a cache refresh.
# @return [Faraday::Response] The response object.
def get(endpoint, force_refresh: false)
# Use the cache policy to determine whether to fetch from cache or refresh.
@cache_policy.fetch(endpoint, force_refresh: force_refresh) do
handle_connection(endpoint) do |connection|
connection.get(endpoint)
end
end
end

private

# Handles the connection to the base URL.
#
# @yield [connection] The Faraday connection object.
# @yieldparam connection [Faraday::Connection] The Faraday connection object.
# @return [Object] The result of the block execution.
# @raise [Faraday::Error] If there is an error with the connection.
def handle_connection(endpoint)
connection = build_faraday_connection(endpoint)
yield(connection)
rescue Faraday::Error => e
handle_faraday_error(e)
end

# Builds the Faraday connection with proper configuration.
#
# @param endpoint [String] The endpoint for the connection.
# @return [Faraday::Connection] The configured Faraday connection.
def build_faraday_connection(endpoint)
Faraday.new(url: @base_url) do |faraday|
configure_faraday(faraday)
apply_cache_policy(faraday, endpoint) if @cache_policy.use_cache?(endpoint)
faraday.adapter Faraday.default_adapter
end
end

# Configures Faraday with default headers and request options.
#
# @param faraday [Faraday::Connection] The Faraday connection object.
def configure_faraday(faraday)
faraday.request :json
faraday.headers['User-Agent'] = 'ApiWrapperClient/1.0'
faraday.response :json
faraday.headers['Accept'] = 'application/json'
end

# Applies the cache policy to the Faraday connection if caching is enabled.
#
# @param faraday [Faraday::Connection] The Faraday connection object.
# @param endpoint [String] The endpoint to be cached.
def apply_cache_policy(faraday, endpoint)
ttl = @cache_policy.ttl_for(endpoint)
faraday.use :http_cache, store: @cache_policy.cache_store, expire_after: ttl
end

# Handles any Faraday errors.
#
# @param error [Faraday::Error] The error raised by Faraday.
# @raise [StandardError] Raises the original error with full context.
def handle_faraday_error(error)
raise StandardError, "Faraday error: #{error.message}"
end
end
end
end
4 changes: 4 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
end

require 'api_wrapper'
require 'webmock/rspec'

# Disable external requests to ensure all requests are stubbed
WebMock.disable_net_connect!(allow_localhost: true)

RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
Expand Down
29 changes: 29 additions & 0 deletions spec/unit/http_client/base_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

require 'spec_helper'
require 'api_wrapper/http_client/base_client'

RSpec.describe ApiWrapper::HttpClient::BaseClient do
let(:base_url) { 'https://api.example.com' }
let(:cache_policy) { double('CachePolicy') }

subject { described_class.new(base_url, cache_policy) }

describe '#initialize' do
it 'sets the base_url' do
expect(subject.instance_variable_get(:@base_url)).to eq(base_url)
end

it 'sets the cache_policy' do
expect(subject.instance_variable_get(:@cache_policy)).to eq(cache_policy)
end
end

describe '#get' do
it 'raises NotImplementedError when calling get' do
expect do
subject.get('/endpoint')
end.to raise_error(NotImplementedError, 'Subclasses must implement the get method')
end
end
end
106 changes: 106 additions & 0 deletions spec/unit/http_client/faraday_client_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# frozen_string_literal: true

# spec/nse_data/http_client/faraday_client_spec.rb
require 'spec_helper'
require 'api_wrapper/http_client/faraday_client'
require 'api_wrapper/cache/cache_policy'
require 'api_wrapper/cache/cache_store'

RSpec.describe ApiWrapper::HttpClient::FaradayClient do
let(:base_url) { 'https://www.nseindia.com/api/' }
let(:cache_store) { ApiWrapper::Cache::CacheStore.new }
let(:cache_policy) { ApiWrapper::Cache::CachePolicy.new(cache_store) }
let(:client) { described_class.new(base_url, cache_policy) }

describe '#get' do
let(:endpoint) { 'special-preopen-listing' }
let(:response_body) { { 'status' => 'success' }.to_json }

context 'when a connection is successful and data is cached' do
it 'returns the cached response if available' do
# Cache the response
cache_store.write(endpoint, response_body, 300)

result = client.get(endpoint)
expect(JSON.parse(result.body)).to eq('status' => 'success')
end

it 'fetches fresh data if cache is expired' do
# Store data in cache but simulate it being expired by setting TTL to 0
cache_store.write(endpoint, response_body, 0)

stub_request(:get, "#{base_url}#{endpoint}")
.with(
headers: {
'Accept' => 'application/json',
'User-Agent' => 'ApiWrapperClient/1.0'
}
)
.to_return(status: 200, body: response_body, headers: {})

result = client.get(endpoint)
expect(JSON.parse(result.body)).to eq('status' => 'success')
end
end

context 'when a connection is successful but data is not cached' do
it 'fetches fresh data and stores it in the cache' do
# Ensure cache is empty
expect(cache_store.read(endpoint)).to be_nil

# Simulate a successful API response
stub_request(:get, "#{base_url}#{endpoint}")
.with(
headers: {
'Accept' => 'application/json',
'User-Agent' => 'ApiWrapperClient/1.0'
}
)
.to_return(status: 200, body: response_body, headers: {})

result = client.get(endpoint)
expect(JSON.parse(result.body)).to eq('status' => 'success')

# Check if the data is cached
cached_data = cache_store.read(endpoint)
expect(JSON.parse(cached_data)).to eq('status' => 'success')
end
end

context 'when a connection failure occurs' do
it 'raises a Faraday::Error' do
endpoint = 'some-endpoint'

# Simulate a Faraday connection failure
allow_any_instance_of(Faraday::Connection).to receive(:get)
.and_raise(Faraday::Error.new('Connection failed'))

expect { client.get(endpoint) }.to raise_error(StandardError, 'Faraday error: Connection failed')
end
end

context 'when force_refresh is true' do
it 'bypasses the cache and fetches fresh data' do
# Cache the response
cache_store.write(endpoint, response_body, 300)

# Simulate a successful API response
stub_request(:get, "#{base_url}#{endpoint}")
.with(
headers: {
'Accept' => 'application/json',
'User-Agent' => 'ApiWrapperClient/1.0'
}
)
.to_return(status: 200, body: response_body, headers: {})

result = client.get(endpoint, force_refresh: true)
expect(JSON.parse(result.body)).to eq('status' => 'success')

# Ensure cache is still there but fresh data was fetched
cached_data = cache_store.read(endpoint)
expect(JSON.parse(cached_data)).to eq('status' => 'success')
end
end
end
end
Loading