From c8ac9b0ab2fc0310532e9b5084de648c5504b17e Mon Sep 17 00:00:00 2001 From: MarketDataApp Date: Mon, 19 Feb 2024 12:48:32 -0300 Subject: [PATCH] simplified client code to use a singleton client --- README.md | 2 +- baseRequest.go | 23 ++-- client.go | 279 +++++++++++++++++++++++++++------------------- client_helper.go | 4 - client_test.go | 49 ++++---- endpoints.go | 3 + logging_test.go | 2 + stocks_candles.go | 7 +- 8 files changed, 200 insertions(+), 169 deletions(-) diff --git a/README.md b/README.md index b40114a..73f7de5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ [![License](https://img.shields.io/github/license/MarketDataApp/sdk-go.svg)](https://github.com/MarketDataApp/sdk-go/blob/master/LICENSE) ![SDK Version](https://img.shields.io/badge/version-1.0.0-blue.svg) ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/MarketDataApp/sdk-go) -![Lines of Code](https://img.shields.io/badge/lines_of_code-8585-blue) +![Lines of Code](https://img.shields.io/badge/lines_of_code-8608-blue) #### Connect With The Market Data Community diff --git a/baseRequest.go b/baseRequest.go index 89231aa..4a141e9 100644 --- a/baseRequest.go +++ b/baseRequest.go @@ -211,6 +211,11 @@ func (br *baseRequest) getPath() (string, error) { if br == nil { return "", fmt.Errorf("path is nil") } + + if br.path == "" { + return "", fmt.Errorf("path is empty") + } + return br.path, nil } @@ -246,29 +251,17 @@ func (br *baseRequest) getError() error { return br.Error } -// Raw executes the request and returns the raw resty.Response. This method allows for an optional MarketDataClient -// to be passed which, if provided, replaces the client used in the request. -// -// # Parameters -// -// - ...*MarketDataClient: A variadic parameter that can accept zero or one MarketDataClient pointer. If provided, -// the first MarketDataClient in the slice replaces the current client for this request. +// Raw executes the request and returns the raw resty.Response. // // # Returns // // - *resty.Response: The raw response from the executed request. -// - error: An error object if the baseRequest is nil, the MarketDataClient is nil, or if an error occurs during the request execution. -func (request *baseRequest) Raw(optionalClients ...*MarketDataClient) (*resty.Response, error) { +// - error: An error object if the baseRequest is nil, or if an error occurs during the request execution. +func (request *baseRequest) Raw() (*resty.Response, error) { if request == nil { return nil, fmt.Errorf("baseRequest is nil") } - // Replace the client if an optional client is provided - if len(optionalClients) > 0 && optionalClients[0] != nil { - request.client = optionalClients[0] - } - - // Check if the client is nil after potentially replacing it if request.client == nil { return nil, fmt.Errorf("MarketDataClient is nil") } diff --git a/client.go b/client.go index 45a5fcc..88f0a70 100644 --- a/client.go +++ b/client.go @@ -5,11 +5,11 @@ // // # Get Started Quickly with the MarketDataClient // -// 1. Use [GetClient] to fetch the [MarketDataClient] instance and set the API token. -// 2. Turn on Debug mode to log detailed request and response information to disk as you learn how to use the SDK. -// 3. Make a test request. -// 4. Check the rate limit in the client to keep track of your requests. -// 5. Check the in-memory logs to see the raw request and response details. +// 1. Use [GetClient] to fetch the [MarketDataClient] instance and set the API token. +// 2. Turn on Debug mode to log detailed request and response information to disk as you learn how to use the SDK. +// 3. Make a test request. +// 4. Check the rate limit in the client to keep track of your requests. +// 5. Check the in-memory logs to see the raw request and response details. // // [Market Data Go Client]: https://www.marketdata.app/docs/sdk/go/client package client @@ -30,6 +30,31 @@ import ( _ "github.com/joho/godotenv/autoload" ) +// Environment represents the type for different environments in which the MarketDataClient can operate. +// Customers do not need to set the environment. The [MarketDataClient] will automatically be initialized with a Production +// environment if no environment is set. +// +// Market Data's Go Client supports three environments: +// +// 1. Production +// 2. Test +// 3. Development. +// +// It is used to configure the client to point to the appropriate base URL depending on the environment. +// This is used for development or testing by Market Data employees. +type Environment string + +const ( + // Production specifies the production environment. It is used when the client is interacting with the live Market Data API. + Production Environment = "prod" + + // Test specifies the testing environment. It is used for testing purposes, allowing interaction with a sandbox version of the Market Data API. + Test Environment = "test" + + // Development specifies the development environment. It is typically used during the development phase, pointing to a local or staged version of the Market Data API. + Development Environment = "dev" +) + // MarketDataClient struct defines the structure for the MarketData client instance. // It embeds the resty.Client to inherit the HTTP client functionalities. // Additionally, it includes fields for managing rate limits and synchronization, @@ -38,10 +63,10 @@ import ( // // # Setter Methods // -// - Debug(bool) *MarketDataClient: Enables or disables debug mode for logging detailed request and response information. -// - Environment(string) *MarketDataClient: Sets the environment for the MarketDataClient. -// - Timeout(int) *MarketDataClient: Sets the request timeout for the MarketDataClient. -// - Token(string) *MarketDataClient: Sets the authentication token for the MarketDataClient. +// - Debug(bool): Enables or disables debug mode for logging detailed request and response information. +// - Environment(Environment): Sets the environment for the MarketDataClient. +// - Timeout(int): Sets the request timeout for the MarketDataClient. +// - Token(string) error: Sets the authentication token for the MarketDataClient. // // # Methods // @@ -53,8 +78,17 @@ type MarketDataClient struct { RateLimitRemaining int // RateLimitRemaining tracks the number of requests that can still be made before hitting the rate limit. RateLimitReset time.Time // RateLimitReset indicates the time when the rate limit will be reset. mu sync.Mutex // mu is used to ensure thread-safe access to the client's fields. - Error error // Error captures any errors that occur during the execution of API calls. - debug bool // debug indicates whether debug mode is enabled, controlling the verbosity of logs. + debug bool // Debug indicates whether debug mode is enabled, controlling the verbosity of logs. +} + +// Debug enables or disables debug mode for the MarketDataClient. When debug mode is enabled, the client logs detailed request and response information, +// which can be useful for development and troubleshooting. +// +// # Parameters +// +// - bool: A boolean value indicating whether to enable or disable debug mode. +func (c *MarketDataClient) Debug(enable bool) { + c.debug = enable } // RateLimitExceeded checks if the rate limit for API requests has been exceeded. @@ -90,7 +124,7 @@ func (c *MarketDataClient) RateLimitExceeded() bool { // calculates request latency, and constructs a log entry with these details. // If debug mode is enabled, the log entry is printed in a human-readable format. // Regardless of debug mode, the log entry is written to the log. -func (c *MarketDataClient) addLogFromRequestResponse(req *resty.Request, resp *resty.Response) { +func (c *MarketDataClient) addLogFromRequestResponse(req *resty.Request, resp *resty.Response) error { // Redact sensitive information from request headers. redactedHeaders := redactAuthorizationHeader(req.Header) // Extract response headers. @@ -98,16 +132,12 @@ func (c *MarketDataClient) addLogFromRequestResponse(req *resty.Request, resp *r // Attempt to extract rate limit consumed information from the response. rateLimitConsumed, err := getRateLimitConsumed(resp) if err != nil { - // If an error occurs, set the client's error field and return early. - c.Error = err - return + return err } // Attempt to extract the ray ID from the response. rayID, err := getRayIDFromResponse(resp) if err != nil { - // If an error occurs, set the client's error field and return early. - c.Error = err - return + return err } // Calculate the latency of the request. delay := getLatencyFromRequest(req) @@ -126,13 +156,18 @@ func (c *MarketDataClient) addLogFromRequestResponse(req *resty.Request, resp *r if logEntry != nil { logEntry.WriteToLog(c.debug) } + return nil } // getEnvironment determines the environment the client is operating in based on the host URL. // It parses the host URL to extract the hostname and matches it against predefined hostnames // for production, testing, and development environments. If a match is found, it returns the // corresponding environment name; otherwise, it defaults to "Unknown". -func (c *MarketDataClient) getEnvironment() string { +func (c *MarketDataClient) getEnvironment() Environment { + if c == nil || c.Client == nil { + return "Unknown" + } + u, err := url.Parse(c.Client.HostURL) // Parse the host URL to extract the hostname. if err != nil { log.Printf("Error parsing host URL: %v", err) // Log any error encountered during URL parsing. @@ -140,11 +175,11 @@ func (c *MarketDataClient) getEnvironment() string { } switch u.Hostname() { // Match the extracted hostname against predefined hostnames. case prodHost: - return prodEnv // Return the production environment name if matched. + return Production // Return the production environment name if matched. case testHost: - return testEnv // Return the testing environment name if matched. + return Test // Return the testing environment name if matched. case devHost: - return devEnv // Return the development environment name if matched. + return Development // Return the development environment name if matched. default: return "Unknown" // Default to "Unknown" if no matches are found. } @@ -156,6 +191,11 @@ func (c *MarketDataClient) getEnvironment() string { // // - string: A formatted string containing the client's environment, rate limit information, and rate limit reset time. func (c *MarketDataClient) String() string { + // Check if the MarketDataClient instance is nil + if c == nil { + return "MarketDataClient instance is nil" + } + clientType := c.getEnvironment() // Determine the client's environment. // Format and return the string representation. return fmt.Sprintf("Client Type: %s, RateLimitLimit: %d, RateLimitRemaining: %d, RateLimitReset: %v", clientType, c.RateLimitLimit, c.RateLimitRemaining, c.RateLimitReset) @@ -188,7 +228,25 @@ func (c *MarketDataClient) setDefaultResetTime() { // # Returns // // - *MarketDataClient: A pointer to the newly created MarketDataClient instance with default configurations applied. -func new() *MarketDataClient { +func NewClient(token string) error { + client := newClient() + + // Set the client's token. + err := client.Token(token) + if err != nil { + return err + } + + // Set the global client if there are no errors + if err == nil { + marketDataClient = client + return nil + } + + return errors.New("error setting token") +} + +func newClient() *MarketDataClient { // Initialize a new MarketDataClient with default resty client and debug mode disabled. client := &MarketDataClient{ Client: resty.New(), @@ -199,7 +257,7 @@ func new() *MarketDataClient { client.setDefaultResetTime() // Set the client environment to production. - client.Environment(prodEnv) + client.Environment(Production) // Set the "User-Agent" header to include the SDK version. client.Client.SetHeader("User-Agent", "sdk-go/"+Version) @@ -228,26 +286,9 @@ func new() *MarketDataClient { return nil }) - // Return the initialized MarketDataClient instance. return client } -// Debug is a method that enables or disables the debug mode of the client. -// Debug mode will result in the request and response headers being printed to -// the terminal with each request. -// -// # Parameters -// -// - enable: A boolean value indicating whether to enable or disable debug mode. By default, debug mode is disabled. -// -// # Returns -// -// - *MarketDataClient: A pointer to the MarketDataClient instance, allowing for method chaining. -func (c *MarketDataClient) Debug(enable bool) *MarketDataClient { - c.debug = enable - return c -} - // Timeout sets the request timeout for the MarketDataClient. // // This method allows users to specify a custom timeout duration for all HTTP requests @@ -423,50 +464,25 @@ func (c *MarketDataClient) getRawResponse(br *baseRequest) (*resty.Response, err return c.prepareAndExecuteRequest(br, nil) } -// GetClient initializes and returns a singleton instance of MarketDataClient. -// If a token is provided as an argument, it creates a new client instance with that token. -// If no token is provided, it attempts to use a token from the environment variable "MARKETDATA_TOKEN". -// This function ensures that only one instance of the client is active at any time, -// reusing the existing instance if no new token is provided and no errors are present in the current client. -// -// # Parameters -// -// - ...string: A variadic string parameter where the first element, if provided, is used as the authentication token for the [MarketDataClient]. If not provided, the function looks for a token in the "MARKETDATA_TOKEN" environment variable. +// GetClient checks for an existing instance of MarketDataClient and returns it. +// If the client is not already initialized, it attempts to initialize it. // // # Returns // -// - *MarketDataClient: A pointer to the initialized MarketDataClient instance. This client is configured with the provided or environment-sourced token. -// - error: An error object that indicates a failure in client initialization. Possible errors include missing token (if no token is provided and none is found in the environment) and any errors encountered during the client's token configuration process. -func GetClient(token ...string) (*MarketDataClient, error) { - if len(token) == 0 { - if marketDataClient != nil { - if marketDataClient.Error != nil { - return nil, marketDataClient.Error - } - return marketDataClient, nil +// - *MarketDataClient: A pointer to the existing or newly initialized MarketDataClient instance. +// - error: An error object if the client cannot be initialized. +func GetClient() (*MarketDataClient, error) { + // Check if the global client exists + if marketDataClient == nil { + // Attempt to initialize the client if it's not already + err := tryNewClient() + if err != nil { + return nil, err // Return the error if client initialization fails } - token = append(token, os.Getenv("MARKETDATA_TOKEN")) - } - - if token[0] == "" { - return nil, errors.New("no token provided") } - // Always create a new client when a token is provided - client := new() - if client.Error != nil { - return nil, client.Error - } - - client.Token(token[0]) - if client.Error != nil { - return nil, client.Error - } - - // Save the new client to the global variable if no errors are present - marketDataClient = client - - return client, nil + // Return the global client if it is initialized + return marketDataClient, nil } // Environment configures the base URL of the MarketDataClient based on the provided environment string. @@ -481,45 +497,63 @@ func GetClient(token ...string) (*MarketDataClient, error) { // - *MarketDataClient: A pointer to the *MarketDataClient instance with the configured environment. This allows for method chaining. // // If an invalid environment is provided, the client's Error field is set, and the same instance is returned. -func (c *MarketDataClient) Environment(env string) *MarketDataClient { +func (c *MarketDataClient) Environment(env Environment) error { + if c == nil || c.Client == nil { + return errors.New("MarketDataClient is nil") + } + var baseURL string switch env { - case prodEnv: + case Production: baseURL = prodProtocol + "://" + prodHost // Set baseURL for production environment - case testEnv: + case Test: baseURL = testProtocol + "://" + testHost // Set baseURL for testing environment - case devEnv: + case Development: baseURL = devProtocol + "://" + devHost // Set baseURL for development environment default: - c.Error = fmt.Errorf("invalid environment: %s", env) // Set error for invalid environment - return c + return fmt.Errorf("invalid environment: %s", env) // Set error for invalid environment } c.Client.SetBaseURL(baseURL) // Configure the client with the determined baseURL - return c + return nil +} +func tryNewClient() error { + // Default to Production if MARKETDATA_ENV is empty, doesn't exist, or is not a valid option + token := os.Getenv("MARKETDATA_TOKEN") // Retrieve the market data token from environment variables + + if token != "" { + err := NewClient(token) + + if err != nil { + return err + } + return nil + + } + return errors.New("env variable MARKETDATA_TOKEN not set") } -// init initializes the global marketDataClient with a token and environment fetched from environment variables. -// It retrieves the "MARKETDATA_TOKEN" variable and uses it to configure the marketDataClient. // It also attempts to retrieve the "MARKETDATA_ENV" variable. If "MARKETDATA_ENV" is empty, doesn't exist, or doesn't use a valid option, it defaults to prodEnv. // A new MarketDataClient instance is created and configured with the environment and token, then assigned to the global marketDataClient variable. func init() { - token := os.Getenv("MARKETDATA_TOKEN") // Retrieve the market data token from environment variables - env := os.Getenv("MARKETDATA_ENV") // Attempt to retrieve the environment from environment variables + envValue := os.Getenv("MARKETDATA_ENV") + env := Environment(envValue) // Convert the string value to Environment type - // Default to prodEnv if MARKETDATA_ENV is empty, doesn't exist, or is not a valid option - if env != prodEnv && env != testEnv && env != devEnv { - env = prodEnv + // Default to Production if MARKETDATA_ENV is empty, doesn't exist, or is not a valid option + if env != Production && env != Test && env != Development { + env = Production } - // Proceed only if the token is not empty - if token != "" { - // Create and configure a new MarketDataClient instance with the environment and token - client := new().Environment(env).Token(token) - if client.Error != nil { - marketDataClient = client - } + err := tryNewClient() + if err != nil { + fmt.Println("Error initializing MarketDataClient:", err) + return + } + + // Assign the environment to the global marketDataClient after successful initialization + if marketDataClient != nil { + marketDataClient.Environment(env) } } @@ -533,30 +567,43 @@ func init() { // // # Returns // -// - *MarketDataClient: A pointer to the MarketDataClient instance with the configured authentication token, which allows for method chaining. +// - error: An error if the authorization was not successful or nil if it was. // -// If an error occurs during the initial request or if the response indicates a failure, the client's Error field is set, -// and the same instance is returned. -func (c *MarketDataClient) Token(bearerToken string) *MarketDataClient { - // Set the authentication scheme to "Bearer" - c.Client.SetAuthScheme("Bearer") +// # Notes +// +// If an error occurs during the initial authorization request or if the response indicates a failure, the client remains unmodified. The token will only be set if authorization is successful. +func (c *MarketDataClient) Token(bearerToken string) error { + if c == nil || c.Client == nil { + return fmt.Errorf("MarketDataClient is nil") + } - // Set the authentication token - c.Client.SetAuthToken(bearerToken) + // Create a temporary client to make the initial request without modifying the original client + tempClient := resty.New().SetAuthScheme("Bearer").SetAuthToken(bearerToken) - // Make an initial request to authorize the token and load the rate limit information - resp, err := c.Client.R().Get("https://api.marketdata.app/user/") + // Make an initial request to authorize the token + resp, err := tempClient.R().Get(user_endpoint) if err != nil { - c.Error = err // Set error if there's an issue with the request - return c + return err // Return error if there's an issue with the request } if !resp.IsSuccess() { - err = fmt.Errorf("received non-OK status: %s", resp.Status()) // Create error for non-successful response - c.Error = err - return c + return fmt.Errorf("invalid token. received non-OK status: %s", resp.Status()) // Return error for non-successful response } - return c + // If the token is valid, set the authentication scheme and token on the original client + c.Client.SetAuthScheme("Bearer") + c.Client.SetAuthToken(bearerToken) + + // Make a second request to load the rate limit information + resp, err = c.Client.R().Get(user_endpoint) + if err != nil { + return err + } + + if resp.IsSuccess() { + return nil + } + + return fmt.Errorf("invalid token. received non-OK status: %s", resp.Status()) // Return error for non-successful response } // GetLogs retrieves a pointer to the HttpRequestLogs instance, allowing access to the logs collected during HTTP requests. diff --git a/client_helper.go b/client_helper.go index 9407f5c..7e1a914 100644 --- a/client_helper.go +++ b/client_helper.go @@ -15,10 +15,6 @@ var marketDataClient *MarketDataClient const ( Version = "1.1.0" // Version specifies the current version of the SDK. - prodEnv = "prod" // prodEnv is the environment name for production. - testEnv = "test" // testEnv is the environment name for testing. - devEnv = "dev" // devEnv is the environment name for development. - prodHost = "api.marketdata.app" // prodHost is the hostname for the production environment. testHost = "tst.marketdata.app" // testHost is the hostname for the testing environment. devHost = "localhost" // devHost is the hostname for the development environment. diff --git a/client_test.go b/client_test.go index 8c2d470..c6e2064 100644 --- a/client_test.go +++ b/client_test.go @@ -1,31 +1,28 @@ package client import ( - "os" "testing" - "github.com/go-resty/resty/v2" _ "github.com/joho/godotenv/autoload" ) func TestGetClient(t *testing.T) { // Generate a new client with the actual token - client, err := GetClient(os.Getenv("MARKETDATA_TOKEN")) + + client, err := GetClient() if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Errorf("Expected no error with token, got %v", err) } if client == nil { t.Errorf("Expected a client, got nil") } - // Generate a new client with an invalid token - client, err = GetClient("invalid_token") + client_err := newClient() + + err = client_err.Token("invalid_token") if err == nil { t.Errorf("Expected an error, got nil") } - if client != nil { - t.Errorf("Expected nil, got a client") - } } // TestGetEnvironment tests the getEnvironment method for various host URLs. @@ -34,22 +31,22 @@ func TestGetEnvironment(t *testing.T) { tests := []struct { name string hostURL string - expected string + expected Environment }{ { name: "Production Environment", hostURL: "https://api.marketdata.app", - expected: prodEnv, + expected: Production, }, { name: "Testing Environment", hostURL: "https://tst.marketdata.app", - expected: testEnv, + expected: Test, }, { name: "Development Environment", hostURL: "http://localhost", - expected: devEnv, + expected: Development, }, { name: "Unknown Environment", @@ -61,12 +58,10 @@ func TestGetEnvironment(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Create a new MarketDataClient instance - client := &MarketDataClient{ - Client: resty.New(), - } + client := newClient() // Set the HostURL to the test case's host URL - client.Client.SetHostURL(tc.hostURL) + client.Client.SetBaseURL(tc.hostURL) // Call getEnvironment and check the result result := client.getEnvironment() @@ -82,25 +77,25 @@ func TestEnvironmentMethod(t *testing.T) { // Define test cases tests := []struct { name string - environment string + environment Environment expectedURL string expectError bool }{ { name: "Set Production Environment", - environment: prodEnv, + environment: Production, expectedURL: prodProtocol + "://" + prodHost, expectError: false, }, { name: "Set Testing Environment", - environment: testEnv, + environment: Test, expectedURL: testProtocol + "://" + testHost, expectError: false, }, { name: "Set Development Environment", - environment: devEnv, + environment: Development, expectedURL: devProtocol + "://" + devHost, expectError: false, }, @@ -114,20 +109,20 @@ func TestEnvironmentMethod(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - // Create a new MarketDataClient instance - client := new() + // Get the MarketDataClient instance + client := newClient() // Set the environment using the Environment method - client = client.Environment(tc.environment) + err := client.Environment(tc.environment) // Check if an error was expected if tc.expectError { - if client.Error == nil { + if err == nil { t.Errorf("Expected an error for environment %s, but got none", tc.environment) } } else { - if client.Error != nil { - t.Errorf("Did not expect an error for environment %s, but got: %v", tc.environment, client.Error) + if err != nil { + t.Errorf("Did not expect an error for environment %s, but got: %v", tc.environment, err) } // Verify that the baseURL was set correctly diff --git a/endpoints.go b/endpoints.go index 5068528..234dee1 100644 --- a/endpoints.go +++ b/endpoints.go @@ -1,5 +1,8 @@ package client +// user_endpoint is the endpoint for the user info API call. +var user_endpoint = "https://api.marketdata.app/user/" + // endpoints maps API calls to their corresponding endpoints. var endpoints = map[int]map[string]map[string]string{ 1: { diff --git a/logging_test.go b/logging_test.go index 5b85d2c..10689b8 100644 --- a/logging_test.go +++ b/logging_test.go @@ -9,6 +9,7 @@ import ( func TestLogging(t *testing.T) { // Initialize the MarketData client client, err := GetClient() + client.Debug(true) if err != nil { log.Fatalf("Failed to get market data client: %v", err) } @@ -17,6 +18,7 @@ func TestLogging(t *testing.T) { sc, err := StockCandles().Resolution("D").Symbol("AAPL").Date("2023-01-03").Raw() if err != nil { fmt.Print(err) + fmt.Print(client) t.FailNow() } diff --git a/stocks_candles.go b/stocks_candles.go index bd2ebbe..a09d60d 100644 --- a/stocks_candles.go +++ b/stocks_candles.go @@ -268,12 +268,7 @@ func (scr *StockCandlesRequest) Raw() (*resty.Response, error) { } // Packed sends the StockCandlesRequest and returns the StockCandlesResponse. -// An optional MarketDataClient can be passed to replace the client used in the request. -// -// # Parameters -// -// - ...*MarketDataClient: A variadic parameter that can accept zero or one MarketDataClient pointer. If a client is provided, it replaces the current client for this request. -// +//// // # Returns // // - *models.StockCandlesResponse: A pointer to the StockCandlesResponse obtained from the request.