From 9d3ad1502114325b71b83e3aa384bb5392e757a4 Mon Sep 17 00:00:00 2001 From: Joe Pill Date: Thu, 20 Jun 2024 14:45:33 -0500 Subject: [PATCH] feat: add httpResponse to exceptions, modify request, shipengine interface --- ShipEngine/Models/ShipEngineException.cs | 8 +- ShipEngine/ShipEngine.cs | 369 ++++++++++++++++++++++- ShipEngine/ShipEngineClient.cs | 40 ++- 3 files changed, 401 insertions(+), 16 deletions(-) diff --git a/ShipEngine/Models/ShipEngineException.cs b/ShipEngine/Models/ShipEngineException.cs index 6d687e0a..b58bae78 100644 --- a/ShipEngine/Models/ShipEngineException.cs +++ b/ShipEngine/Models/ShipEngineException.cs @@ -1,6 +1,7 @@ #pragma warning disable 1591 using System; +using System.Net.Http; namespace ShipEngineSDK { @@ -32,18 +33,21 @@ public class ShipEngineException : Exception /// public ErrorCode ErrorCode { get; set; } + public HttpResponseMessage? Response { get; set; } public ShipEngineException( string message, ErrorSource errorSource = ErrorSource.Shipengine, ErrorType errorType = ErrorType.System, ErrorCode errorCode = ErrorCode.Unspecified, - string requestID = null) : base(message) + HttpResponseMessage? response = null, + string? requestId = null) : base(message) { ErrorSource = errorSource; ErrorType = errorType; ErrorCode = errorCode; - RequestId = requestID; + Response = response; + RequestId = requestId; } } } \ No newline at end of file diff --git a/ShipEngine/ShipEngine.cs b/ShipEngine/ShipEngine.cs index 4b9f6ad7..818cebd7 100644 --- a/ShipEngine/ShipEngine.cs +++ b/ShipEngine/ShipEngine.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using ShipEngineSDK.Common; +using ShipEngineSDK.Manifests; +using Result = ShipEngineSDK.ValidateAddresses.Result; using System; using System.Collections.Generic; using System.Net.Http; @@ -9,14 +11,23 @@ namespace ShipEngineSDK { + /// + /// Extension method to allow customized client configuration + /// public static class ShipEngineExtensions { + /// + /// Adds ShipEngine to the host builder and configures the client. + /// + /// + /// + /// public static IHostApplicationBuilder AddShipEngine(this IHostApplicationBuilder builder, Action? configureClient = null) { builder.Services.AddHttpClient(c => { var baseUri = builder.Configuration["ShipEngine:BaseUrl"] ?? "https://api.shipengine.com"; - var apiKey = builder.Configuration["ShipEngine:ApiKey"]; + var apiKey = builder.Configuration["ShipEngine:ApiKey"] ?? ""; ShipEngineClient.ConfigureHttpClient(c, apiKey, new Uri(baseUri)); configureClient?.Invoke(c); }); @@ -28,16 +39,18 @@ public static IHostApplicationBuilder AddShipEngine(this IHostApplicationBuilder /// /// Contains methods for interacting with the ShipEngine API. /// - public class ShipEngine : ShipEngineClient, IDisposable + public class ShipEngine : ShipEngineClient, IDisposable, IShipEngine { /// /// Global HttpClient for ShipEngine instance. /// + // ReSharper disable once InconsistentNaming public HttpClient _client; /// /// Global config for ShipEngine instance. /// + // ReSharper disable once InconsistentNaming public Config _config; /// @@ -69,14 +82,23 @@ public ShipEngine(HttpClient httpClient) : base() _client = httpClient; } - // - // Dispose of the ShipEngine client - // + /// + /// Dispose of the ShipEngine client + /// public void Dispose() { _client.Dispose(); } + /// + /// Modify the ShipEngine request + /// + public new ShipEngine ModifyRequest(Action modifyRequest) + { + base.ModifyRequest = modifyRequest; + return this; + } + /// /// Validates an address in nearly any country in the world. /// @@ -398,4 +420,341 @@ public void Dispose() return labelResult; } } + + /// + /// Mock implementation of IShipEngine + /// + public class ShipEngineMock : IShipEngine + { + /// + /// Validates an address in nearly any country in the world. + /// + /// The address to validate. This can even be an incomplete or improperly formatted address + /// An address validation result object + public virtual Task> ValidateAddresses(List
addresses) + { + return Task.FromResult(new List()); + } + + /// + /// Validates an address in nearly any country in the world. + /// + /// The address to validate. This can even be an incomplete or improperly formatted address + /// Configuration object that overrides the global config for this method call. + /// An address validation result object + public virtual Task> ValidateAddresses(List
addresses, Config methodConfig) + { + return Task.FromResult(new List()); + } + + /// + /// Retrieve a list of all carriers that have been added to this account + /// + /// A list of carriers + public virtual Task ListCarriers() + { + return Task.FromResult(new ListCarriers.Result()); + } + + /// + /// Retrieve a list of all carriers that have been added to this account + /// + /// Configuration object that overrides the global config for this method call. + /// A list of carriers + public virtual Task ListCarriers(Config methodConfig) + { + return Task.FromResult(new ListCarriers.Result()); + } + + /// + /// Create a manifest + /// + /// The details of the manifest you want to create. + /// + public virtual Task CreateManifest(Params manifestParams) + { + return Task.FromResult(new Manifests.Result()); + } + + /// + /// Create a manifest + /// + /// Configuration object that overrides the global config for this method call. + /// The details of the manifest you want to create. + /// + public virtual Task CreateManifest(Config methodConfig, Params manifestParams) + { + return Task.FromResult(new Manifests.Result()); + } + + /// + /// Void a label by ID to get a refund. + /// + /// The id of the label to void + /// Result object indicating the success of the void label attempt + public virtual Task VoidLabelWithLabelId(string labelId) + { + return Task.FromResult(new VoidLabelWithLabelId.Result()); + } + + /// + /// Void a label by ID to get a refund. + /// + /// The id of the label to void + /// Configuration object that overrides the global config for this method call + /// Result object indicating the success of the void label attempt + public virtual Task VoidLabelWithLabelId(string labelId, Config methodConfig) + { + return Task.FromResult(new VoidLabelWithLabelId.Result()); + } + + /// + /// Track a shipment using the label id + /// + /// The label id associated with the shipment + /// An object that contains the label id tracking information + public virtual Task TrackUsingLabelId(string labelId) + { + return Task.FromResult(new TrackUsingLabelId.Result()); + } + + /// + /// Track a shipment using the label id + /// + /// The label id associated with the shipment + /// Configuration object that overrides the global config for this method call + /// An object that contains the label id tracking information + public virtual Task TrackUsingLabelId(string labelId, Config methodConfig) + { + return Task.FromResult(new TrackUsingLabelId.Result()); + } + + /// + /// Tracks a package based on the trackingNumber and carrierCode. + /// + /// The tracking number of the package you wish to track. + /// The carrierCode for the trackingNumber you are using to track the package. + /// + public virtual Task TrackUsingCarrierCodeAndTrackingNumber(string trackingNumber, string carrierCode) + { + return Task.FromResult(new TrackUsingCarrierCodeAndTrackingNumber.Result()); + } + + /// + /// Tracks a package based on the trackingNumber and carrierCode. + /// + /// The tracking number of the package you wish to track. + /// The carrierCode for the trackingNumber you are using to track the package. + /// Configuration object that overrides the global config for this method call + /// + public virtual Task TrackUsingCarrierCodeAndTrackingNumber(string trackingNumber, string carrierCode, Config methodConfig) + { + return Task.FromResult(new TrackUsingCarrierCodeAndTrackingNumber.Result()); + } + + /// + /// Create a label from shipment details + /// + /// Details of the label that you want to create + /// Object containing the created label information + public virtual Task CreateLabelFromShipmentDetails(CreateLabelFromShipmentDetails.Params labelParams) + { + return Task.FromResult(new CreateLabelFromShipmentDetails.Result()); + } + + /// + /// Create a label from shipment details + /// + /// Details of the label that you want to create + /// Configuration object that overrides the global config for this method call + /// Object containing the created label information + public virtual Task CreateLabelFromShipmentDetails(CreateLabelFromShipmentDetails.Params labelParams, Config methodConfig) + { + return Task.FromResult(new CreateLabelFromShipmentDetails.Result()); + } + + /// + /// Create a label from a rate id + /// + /// The details of the rate that you want to use to purchase a label + /// Object containing the created label information + public virtual Task CreateLabelFromRate(CreateLabelFromRate.Params createLabelFromRateParams) + { + return Task.FromResult(new CreateLabelFromRate.Result()); + } + + /// + /// Create a label from a rate id + /// + /// The details of the rate that you want to use to purchase a label + /// Configuration object that overrides the global config for this method call + /// Object containing the created label information + public virtual Task CreateLabelFromRate(CreateLabelFromRate.Params createLabelFromRateParams, Config methodConfig) + { + return Task.FromResult(new CreateLabelFromRate.Result()); + } + + /// + /// Retrieve rates for a package with the provided shipment details. + /// + /// + /// The rates result + public virtual Task GetRatesWithShipmentDetails(GetRatesWithShipmentDetails.Params rateParams) + { + return Task.FromResult(new GetRatesWithShipmentDetails.Result()); + } + + /// + /// Retrieve rates for a package with the provided shipment details. + /// + /// + /// Configuration object that overrides the global config for this method call + /// The rates result + public virtual Task GetRatesWithShipmentDetails(GetRatesWithShipmentDetails.Params rateParams, Config methodConfig) + { + return Task.FromResult(new GetRatesWithShipmentDetails.Result()); + } + } + + + /// + /// Interface for ShipEngine + /// + public interface IShipEngine + { + /// + /// Validates an address in nearly any country in the world. + /// + /// The address to validate. This can even be an incomplete or improperly formatted address + /// An address validation result object + Task> ValidateAddresses(List
addresses); + + /// + /// Validates an address in nearly any country in the world. + /// + /// The address to validate. This can even be an incomplete or improperly formatted address + /// Configuration object that overrides the global config for this method call + /// An address validation result object + Task> ValidateAddresses(List
addresses, Config methodConfig); + + /// + /// Retrieve a list of all carriers that have been added to this account + /// + /// A list of carriers + Task ListCarriers(); + + /// + /// Retrieve a list of all carriers that have been added to this account + /// + /// Configuration object that overrides the global config for this method call. + /// A list of carriers + Task ListCarriers(Config methodConfig); + + /// + /// Create a manifest + /// + /// The details of the manifest you want to create. + /// + Task CreateManifest(Manifests.Params manifestParams); + + /// + /// Create a manifest + /// + /// Configuration object that overrides the global config for this method call. + /// The details of the manifest you want to create. + /// + Task CreateManifest(Config methodConfig, Manifests.Params manifestParams); + + /// + /// Void a label by ID to get a refund. + /// + /// The id of the label to void + /// Result object indicating the success of the void label attempt + Task VoidLabelWithLabelId(string labelId); + + /// + /// Void a label by ID to get a refund. + /// + /// The id of the label to void + /// Configuration object that overrides the global config for this method call + /// Result object indicating the success of the void label attempt + Task VoidLabelWithLabelId(string labelId, Config methodConfig); + + /// + /// Track a shipment using the label id + /// + /// The label id associated with the shipment + /// An object that contains the label id tracking information + Task TrackUsingLabelId(string labelId); + + /// + /// Track a shipment using the label id + /// + /// The label id associated with the shipment + /// Configuration object that overrides the global config for this method call + /// An object that contains the label id tracking information + Task TrackUsingLabelId(string labelId, Config methodConfig); + + /// + /// Tracks a package based on the trackingNumber and carrierCode. + /// + /// The tracking number of the package you wish to track. + /// The carrierCode for the trackingNumber you are using to track the package. + /// + Task TrackUsingCarrierCodeAndTrackingNumber(string trackingNumber, string carrierCode); + + /// + /// Tracks a package based on the trackingNumber and carrierCode. + /// + /// The tracking number of the package you wish to track. + /// The carrierCode for the trackingNumber you are using to track the package. + /// Configuration object that overrides the global config for this method call + /// + Task TrackUsingCarrierCodeAndTrackingNumber(string trackingNumber, string carrierCode, Config methodConfig); + + /// + /// Create a label from shipment details + /// + /// Details of the label that you want to create + /// Object containing the created label information + Task CreateLabelFromShipmentDetails(CreateLabelFromShipmentDetails.Params labelParams); + + /// + /// Create a label from shipment details + /// + /// Details of the label that you want to create + /// Configuration object that overrides the global config for this method call + /// Object containing the created label information + Task CreateLabelFromShipmentDetails(CreateLabelFromShipmentDetails.Params labelParams, Config methodConfig); + + /// + /// Create a label from a rate id + /// + /// The details of the rate that you want to use to purchase a label + /// Object containing the created label information + Task CreateLabelFromRate(CreateLabelFromRate.Params createLabelFromRateParams); + + /// + /// Create a label from a rate id + /// + /// The details of the rate that you want to use to purchase a label + /// Configuration object that overrides the global config for this method call + /// Object containing the created label information + Task CreateLabelFromRate(CreateLabelFromRate.Params createLabelFromRateParams, Config methodConfig); + + /// + /// Retrieve rates for a package with the provided shipment details. + /// + /// + /// The rates result + Task GetRatesWithShipmentDetails(GetRatesWithShipmentDetails.Params rateParams); + + /// + /// Retrieve rates for a package with the provided shipment details. + /// + /// + /// Configuration object that overrides the global config for this method call + /// The rates result + Task GetRatesWithShipmentDetails(GetRatesWithShipmentDetails.Params rateParams, Config methodConfig); + } } \ No newline at end of file diff --git a/ShipEngine/ShipEngineClient.cs b/ShipEngine/ShipEngineClient.cs index 8c094dbb..980b38be 100644 --- a/ShipEngine/ShipEngineClient.cs +++ b/ShipEngine/ShipEngineClient.cs @@ -6,11 +6,11 @@ using System.Net.Http.Headers; using System.Text.Json; using System.Text.Json.Serialization; +using System.Threading; using System.Threading.Tasks; namespace ShipEngineSDK { - /// /// ShipEngine Client is used for handling generic calls and settings that /// are needed for all ShipEngine API calls. @@ -32,9 +32,19 @@ public class ShipEngineClient private const string JsonMediaType = "application/json"; + /// + /// Modifies the client request before it is sent + /// + public Action? ModifyRequest { get; set; } + + /// + /// Token to cancel the request + /// + public CancellationToken CancellationToken { get; set; } + /// /// Sets the HttpClient User agent, the json media type, and the API key to be used - /// for all ShipEngine API calls unless overrwritten at the method level. + /// for all ShipEngine API calls unless overwritten at the method level. /// /// Config object used to configure the HttpClient /// The HttpClient to be configured @@ -63,6 +73,15 @@ public static HttpClient ConfigureHttpClient(Config config, HttpClient client) return client; } + /// + /// Sets the HttpClient User agent, the json media type, and the API key to be used + /// for all ShipEngine API calls unless overwritten at the method level. + /// + /// + /// + /// + /// + /// public static HttpClient ConfigureHttpClient(HttpClient client, string apiKey, Uri? baseUri, TimeSpan? timeout = null) { client.DefaultRequestHeaders.Accept.Clear(); @@ -111,6 +130,7 @@ private async Task DeserializedResultOrThrow(HttpResponseMessage response) error.ErrorSource, error.ErrorType, error.ErrorCode, + response, deserializedError.RequestId ); } @@ -143,14 +163,15 @@ public virtual async Task SendHttpRequestAsync(HttpMethod method, string p { int retry = 0; - HttpResponseMessage response = null; + HttpResponseMessage? response = null; ShipEngineException requestException; while (true) { try { var request = BuildRequest(method, path, jsonContent); - var streamTask = client.SendAsync(request); + ModifyRequest?.Invoke(request); + var streamTask = client.SendAsync(request, CancellationToken); response = await streamTask; var deserializedResult = await DeserializedResultOrThrow(response); @@ -192,13 +213,13 @@ public virtual async Task SendHttpRequestAsync(HttpMethod method, string p } } - private async Task WaitAndRetry(HttpResponseMessage response, Config config, ShipEngineException ex) + private async Task WaitAndRetry(HttpResponseMessage? response, Config config, ShipEngineException ex) { int? retryAfter; try { - retryAfter = Int32.Parse(response?.Headers.GetValues("RetryAfter").First()); + retryAfter = Int32.Parse(response?.Headers.GetValues("RetryAfter").First() ?? string.Empty); } catch { @@ -212,14 +233,15 @@ private async Task WaitAndRetry(HttpResponseMessage response, Config config, Shi ErrorSource.Shipengine, ErrorType.System, ErrorCode.Timeout, + response, ex.RequestId ); } - await Task.Delay((int)retryAfter * 1000).ConfigureAwait(false); + await Task.Delay((int)retryAfter * 1000, CancellationToken).ConfigureAwait(false); } - private HttpRequestMessage BuildRequest(HttpMethod method, string path, string? jsonContent) + private static HttpRequestMessage BuildRequest(HttpMethod method, string path, string? jsonContent) { var request = new HttpRequestMessage(method, path); @@ -231,7 +253,7 @@ private HttpRequestMessage BuildRequest(HttpMethod method, string path, string? return request; } - private bool ShouldRetry( + private static bool ShouldRetry( int numRetries, HttpStatusCode? statusCode, HttpHeaders? headers,