From 2beaf2db32a9a5e65c40583b293be71f838fcfdb Mon Sep 17 00:00:00 2001 From: Sonu Saha Date: Tue, 17 Sep 2024 12:39:02 +0530 Subject: [PATCH 1/7] feat: use api_wrapper to perform api operations Signed-off-by: Sonu Saha --- lib/nse_data.rb | 57 ++++++++++------------------- lib/nse_data/nse_api_client.rb | 65 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 38 deletions(-) create mode 100644 lib/nse_data/nse_api_client.rb diff --git a/lib/nse_data.rb b/lib/nse_data.rb index 5c0e4d3..6ea0cde 100644 --- a/lib/nse_data.rb +++ b/lib/nse_data.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true require_relative 'nse_data/version' -require_relative 'nse_data/api_manager' -require_relative 'nse_data/config/logger' +require_relative 'nse_data/nse_api_client' # The NseData module serves as the namespace for the NSE Data gem, # which provides an interface to interact with and retrieve stock market data @@ -11,12 +10,16 @@ module NseData class Error < StandardError; end class << self - # This module provides functionality for accessing NSE data. + # Caches the instance of NseApiClient. + def nse_api_client + @nse_api_client ||= NseApiClient.new + end + + # Dynamically define fetch methods for each API endpoint. def define_api_methods - api_manager = APIManager.new - api_manager.endpoints.each_key do |method_name| - define_singleton_method("fetch_#{method_name}") do - api_manager.fetch_data(method_name).body + nse_api_client.endpoints.each_key do |method_name| + define_singleton_method("fetch_#{method_name}") do |force_refresh: false| + nse_api_client.fetch_data(method_name, force_refresh:) end end end @@ -25,41 +28,19 @@ def define_api_methods # # @return [Array] An array of endpoint names. def list_all_endpoints - @list_all_endpoints ||= APIManager.new.load_endpoints - end - - # Configure the logger for the NseData gem. - # - # This method allows users to customize the logger used throughout the library. - # To use it, call `NseData.configure` and provide a block to set up the logger. - # - # Example: - # - # NseData.configure do |config| - # custom_logger = Logger.new('nse_data.log') - # custom_logger.level = Logger::DEBUG - # config.logger = custom_logger - # end - # - # @yieldparam [NseData::Config::Logger] config The configuration object to be customized. - def configure - @logger_config ||= Config::Logger.new - yield(@logger_config) if block_given? + nse_api_client.endpoints end - # Access the configured logger. + # Fetches data from a specific API endpoint. # - # This method returns the Logger instance configured through `NseData.configure`. - # - # @return [Logger] The configured Logger instance. - # @raise [RuntimeError] If the logger has not been configured. - def logger - @logger_config&.logger || (raise 'Logger not configured. Please call NseData.configure first.') + # @param endpoint [String] The endpoint key. + # @param force_refresh [Boolean] Skip cache if true. + # @return [Hash, String] The API response. + def fetch_data(endpoint, force_refresh: false) + nse_api_client.fetch_data(endpoint, force_refresh:) end end - # Initialize configuration with default settings. - @logger_config = Config::Logger.new + # Define API methods at runtime. + define_api_methods end - -NseData.define_api_methods diff --git a/lib/nse_data/nse_api_client.rb b/lib/nse_data/nse_api_client.rb new file mode 100644 index 0000000..2865b32 --- /dev/null +++ b/lib/nse_data/nse_api_client.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'api_wrapper' + +module NseData + # Handles API interactions with the NSE using ApiWrapper. + class NseApiClient + attr_reader :api_configuration_path, :cache_store + + # Initializes with optional custom settings. + # + # @param api_configuration_path [String] Path to the API configuration file. + # @param cache_store [Object] Custom cache store for caching responses. + def initialize(api_configuration_path: nil, cache_store: nil) + @api_configuration_path = api_configuration_path || default_configuration_path + @cache_store = cache_store + configure_api_wrapper + end + + # Fetches data from the specified API endpoint. + # + # @param endpoint_key [String] Key for the API endpoint. + # @param force_refresh [Boolean] Skip cache if true. + # @return [Hash, String] Response data or raises an error if unsuccessful. + def fetch_data(endpoint_key, force_refresh: false) + response = ApiWrapper.fetch_data(endpoint_key, force_refresh:) + handle_response(response) + end + + # Returns all API endpoints available in the configuration. + # + # @return [Hash] List of endpoints. + def endpoints + @endpoints ||= ApiWrapper::ApiManager.new(api_configuration_path).endpoints + end + + private + + # Configures ApiWrapper with the provided settings. + def configure_api_wrapper + ApiWrapper.configure do |config| + config.api_configuration_path = api_configuration_path + config.cache_store = cache_store if cache_store + end + end + + # Processes the API response. + # + # @param response [Faraday::Response] The API response. + # @return [Hash, String] Parsed response or error message. + def handle_response(response) + raise ApiError, "Error: #{response.status} - #{response.body}" unless response.success? + + response.body + end + + # Default path to the API configuration file. + def default_configuration_path + File.join(__dir__, 'config', 'api_endpoints.yml') + end + end + + # Custom error class for handling API errors. + class ApiError < StandardError; end +end From b010df9d5e198614d8af43a9e95111abd8e94b63 Mon Sep 17 00:00:00 2001 From: Sonu Saha Date: Tue, 17 Sep 2024 12:39:32 +0530 Subject: [PATCH 2/7] specs: add fixtures for nse api client & updated nse_data Signed-off-by: Sonu Saha --- spec/fixtures/api_endpoints.yml | 11 +++++++++ spec/nse_api_client_spec.rb | 41 +++++++++++++++++++++++++++++++++ spec/nse_data_spec.rb | 39 +++++++++++-------------------- 3 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 spec/fixtures/api_endpoints.yml create mode 100644 spec/nse_api_client_spec.rb diff --git a/spec/fixtures/api_endpoints.yml b/spec/fixtures/api_endpoints.yml new file mode 100644 index 0000000..946a85b --- /dev/null +++ b/spec/fixtures/api_endpoints.yml @@ -0,0 +1,11 @@ +# spec/fixtures/api_configuration.yml +base_url: https://api.example.com/ +apis: + endpoint1: + path: 'path/to/endpoint1' + description: 'Endpoint 1 description' + no_cache: true + endpoint2: + path: 'path/to/endpoint2' + description: 'Endpoint 2 description' + ttl: 600 \ No newline at end of file diff --git a/spec/nse_api_client_spec.rb b/spec/nse_api_client_spec.rb new file mode 100644 index 0000000..3a6532d --- /dev/null +++ b/spec/nse_api_client_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'api_wrapper' +require_relative '../lib/nse_data/nse_api_client' + +RSpec.describe NseData::NseApiClient do + let(:api_config_path) { File.join(__dir__, 'fixtures', 'api_endpoints.yml') } + let(:endpoints) do + { + 'endpoint1' => { + 'path' => 'path/to/endpoint1', + 'description' => 'Endpoint 1 description', + 'no_cache' => true + } + } + end + let(:response_body) { { 'status' => 'success' }.to_json } + + subject(:client) { described_class.new(api_configuration_path: api_config_path) } + + describe '#endpoints' do + it 'returns the endpoints from ApiManager' do + expect(client.endpoints).to include endpoints + end + end + + describe '#fetch_date' do + it 'returns the body of the response when success' do + stub_request(:get, 'https://api.example.com/path/to/endpoint1') + .to_return(status: 200, body: response_body, headers: {}) + expect(client.fetch_data('endpoint1')).to eq(response_body) + end + + it 'raises an ApiError when response is not success' do + stub_request(:get, 'https://api.example.com/path/to/endpoint2') + .to_return(status: 404, body: {}.to_json, headers: {}) + expect { client.fetch_data('endpoint2') }.to raise_error(NseData::ApiError, 'Error: 404 - {}') + end + end +end diff --git a/spec/nse_data_spec.rb b/spec/nse_data_spec.rb index bee9335..34c152b 100644 --- a/spec/nse_data_spec.rb +++ b/spec/nse_data_spec.rb @@ -2,45 +2,34 @@ require 'spec_helper' require 'nse_data' -require 'nse_data/api_manager' +require 'nse_data/nse_api_client' RSpec.describe NseData do - let(:api_manager) { instance_double('NseData::APIManager') } - let(:endpoints) do - { - 'market_data' => { 'path' => '/market/data' }, - 'stock_summary' => { 'path' => '/stock/summary' } - } + let(:client) do + instance_double(NseData::NseApiClient, fetch_data: { key: 'value' }, + endpoints: { 'endpoint_key' => 'path/to/endpoint' }) end before do - allow(NseData::APIManager).to receive(:new).and_return(api_manager) - allow(api_manager).to receive(:load_endpoints).and_return(endpoints) - allow(api_manager).to receive(:fetch_data).and_return(double('Faraday::Response', body: '{}')) - allow(api_manager).to receive(:endpoints).and_return(endpoints) + allow(NseData).to receive(:nse_api_client).and_return(client) NseData.define_api_methods end describe '.define_api_methods' do - it 'dynamically defines methods for each endpoint' do - expect(NseData).to respond_to(:fetch_market_data) - expect(NseData).to respond_to(:fetch_stock_summary) + it 'dynamically defines fetch methods for each endpoint' do + expect(NseData).to respond_to(:fetch_endpoint_key) end + end - it 'calls the APIManager to fetch data when method is invoked' do - NseData.fetch_market_data - expect(api_manager).to have_received(:fetch_data).with('market_data') - - NseData.fetch_stock_summary - expect(api_manager).to have_received(:fetch_data).with('stock_summary') + describe '.list_all_endpoints' do + it 'returns a list of all endpoints' do + expect(NseData.list_all_endpoints).to eq({ 'endpoint_key' => 'path/to/endpoint' }) end end - describe '.list_all_endpoints' do - it 'returns a list of all available endpoints' do - expected_endpoints = { 'market_data' => { 'path' => '/market/data' }, - 'stock_summary' => { 'path' => '/stock/summary' } } - expect(NseData.list_all_endpoints).to eq(expected_endpoints) + describe '.fetch_data' do + it 'fetches data from the specified endpoint' do + expect(NseData.fetch_data('endpoint_key')).to eq({ key: 'value' }) end end end From 6e2ee97809cf35d870e8c3c920ceaabd0c1b82b5 Mon Sep 17 00:00:00 2001 From: Sonu Saha Date: Tue, 17 Sep 2024 12:40:30 +0530 Subject: [PATCH 3/7] gem: add api_wrapper in gemspec Signed-off-by: Sonu Saha --- nse_data.gemspec | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nse_data.gemspec b/nse_data.gemspec index 9a247b8..863ddac 100644 --- a/nse_data.gemspec +++ b/nse_data.gemspec @@ -29,6 +29,5 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_dependency 'faraday', '~> 2.11' - spec.add_dependency 'faraday-http-cache', '~> 2.5', '>= 2.5.1' + spec.add_dependency 'api_wrapper' end From e15e77f4128e760f5c801e8c1c5e96cfd3cf6175 Mon Sep 17 00:00:00 2001 From: Sonu Saha Date: Tue, 17 Sep 2024 12:41:18 +0530 Subject: [PATCH 4/7] fix: update yml file with updated values as per api_wrapper requirement Signed-off-by: Sonu Saha --- lib/nse_data/config/api_endpoints.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/nse_data/config/api_endpoints.yml b/lib/nse_data/config/api_endpoints.yml index ab92f25..08ba741 100644 --- a/lib/nse_data/config/api_endpoints.yml +++ b/lib/nse_data/config/api_endpoints.yml @@ -1,3 +1,4 @@ +base_url: https://www.nseindia.com/api/ apis: all_indices: path: "allIndices" From 1c5b1101c618c5c714ea2899019bb5e50926044d Mon Sep 17 00:00:00 2001 From: Sonu Saha Date: Tue, 17 Sep 2024 12:45:35 +0530 Subject: [PATCH 5/7] chore: remove files as it is moved to api_wrapper Signed-off-by: Sonu Saha --- lib/nse_data/api_manager.rb | 83 ------------ lib/nse_data/cache/README.md | 77 ------------ lib/nse_data/cache/cache_policy.rb | 118 ------------------ lib/nse_data/cache/cache_store.rb | 84 ------------- lib/nse_data/cache/redis_cache_store.rb | 3 - lib/nse_data/config/base.rb | 38 ------ lib/nse_data/config/logger.rb | 39 ------ lib/nse_data/http_client/base_client.rb | 26 ---- lib/nse_data/http_client/faraday_client.rb | 68 ---------- spec/nse_data/api_manager_spec.rb | 62 --------- spec/nse_data/cache/cache_policy_spec.rb | 51 -------- spec/nse_data/cache/cache_store_spec.rb | 79 ------------ .../http_client/faraday_client_spec.rb | 106 ---------------- 13 files changed, 834 deletions(-) delete mode 100644 lib/nse_data/api_manager.rb delete mode 100644 lib/nse_data/cache/README.md delete mode 100644 lib/nse_data/cache/cache_policy.rb delete mode 100644 lib/nse_data/cache/cache_store.rb delete mode 100644 lib/nse_data/cache/redis_cache_store.rb delete mode 100644 lib/nse_data/config/base.rb delete mode 100644 lib/nse_data/config/logger.rb delete mode 100644 lib/nse_data/http_client/base_client.rb delete mode 100644 lib/nse_data/http_client/faraday_client.rb delete mode 100644 spec/nse_data/api_manager_spec.rb delete mode 100644 spec/nse_data/cache/cache_policy_spec.rb delete mode 100644 spec/nse_data/cache/cache_store_spec.rb delete mode 100644 spec/nse_data/http_client/faraday_client_spec.rb diff --git a/lib/nse_data/api_manager.rb b/lib/nse_data/api_manager.rb deleted file mode 100644 index 555dc5b..0000000 --- a/lib/nse_data/api_manager.rb +++ /dev/null @@ -1,83 +0,0 @@ -# frozen_string_literal: true - -require 'yaml' -require_relative 'http_client/faraday_client' -require_relative 'cache/cache_policy' -require_relative 'cache/cache_store' - -module NseData - # APIManager class to handle API calls to NSE (National Stock Exchange) India website. - class APIManager - BASE_URL = 'https://www.nseindia.com/api/' - - # Initializes a new instance of the APIManager class. - # - # @param cache_store [CacheStore, RedisCacheStore, nil] The cache store to use for caching. - # If nil, in-memory cache is used. - def initialize(cache_store: nil) - # Initialize cache policy with the provided cache store or default to in-memory cache. - @cache_policy = NseData::Cache::CachePolicy.new(cache_store || NseData::Cache::CacheStore.new) - - # Configure cache policy (e.g., setting endpoints with no cache or custom TTL). - configure_cache_policy - - # Initialize Faraday client with the base URL and cache policy. - @client = NseData::HttpClient::FaradayClient.new(BASE_URL, @cache_policy) - - # Load API endpoints from the configuration file. - @endpoints = load_endpoints - end - - # Fetches data from the specified API endpoint. - # - # @param endpoint_key [String] The key of the API endpoint to fetch data from. - # @param force_refresh [Boolean] Whether to force refresh the data, bypassing the cache. - # @return [Faraday::Response] The response object containing the fetched data. - # @raise [ArgumentError] If the provided endpoint key is invalid. - def fetch_data(endpoint_key, force_refresh: false) - NseData.logger.debug("#{self.class}##{__method__}: fetching data for #{endpoint_key}") - endpoint = @endpoints[endpoint_key] - raise ArgumentError, "Invalid endpoint key: #{endpoint_key}" unless endpoint - - # Use cache policy to fetch data, with an option to force refresh. - @cache_policy.fetch(endpoint['path'], force_refresh:) do - @client.get(endpoint['path']) - end - end - - # Loads the API endpoints from the configuration file. - # - # @return [Hash] The hash containing the loaded API endpoints. - # @raise [RuntimeError] If the configuration file is missing or has syntax errors. - def load_endpoints - yaml_content = YAML.load_file(File.expand_path('config/api_endpoints.yml', __dir__)) - yaml_content['apis'] - rescue Errno::ENOENT => e - raise "Configuration file not found: #{e.message}" - rescue Psych::SyntaxError => e - raise "YAML syntax error: #{e.message}" - end - - # @return [Hash] The hash containing the loaded API endpoints. - attr_reader :endpoints - - private - - # Configures the cache policy with specific settings. - # - # This method sets endpoints that should not be cached and custom TTL values for specific endpoints. - def configure_cache_policy - # TODO: Review and refine cache policy for endpoints. - # Plan to analyze the API responses and categorize endpoints into: - # - No cache - # - Cacheable - # - Custom TTL - - # Set specific endpoints that should not be cached. - @cache_policy.add_no_cache_endpoint('market_status') - - # Set custom TTL (time-to-live) for specific endpoints. - @cache_policy.add_custom_ttl('equity_master', 600) # Custom TTL: 10 mins - end - end -end diff --git a/lib/nse_data/cache/README.md b/lib/nse_data/cache/README.md deleted file mode 100644 index 7a07e77..0000000 --- a/lib/nse_data/cache/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Caching Mechanism Usage and Customization - -## Overview - -The **NseData** gem includes a flexible caching mechanism to improve performance and reduce redundant API calls. This documentation provides an overview of how to use and customize the caching mechanism. - -## Cache Policy - -The `CachePolicy` class is responsible for managing caching behavior, including setting global TTLs, custom TTLs for specific endpoints, and controlling which endpoints should bypass the cache. - -### Initializing CachePolicy - -To use caching, you need to initialize the `CachePolicy` with a cache store and optional global TTL: - -```ruby -require 'nse_data/cache/cache_policy' -require 'nse_data/cache/cache_store' - -# Initialize the cache store (e.g., in-memory cache store) -cache_store = NseData::Cache::CacheStore.new - -# Initialize the CachePolicy with the cache store and a global TTL of 300 seconds (5 minutes) -cache_policy = NseData::Cache::CachePolicy.new(cache_store, global_ttl: 300) -``` - -### Configuring Cache Policy -#### Adding No-Cache Endpoints -You can specify endpoints that should bypass the cache: -```ruby -# Add an endpoint to the no-cache list -cache_policy.add_no_cache_endpoint('/no-cache') -``` - -#### Adding Custom TTLs -You can define custom TTLs for specific endpoints: - -```ruby -# Set a custom TTL of 600 seconds (10 minutes) for a specific endpoint -cache_policy.add_custom_ttl('/custom-ttl', ttl: 600) -``` - -#### Fetching Data with Cache -Use the fetch method to retrieve data with caching applied: - -```ruby -# Fetch data for an endpoint with optional cache -data = cache_policy.fetch('/some-endpoint') do - # The block should fetch fresh data if cache is not used or is stale - # e.g., perform an API call or other data retrieval operation - Faraday::Response.new(body: 'fresh data') -end -``` -## Custom Cache Stores -You can extend the caching mechanism to support different types of cache stores. Implement a custom cache store by inheriting from the CacheStore base class and overriding the read and write methods. - -### Example Custom Cache Store -```ruby -class CustomCacheStore < NseData::Cache::CacheStore - def read(key) - # Implement custom read logic - end - - def write(key, value, ttl) - # Implement custom write logic - end -end -``` -### Using a Custom Cache Store -To use a custom cache store, initialize CachePolicy with an instance of your custom cache store: - -```ruby -# Initialize the custom cache store -custom_cache_store = CustomCacheStore.new - -# Initialize CachePolicy with the custom cache store -cache_policy = NseData::Cache::CachePolicy.new(custom_cache_store, global_ttl: 300) -``` \ No newline at end of file diff --git a/lib/nse_data/cache/cache_policy.rb b/lib/nse_data/cache/cache_policy.rb deleted file mode 100644 index a605b6e..0000000 --- a/lib/nse_data/cache/cache_policy.rb +++ /dev/null @@ -1,118 +0,0 @@ -# frozen_string_literal: true - -module NseData - module Cache - # CachePolicy manages caching behavior, including cache storage and time-to-live (TTL) settings. - # - # It allows setting global TTLs, custom TTLs for specific endpoints, and controlling which - # endpoints should not use the cache. - # - # @attr_reader [CacheStore] cache_store The cache store used for storing cached data. - class CachePolicy - attr_reader :cache_store - - # Initializes the CachePolicy with a cache store and global TTL. - # - # @param cache_store [CacheStore, RedisCacheStore] The cache store to use for caching. - # @param global_ttl [Integer] The default TTL (in seconds) for caching. - def initialize(cache_store, global_ttl = 300) - @cache_store = cache_store - @global_ttl = global_ttl - @custom_ttls = {} - @no_cache_endpoints = [] - end - - # Adds an endpoint that should bypass the cache. - # - # @param endpoint [String] The endpoint to exclude from caching. - def add_no_cache_endpoint(endpoint) - @no_cache_endpoints << endpoint - end - - # Adds a custom TTL for a specific endpoint. - # - # @param endpoint [String] The endpoint to apply a custom TTL to. - # @param ttl [Integer] The custom TTL value in seconds. - def add_custom_ttl(endpoint, ttl = 300) - @custom_ttls[endpoint] = ttl - end - - # Returns the TTL for a specific endpoint. Defaults to the global TTL if no custom TTL is set. - # - # @param endpoint [String] The endpoint to fetch the TTL for. - # @return [Integer] The TTL in seconds. - def ttl_for(endpoint) - @custom_ttls.fetch(endpoint, @global_ttl) - end - - # Determines if caching should be used for the given endpoint. - # - # @param endpoint [String] The endpoint to check. - # @return [Boolean] True if caching is enabled for the endpoint, false otherwise. - def use_cache?(endpoint) - !@no_cache_endpoints.include?(endpoint) - end - - # Fetches the data for the given endpoint, using cache if applicable. - # - # @param endpoint [String] The endpoint to fetch data for. - # @param force_refresh [Boolean] Whether to force refresh the data, bypassing the cache. - # @yield The block that fetches fresh data if cache is not used or is stale. - # @return [Object] The data fetched from cache or fresh data. - def fetch(endpoint, force_refresh: false, &block) - if force_refresh || !use_cache?(endpoint) - fetch_fresh_data(endpoint, &block) - else - fetch_cached_or_fresh_data(endpoint, &block) - end - end - - private - - # Fetches fresh data and writes it to the cache if applicable. - # - # @param endpoint [String] The endpoint to fetch fresh data for. - # @yield The block that fetches fresh data. - # @return [Object] The fresh data. - def fetch_fresh_data(endpoint) - NseData.logger.debug("#{self.class}##{__method__}: fetching fresh data for #{endpoint}") - - fresh_data = yield - cache_fresh_data(endpoint, fresh_data) - fresh_data - end - - # Fetches cached data or fresh data if not available in the cache. - # - # @param endpoint [String] The endpoint to fetch data for. - # @yield The block that fetches fresh data if cache is not used or is stale. - # @return [Object] The cached or fresh data. - def fetch_cached_or_fresh_data(endpoint, &block) - cached_data = @cache_store.read(endpoint) - if cached_data - NseData.logger.debug("#{self.class}##{__method__}: fetching cached data for #{endpoint}") - Faraday::Response.new(body: cached_data) - else - fetch_fresh_data(endpoint, &block) - end - end - - # Writes fresh data to the cache. - # - # @param endpoint [String] The endpoint for which to store the data. - # @param fresh_data [Object] The data to be stored in the cache. - def cache_fresh_data(endpoint, fresh_data) - ttl = determine_ttl(endpoint) - @cache_store.write(endpoint, fresh_data.body, ttl) if fresh_data.is_a?(Faraday::Response) - end - - # Determines the TTL value for the given endpoint. - # - # @param endpoint [String] The endpoint to fetch the TTL for. - # @return [Integer] The TTL value in seconds. - def determine_ttl(endpoint) - @custom_ttls.fetch(endpoint, @global_ttl) - end - end - end -end diff --git a/lib/nse_data/cache/cache_store.rb b/lib/nse_data/cache/cache_store.rb deleted file mode 100644 index 19f16f9..0000000 --- a/lib/nse_data/cache/cache_store.rb +++ /dev/null @@ -1,84 +0,0 @@ -# frozen_string_literal: true - -module NseData - module Cache - # CacheStore class provides an in-memory caching mechanism. - class CacheStore - def initialize - @store = {} - end - - # Retrieves the cached data for the given key, or fetches fresh data if not cached or expired. - # - # @param key [String] The cache key. - # @param ttl [Integer] The time-to-live in seconds. - # @yield Fetches fresh data if cache is expired or not present. - # @return [Object] The cached data or the result of the block if not cached or expired. - def fetch(key, ttl) - if cached?(key, ttl) - @store[key][:data] - else - fresh_data = yield - store(key, fresh_data, ttl) - fresh_data - end - end - - # Reads data from the cache. - # - # @param key [String] The cache key. - # @return [Object, nil] The cached data, or nil if not present. - def read(key) - cached?(key) ? @store[key][:data] : nil - end - - # Writes data to the cache with an expiration time. - # - # @param key [String] The cache key. - # @param data [Object] The data to cache. - # @param ttl [Integer] The time-to-live in seconds. - def write(key, data, ttl) - store(key, data, ttl) - end - - # Deletes data from the cache. - # - # @param key [String] The cache key. - def delete(key) - @store.delete(key) - end - - private - - # Checks if the data for the given key is cached and not expired. - # - # @param key [String] The cache key. - # @param ttl [Integer] The time-to-live in seconds. - # @return [Boolean] Whether the data is cached and valid. - def cached?(key, ttl = nil) - return false unless @store.key?(key) - - !expired?(key, ttl) - end - - # Checks if the cached data for the given key has expired. - # - # @param key [String] The cache key. - # @param ttl [Integer] The time-to-live in seconds. - # @return [Boolean] Whether the cached data has expired. - def expired?(key, ttl) - stored_time = @store[key][:timestamp] - ttl && (Time.now - stored_time) >= ttl - end - - # Stores the data in the cache. - # - # @param key [String] The cache key. - # @param data [Object] The data to cache. - # @param ttl [Integer] The time-to-live in seconds. - def store(key, data, ttl) - @store[key] = { data:, timestamp: Time.now, ttl: } - end - end - end -end diff --git a/lib/nse_data/cache/redis_cache_store.rb b/lib/nse_data/cache/redis_cache_store.rb deleted file mode 100644 index f4cd5aa..0000000 --- a/lib/nse_data/cache/redis_cache_store.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -# TODO: Implement in near future :) diff --git a/lib/nse_data/config/base.rb b/lib/nse_data/config/base.rb deleted file mode 100644 index 7aa47eb..0000000 --- a/lib/nse_data/config/base.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module NseData - module Config - # Base serves as the base class for configuration settings. - # - # It allows for storing and accessing various configuration settings - # through a hash-like interface. - # - # @attr_reader [Hash] settings The hash storing configuration settings. - class Base - attr_reader :settings - - # Initializes the Base with optional initial settings. - # - # @param initial_settings [Hash] Optional hash of initial settings. - def initialize(initial_settings = {}) - @settings = initial_settings - end - - # Retrieves a configuration setting by key. - # - # @param key [Symbol, String] The key for the setting. - # @return [Object] The value associated with the key. - def [](key) - @settings[key] - end - - # Sets a configuration setting by key. - # - # @param key [Symbol, String] The key for the setting. - # @param value [Object] The value to set for the key. - def []=(key, value) - @settings[key] = value - end - end - end -end diff --git a/lib/nse_data/config/logger.rb b/lib/nse_data/config/logger.rb deleted file mode 100644 index 054421b..0000000 --- a/lib/nse_data/config/logger.rb +++ /dev/null @@ -1,39 +0,0 @@ -# frozen_string_literal: true - -require 'logger' -require 'tempfile' -require_relative 'base' - -module NseData - module Config - # Logger handles configuration specifically for logging. - # - # It inherits from Base to utilize the hash-like configuration - # interface and provides default settings for logging. - # - # @attr_reader [Logger] logger The Logger instance configured by this class. - class Logger < Base - # Creates a default temporary log file. - def self.default_log_file - return $stdout if ENV['NSE_DEV'] - - temp_file = Tempfile.new(['nse_data', '.log']) - temp_file.close # Close the file immediately after creation - temp_file.path # Return the file path for the Logger - end - - # Initializes the Logger with a default or custom Logger instance. - # - # @param logger [Logger] Optional custom Logger instance. - def initialize(logger = ::Logger.new(self.class.default_log_file)) - super() - @logger = logger - @settings[:logger] = logger - # puts "Log file created at: #{self.class.default_log_file}" - end - - # Retrieves/Sets the Logger instance. - attr_accessor :logger - end - end -end diff --git a/lib/nse_data/http_client/base_client.rb b/lib/nse_data/http_client/base_client.rb deleted file mode 100644 index fd464c4..0000000 --- a/lib/nse_data/http_client/base_client.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module NseData - 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 diff --git a/lib/nse_data/http_client/faraday_client.rb b/lib/nse_data/http_client/faraday_client.rb deleted file mode 100644 index 3977bbf..0000000 --- a/lib/nse_data/http_client/faraday_client.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'faraday' -require 'faraday-http-cache' -require 'json' -require_relative 'base_client' - -module NseData - 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) - NseData.logger.debug("#{self.class}##{__method__}: invoking the endpoint #{endpoint}") - # Use the cache policy to determine whether to fetch from cache or refresh. - @cache_policy.fetch(endpoint, 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 - NseData.logger.error("#{self.class}##{__method__}: exception is raised - #{e.message}") - handle_faraday_error(e) - end - - 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 - - def configure_faraday(faraday) - faraday.request :json - faraday.headers['User-Agent'] = 'NSEDataClient/1.0' - faraday.response :json - faraday.headers['Accept'] = 'application/json' - end - - 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 - - def handle_faraday_error(error) - raise error.message - end - end - end -end diff --git a/spec/nse_data/api_manager_spec.rb b/spec/nse_data/api_manager_spec.rb deleted file mode 100644 index 3522df6..0000000 --- a/spec/nse_data/api_manager_spec.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -# spec/api_manager_spec.rb -require 'spec_helper' -require 'yaml' -require 'nse_data/api_manager' -require 'nse_data/cache/cache_policy' -require 'nse_data/cache/cache_store' - -RSpec.describe NseData::APIManager do - let(:api_endpoints) do - { - 'endpoint1' => { 'path' => '/api/endpoint1' }, - 'endpoint2' => { 'path' => '/api/endpoint2' } - } - end - - let(:yaml_content) { { 'apis' => api_endpoints } } - let(:faraday_client) { instance_double('NseData::HttpClient::FaradayClient') } - let(:cache_store) { instance_double('NseData::Cache::CacheStore') } - let(:cache_policy) { instance_double('NseData::Cache::CachePolicy') } - - before do - allow(YAML).to receive(:load_file).and_return(yaml_content) - allow(NseData::HttpClient::FaradayClient).to receive(:new).with(any_args).and_return(faraday_client) - allow(NseData::Cache::CachePolicy).to receive(:new).and_return(cache_policy) - - # Mock the methods that are called in the configure_cache_policy - allow(cache_policy).to receive(:add_no_cache_endpoint).with('market_status') - allow(cache_policy).to receive(:add_custom_ttl).with('equity_master', 600) - end - - describe '#fetch_data' do - before do - allow(faraday_client).to receive(:get).with('/api/endpoint1').and_return('data1') - allow(faraday_client).to receive(:get).with('/api/endpoint2').and_return('data2') - allow(cache_policy).to receive(:fetch).with('/api/endpoint1', force_refresh: false).and_yield.and_return('data1') - allow(cache_policy).to receive(:fetch).with('/api/endpoint2', force_refresh: false).and_yield.and_return('data2') - end - - it 'fetches data from the correct endpoint using the cache policy' do - manager = described_class.new(cache_store:) - - expect(manager.fetch_data('endpoint1')).to eq('data1') - expect(manager.fetch_data('endpoint2')).to eq('data2') - end - - it 'raises an error for invalid endpoint keys' do - manager = described_class.new(cache_store:) - - expect { manager.fetch_data('invalid_key') }.to raise_error(ArgumentError, 'Invalid endpoint key: invalid_key') - end - - it 'forces cache refresh when force_refresh is true' do - manager = described_class.new(cache_store:) - - expect(cache_policy).to receive(:fetch).with('/api/endpoint1', force_refresh: true).and_yield.and_return('data1') - - expect(manager.fetch_data('endpoint1', force_refresh: true)).to eq('data1') - end - end -end diff --git a/spec/nse_data/cache/cache_policy_spec.rb b/spec/nse_data/cache/cache_policy_spec.rb deleted file mode 100644 index ebb8028..0000000 --- a/spec/nse_data/cache/cache_policy_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'nse_data/cache/cache_policy' -require 'nse_data/cache/cache_store' -require 'faraday' - -RSpec.describe NseData::Cache::CachePolicy do - let(:cache_store) { NseData::Cache::CacheStore.new } - let(:cache_policy) { described_class.new(cache_store) } - - before do - cache_policy.add_no_cache_endpoint('/no-cache') - cache_policy.add_custom_ttl('/custom-ttl', 600) # 10 minutes - end - - describe '#use_cache?' do - it 'returns true for endpoints that are not in the no-cache list' do - expect(cache_policy.use_cache?('/some-endpoint')).to be(true) - end - - it 'returns false for endpoints that are in the no-cache list' do - expect(cache_policy.use_cache?('/no-cache')).to be(false) - end - end - - describe '#fetch' do - it 'uses cache if available and not forced to refresh' do - # Simulate a Faraday::Response object - cached_response = Faraday::Response.new(body: 'cached data') - cache_policy.fetch('/some-endpoint') { cached_response } - result = cache_policy.fetch('/some-endpoint') - expect(result.body).to eq('cached data') - end - - it 'bypasses cache if force_refresh is true' do - cached_response = Faraday::Response.new(body: 'cached data') - cache_policy.fetch('/some-endpoint') { cached_response } - fresh_response = Faraday::Response.new(body: 'fresh data') - result = cache_policy.fetch('/some-endpoint', force_refresh: true) { fresh_response } - expect(result.body).to eq('fresh data') - end - - it 'uses custom TTL for specific endpoints' do - fresh_response = Faraday::Response.new(body: 'data with custom ttl') - cache_policy.fetch('/custom-ttl') { fresh_response } - result = cache_policy.fetch('/custom-ttl') - expect(result.body).to eq('data with custom ttl') - end - end -end diff --git a/spec/nse_data/cache/cache_store_spec.rb b/spec/nse_data/cache/cache_store_spec.rb deleted file mode 100644 index bb9e3cc..0000000 --- a/spec/nse_data/cache/cache_store_spec.rb +++ /dev/null @@ -1,79 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' -require 'nse_data/cache/cache_store' - -RSpec.describe NseData::Cache::CacheStore do - let(:cache_store) { described_class.new } - let(:key) { 'test_key' } - let(:data) { 'test_data' } - let(:ttl) { 5 } # 5 seconds TTL - - describe '#fetch' do - context 'when data is not cached' do - it 'fetches new data and stores it' do - result = cache_store.fetch(key, ttl) { data } - expect(result).to eq(data) - expect(cache_store.read(key)).to eq(data) - end - end - - context 'when data is cached' do - before do - cache_store.fetch(key, ttl) { data } - end - - it 'returns cached data within TTL' do - result = cache_store.fetch(key, ttl) { 'new_data' } - expect(result).to eq(data) - end - - it 'returns fresh data after TTL expires' do - # Use Timecop to freeze time for precise TTL testing - require 'timecop' - Timecop.travel(Time.now + ttl + 1) do - result = cache_store.fetch(key, ttl) { 'new_data' } - expect(result).to eq('new_data') - end - end - end - end - - describe '#read' do - context 'when data is cached' do - before do - cache_store.fetch(key, ttl) { data } - end - - it 'returns the cached data' do - expect(cache_store.read(key)).to eq(data) - end - end - - context 'when data is not cached' do - it 'returns nil' do - expect(cache_store.read(key)).to be_nil - end - end - end - - describe '#write' do - it 'stores data in the cache' do - cache_store.write(key, data, ttl) - expect(cache_store.read(key)).to eq(data) - end - end - - describe '#delete' do - context 'when data is cached' do - before do - cache_store.write(key, data, ttl) - end - - it 'removes the data from the cache' do - cache_store.delete(key) - expect(cache_store.read(key)).to be_nil - end - end - end -end diff --git a/spec/nse_data/http_client/faraday_client_spec.rb b/spec/nse_data/http_client/faraday_client_spec.rb deleted file mode 100644 index e5dd5db..0000000 --- a/spec/nse_data/http_client/faraday_client_spec.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -# spec/nse_data/http_client/faraday_client_spec.rb -require 'spec_helper' -require 'nse_data/http_client/faraday_client' -require 'nse_data/cache/cache_policy' -require 'nse_data/cache/cache_store' - -RSpec.describe NseData::HttpClient::FaradayClient do - let(:base_url) { 'https://www.nseindia.com/api/' } - let(:cache_store) { NseData::Cache::CacheStore.new } - let(:cache_policy) { NseData::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' => 'NSEDataClient/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' => 'NSEDataClient/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('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' => 'NSEDataClient/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 From 59f5a922bfb4fab6183be44fe03e6e9ce8a431d9 Mon Sep 17 00:00:00 2001 From: Sonu Saha Date: Wed, 18 Sep 2024 20:39:10 +0530 Subject: [PATCH 6/7] ci: update deprecated action Signed-off-by: Sonu Saha --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9218ad4..9636030 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ jobs: run: bundle exec rspec - name: Upload coverage report - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage/ From 0916fbf79051f134a9c2ec32a2e4f8153732e56c Mon Sep 17 00:00:00 2001 From: Sonu Saha Date: Wed, 18 Sep 2024 20:41:58 +0530 Subject: [PATCH 7/7] ci: temporarily disable upload of coverage report Signed-off-by: Sonu Saha --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9636030..1508b90 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,8 +32,8 @@ jobs: - name: Run tests run: bundle exec rspec - - name: Upload coverage report - uses: actions/upload-artifact@v4 - with: - name: coverage-report - path: coverage/ + # - name: Upload coverage report + # uses: actions/upload-artifact@v4 + # with: + # name: coverage-report + # path: coverage/