From 7300578b74e95f42d472c4f467a63d8cb0d98827 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb Date: Tue, 27 May 2025 13:12:38 +0500 Subject: [PATCH 01/24] feat: adding initial implementation for pagination --- APIMatic.Core/ApiCall.cs | 61 +++++++++++++++ .../Authentication/AuthGroupBuilder.cs | 17 +++++ APIMatic.Core/Authentication/AuthManager.cs | 7 ++ .../Pagination/IPaginationDataManager.cs | 9 +++ APIMatic.Core/Pagination/PaginatedResult.cs | 19 +++++ APIMatic.Core/Pagination/PaginationInfo.cs | 25 +++++++ .../Pagination/Types/CursorPagination.cs | 48 ++++++++++++ .../Pagination/Types/LinkPagination.cs | 47 ++++++++++++ .../Pagination/Types/OffsetPagination.cs | 44 +++++++++++ .../Pagination/Types/PagePagination.cs | 44 +++++++++++ .../Parameters/AdditionalFormParams.cs | 14 ++++ .../Parameters/AdditionalHeaderParams.cs | 14 ++++ .../Parameters/AdditionalQueryParams.cs | 14 ++++ APIMatic.Core/Request/Parameters/BodyParam.cs | 12 +++ APIMatic.Core/Request/Parameters/FormParam.cs | 18 +++++ .../Request/Parameters/HeaderParam.cs | 11 +++ APIMatic.Core/Request/Parameters/Parameter.cs | 39 ++++++++++ .../Request/Parameters/QueryParam.cs | 11 +++ .../Request/Parameters/TemplateParam.cs | 13 +++- APIMatic.Core/Request/RequestBuilder.cs | 75 ++++++++++++++++++- APIMatic.Core/Utilities/CoreHelper.cs | 67 +++++++++++++++++ 21 files changed, 606 insertions(+), 3 deletions(-) create mode 100644 APIMatic.Core/Pagination/IPaginationDataManager.cs create mode 100644 APIMatic.Core/Pagination/PaginatedResult.cs create mode 100644 APIMatic.Core/Pagination/PaginationInfo.cs create mode 100644 APIMatic.Core/Pagination/Types/CursorPagination.cs create mode 100644 APIMatic.Core/Pagination/Types/LinkPagination.cs create mode 100644 APIMatic.Core/Pagination/Types/OffsetPagination.cs create mode 100644 APIMatic.Core/Pagination/Types/PagePagination.cs diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index 0ef4dc0d..64fec9a1 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using APIMatic.Core.Http.Configuration; +using APIMatic.Core.Pagination; using APIMatic.Core.Request; using APIMatic.Core.Response; using APIMatic.Core.Types.Sdk; @@ -82,6 +83,12 @@ public ApiCall RequestBuilder(RequestBuilder _requestBuilder) + { + requestBuilder = _requestBuilder; + return this; + } + /// /// Setup the response handler /// @@ -111,5 +118,59 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = var context = new CoreContext(request, response); return responseHandler.Result(context, returnTypeCreator); } + + private async Task> ExecutePaginationAsync( + IPaginationDataManager manager, + Func> converter, + Func, TPageMetadata> pageResponseConverter, + CancellationToken cancellationToken = default) + { + requestBuilder.AcceptHeader = responseHandler.AcceptHeader; + CoreRequest request = await requestBuilder.Build().ConfigureAwait(false); + globalConfiguration.ApiCallback?.OnBeforeHttpRequestEventHandler(request); + _sdkLogger.LogRequest(request); + var response = await globalConfiguration.HttpClient.ExecuteAsync(request, cancellationToken) + .ConfigureAwait(false); + globalConfiguration.ApiCallback?.OnAfterHttpResponseEventHandler(response); + _sdkLogger.LogResponse(response); + var context = new CoreContext(request, response); + var page = responseHandler.Result(context, returnTypeCreator); + var pageItems = converter(page); + var pageMeta = pageResponseConverter(page, manager, pageItems); + return new PaginatedResult(response, pageItems, pageMeta); + } + + public TEnumerable Paginate( + Func> converter, + Func, TPageMetadata> pageResponseConverter, + Func< + Func>>, + RequestBuilder, IPaginationDataManager[], + TEnumerable> returnTypeGetter, + CancellationToken cancellationToken = default, + params IPaginationDataManager[] dataManagers) + { + return returnTypeGetter( + (reqBuilder, manager) => RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter, cancellationToken), + requestBuilder.Clone(), + dataManagers); + } + + public TEnumerable PaginateAsync( + Func> converter, + Func, TPageMetadata> pageResponseConverter, + Func< + Func>>, + RequestBuilder, IPaginationDataManager[], + TEnumerable> returnTypeGetter, + CancellationToken cancellationTokenOld = default, // Merge these cancellationTokens + params IPaginationDataManager[] dataManagers) + { + return returnTypeGetter( + (reqBuilder, manager, cancellationToken) => + RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter,cancellationToken == default ? cancellationTokenOld : cancellationToken), + requestBuilder.Clone(), + dataManagers); + } } } diff --git a/APIMatic.Core/Authentication/AuthGroupBuilder.cs b/APIMatic.Core/Authentication/AuthGroupBuilder.cs index 9913b4b7..50a32ad0 100644 --- a/APIMatic.Core/Authentication/AuthGroupBuilder.cs +++ b/APIMatic.Core/Authentication/AuthGroupBuilder.cs @@ -121,5 +121,22 @@ public override async Task Apply(RequestBuilder requestBuilder) await authManager.Apply(requestBuilder).ConfigureAwait(false); } } + + public void Clone(AuthGroupBuilder clone) + { + foreach (var kvp in this.authManagersMap) + { + clone.authManagersMap[kvp.Key] = kvp.Value.Clone(); + } + foreach (var parameter in this.authManagers) + { + clone.authManagers.Add(parameter.Clone()); + } + foreach (var parameter in this.validatedAuthManagers) + { + clone.validatedAuthManagers.Add(parameter.Clone()); + } + clone.isAndGroup = isAndGroup; + } } } diff --git a/APIMatic.Core/Authentication/AuthManager.cs b/APIMatic.Core/Authentication/AuthManager.cs index 7345bdbf..30465990 100644 --- a/APIMatic.Core/Authentication/AuthManager.cs +++ b/APIMatic.Core/Authentication/AuthManager.cs @@ -44,5 +44,12 @@ public virtual Task Apply(RequestBuilder requestBuilder) parameters.Apply(requestBuilder); return Task.CompletedTask; } + + public AuthManager Clone() + { + var clone = new AuthManager(); + this.parameters.Clone(clone.parameters); + return clone; + } } } diff --git a/APIMatic.Core/Pagination/IPaginationDataManager.cs b/APIMatic.Core/Pagination/IPaginationDataManager.cs new file mode 100644 index 00000000..a7e355bd --- /dev/null +++ b/APIMatic.Core/Pagination/IPaginationDataManager.cs @@ -0,0 +1,9 @@ +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination +{ + public interface IPaginationDataManager + { + bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder); + } +} diff --git a/APIMatic.Core/Pagination/PaginatedResult.cs b/APIMatic.Core/Pagination/PaginatedResult.cs new file mode 100644 index 00000000..4bd1dc7d --- /dev/null +++ b/APIMatic.Core/Pagination/PaginatedResult.cs @@ -0,0 +1,19 @@ +using APIMatic.Core.Types.Sdk; +using System.Collections.Generic; + +namespace APIMatic.Core.Pagination +{ + public class PaginatedResult + { + public CoreResponse Response { get; } + public IEnumerable Items { get; } + public TPage PageMetadata { get; } + + public PaginatedResult(CoreResponse response, IEnumerable items, TPage pageMetadata) + { + Response = response; + Items = items; + PageMetadata = pageMetadata; + } + } +} diff --git a/APIMatic.Core/Pagination/PaginationInfo.cs b/APIMatic.Core/Pagination/PaginationInfo.cs new file mode 100644 index 00000000..c69827d8 --- /dev/null +++ b/APIMatic.Core/Pagination/PaginationInfo.cs @@ -0,0 +1,25 @@ +using APIMatic.Core.Request; +using APIMatic.Core.Types.Sdk; +using APIMatic.Core.Utilities; + +namespace APIMatic.Core.Pagination +{ + public class PaginationInfo + { + private readonly int _lastDataSize; + private readonly CoreResponse _lastResponse; + private readonly RequestBuilder _lastRequestBuilder; + + public PaginationInfo(int lastDataSize, CoreResponse lastResponse, RequestBuilder lastRequestBuilder) + { + _lastDataSize = lastDataSize; + _lastResponse = lastResponse; + _lastRequestBuilder = lastRequestBuilder; + } + + internal RequestBuilder GetLastRequestBuilder() => _lastRequestBuilder; + internal string GetLastResponseBody() => _lastResponse?.Body; + internal string GetLastResponseHeaders() => CoreHelper.JsonSerialize(_lastResponse?.Headers); + internal int GetLastDataSize() => _lastDataSize; + } +} diff --git a/APIMatic.Core/Pagination/Types/CursorPagination.cs b/APIMatic.Core/Pagination/Types/CursorPagination.cs new file mode 100644 index 00000000..de4ccfd2 --- /dev/null +++ b/APIMatic.Core/Pagination/Types/CursorPagination.cs @@ -0,0 +1,48 @@ +using APIMatic.Core.Request; +using APIMatic.Core.Utilities; + +namespace APIMatic.Core.Pagination.Types +{ + public class CursorPagination : IPaginationDataManager + { + private readonly string _output; + private readonly string _input; + + public string CursorValue { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// JsonPointer of a field received in the response, representing the next cursor. + /// JsonPointer of a field in the request, representing the cursor. + public CursorPagination(string output, string input) + { + _output = output; + _input = input; + } + + public bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder) + { + updatedRequestBuilder = paginationInfo.GetLastRequestBuilder(); + + CursorValue = CoreHelper.GetValueByReference( + _output, + paginationInfo.GetLastResponseBody(), + paginationInfo.GetLastResponseHeaders() + ); + + bool isUpdated = false; + updatedRequestBuilder.UpdateByReference(_input, old => + { + if (CursorValue == null) + { + CursorValue = old.ToString(); + } + isUpdated = true; + return CursorValue; + }); + + return isUpdated; + } + } +} diff --git a/APIMatic.Core/Pagination/Types/LinkPagination.cs b/APIMatic.Core/Pagination/Types/LinkPagination.cs new file mode 100644 index 00000000..8f456d99 --- /dev/null +++ b/APIMatic.Core/Pagination/Types/LinkPagination.cs @@ -0,0 +1,47 @@ +using APIMatic.Core.Utilities; +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination.Types +{ + public class LinkPagination : IPaginationDataManager + { + private readonly string _next; + + public string LinkValue { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// JsonPointer of a field in the response, representing the next request query URL. + public LinkPagination(string next) + { + _next = next; + } + + public bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder) + { + updatedRequestBuilder = paginationInfo.GetLastRequestBuilder(); + + LinkValue = CoreHelper.GetValueByReference( + _next, + paginationInfo.GetLastResponseBody(), + paginationInfo.GetLastResponseHeaders() + ); + + if (string.IsNullOrEmpty(LinkValue)) + { + return false; + } + + updatedRequestBuilder.Parameters(_parameters => + { + foreach (var pair in CoreHelper.ExtractQueryParametersForUrl(LinkValue)) + { + _parameters.Query(_query => _query.Setup(pair.Key, pair.Value)); + } + }); + + return true; + } + } +} diff --git a/APIMatic.Core/Pagination/Types/OffsetPagination.cs b/APIMatic.Core/Pagination/Types/OffsetPagination.cs new file mode 100644 index 00000000..73a0b1ae --- /dev/null +++ b/APIMatic.Core/Pagination/Types/OffsetPagination.cs @@ -0,0 +1,44 @@ +using System; +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination.Types +{ + + public class OffsetPagination : IPaginationDataManager + { + private readonly string input; + + public int OffsetValue { get; private set; } + /// + /// Constructor accepting a JSON pointer to the offset field in the request. + /// + /// JsonPointer of a field in request, representing offset. + public OffsetPagination(string input) + { + this.input = input; + } + + public bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder) + { + updatedRequestBuilder = paginationInfo.GetLastRequestBuilder(); + + if (input == null) + { + return false; + } + + bool isUpdated = false; + + updatedRequestBuilder.UpdateByReference(input, old => + { + int oldValue = Convert.ToInt32(old); + int newValue = oldValue + paginationInfo.GetLastDataSize(); + isUpdated = true; + OffsetValue = newValue; + return OffsetValue; + }); + + return isUpdated; + } + } +} diff --git a/APIMatic.Core/Pagination/Types/PagePagination.cs b/APIMatic.Core/Pagination/Types/PagePagination.cs new file mode 100644 index 00000000..a207b562 --- /dev/null +++ b/APIMatic.Core/Pagination/Types/PagePagination.cs @@ -0,0 +1,44 @@ +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination.Types +{ + public class PagePagination : IPaginationDataManager + { + private readonly string _input; + + public int CurrentPage { get; private set; } + /// + /// Initializes a new instance of the class. + /// + /// JsonPointer of a field in the request, representing the page number. + public PagePagination(string input) + { + _input = input; + } + + public bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder) + { + updatedRequestBuilder = paginationInfo.GetLastRequestBuilder(); + + if (string.IsNullOrEmpty(_input)) + { + return false; + } + + bool isUpdated = false; + updatedRequestBuilder.UpdateByReference(_input, oldValue => + { + if (!int.TryParse(oldValue?.ToString(), out int currentPage)) + { + currentPage = 1; + } + + isUpdated = true; + CurrentPage = currentPage + 1; + return CurrentPage; + }); + + return isUpdated; + } + } +} diff --git a/APIMatic.Core/Request/Parameters/AdditionalFormParams.cs b/APIMatic.Core/Request/Parameters/AdditionalFormParams.cs index 99832fc5..96826d34 100644 --- a/APIMatic.Core/Request/Parameters/AdditionalFormParams.cs +++ b/APIMatic.Core/Request/Parameters/AdditionalFormParams.cs @@ -15,5 +15,19 @@ public override MultipleParams Setup(string key, object value) parameters.Form(f => f.Setup(key, value)); return this; } + + public override Parameter Clone() + { + var clone = new AdditionalFormParams + { + key = this.key, + value = this.value, + validated = this.validated, + typeName = this.typeName + }; + + this.parameters.Clone(clone.parameters); + return clone; + } } } diff --git a/APIMatic.Core/Request/Parameters/AdditionalHeaderParams.cs b/APIMatic.Core/Request/Parameters/AdditionalHeaderParams.cs index 677d123a..3f809722 100644 --- a/APIMatic.Core/Request/Parameters/AdditionalHeaderParams.cs +++ b/APIMatic.Core/Request/Parameters/AdditionalHeaderParams.cs @@ -15,5 +15,19 @@ public override MultipleParams Setup(string key, object value) parameters.Header(h => h.Setup(key, value)); return this; } + + public override Parameter Clone() + { + var clone = new AdditionalHeaderParams + { + key = this.key, + value = this.value, + validated = this.validated, + typeName = this.typeName + }; + + this.parameters.Clone(clone.parameters); + return clone; + } } } diff --git a/APIMatic.Core/Request/Parameters/AdditionalQueryParams.cs b/APIMatic.Core/Request/Parameters/AdditionalQueryParams.cs index 38f09fb5..889d806b 100644 --- a/APIMatic.Core/Request/Parameters/AdditionalQueryParams.cs +++ b/APIMatic.Core/Request/Parameters/AdditionalQueryParams.cs @@ -15,5 +15,19 @@ public override MultipleParams Setup(string key, object value) parameters.Query(q => q.Setup(key, value)); return this; } + + public override Parameter Clone() + { + var clone = new AdditionalQueryParams + { + key = this.key, + value = this.value, + validated = this.validated, + typeName = this.typeName + }; + + this.parameters.Clone(clone.parameters); + return clone; + } } } diff --git a/APIMatic.Core/Request/Parameters/BodyParam.cs b/APIMatic.Core/Request/Parameters/BodyParam.cs index 06b69159..423c2c9f 100644 --- a/APIMatic.Core/Request/Parameters/BodyParam.cs +++ b/APIMatic.Core/Request/Parameters/BodyParam.cs @@ -36,5 +36,17 @@ internal override void Apply(RequestBuilder requestBuilder) } requestBuilder.bodyParameters[key] = value; } + + public override Parameter Clone() + { + return new BodyParam + { + key = this.key, + value = this.value, + validated = this.validated, + typeName = this.typeName, + valueType = this.valueType + }; + } } } diff --git a/APIMatic.Core/Request/Parameters/FormParam.cs b/APIMatic.Core/Request/Parameters/FormParam.cs index bd417da6..684e031d 100644 --- a/APIMatic.Core/Request/Parameters/FormParam.cs +++ b/APIMatic.Core/Request/Parameters/FormParam.cs @@ -69,5 +69,23 @@ internal override void Apply(RequestBuilder requestBuilder) } requestBuilder.formParameters.AddRange(PrepareFormParameters(requestBuilder.ArraySerialization)); } + + public override Parameter Clone() + { + var clone = new FormParam + { + key = this.key, + value = this.value, + validated = this.validated, + typeName = this.typeName, + }; + + foreach (var header in this.encodingHeaders) + { + clone.encodingHeaders.Add(header.Key, header.Value); + } + + return clone; + } } } diff --git a/APIMatic.Core/Request/Parameters/HeaderParam.cs b/APIMatic.Core/Request/Parameters/HeaderParam.cs index c9c1a9f4..119941a1 100644 --- a/APIMatic.Core/Request/Parameters/HeaderParam.cs +++ b/APIMatic.Core/Request/Parameters/HeaderParam.cs @@ -25,5 +25,16 @@ internal override void Apply(RequestBuilder requestBuilder) } requestBuilder.headers[key] = headerValue; } + + public override Parameter Clone() + { + return new HeaderParam + { + key = this.key, + value = this.value, + validated = this.validated, + typeName = this.typeName + }; + } } } diff --git a/APIMatic.Core/Request/Parameters/Parameter.cs b/APIMatic.Core/Request/Parameters/Parameter.cs index 90e66fdb..746b02f2 100644 --- a/APIMatic.Core/Request/Parameters/Parameter.cs +++ b/APIMatic.Core/Request/Parameters/Parameter.cs @@ -5,6 +5,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using APIMatic.Core.Utilities; +using Microsoft.Json.Pointer; +using Newtonsoft.Json.Linq; namespace APIMatic.Core.Request.Parameters { @@ -72,6 +75,8 @@ internal virtual void Validate() internal abstract void Apply(RequestBuilder requestBuilder); + public abstract Parameter Clone(); + /// /// Used to build a request by creating and adding multiple parameters into the RequestBuilder /// @@ -222,6 +227,40 @@ internal void Apply(RequestBuilder requestBuilder) p.Apply(requestBuilder); } } + + internal void UpdateParameterValueByPointer(Func setter, JsonPointer pointer) + { + try + { + var paramsDictionary = parameters.ToDictionary(p => p.key, p => p.value); + var json = CoreHelper.JsonSerialize(paramsDictionary); + if (string.IsNullOrWhiteSpace(json)) { return; } + + JObject jsonObject = JObject.Parse(json); + JToken jsonToken = pointer.Evaluate(jsonObject); + object updatedValue = setter(jsonToken.ToObject()); + jsonToken.Replace(JToken.FromObject(updatedValue)); + + foreach (var parameter in parameters) + { + if (!jsonObject.TryGetValue(parameter.key, out JToken valueToken)) { continue; } + + parameter.Setup(parameter.key, valueToken.ToObject()); + } + } + catch + { + // return value + } + } + + internal void Clone(Builder clone) + { + foreach (var parameter in parameters) + { + clone.parameters.Add(parameter.Clone()); + } + } } } } diff --git a/APIMatic.Core/Request/Parameters/QueryParam.cs b/APIMatic.Core/Request/Parameters/QueryParam.cs index 11529b42..bc7a959b 100644 --- a/APIMatic.Core/Request/Parameters/QueryParam.cs +++ b/APIMatic.Core/Request/Parameters/QueryParam.cs @@ -18,5 +18,16 @@ internal override void Apply(RequestBuilder requestBuilder) } requestBuilder.queryParameters[key] = value; } + + public override Parameter Clone() + { + return new QueryParam + { + key = this.key, + value = this.value, + validated = this.validated, + typeName = this.typeName, + }; + } } } diff --git a/APIMatic.Core/Request/Parameters/TemplateParam.cs b/APIMatic.Core/Request/Parameters/TemplateParam.cs index e7a4d12a..1897e611 100644 --- a/APIMatic.Core/Request/Parameters/TemplateParam.cs +++ b/APIMatic.Core/Request/Parameters/TemplateParam.cs @@ -48,7 +48,18 @@ internal override void Apply(RequestBuilder requestBuilder) } CoreHelper.TryGetInnerValueForContainer(value, out var innerValue); string replacerValue = Uri.EscapeUriString(GetReplacerValue(innerValue ?? value)); - requestBuilder.QueryUrl.Replace($"{{{key}}}", replacerValue); + requestBuilder.QueryUrl.Replace(string.Format("{{{0}}}", key), replacerValue); + } + + public override Parameter Clone() + { + return new TemplateParam + { + key = this.key, + value = this.value, + validated = this.validated, + typeName = this.typeName + }; } } } diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 33803672..a2687e40 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -13,6 +13,8 @@ using APIMatic.Core.Request.Parameters; using APIMatic.Core.Types.Sdk; using APIMatic.Core.Utilities; +using Microsoft.Json.Pointer; +using Newtonsoft.Json.Linq; namespace APIMatic.Core.Request { @@ -142,6 +144,29 @@ public RequestBuilder XmlBodySerializer(Func xmlSerializer) return this; } + public RequestBuilder UpdateByReference(string pointerString, Func setter) + { + if (pointerString == null) + return this; + + var parts = pointerString.Split('#'); + + if (parts.Length <= 1) + return this; + + var jsonPointer = new JsonPointer(parts[1]); + + switch (parts[0]) + { + case "$request.path": case "$request.query": case "$request.headers": + parameters.UpdateParameterValueByPointer(setter, jsonPointer); + parameters.Apply(this); + return this; + default: + return this; + } + } + /// /// This applies all the configuration and build an instance of CoreRequest /// @@ -151,11 +176,12 @@ public async Task Build() parameters.Validate().Apply(this); configuration.RuntimeParameters.Validate().Apply(this); await authGroup.Apply(this).ConfigureAwait(false); - CoreHelper.AppendUrlWithQueryParameters(QueryUrl, queryParameters, ArraySerialization); + var queryUrl = new StringBuilder(QueryUrl.ToString()); + CoreHelper.AppendUrlWithQueryParameters(queryUrl, queryParameters, ArraySerialization); body = bodyParameters.Any() ? bodyParameters : body; AppendContentTypeHeader(); AppendAcceptHeader(); - return new CoreRequest(httpMethod, CoreHelper.CleanUrl(QueryUrl), headers, bodySerializer(body), formParameters, queryParameters) + return new CoreRequest(httpMethod, CoreHelper.CleanUrl(queryUrl), headers, bodySerializer(body), formParameters, queryParameters) { RetryOption = retryOption, HasBinaryResponse = HasBinaryResponse @@ -232,5 +258,50 @@ private static object PrepareBody(dynamic value) } return CoreHelper.JsonSerialize(value); } + + public RequestBuilder Clone() + { + var clone = new RequestBuilder(configuration) + .WithRetryOption(retryOption) + .DisableContentType(); + clone.contentTypeAllowed = contentTypeAllowed; + clone.httpMethod = this.httpMethod; + clone.QueryUrl.Append(this.QueryUrl); + clone.AcceptHeader = this.AcceptHeader; + clone.ArraySerialization = this.ArraySerialization; + clone.HasBinaryResponse = this.HasBinaryResponse; + clone.xmlRequest = this.xmlRequest; + clone.bodySerializer = this.bodySerializer; + clone.body = this.body; + clone.bodyType = this.bodyType; + + foreach (var kvp in this.headers) + { + clone.headers.Add(kvp.Key, kvp.Value); + } + + foreach (var kvp in this.bodyParameters) + { + clone.bodyParameters.Add(kvp.Key, kvp.Value); + } + + foreach (var kvp in this.formParameters) + { + clone.formParameters.Add(new KeyValuePair(kvp.Key, kvp.Value)); + } + + foreach (var kvp in this.queryParameters) + { + clone.queryParameters.Add(kvp.Key, kvp.Value); + } + + // Clone parameters builder + this.parameters.Clone(clone.parameters); + + // Clone authGroup + this.authGroup.Clone(clone.authGroup); + + return clone; + } } } diff --git a/APIMatic.Core/Utilities/CoreHelper.cs b/APIMatic.Core/Utilities/CoreHelper.cs index 64819b0d..58718e11 100644 --- a/APIMatic.Core/Utilities/CoreHelper.cs +++ b/APIMatic.Core/Utilities/CoreHelper.cs @@ -13,9 +13,11 @@ using APIMatic.Core.Http.Configuration; using APIMatic.Core.Types; using APIMatic.Core.Types.Sdk; +using Microsoft.Json.Pointer; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; +using System.Net; namespace APIMatic.Core.Utilities { @@ -94,6 +96,22 @@ public static T JsonDeserialize(string json, JsonConverter converter = null) } } + public static IDictionary ExtractQueryParametersForUrl(string queryUrl) + { + var queryParams = queryUrl.Split('?'); + + if (queryParams.Length <= 1) + { + return new Dictionary(); + } + + return queryParams[1].Split('&').Select(param => param.Split(new []{'='}, 2)) + .ToDictionary( + pair => WebUtility.UrlDecode(pair[0]), + pair => (object)(pair.Length > 1 ? WebUtility.UrlDecode(pair[1]) : string.Empty) + ); + } + /// /// Appends the given set of parameters to the given query string. /// @@ -121,6 +139,55 @@ public static void AppendUrlWithQueryParameters(StringBuilder queryBuilder, IEnu } } + public static string GetValueByReference(string pointerString, string jsonBody, string jsonHeaders) + { + if (pointerString == null) + return null; + + var parts = pointerString.Split('#'); + + if (parts.Length <= 1) + return null; + + var jsonPointer = new JsonPointer(parts[1]); + + switch (parts[0]) + { + case "$response.body": + return GetJsonValueUsingPointer(jsonPointer, jsonBody); + case "$response.headers": + return GetJsonValueUsingPointer(jsonPointer, jsonHeaders); + default: + return null; + } + } + + private static string GetJsonValueUsingPointer(JsonPointer pointer, string json) + { + if (pointer == null || json == null) + return null; + + var jsonObject = JObject.Parse(json); + JToken jsonToken; + try + { + jsonToken = pointer.Evaluate(jsonObject); + } + catch + { + return null; + } + + if (jsonToken.Type == JTokenType.Null) + return null; + + if (jsonToken.Type == JTokenType.String) + return jsonToken.Value(); + + return jsonToken.ToString(); + } + + private static void AppendParameters(StringBuilder queryBuilder, ArraySerialization arraySerialization, KeyValuePair pair) { string paramKeyValPair; From f023b0fc8f69683bc40b54a8cbf3b3f8da3a130f Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb Date: Fri, 30 May 2025 11:59:23 +0500 Subject: [PATCH 02/24] Adding page to item converter in ApiCall --- APIMatic.Core/ApiCall.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index 64fec9a1..e776a570 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -159,9 +159,10 @@ public TEnumerable Paginate( public TEnumerable PaginateAsync( Func> converter, Func, TPageMetadata> pageResponseConverter, + Func> pagedResponseItemConverter, Func< Func>>, - RequestBuilder, IPaginationDataManager[], + RequestBuilder, Func>, IPaginationDataManager[], TEnumerable> returnTypeGetter, CancellationToken cancellationTokenOld = default, // Merge these cancellationTokens params IPaginationDataManager[] dataManagers) @@ -170,6 +171,7 @@ public TEnumerable PaginateAsync( (reqBuilder, manager, cancellationToken) => RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter,cancellationToken == default ? cancellationTokenOld : cancellationToken), requestBuilder.Clone(), + pagedResponseItemConverter, dataManagers); } } From 9d467f8434ba8cb7159fdcbc1fb2c0fca5e12805 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Fri, 30 May 2025 12:37:59 +0500 Subject: [PATCH 03/24] Update ApiCall.cs --- APIMatic.Core/ApiCall.cs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index 64fec9a1..fa479d92 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -144,14 +144,14 @@ public TEnumerable Paginate( Func> converter, Func, TPageMetadata> pageResponseConverter, Func< - Func>>, - RequestBuilder, IPaginationDataManager[], - TEnumerable> returnTypeGetter, - CancellationToken cancellationToken = default, - params IPaginationDataManager[] dataManagers) + Func>>, + RequestBuilder, IPaginationDataManager[], + TEnumerable> returnTypeGetter, + params IPaginationDataManager[] dataManagers) { return returnTypeGetter( - (reqBuilder, manager) => RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter, cancellationToken), + (reqBuilder, manager) => RequestBuilder(reqBuilder) + .ExecutePaginationAsync(manager, converter, pageResponseConverter), requestBuilder.Clone(), dataManagers); } @@ -159,17 +159,20 @@ public TEnumerable Paginate( public TEnumerable PaginateAsync( Func> converter, Func, TPageMetadata> pageResponseConverter, + Func> pagedResponseItemConverter, Func< - Func>>, - RequestBuilder, IPaginationDataManager[], + Func>>, + RequestBuilder, Func>, IPaginationDataManager[], TEnumerable> returnTypeGetter, - CancellationToken cancellationTokenOld = default, // Merge these cancellationTokens params IPaginationDataManager[] dataManagers) { return returnTypeGetter( (reqBuilder, manager, cancellationToken) => - RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter,cancellationToken == default ? cancellationTokenOld : cancellationToken), + RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter, + cancellationToken), requestBuilder.Clone(), + pagedResponseItemConverter, dataManagers); } } From f53304cd2a68bbbb4501b24d74e883656da11c9f Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Fri, 30 May 2025 12:41:23 +0500 Subject: [PATCH 04/24] Update ApiCall.cs --- APIMatic.Core/ApiCall.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index a2afe50f..49758d33 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -161,8 +161,6 @@ public TEnumerable PaginateAsync( Func, TPageMetadata> pageResponseConverter, Func> pagedResponseItemConverter, Func< - Func>>, Func>>, RequestBuilder, Func>, IPaginationDataManager[], TEnumerable> returnTypeGetter, From 0970d4d3a8790b06d23dfa0a0e33f9376cd76b39 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Fri, 30 May 2025 14:50:06 +0500 Subject: [PATCH 05/24] Update ApiCall.cs --- APIMatic.Core/ApiCall.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index 49758d33..35139e5b 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -167,9 +167,10 @@ public TEnumerable PaginateAsync( params IPaginationDataManager[] dataManagers) { return returnTypeGetter( - (reqBuilder, manager, cancellationToken) => - RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter, - cancellationToken), + async (reqBuilder, manager, cancellationToken) => + await RequestBuilder(reqBuilder) + .ExecutePaginationAsync(manager, converter, pageResponseConverter, cancellationToken) + .ConfigureAwait(false), requestBuilder.Clone(), pagedResponseItemConverter, dataManagers); From 731d6447a1428e1b9b641ba42bba0610b4fcc6d9 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 5 Jun 2025 00:39:39 +0500 Subject: [PATCH 06/24] add unit test for AsyncPaginator for query --- APIMatic.Core.Test/Api/ApiCallTest.cs | 38 +-- ...llPostXMLTest.cs => ApiCallPostXmlTest.cs} | 12 +- .../Api/Pagination/AsyncPaginator.cs | 305 ++++++++++++++++++ .../Models/Pagination/Transaction.cs | 45 +++ .../Models/Pagination/TransactionsCursored.cs | 72 +++++ .../Models/Pagination/TransactionsLinked.cs | 79 +++++ .../Models/Pagination/TransactionsOffset.cs | 24 ++ .../MetaData/CursorPagedResponse.cs | 32 ++ .../Pagination/MetaData/LinkPagedResponse.cs | 32 ++ .../MetaData/NumberPagedResponse.cs | 27 ++ .../MetaData/OffsetPagedResponse.cs | 28 ++ .../MockTypes/Pagination/PageableFactory.cs | 21 ++ .../MockTypes/Pagination/PagedResponse.cs | 51 +++ .../Utilities/PaginationHelper.cs | 200 ++++++++++++ APIMatic.Core/APIMatic.Core.csproj | 2 + APIMatic.Core/ApiCall.cs | 35 +- .../Authentication/AuthGroupBuilder.cs | 17 - APIMatic.Core/Authentication/AuthManager.cs | 7 - APIMatic.Core/Pagination/AsyncPaginator.cs | 70 ++++ .../Pagination/IPaginationDataManager.cs | 9 - APIMatic.Core/Pagination/PaginationContext.cs | 31 ++ APIMatic.Core/Pagination/PaginationInfo.cs | 25 -- APIMatic.Core/Pagination/Paginator.cs | 67 ++++ .../Pagination/Strategies/CursorPagination.cs | 58 ++++ .../Strategies/IPaginationStrategy.cs | 9 + .../Pagination/Strategies/LinkPagination.cs | 44 +++ .../Pagination/Strategies/OffsetPagination.cs | 46 +++ .../Pagination/Strategies/PagePagination.cs | 46 +++ .../Pagination/Types/CursorPagination.cs | 48 --- .../Pagination/Types/LinkPagination.cs | 47 --- .../Pagination/Types/OffsetPagination.cs | 44 --- .../Pagination/Types/PagePagination.cs | 44 --- .../Parameters/AdditionalFormParams.cs | 14 - .../Parameters/AdditionalHeaderParams.cs | 14 - .../Parameters/AdditionalQueryParams.cs | 14 - APIMatic.Core/Request/Parameters/BodyParam.cs | 12 - APIMatic.Core/Request/Parameters/FormParam.cs | 18 -- .../Request/Parameters/HeaderParam.cs | 11 - APIMatic.Core/Request/Parameters/Parameter.cs | 36 --- .../Request/Parameters/QueryParam.cs | 11 - .../Request/Parameters/TemplateParam.cs | 11 - APIMatic.Core/Request/RequestBuilder.cs | 167 +++++++--- APIMatic.Core/Utilities/CoreHelper.cs | 33 ++ 43 files changed, 1486 insertions(+), 470 deletions(-) rename APIMatic.Core.Test/Api/HttpPost/{ApiCallPostXMLTest.cs => ApiCallPostXmlTest.cs} (92%) create mode 100644 APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs create mode 100644 APIMatic.Core.Test/MockTypes/Models/Pagination/Transaction.cs create mode 100644 APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsCursored.cs create mode 100644 APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsLinked.cs create mode 100644 APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsOffset.cs create mode 100644 APIMatic.Core.Test/MockTypes/Pagination/MetaData/CursorPagedResponse.cs create mode 100644 APIMatic.Core.Test/MockTypes/Pagination/MetaData/LinkPagedResponse.cs create mode 100644 APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs create mode 100644 APIMatic.Core.Test/MockTypes/Pagination/MetaData/OffsetPagedResponse.cs create mode 100644 APIMatic.Core.Test/MockTypes/Pagination/PageableFactory.cs create mode 100644 APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs create mode 100644 APIMatic.Core.Test/Utilities/PaginationHelper.cs create mode 100644 APIMatic.Core/Pagination/AsyncPaginator.cs delete mode 100644 APIMatic.Core/Pagination/IPaginationDataManager.cs create mode 100644 APIMatic.Core/Pagination/PaginationContext.cs delete mode 100644 APIMatic.Core/Pagination/PaginationInfo.cs create mode 100644 APIMatic.Core/Pagination/Paginator.cs create mode 100644 APIMatic.Core/Pagination/Strategies/CursorPagination.cs create mode 100644 APIMatic.Core/Pagination/Strategies/IPaginationStrategy.cs create mode 100644 APIMatic.Core/Pagination/Strategies/LinkPagination.cs create mode 100644 APIMatic.Core/Pagination/Strategies/OffsetPagination.cs create mode 100644 APIMatic.Core/Pagination/Strategies/PagePagination.cs delete mode 100644 APIMatic.Core/Pagination/Types/CursorPagination.cs delete mode 100644 APIMatic.Core/Pagination/Types/LinkPagination.cs delete mode 100644 APIMatic.Core/Pagination/Types/OffsetPagination.cs delete mode 100644 APIMatic.Core/Pagination/Types/PagePagination.cs diff --git a/APIMatic.Core.Test/Api/ApiCallTest.cs b/APIMatic.Core.Test/Api/ApiCallTest.cs index b140509d..23dd2b46 100644 --- a/APIMatic.Core.Test/Api/ApiCallTest.cs +++ b/APIMatic.Core.Test/Api/ApiCallTest.cs @@ -11,9 +11,9 @@ namespace APIMatic.Core.Test.Api { public class ApiCallTest : TestBase { - private static readonly MockServer _server = MockServer.Server1; - private static readonly CompatibilityFactory _compatibilityFactory = new(); - private static readonly Dictionary> _globalErrors = new() + private static readonly MockServer Server = MockServer.Server1; + private static readonly CompatibilityFactory CompatibilityFactory = new(); + private static readonly Dictionary> GlobalErrors = new() { { "400", CreateErrorCase("400 Global Child 1", (reason, context) => new Child1Exception(reason, context)) }, { "402", CreateErrorCase("402 Global Child 2", (reason, context) => new Child2Exception(reason, context)) }, @@ -25,34 +25,34 @@ public class ApiCallTest : TestBase protected static ErrorCase CreateErrorCase(string reason, Func error, bool isErrorTemplate = false) => new(reason, error, isErrorTemplate); - protected ApiCall, T> CreateApiCall() + protected static ApiCall, T> CreateApiCall() => new ApiCall, T>( LazyGlobalConfiguration.Value, - _compatibilityFactory, - globalErrors: _globalErrors, + CompatibilityFactory, + globalErrors: GlobalErrors, returnTypeCreator: (response, result) => new ApiResponse(response.StatusCode, response.Headers, result) - ).Server(_server); + ).Server(Server); - protected ApiCall, T> CreateApiCallWithoutReturnTypeCreator() + protected static ApiCall, T> CreateApiCallWithoutReturnTypeCreator() => new ApiCall, T>( LazyGlobalConfiguration.Value, - _compatibilityFactory - ).Server(_server); + CompatibilityFactory + ).Server(Server); - protected ApiCall CreateSimpleApiCall() + protected static ApiCall CreateSimpleApiCall() => new ApiCall( LazyGlobalConfiguration.Value, - _compatibilityFactory, - globalErrors: _globalErrors - ).Server(_server); + CompatibilityFactory, + globalErrors: GlobalErrors + ).Server(Server); - protected ApiCall CreateSimpleApiCallWithoutErrors() + protected static ApiCall CreateSimpleApiCallWithoutErrors() => new ApiCall( LazyGlobalConfiguration.Value, - _compatibilityFactory - ).Server(_server); - - protected string GetCompleteUrl(string path) + CompatibilityFactory + ).Server(Server); + + protected static string GetCompleteUrl(string path) { return $"{LazyGlobalConfiguration.Value.ServerUrl()}{path}"; } diff --git a/APIMatic.Core.Test/Api/HttpPost/ApiCallPostXMLTest.cs b/APIMatic.Core.Test/Api/HttpPost/ApiCallPostXmlTest.cs similarity index 92% rename from APIMatic.Core.Test/Api/HttpPost/ApiCallPostXMLTest.cs rename to APIMatic.Core.Test/Api/HttpPost/ApiCallPostXmlTest.cs index 852e0409..7b24cc8d 100644 --- a/APIMatic.Core.Test/Api/HttpPost/ApiCallPostXMLTest.cs +++ b/APIMatic.Core.Test/Api/HttpPost/ApiCallPostXmlTest.cs @@ -9,14 +9,14 @@ namespace APIMatic.Core.Test.Api.HttpPost { [TestFixture] - internal class ApiCallPostXMLTest : ApiCallTest + internal class ApiCallPostXmlTest : ApiCallTest { [Test] public void ApiCall_PostXmlLong_OKResponse() { //Arrange - long body = 100; - var url = "/apicall/post-xml-long/200"; + const long body = 100; + const string url = "/apicall/post-xml-long/200"; var expected = new ServerResponse() { @@ -28,7 +28,7 @@ public void ApiCall_PostXmlLong_OKResponse() handlerMock.When(GetCompleteUrl(url)) .With(req => { - Assert.AreEqual(expected.Message, req.Content.ReadAsStringAsync().Result); + Assert.AreEqual(expected.Message, req.Content?.ReadAsStringAsync().Result); return true; }) .Respond(HttpStatusCode.OK, content); @@ -57,7 +57,7 @@ public void ApiCall_PostXmlLong_OKResponse() public void ApiCall_PostXmlString_OKResponse() { //Arrange - string body = "This is a xml body."; + var body = "This is a xml body."; var url = "/apicall/post-xml-string/200"; var expected = new ServerResponse() @@ -70,7 +70,7 @@ public void ApiCall_PostXmlString_OKResponse() handlerMock.When(GetCompleteUrl(url)) .With(req => { - Assert.AreEqual(expected.Message, req.Content.ReadAsStringAsync().Result); + Assert.AreEqual(expected.Message, req.Content?.ReadAsStringAsync().Result); return true; }) .Respond(HttpStatusCode.OK, content); diff --git a/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs b/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs new file mode 100644 index 00000000..9d1f7af6 --- /dev/null +++ b/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs @@ -0,0 +1,305 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Json; +using System.Threading.Tasks; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.MockTypes.Models.Pagination; +using APIMatic.Core.Test.MockTypes.Pagination; +using APIMatic.Core.Test.MockTypes.Pagination.MetaData; +using APIMatic.Core.Test.Utilities; +using NUnit.Framework; +using RichardSzalay.MockHttp; + +namespace APIMatic.Core.Test.Api.Pagination +{ + [TestFixture] + public class AsyncPaginator : ApiCallTest + { + [Test] + public async Task PaginateAsync_QueryOffsetPaginationYieldsAllItems_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions/offset"; + + var allTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + var transactionOffsets = PaginationHelper.ChunkTransactions(allTransactions, limit); + handlerMock.MockOffsetPaginatedResponses(transactionOffsets, GetCompleteUrl(url)); + + var asyncPageable = CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("offset", 0)) + .Query(q => q.Setup("limit", limit)))) + .PaginateAsync( + res => res.Data.Data, + OffsetPagedResponseFactory.Create, + page => page.Items, + PageableFactory.Create, + new OffsetPagination("$request.query#/offset") + ); + + var collected = new List(); + + // Act + await foreach (var item in asyncPageable) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(allTransactions.Count, collected.Count); + foreach (var tx in allTransactions) + { + Assert.IsTrue(collected.Any(t => t.Id == tx.Id)); + } + } + + [Test] + public async Task PaginateAsync_QueryCursorPaginationYieldsAllItems_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions"; + + var allTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + var transactionsWithCursor = PaginationHelper.ChunkTransactionsWithCursor(allTransactions, limit); + handlerMock.MockCursorPaginatedResponses(transactionsWithCursor, $"{GetCompleteUrl(url)}?limit={limit}"); + + var asyncPageable = CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("cursor", "cursor0")) + .Query(q => q.Setup("limit", limit)))) + .PaginateAsync( + res => res.Data.Data, + CursorPagedResponseFactory.Create, + page => page.Items, + PageableFactory.Create, + new CursorPagination("$response.body#/nextCursor", "$request.query#/cursor") + ); + + var collected = new List(); + + // Act + await foreach (var item in asyncPageable) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(allTransactions.Count, collected.Count); + foreach (var tx in allTransactions) + { + Assert.IsTrue(collected.Any(t => t.Id == tx.Id)); + } + } + + [Test] + public async Task PaginateAsync_QueryCursorPaginationInvalidPointer_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions"; + + var allTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + var transactionsWithCursor = PaginationHelper.ChunkTransactionsWithCursor(allTransactions, limit); + handlerMock.MockCursorPaginatedResponses(transactionsWithCursor, $"{GetCompleteUrl(url)}?limit={limit}"); + + var asyncPageable = CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("cursor", "cursor0")) + .Query(q => q.Setup("limit", limit)))) + .PaginateAsync( + res => res.Data.Data, + CursorPagedResponseFactory.Create, + page => page.Items, + PageableFactory.Create, + new CursorPagination("$response.body/nextCursor", "$request.query#/cursor") + ); + + var collected = new List(); + + // Act + await foreach (var item in asyncPageable) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(limit, collected.Count); + var expected = allTransactions.Take(limit).ToList(); + CollectionAssert.AreEquivalent(expected.Select(t => t.Id), collected.Select(t => t.Id)); + } + + [Test] + public async Task PaginateAsync_QueryLinkPaginationYieldsAllItems_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions"; + + var allTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + var linkedPages = PaginationHelper.ChunkTransactionsWithLinks(allTransactions, limit, url); + handlerMock.MockLinkedPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty)); + + var asyncPageable = CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))) + .PaginateAsync( + res => res.Data.Data, + LinkPagedResponseFactory.Create, + page => page.Items, + PageableFactory.Create, + new LinkPagination("$response.body#/links/next") + ); + + var collected = new List(); + + // Act + await foreach (var item in asyncPageable) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(allTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + + [Test] + public async Task PaginateAsync_QueryPagePaginationYieldsAllItemsAcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions/page"; + + var allTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + var pages = PaginationHelper.ChunkTransactions(allTransactions, limit); + handlerMock.MockPagePaginatedResponse(pages, GetCompleteUrl(url), limit); + + var asyncPageable = CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", 0)) // Initial page + .Query(q => q.Setup("limit", limit)))) + .PaginateAsync( + res => res.Data.Data, + NumberPagedResponseFactory.Create, + page => page.Items, + PageableFactory.Create, + new PagePagination("$request.query#/page") + ); + + var collected = new List(); + + // Act + await foreach (var tx in asyncPageable) + { + collected.Add(tx); + } + + // Assert + Assert.AreEqual(allTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + + [Test] + public async Task PaginateAsync_QueryMultiPaginationYieldsAllItems_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions"; + + var allTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + var linkedPages = PaginationHelper.ChunkTransactionsWithLinksAndPages(allTransactions, limit, url); + handlerMock.MockMultiPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty), limit); + + var asyncPageable = CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))) + .PaginateAsync( + res => res.Data.Data, + PagedResponseFactory.Create, + page => page.Items, + PageableFactory.Create, + new LinkPagination("$response.body#/links/next"), + new PagePagination("$request.query#/page") + ); + + var collected = new List(); + + // Act + await foreach (var item in asyncPageable) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(allTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Models/Pagination/Transaction.cs b/APIMatic.Core.Test/MockTypes/Models/Pagination/Transaction.cs new file mode 100644 index 00000000..04de2e5d --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Models/Pagination/Transaction.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using APIMatic.Core.Test.MockTypes.Convertors; +using Newtonsoft.Json; + +namespace APIMatic.Core.Test.MockTypes.Models.Pagination +{ + public class Transaction + { + /// + /// Initializes a new instance of the class. + /// + /// id. + /// amount. + /// timestamp. + public Transaction( + string id = null, + double? amount = null, + DateTime? timestamp = null) + { + this.Id = id; + this.Amount = amount; + this.Timestamp = timestamp; + } + + /// + /// Gets or sets Id. + /// + [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] + public string Id { get; set; } + + /// + /// Gets or sets Amount. + /// + [JsonProperty("amount", NullValueHandling = NullValueHandling.Ignore)] + public double? Amount { get; set; } + + /// + /// Gets or sets Timestamp. + /// + [JsonConverter(typeof(CustomDateTimeConverter), "r")] + [JsonProperty("timestamp", NullValueHandling = NullValueHandling.Ignore)] + public DateTime? Timestamp { get; set; } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsCursored.cs b/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsCursored.cs new file mode 100644 index 00000000..bbea5e37 --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsCursored.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace APIMatic.Core.Test.MockTypes.Models.Pagination +{ + public class TransactionsCursored + { + private string nextCursor; + private readonly Dictionary shouldSerialize = new Dictionary + { + { "nextCursor", false }, + }; + + /// + /// Initializes a new instance of the class. + /// + /// data. + /// nextCursor. + public TransactionsCursored( + List data = null, + string nextCursor = null) + { + this.Data = data; + + if (nextCursor != null) + { + this.NextCursor = nextCursor; + } + } + + /// + /// Gets or sets Data. + /// + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public List Data { get; set; } + + /// + /// Cursor for the next page of results. + /// + [JsonProperty("nextCursor")] + public string NextCursor + { + get + { + return this.nextCursor; + } + + set + { + this.shouldSerialize["nextCursor"] = true; + this.nextCursor = value; + } + } + + /// + /// Marks the field to not be serialized. + /// + public void UnsetNextCursor() + { + this.shouldSerialize["nextCursor"] = false; + } + + /// + /// Checks if the field should be serialized or not. + /// + /// A boolean weather the field should be serialized or not. + public bool ShouldSerializeNextCursor() + { + return this.shouldSerialize["nextCursor"]; + } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsLinked.cs b/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsLinked.cs new file mode 100644 index 00000000..d1b2ccc0 --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsLinked.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace APIMatic.Core.Test.MockTypes.Models.Pagination +{ + public class TransactionsLinked + { + /// + /// Initializes a new instance of the class. + /// + /// data. + /// links. + public TransactionsLinked( + List data = null, + Links links = null) + { + this.Data = data; + this.Links = links; + } + + /// + /// Gets or sets Data. + /// + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public List Data { get; set; } + + /// + /// Gets or sets Links. + /// + [JsonProperty("links", NullValueHandling = NullValueHandling.Ignore)] + public Links Links { get; set; } + } + + public class Links + { + /// + /// Initializes a new instance of the class. + /// + /// first. + /// last. + /// prev. + /// next. + public Links( + string first = null, + string last = null, + string prev = null, + string next = null) + { + this.First = first; + this.Last = last; + this.Prev = prev; + this.Next = next; + } + + /// + /// Gets or sets First. + /// + [JsonProperty("first", NullValueHandling = NullValueHandling.Ignore)] + public string First { get; set; } + + /// + /// Gets or sets Last. + /// + [JsonProperty("last", NullValueHandling = NullValueHandling.Ignore)] + public string Last { get; set; } + + /// + /// Gets or sets Prev. + /// + [JsonProperty("prev", NullValueHandling = NullValueHandling.Ignore)] + public string Prev { get; set; } + + /// + /// Gets or sets Next. + /// + [JsonProperty("next", NullValueHandling = NullValueHandling.Ignore)] + public string Next { get; set; } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsOffset.cs b/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsOffset.cs new file mode 100644 index 00000000..eab67504 --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Models/Pagination/TransactionsOffset.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace APIMatic.Core.Test.MockTypes.Models.Pagination +{ + public class TransactionsOffset + { + /// + /// Initializes a new instance of the class. + /// + /// data. + public TransactionsOffset( + List data = null) + { + this.Data = data; + } + + /// + /// Gets or sets Data. + /// + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public List Data { get; set; } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/CursorPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/CursorPagedResponse.cs new file mode 100644 index 00000000..6760ce3a --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/CursorPagedResponse.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.MockTypes.Http.Response; + +namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData +{ + public class CursorPagedResponse : PagedResponse + { + public CursorPagedResponse(ApiResponse pageData, CursorPagination manager, IEnumerable pageItems) : base( + pageData, pageItems) + { + Type = PaginationTypes.Cursor; + NextCursor = manager.CursorValue; + } + + public string NextCursor { get; } + } + + internal static class CursorPagedResponseFactory + { + public static CursorPagedResponse Create( + ApiResponse pageData, + IPaginationStrategy manager, + IEnumerable pageItems) + { + return new CursorPagedResponse( + pageData, + manager as CursorPagination, + pageItems); + } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/LinkPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/LinkPagedResponse.cs new file mode 100644 index 00000000..718feff7 --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/LinkPagedResponse.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.MockTypes.Http.Response; + +namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData +{ + public class LinkPagedResponse : PagedResponse + { + public LinkPagedResponse(ApiResponse pageData, LinkPagination manager, IEnumerable pageItems) : base( + pageData, pageItems) + { + Type = PaginationTypes.Link; + NextLink = manager.CurrentLinkValue; + } + + public string NextLink { get; } + } + + internal static class LinkPagedResponseFactory + { + public static LinkPagedResponse Create( + ApiResponse pageData, + IPaginationStrategy manager, + IEnumerable pageItems) + { + return new LinkPagedResponse( + pageData, + manager as LinkPagination, + pageItems); + } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs new file mode 100644 index 00000000..f43aab41 --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.MockTypes.Http.Response; + +namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData +{ + public class NumberPagedResponse : PagedResponse + { + public NumberPagedResponse(ApiResponse pageData, PagePagination manager, IEnumerable pageItems) : base( + pageData, pageItems) + { + Type = PaginationTypes.Page; + PageNumber = manager.CurrentPage; + } + + public int PageNumber { get; } + } + + internal static class NumberPagedResponseFactory + { + public static NumberPagedResponse Create( + ApiResponse pageData, + IPaginationStrategy manager, + IEnumerable pageItems) => + new NumberPagedResponse(pageData, manager as PagePagination, pageItems); + } +} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/OffsetPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/OffsetPagedResponse.cs new file mode 100644 index 00000000..35c2eeea --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/OffsetPagedResponse.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.MockTypes.Http.Response; + +namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData +{ + public class OffsetPagedResponse : PagedResponse + { + public OffsetPagedResponse(ApiResponse pageData, OffsetPagination manager, IEnumerable pageItems) : base( + pageData, pageItems) + { + Type = PaginationTypes.Offset; + Offset = manager.OffsetValue; + } + + public int Offset { get; } + } + + internal static class OffsetPagedResponseFactory + { + public static OffsetPagedResponse Create( + ApiResponse pageData, + IPaginationStrategy manager, + IEnumerable pageItems) => + new OffsetPagedResponse(pageData, manager as OffsetPagination, pageItems); + } +} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/PageableFactory.cs b/APIMatic.Core.Test/MockTypes/Pagination/PageableFactory.cs new file mode 100644 index 00000000..5c2ba291 --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Pagination/PageableFactory.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Pagination; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Request; + +namespace APIMatic.Core.Test.MockTypes.Pagination +{ + internal static class PageableFactory + { + public static AsyncPaginator Create( + Func>> + apiCallExecutor, + RequestBuilder requestBuilder, + Func> pagedResponseItemConverter, + params IPaginationStrategy[] dataManagers) => new AsyncPaginator(apiCallExecutor, + requestBuilder, pagedResponseItemConverter, dataManagers); + } +} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs new file mode 100644 index 00000000..47b311fa --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.MockTypes.Http.Response; +using APIMatic.Core.Test.MockTypes.Pagination.MetaData; + +namespace APIMatic.Core.Test.MockTypes.Pagination +{ + public class PagedResponse : ApiResponse + { + protected PagedResponse(ApiResponse pageData, IEnumerable pageItems) : base(pageData.StatusCode, + pageData.Headers, pageData.Data) + { + this.Items = pageItems; + } + + public IEnumerable Items { get; } + + public PaginationTypes Type { get; protected set; } + } + + public enum PaginationTypes + { + Offset, + Cursor, + Page, + Link + } + + internal static class PagedResponseFactory + { + public static PagedResponse Create( + ApiResponse pageData, + IPaginationStrategy manager, + IEnumerable pageItems) + { + return manager switch + { + OffsetPagination offsetPagination => OffsetPagedResponseFactory.Create(pageData, + offsetPagination, pageItems), + CursorPagination cursorPagination => CursorPagedResponseFactory.Create(pageData, + cursorPagination, pageItems), + PagePagination pagePagination => NumberPagedResponseFactory.Create(pageData, + pagePagination, pageItems), + LinkPagination linkPagination => LinkPagedResponseFactory.Create(pageData, linkPagination, + pageItems), + _ => throw new NotImplementedException("Unknown pagination type") + }; + } + } +} diff --git a/APIMatic.Core.Test/Utilities/PaginationHelper.cs b/APIMatic.Core.Test/Utilities/PaginationHelper.cs new file mode 100644 index 00000000..0b50b127 --- /dev/null +++ b/APIMatic.Core.Test/Utilities/PaginationHelper.cs @@ -0,0 +1,200 @@ +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http.Json; +using APIMatic.Core.Test.MockTypes.Models.Pagination; +using APIMatic.Core.Utilities; +using NUnit.Framework; +using RichardSzalay.MockHttp; + +namespace APIMatic.Core.Test.Utilities +{ + internal static class PaginationHelper + { + public static List ChunkTransactions(List transactions, int limit) + { + var chunks = transactions + .Select((tx, index) => new { tx, index }) + .GroupBy(x => x.index / limit) + .Select(g => new TransactionsOffset(g.Select(x => x.tx).ToList())) + .ToList(); + + chunks.Add(new TransactionsOffset(new List())); + + return chunks; + } + + public static void MockOffsetPaginatedResponses(this MockHttpMessageHandler handlerMock, + List pages, string baseUrl) + { + int offset = 0; + foreach (var page in pages) + { + var pagedContent = JsonContent.Create(page); + + handlerMock.When($"{baseUrl}?offset={offset}") + .With(req => + { + Assert.AreEqual("application/json", req.Headers.Accept.ToString()); + return true; + }) + .Respond(HttpStatusCode.OK, pagedContent); + + offset += page.Data?.Count ?? 0; + } + } + + public static void MockPagePaginatedResponse(this MockHttpMessageHandler handlerMock, + List pages, string baseUrl, int limit) + { + for (int i = 0; i < pages.Count; i++) + { + var content = JsonContent.Create(pages[i]); + var requestUrl = $"{baseUrl}?page={i}&limit={limit}"; + + handlerMock.When(requestUrl) + .With(req => + { + Assert.AreEqual("application/json", req.Headers.Accept.ToString()); + return true; + }) + .Respond(HttpStatusCode.OK, content); + } + } + + public static List ChunkTransactionsWithCursor(List transactions, int limit) + { + var chunks = transactions + .Select((tx, index) => new { tx, index }) + .GroupBy(x => x.index / limit) + .Select((g, i) => + new TransactionsCursored(g.Select(x => x.tx).ToList(), $"cursor{(i * limit) + g.Count()}")) + .ToList(); + + // Final empty page with null cursor + chunks.Add(new TransactionsCursored(new List(), null)); + return chunks; + } + + public static void MockCursorPaginatedResponses(this MockHttpMessageHandler handlerMock, + List pages, string baseUrl) + { + var cursor = "cursor0"; + foreach (var page in pages) + { + var pagedContent = JsonContent.Create(page); + var url = $"{baseUrl}&cursor={cursor}"; + + handlerMock.When(url) + .With(req => + { + Assert.AreEqual("application/json", req.Headers.Accept.ToString()); + return true; + }) + .Respond(HttpStatusCode.OK, pagedContent); + + cursor = page.NextCursor; + } + } + + public static List ChunkTransactionsWithLinks(List transactions, int limit, + string urlPath) + { + var chunks = transactions + .Select((tx, index) => new { tx, index }) + .GroupBy(x => x.index / limit) + .Select((g, i) => + { + var pageNumber = i + 1; + var nextLink = (i + 1) * limit < transactions.Count + ? $"{urlPath}?page={pageNumber + 1}&limit={limit}" + : null; + + var links = new Links(next: nextLink); + return new TransactionsLinked(g.Select(x => x.tx).ToList(), links); + }) + .ToList(); + + return chunks; + } + + public static void MockLinkedPaginatedResponses(this MockHttpMessageHandler handlerMock, + List pages, string baseUrl) + { + var currentLink = "/transactions?page=1&limit=2"; + for (int i = 0; i < pages.Count; i++) + { + var page = pages[i]; + var currentUrl = $"{baseUrl}{currentLink}"; + + handlerMock.When(currentUrl) + .With(req => + { + Assert.AreEqual("application/json", req.Headers.Accept.ToString()); + return true; + }) + .Respond(HttpStatusCode.OK, JsonContent.Create(page)); + currentLink = page.Links.Next; + } + } + + public static List ChunkTransactionsWithLinksAndPages(List transactions, + int limit, string urlPath) + { + var chunks = transactions + .Select((tx, index) => new { tx, index }) + .GroupBy(x => x.index / limit) + .Select((g, i) => + { + var pageNumber = i + 1; + var nextLink = (i + 1) * limit < transactions.Count + ? $"{urlPath}?page={pageNumber + 1}&limit={limit}" + : null; + + var links = i >= 1 ? new Links(next: null) : new Links(next: nextLink); + return new TransactionsLinked(g.Select(x => x.tx).ToList(), links); + }) + .ToList(); + + chunks.Add(new TransactionsLinked(new List(), null)); + + return chunks; + } + + public static void MockMultiPaginatedResponses(this MockHttpMessageHandler handlerMock, + List pages, string baseUrl, int limit) + { + var currentLink = "/transactions?page=1&limit=2"; + var linkPaginationPages = pages.Take(2).ToList(); + + for (int i = 0; i < linkPaginationPages.Count; i++) + { + var page = linkPaginationPages[i]; + var currentUrl = $"{baseUrl}{currentLink}"; + + handlerMock.When(currentUrl) + .With(req => + { + Assert.AreEqual("application/json", req.Headers.Accept.ToString()); + return true; + }) + .Respond(HttpStatusCode.OK, JsonContent.Create(page)); + currentLink = page.Links?.Next; + } + + for (int i = 2; i < pages.Count; i++) + { + var content = JsonContent.Create(pages[i]); + var requestUrl = $"{baseUrl}/transactions?page={i + 1}&limit={limit}"; + + handlerMock.When(requestUrl) + .With(req => + { + Assert.AreEqual("application/json", req.Headers.Accept.ToString()); + return true; + }) + .Respond(HttpStatusCode.OK, content); + } + } + } +} diff --git a/APIMatic.Core/APIMatic.Core.csproj b/APIMatic.Core/APIMatic.Core.csproj index e951f270..77f8288b 100644 --- a/APIMatic.Core/APIMatic.Core.csproj +++ b/APIMatic.Core/APIMatic.Core.csproj @@ -20,6 +20,7 @@ APIMatic;dotnet;core;Core Library https://www.apimatic.io icon.png + 8 @@ -29,6 +30,7 @@ + diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index 35139e5b..cafa62a4 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using APIMatic.Core.Http.Configuration; using APIMatic.Core.Pagination; +using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Request; using APIMatic.Core.Response; using APIMatic.Core.Types.Sdk; @@ -120,9 +121,9 @@ public async Task ExecuteAsync(CancellationToken cancellationToken = } private async Task> ExecutePaginationAsync( - IPaginationDataManager manager, + IPaginationStrategy manager, Func> converter, - Func, TPageMetadata> pageResponseConverter, + Func, TPageMetadata> pageResponseConverter, CancellationToken cancellationToken = default) { requestBuilder.AcceptHeader = responseHandler.AcceptHeader; @@ -142,36 +143,36 @@ private async Task> ExecutePaginationAsync public TEnumerable Paginate( Func> converter, - Func, TPageMetadata> pageResponseConverter, + Func, TPageMetadata> pageResponseConverter, + Func> pagedResponseItemConverter, Func< - Func>>, - RequestBuilder, IPaginationDataManager[], + Func>>, + RequestBuilder, Func>, IPaginationStrategy[], TEnumerable> returnTypeGetter, - params IPaginationDataManager[] dataManagers) + params IPaginationStrategy[] dataManagers) { return returnTypeGetter( (reqBuilder, manager) => RequestBuilder(reqBuilder) .ExecutePaginationAsync(manager, converter, pageResponseConverter), - requestBuilder.Clone(), + requestBuilder, + pagedResponseItemConverter, dataManagers); } public TEnumerable PaginateAsync( Func> converter, - Func, TPageMetadata> pageResponseConverter, + Func, TPageMetadata> pageResponseConverter, Func> pagedResponseItemConverter, Func< - Func>>, - RequestBuilder, Func>, IPaginationDataManager[], + Func>>, + RequestBuilder, Func>, IPaginationStrategy[], TEnumerable> returnTypeGetter, - params IPaginationDataManager[] dataManagers) + params IPaginationStrategy[] dataManagers) { - return returnTypeGetter( - async (reqBuilder, manager, cancellationToken) => - await RequestBuilder(reqBuilder) - .ExecutePaginationAsync(manager, converter, pageResponseConverter, cancellationToken) - .ConfigureAwait(false), - requestBuilder.Clone(), + return returnTypeGetter(async (reqBuilder, manager, cancellationToken) => + await RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter, + cancellationToken).ConfigureAwait(false), + requestBuilder, pagedResponseItemConverter, dataManagers); } diff --git a/APIMatic.Core/Authentication/AuthGroupBuilder.cs b/APIMatic.Core/Authentication/AuthGroupBuilder.cs index 50a32ad0..9913b4b7 100644 --- a/APIMatic.Core/Authentication/AuthGroupBuilder.cs +++ b/APIMatic.Core/Authentication/AuthGroupBuilder.cs @@ -121,22 +121,5 @@ public override async Task Apply(RequestBuilder requestBuilder) await authManager.Apply(requestBuilder).ConfigureAwait(false); } } - - public void Clone(AuthGroupBuilder clone) - { - foreach (var kvp in this.authManagersMap) - { - clone.authManagersMap[kvp.Key] = kvp.Value.Clone(); - } - foreach (var parameter in this.authManagers) - { - clone.authManagers.Add(parameter.Clone()); - } - foreach (var parameter in this.validatedAuthManagers) - { - clone.validatedAuthManagers.Add(parameter.Clone()); - } - clone.isAndGroup = isAndGroup; - } } } diff --git a/APIMatic.Core/Authentication/AuthManager.cs b/APIMatic.Core/Authentication/AuthManager.cs index 30465990..7345bdbf 100644 --- a/APIMatic.Core/Authentication/AuthManager.cs +++ b/APIMatic.Core/Authentication/AuthManager.cs @@ -44,12 +44,5 @@ public virtual Task Apply(RequestBuilder requestBuilder) parameters.Apply(requestBuilder); return Task.CompletedTask; } - - public AuthManager Clone() - { - var clone = new AuthManager(); - this.parameters.Clone(clone.parameters); - return clone; - } } } diff --git a/APIMatic.Core/Pagination/AsyncPaginator.cs b/APIMatic.Core/Pagination/AsyncPaginator.cs new file mode 100644 index 00000000..453773fa --- /dev/null +++ b/APIMatic.Core/Pagination/AsyncPaginator.cs @@ -0,0 +1,70 @@ +using APIMatic.Core.Request; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Pagination.Strategies; + +namespace APIMatic.Core.Pagination +{ + public class AsyncPaginator : IAsyncEnumerable + { + private readonly Func>> _apiCallExecutor; + private readonly RequestBuilder _requestBuilder; + private readonly IPaginationStrategy[] _paginationStrategies; + private readonly Func> _pagedResponseItemConverter; + + public AsyncPaginator( + Func>> apiCallExecutor, + RequestBuilder requestBuilder, + Func> pagedResponseItemConverter, + IPaginationStrategy[] paginationStrategies) + { + _apiCallExecutor = apiCallExecutor; + _requestBuilder = requestBuilder; + _pagedResponseItemConverter = pagedResponseItemConverter; + _paginationStrategies = paginationStrategies; + } + + public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) + { + await foreach (var pageMetaData in GetPagesAsync(cancellationToken)) + { + foreach (var item in _pagedResponseItemConverter(pageMetaData)) + { + yield return item; + } + } + } + + public async IAsyncEnumerable GetPagesAsync(CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + var paginationContext = PaginationContext.CreateDefault(_requestBuilder); + + foreach (var paginationStrategy in _paginationStrategies) + { + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + var requestBuilder = paginationStrategy.Apply(paginationContext); + if (requestBuilder == null) + break; + + var result = await _apiCallExecutor(requestBuilder, paginationStrategy, cancellationToken) + .ConfigureAwait(false); + + var items = result?.Items; + if (items == null || !items.Any()) + break; + + paginationContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); + + yield return result.PageMetadata; + } + } + } + } +} diff --git a/APIMatic.Core/Pagination/IPaginationDataManager.cs b/APIMatic.Core/Pagination/IPaginationDataManager.cs deleted file mode 100644 index a7e355bd..00000000 --- a/APIMatic.Core/Pagination/IPaginationDataManager.cs +++ /dev/null @@ -1,9 +0,0 @@ -using APIMatic.Core.Request; - -namespace APIMatic.Core.Pagination -{ - public interface IPaginationDataManager - { - bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder); - } -} diff --git a/APIMatic.Core/Pagination/PaginationContext.cs b/APIMatic.Core/Pagination/PaginationContext.cs new file mode 100644 index 00000000..ad967259 --- /dev/null +++ b/APIMatic.Core/Pagination/PaginationContext.cs @@ -0,0 +1,31 @@ +using APIMatic.Core.Request; +using APIMatic.Core.Types.Sdk; +using APIMatic.Core.Utilities; + +namespace APIMatic.Core.Pagination +{ + public class PaginationContext + { + private readonly CoreResponse _response; + + internal int DataSize { get; } + internal RequestBuilder RequestBuilder { get; } + internal bool HasResponse => _response != null; + internal string ResponseBody => _response?.Body; + internal string ResponseHeaders => CoreHelper.JsonSerialize(_response?.Headers); + + private PaginationContext(RequestBuilder requestBuilder, int dataSize = 0, CoreResponse response = null) + { + _response = response; + + DataSize = dataSize; + RequestBuilder = requestBuilder; + } + + internal static PaginationContext CreateDefault(RequestBuilder requestBuilder) => + new PaginationContext(RequestBuilder.RequestBuilderWithParameters(requestBuilder)); + + internal static PaginationContext Create(int dataSize, CoreResponse response, RequestBuilder requestBuilder) => + new PaginationContext(requestBuilder, dataSize, response); + } +} diff --git a/APIMatic.Core/Pagination/PaginationInfo.cs b/APIMatic.Core/Pagination/PaginationInfo.cs deleted file mode 100644 index c69827d8..00000000 --- a/APIMatic.Core/Pagination/PaginationInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using APIMatic.Core.Request; -using APIMatic.Core.Types.Sdk; -using APIMatic.Core.Utilities; - -namespace APIMatic.Core.Pagination -{ - public class PaginationInfo - { - private readonly int _lastDataSize; - private readonly CoreResponse _lastResponse; - private readonly RequestBuilder _lastRequestBuilder; - - public PaginationInfo(int lastDataSize, CoreResponse lastResponse, RequestBuilder lastRequestBuilder) - { - _lastDataSize = lastDataSize; - _lastResponse = lastResponse; - _lastRequestBuilder = lastRequestBuilder; - } - - internal RequestBuilder GetLastRequestBuilder() => _lastRequestBuilder; - internal string GetLastResponseBody() => _lastResponse?.Body; - internal string GetLastResponseHeaders() => CoreHelper.JsonSerialize(_lastResponse?.Headers); - internal int GetLastDataSize() => _lastDataSize; - } -} diff --git a/APIMatic.Core/Pagination/Paginator.cs b/APIMatic.Core/Pagination/Paginator.cs new file mode 100644 index 00000000..4622a496 --- /dev/null +++ b/APIMatic.Core/Pagination/Paginator.cs @@ -0,0 +1,67 @@ +using APIMatic.Core.Request; +using APIMatic.Core.Utilities; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using APIMatic.Core.Pagination.Strategies; + +namespace APIMatic.Core.Pagination +{ + public class Paginator : IEnumerable + { + private readonly Func>> _apiCallExecutor; + private readonly RequestBuilder _requestBuilder; + private readonly IPaginationStrategy[] _paginationStrategies; + private readonly Func> _pagedResponseItemConverter; + + protected Paginator( + Func>> apiCallExecutor, RequestBuilder requestBuilder, + Func> pagedResponseItemConverter, + IPaginationStrategy[] paginationStrategies) + { + _apiCallExecutor = apiCallExecutor; + _requestBuilder = requestBuilder; + _pagedResponseItemConverter = pagedResponseItemConverter; + _paginationStrategies = paginationStrategies; + } + + public IEnumerator GetEnumerator() + { + foreach (var pageMetaData in GetPages()) + { + foreach (var item in _pagedResponseItemConverter(pageMetaData)) + { + yield return item; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerable GetPages() + { + var paginationContext = PaginationContext.CreateDefault(_requestBuilder); + foreach (var paginationStrategy in _paginationStrategies) + { + while (true) + { + var requestBuilder = paginationStrategy.Apply(paginationContext); + if (requestBuilder == null) + break; + + var result = CoreHelper.RunTask(_apiCallExecutor(requestBuilder, paginationStrategy)); + + var items = result?.Items; + if (items == null || !items.Any()) + break; + + paginationContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); + + yield return result.PageMetadata; + } + } + } + } +} diff --git a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs new file mode 100644 index 00000000..a07f02fc --- /dev/null +++ b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs @@ -0,0 +1,58 @@ +using APIMatic.Core.Request; +using APIMatic.Core.Utilities; + +namespace APIMatic.Core.Pagination.Strategies +{ + public class CursorPagination : IPaginationStrategy + { + private readonly string _output; + private readonly string _input; + + public string CursorValue { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// JsonPointer of a field received in the response, representing the next cursor. + /// JsonPointer of a field in the request, representing the cursor. + public CursorPagination(string output, string input) + { + _output = output; + _input = input; + } + + public RequestBuilder Apply(PaginationContext paginationContext) + { + var isUpdated = false; + var currentRequestBuilder = RequestBuilder.RequestBuilderWithParameters(paginationContext.RequestBuilder) + .UpdateByReference( + _input, old => + { + var oldValue = old?.ToString(); + if (!paginationContext.HasResponse) + { + CursorValue = oldValue; + isUpdated = true; + return oldValue; + } + + var cursorValue = CoreHelper.GetValueByReference( + _output, + paginationContext.ResponseBody, + paginationContext.ResponseHeaders + ); + + if (cursorValue == null) + { + return oldValue; + } + + CursorValue = cursorValue; + isUpdated = true; + return CursorValue; + }); + + return isUpdated ? currentRequestBuilder : null; + } + } +} diff --git a/APIMatic.Core/Pagination/Strategies/IPaginationStrategy.cs b/APIMatic.Core/Pagination/Strategies/IPaginationStrategy.cs new file mode 100644 index 00000000..e4e49576 --- /dev/null +++ b/APIMatic.Core/Pagination/Strategies/IPaginationStrategy.cs @@ -0,0 +1,9 @@ +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination.Strategies +{ + public interface IPaginationStrategy + { + RequestBuilder Apply(PaginationContext paginationContext); + } +} diff --git a/APIMatic.Core/Pagination/Strategies/LinkPagination.cs b/APIMatic.Core/Pagination/Strategies/LinkPagination.cs new file mode 100644 index 00000000..f94199e3 --- /dev/null +++ b/APIMatic.Core/Pagination/Strategies/LinkPagination.cs @@ -0,0 +1,44 @@ +using APIMatic.Core.Utilities; +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination.Strategies +{ + public class LinkPagination : IPaginationStrategy + { + private readonly string _next; + + public string CurrentLinkValue { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// JsonPointer of a field in the response, representing the next request query URL. + public LinkPagination(string next) + { + _next = next; + } + + public RequestBuilder Apply(PaginationContext paginationContext) + { + CurrentLinkValue = CoreHelper.GetValueByReference( + _next, + paginationContext.ResponseBody, + paginationContext.ResponseHeaders + ); + + if (!paginationContext.HasResponse) + return RequestBuilder.RequestBuilderWithParameters(paginationContext.RequestBuilder); + + if (string.IsNullOrEmpty(CurrentLinkValue)) + return null; + + return new RequestBuilder(paginationContext.RequestBuilder).Parameters(parameters => + { + foreach (var pair in CoreHelper.ExtractQueryParametersForUrl(CurrentLinkValue)) + { + parameters.Query(query => query.Setup(pair.Key, pair.Value)); + } + }); + } + } +} diff --git a/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs b/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs new file mode 100644 index 00000000..b3133d56 --- /dev/null +++ b/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs @@ -0,0 +1,46 @@ +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination.Strategies +{ + public class OffsetPagination : IPaginationStrategy + { + private readonly string _input; + + public int OffsetValue { get; private set; } + + /// + /// Constructor accepting a JSON pointer to the offset field in the request. + /// + /// JsonPointer of a field in request, representing offset. + public OffsetPagination(string input) + { + _input = input; + } + + public RequestBuilder Apply(PaginationContext paginationContext) + { + var isUpdated = false; + + var currentRequestBuilder = RequestBuilder.RequestBuilderWithParameters(paginationContext.RequestBuilder) + .UpdateByReference( + _input, old => + { + var oldValue = int.Parse(old?.ToString() ?? "0"); + + if (!paginationContext.HasResponse) + { + OffsetValue = oldValue; + isUpdated = true; + return old; + } + + var newValue = oldValue + paginationContext.DataSize; + OffsetValue = newValue; + isUpdated = true; + return newValue; + }); + + return isUpdated ? currentRequestBuilder : null; + } + } +} diff --git a/APIMatic.Core/Pagination/Strategies/PagePagination.cs b/APIMatic.Core/Pagination/Strategies/PagePagination.cs new file mode 100644 index 00000000..d7890538 --- /dev/null +++ b/APIMatic.Core/Pagination/Strategies/PagePagination.cs @@ -0,0 +1,46 @@ +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination.Strategies +{ + public class PagePagination : IPaginationStrategy + { + private readonly string _input; + + public int CurrentPage { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + /// JsonPointer of a field in the request, representing the page number. + public PagePagination(string input) + { + _input = input; + } + + public RequestBuilder Apply(PaginationContext paginationContext) + { + var isUpdated = false; + + var currentRequestBuilder = RequestBuilder.RequestBuilderWithParameters(paginationContext.RequestBuilder) + .UpdateByReference( + _input, old => + { + var oldValue = int.TryParse(old?.ToString(), out var value) ? value : 0; + + if (!paginationContext.HasResponse) + { + CurrentPage = oldValue; + isUpdated = true; + return old; + } + + var newValue = oldValue + 1; + CurrentPage = newValue; + isUpdated = true; + return newValue; + }); + + return isUpdated ? currentRequestBuilder : null; + } + } +} diff --git a/APIMatic.Core/Pagination/Types/CursorPagination.cs b/APIMatic.Core/Pagination/Types/CursorPagination.cs deleted file mode 100644 index de4ccfd2..00000000 --- a/APIMatic.Core/Pagination/Types/CursorPagination.cs +++ /dev/null @@ -1,48 +0,0 @@ -using APIMatic.Core.Request; -using APIMatic.Core.Utilities; - -namespace APIMatic.Core.Pagination.Types -{ - public class CursorPagination : IPaginationDataManager - { - private readonly string _output; - private readonly string _input; - - public string CursorValue { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// JsonPointer of a field received in the response, representing the next cursor. - /// JsonPointer of a field in the request, representing the cursor. - public CursorPagination(string output, string input) - { - _output = output; - _input = input; - } - - public bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder) - { - updatedRequestBuilder = paginationInfo.GetLastRequestBuilder(); - - CursorValue = CoreHelper.GetValueByReference( - _output, - paginationInfo.GetLastResponseBody(), - paginationInfo.GetLastResponseHeaders() - ); - - bool isUpdated = false; - updatedRequestBuilder.UpdateByReference(_input, old => - { - if (CursorValue == null) - { - CursorValue = old.ToString(); - } - isUpdated = true; - return CursorValue; - }); - - return isUpdated; - } - } -} diff --git a/APIMatic.Core/Pagination/Types/LinkPagination.cs b/APIMatic.Core/Pagination/Types/LinkPagination.cs deleted file mode 100644 index 8f456d99..00000000 --- a/APIMatic.Core/Pagination/Types/LinkPagination.cs +++ /dev/null @@ -1,47 +0,0 @@ -using APIMatic.Core.Utilities; -using APIMatic.Core.Request; - -namespace APIMatic.Core.Pagination.Types -{ - public class LinkPagination : IPaginationDataManager - { - private readonly string _next; - - public string LinkValue { get; private set; } - - /// - /// Initializes a new instance of the class. - /// - /// JsonPointer of a field in the response, representing the next request query URL. - public LinkPagination(string next) - { - _next = next; - } - - public bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder) - { - updatedRequestBuilder = paginationInfo.GetLastRequestBuilder(); - - LinkValue = CoreHelper.GetValueByReference( - _next, - paginationInfo.GetLastResponseBody(), - paginationInfo.GetLastResponseHeaders() - ); - - if (string.IsNullOrEmpty(LinkValue)) - { - return false; - } - - updatedRequestBuilder.Parameters(_parameters => - { - foreach (var pair in CoreHelper.ExtractQueryParametersForUrl(LinkValue)) - { - _parameters.Query(_query => _query.Setup(pair.Key, pair.Value)); - } - }); - - return true; - } - } -} diff --git a/APIMatic.Core/Pagination/Types/OffsetPagination.cs b/APIMatic.Core/Pagination/Types/OffsetPagination.cs deleted file mode 100644 index 73a0b1ae..00000000 --- a/APIMatic.Core/Pagination/Types/OffsetPagination.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using APIMatic.Core.Request; - -namespace APIMatic.Core.Pagination.Types -{ - - public class OffsetPagination : IPaginationDataManager - { - private readonly string input; - - public int OffsetValue { get; private set; } - /// - /// Constructor accepting a JSON pointer to the offset field in the request. - /// - /// JsonPointer of a field in request, representing offset. - public OffsetPagination(string input) - { - this.input = input; - } - - public bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder) - { - updatedRequestBuilder = paginationInfo.GetLastRequestBuilder(); - - if (input == null) - { - return false; - } - - bool isUpdated = false; - - updatedRequestBuilder.UpdateByReference(input, old => - { - int oldValue = Convert.ToInt32(old); - int newValue = oldValue + paginationInfo.GetLastDataSize(); - isUpdated = true; - OffsetValue = newValue; - return OffsetValue; - }); - - return isUpdated; - } - } -} diff --git a/APIMatic.Core/Pagination/Types/PagePagination.cs b/APIMatic.Core/Pagination/Types/PagePagination.cs deleted file mode 100644 index a207b562..00000000 --- a/APIMatic.Core/Pagination/Types/PagePagination.cs +++ /dev/null @@ -1,44 +0,0 @@ -using APIMatic.Core.Request; - -namespace APIMatic.Core.Pagination.Types -{ - public class PagePagination : IPaginationDataManager - { - private readonly string _input; - - public int CurrentPage { get; private set; } - /// - /// Initializes a new instance of the class. - /// - /// JsonPointer of a field in the request, representing the page number. - public PagePagination(string input) - { - _input = input; - } - - public bool TryUpdateRequestBuilder(PaginationInfo paginationInfo, out RequestBuilder updatedRequestBuilder) - { - updatedRequestBuilder = paginationInfo.GetLastRequestBuilder(); - - if (string.IsNullOrEmpty(_input)) - { - return false; - } - - bool isUpdated = false; - updatedRequestBuilder.UpdateByReference(_input, oldValue => - { - if (!int.TryParse(oldValue?.ToString(), out int currentPage)) - { - currentPage = 1; - } - - isUpdated = true; - CurrentPage = currentPage + 1; - return CurrentPage; - }); - - return isUpdated; - } - } -} diff --git a/APIMatic.Core/Request/Parameters/AdditionalFormParams.cs b/APIMatic.Core/Request/Parameters/AdditionalFormParams.cs index 96826d34..99832fc5 100644 --- a/APIMatic.Core/Request/Parameters/AdditionalFormParams.cs +++ b/APIMatic.Core/Request/Parameters/AdditionalFormParams.cs @@ -15,19 +15,5 @@ public override MultipleParams Setup(string key, object value) parameters.Form(f => f.Setup(key, value)); return this; } - - public override Parameter Clone() - { - var clone = new AdditionalFormParams - { - key = this.key, - value = this.value, - validated = this.validated, - typeName = this.typeName - }; - - this.parameters.Clone(clone.parameters); - return clone; - } } } diff --git a/APIMatic.Core/Request/Parameters/AdditionalHeaderParams.cs b/APIMatic.Core/Request/Parameters/AdditionalHeaderParams.cs index 3f809722..677d123a 100644 --- a/APIMatic.Core/Request/Parameters/AdditionalHeaderParams.cs +++ b/APIMatic.Core/Request/Parameters/AdditionalHeaderParams.cs @@ -15,19 +15,5 @@ public override MultipleParams Setup(string key, object value) parameters.Header(h => h.Setup(key, value)); return this; } - - public override Parameter Clone() - { - var clone = new AdditionalHeaderParams - { - key = this.key, - value = this.value, - validated = this.validated, - typeName = this.typeName - }; - - this.parameters.Clone(clone.parameters); - return clone; - } } } diff --git a/APIMatic.Core/Request/Parameters/AdditionalQueryParams.cs b/APIMatic.Core/Request/Parameters/AdditionalQueryParams.cs index 889d806b..38f09fb5 100644 --- a/APIMatic.Core/Request/Parameters/AdditionalQueryParams.cs +++ b/APIMatic.Core/Request/Parameters/AdditionalQueryParams.cs @@ -15,19 +15,5 @@ public override MultipleParams Setup(string key, object value) parameters.Query(q => q.Setup(key, value)); return this; } - - public override Parameter Clone() - { - var clone = new AdditionalQueryParams - { - key = this.key, - value = this.value, - validated = this.validated, - typeName = this.typeName - }; - - this.parameters.Clone(clone.parameters); - return clone; - } } } diff --git a/APIMatic.Core/Request/Parameters/BodyParam.cs b/APIMatic.Core/Request/Parameters/BodyParam.cs index 423c2c9f..06b69159 100644 --- a/APIMatic.Core/Request/Parameters/BodyParam.cs +++ b/APIMatic.Core/Request/Parameters/BodyParam.cs @@ -36,17 +36,5 @@ internal override void Apply(RequestBuilder requestBuilder) } requestBuilder.bodyParameters[key] = value; } - - public override Parameter Clone() - { - return new BodyParam - { - key = this.key, - value = this.value, - validated = this.validated, - typeName = this.typeName, - valueType = this.valueType - }; - } } } diff --git a/APIMatic.Core/Request/Parameters/FormParam.cs b/APIMatic.Core/Request/Parameters/FormParam.cs index 684e031d..bd417da6 100644 --- a/APIMatic.Core/Request/Parameters/FormParam.cs +++ b/APIMatic.Core/Request/Parameters/FormParam.cs @@ -69,23 +69,5 @@ internal override void Apply(RequestBuilder requestBuilder) } requestBuilder.formParameters.AddRange(PrepareFormParameters(requestBuilder.ArraySerialization)); } - - public override Parameter Clone() - { - var clone = new FormParam - { - key = this.key, - value = this.value, - validated = this.validated, - typeName = this.typeName, - }; - - foreach (var header in this.encodingHeaders) - { - clone.encodingHeaders.Add(header.Key, header.Value); - } - - return clone; - } } } diff --git a/APIMatic.Core/Request/Parameters/HeaderParam.cs b/APIMatic.Core/Request/Parameters/HeaderParam.cs index 119941a1..c9c1a9f4 100644 --- a/APIMatic.Core/Request/Parameters/HeaderParam.cs +++ b/APIMatic.Core/Request/Parameters/HeaderParam.cs @@ -25,16 +25,5 @@ internal override void Apply(RequestBuilder requestBuilder) } requestBuilder.headers[key] = headerValue; } - - public override Parameter Clone() - { - return new HeaderParam - { - key = this.key, - value = this.value, - validated = this.validated, - typeName = this.typeName - }; - } } } diff --git a/APIMatic.Core/Request/Parameters/Parameter.cs b/APIMatic.Core/Request/Parameters/Parameter.cs index 746b02f2..c6aa03e9 100644 --- a/APIMatic.Core/Request/Parameters/Parameter.cs +++ b/APIMatic.Core/Request/Parameters/Parameter.cs @@ -75,8 +75,6 @@ internal virtual void Validate() internal abstract void Apply(RequestBuilder requestBuilder); - public abstract Parameter Clone(); - /// /// Used to build a request by creating and adding multiple parameters into the RequestBuilder /// @@ -227,40 +225,6 @@ internal void Apply(RequestBuilder requestBuilder) p.Apply(requestBuilder); } } - - internal void UpdateParameterValueByPointer(Func setter, JsonPointer pointer) - { - try - { - var paramsDictionary = parameters.ToDictionary(p => p.key, p => p.value); - var json = CoreHelper.JsonSerialize(paramsDictionary); - if (string.IsNullOrWhiteSpace(json)) { return; } - - JObject jsonObject = JObject.Parse(json); - JToken jsonToken = pointer.Evaluate(jsonObject); - object updatedValue = setter(jsonToken.ToObject()); - jsonToken.Replace(JToken.FromObject(updatedValue)); - - foreach (var parameter in parameters) - { - if (!jsonObject.TryGetValue(parameter.key, out JToken valueToken)) { continue; } - - parameter.Setup(parameter.key, valueToken.ToObject()); - } - } - catch - { - // return value - } - } - - internal void Clone(Builder clone) - { - foreach (var parameter in parameters) - { - clone.parameters.Add(parameter.Clone()); - } - } } } } diff --git a/APIMatic.Core/Request/Parameters/QueryParam.cs b/APIMatic.Core/Request/Parameters/QueryParam.cs index bc7a959b..11529b42 100644 --- a/APIMatic.Core/Request/Parameters/QueryParam.cs +++ b/APIMatic.Core/Request/Parameters/QueryParam.cs @@ -18,16 +18,5 @@ internal override void Apply(RequestBuilder requestBuilder) } requestBuilder.queryParameters[key] = value; } - - public override Parameter Clone() - { - return new QueryParam - { - key = this.key, - value = this.value, - validated = this.validated, - typeName = this.typeName, - }; - } } } diff --git a/APIMatic.Core/Request/Parameters/TemplateParam.cs b/APIMatic.Core/Request/Parameters/TemplateParam.cs index 1897e611..2f808362 100644 --- a/APIMatic.Core/Request/Parameters/TemplateParam.cs +++ b/APIMatic.Core/Request/Parameters/TemplateParam.cs @@ -50,16 +50,5 @@ internal override void Apply(RequestBuilder requestBuilder) string replacerValue = Uri.EscapeUriString(GetReplacerValue(innerValue ?? value)); requestBuilder.QueryUrl.Replace(string.Format("{{{0}}}", key), replacerValue); } - - public override Parameter Clone() - { - return new TemplateParam - { - key = this.key, - value = this.value, - validated = this.validated, - typeName = this.typeName - }; - } } } diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index a2687e40..5cd41627 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -5,15 +5,18 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; +using System.Web; using APIMatic.Core.Authentication; using APIMatic.Core.Http.Configuration; using APIMatic.Core.Request.Parameters; using APIMatic.Core.Types.Sdk; using APIMatic.Core.Utilities; using Microsoft.Json.Pointer; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace APIMatic.Core.Request @@ -45,7 +48,7 @@ internal RequestBuilder(GlobalConfiguration configuration) authGroup = new AuthGroupBuilder(configuration.AuthManagers); } - internal StringBuilder QueryUrl { get; } = new StringBuilder(); + internal StringBuilder QueryUrl { get; private set; } = new StringBuilder(); internal ContentType AcceptHeader { get; set; } = ContentType.SCALAR; @@ -146,24 +149,56 @@ public RequestBuilder XmlBodySerializer(Func xmlSerializer) public RequestBuilder UpdateByReference(string pointerString, Func setter) { - if (pointerString == null) + var index = pointerString.IndexOf('#'); + if (index < 0) return this; - var parts = pointerString.Split('#'); + var prefix = pointerString.Substring(0, index); + var path = pointerString.Substring(index + 1); + var jsonPointer = new JsonPointer(path); - if (parts.Length <= 1) - return this; + switch (prefix) + { + case "$request.path": + UpdateTemplateParams(setter, jsonPointer); + break; + case "$request.query": + UpdateQueryParams(setter, jsonPointer); + break; + case "$request.headers": + UpdateHeaderParams(setter, jsonPointer); + break; + } + + return this; + } + + private void UpdateTemplateParams(Func setter, JsonPointer jsonPointer) + { + var template = QueryUrl.ToString().ToTemplateParameters(); + QueryUrl = new StringBuilder(CoreHelper.UpdateValueByPointer(template, jsonPointer, setter) + .ToQueryString()); + } + + private void UpdateHeaderParams(Func setter, JsonPointer pointer) + { + var updatedHeaders = + CoreHelper.UpdateValueByPointer(new Dictionary(headers), pointer, setter); + + foreach (var entry in updatedHeaders) + { + headers[entry.Key] = entry.Value; + } + } - var jsonPointer = new JsonPointer(parts[1]); + private void UpdateQueryParams(Func setter, JsonPointer pointer) + { + var updatedQueryParams = + CoreHelper.UpdateValueByPointer(new Dictionary(queryParameters), pointer, setter); - switch (parts[0]) + foreach (var entry in updatedQueryParams) { - case "$request.path": case "$request.query": case "$request.headers": - parameters.UpdateParameterValueByPointer(setter, jsonPointer); - parameters.Apply(this); - return this; - default: - return this; + queryParameters[entry.Key] = entry.Value; } } @@ -259,49 +294,89 @@ private static object PrepareBody(dynamic value) return CoreHelper.JsonSerialize(value); } - public RequestBuilder Clone() + public RequestBuilder(RequestBuilder source) { - var clone = new RequestBuilder(configuration) - .WithRetryOption(retryOption) - .DisableContentType(); - clone.contentTypeAllowed = contentTypeAllowed; - clone.httpMethod = this.httpMethod; - clone.QueryUrl.Append(this.QueryUrl); - clone.AcceptHeader = this.AcceptHeader; - clone.ArraySerialization = this.ArraySerialization; - clone.HasBinaryResponse = this.HasBinaryResponse; - clone.xmlRequest = this.xmlRequest; - clone.bodySerializer = this.bodySerializer; - clone.body = this.body; - clone.bodyType = this.bodyType; - - foreach (var kvp in this.headers) - { - clone.headers.Add(kvp.Key, kvp.Value); - } + this.configuration = source.configuration; + this.retryOption = source.retryOption; + this.contentTypeAllowed = source.contentTypeAllowed; + this.httpMethod = source.httpMethod; + this.QueryUrl = new StringBuilder(source.QueryUrl.ToString()); + this.AcceptHeader = source.AcceptHeader; + this.ArraySerialization = source.ArraySerialization; + this.HasBinaryResponse = source.HasBinaryResponse; + this.xmlRequest = source.xmlRequest; + this.bodySerializer = source.bodySerializer; + this.body = source.body; + this.bodyType = source.bodyType; + + this.headers = new Dictionary(source.headers); + this.bodyParameters = new Dictionary(source.bodyParameters); + this.formParameters = new List>(source.formParameters); + this.queryParameters = new Dictionary(source.queryParameters); + this.authGroup = new AuthGroupBuilder(source.configuration.AuthManagers); + } + + public static RequestBuilder RequestBuilderWithParameters(RequestBuilder source) + { + var requestBuilder = new RequestBuilder(source); + source.parameters.Validate().Apply(requestBuilder); + return requestBuilder; + } + } + + internal static class QueryUrlParser + { + public static Dictionary ToTemplateParameters(this string queryUrl) + { + var uri = new Uri(queryUrl); + var query = HttpUtility.ParseQueryString(uri.Query); - foreach (var kvp in this.bodyParameters) - { - clone.bodyParameters.Add(kvp.Key, kvp.Value); - } + var paramDict = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var kvp in this.formParameters) + foreach (string key in query.AllKeys.Where(k => k != null)) { - clone.formParameters.Add(new KeyValuePair(kvp.Key, kvp.Value)); + var values = query.GetValues(key); + if (values.Length > 1) + { + paramDict[key] = values.Cast().ToList(); + } + else + { + paramDict[key] = values[0]; + } } - foreach (var kvp in this.queryParameters) + return paramDict; + } + + public static string ToQueryString(this Dictionary parameters) + { + if (parameters == null || parameters.Count == 0) + return string.Empty; + + var builder = new StringBuilder(); + + foreach (var kvp in parameters) { - clone.queryParameters.Add(kvp.Key, kvp.Value); + var key = WebUtility.UrlEncode(kvp.Key); + if (kvp.Value is IEnumerable list && !(kvp.Value is string)) + { + foreach (var val in list) + { + builder.Append($"{key}={WebUtility.UrlEncode(val?.ToString())}&"); + } + } + else + { + builder.Append($"{key}={WebUtility.UrlEncode(kvp.Value?.ToString())}&"); + } } - // Clone parameters builder - this.parameters.Clone(clone.parameters); - - // Clone authGroup - this.authGroup.Clone(clone.authGroup); + // Remove trailing '&' if present + if (builder.Length > 0) + builder.Length--; - return clone; + return builder.ToString(); } } } diff --git a/APIMatic.Core/Utilities/CoreHelper.cs b/APIMatic.Core/Utilities/CoreHelper.cs index 58718e11..7840f357 100644 --- a/APIMatic.Core/Utilities/CoreHelper.cs +++ b/APIMatic.Core/Utilities/CoreHelper.cs @@ -187,6 +187,39 @@ private static string GetJsonValueUsingPointer(JsonPointer pointer, string json) return jsonToken.ToString(); } + internal static T UpdateValueByPointer(T value, JsonPointer pointer, Func updater) + { + if (value == null || pointer == null || updater == null) + return value; + + try + { + // Step 1: Serialize the object to JSON + var json = CoreHelper.JsonSerialize(value); + if (string.IsNullOrWhiteSpace(json)) return value; + + // Step 2: Parse JSON and evaluate pointer + var jsonObject = JObject.Parse(json); + var jsonToken = pointer.Evaluate(jsonObject); + if (jsonToken == null) return value; + + // Step 3: Apply updater + var oldValue = jsonToken.ToObject(); + var newValue = updater(oldValue); + if (newValue == null) return value; + + // Step 4: Replace token + jsonToken.Replace(JToken.FromObject(newValue)); + + // Step 5: Deserialize back to T + var updatedJson = jsonObject.ToString(); + return CoreHelper.JsonDeserialize(updatedJson); + } + catch + { + return value; + } + } private static void AppendParameters(StringBuilder queryBuilder, ArraySerialization arraySerialization, KeyValuePair pair) { From b2c32c3d8d15254bee1df96533285bad29d9b057 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Tue, 10 Jun 2025 12:04:08 +0500 Subject: [PATCH 07/24] Unit tests added - Core helper UpdateValueByPointer and GetValueByReference unit tests added. - Code refactored. --- .../Api/Pagination/AsyncPaginator.cs | 26 +-- .../Api/Pagination/Paginator.cs | 110 +++++++++ APIMatic.Core.Test/MockTypes/Models/Person.cs | 13 ++ .../MetaData/NumberPagedResponse.cs | 4 +- .../MockTypes/Pagination/PagedResponse.cs | 2 +- ...PageableFactory.cs => PaginatorFactory.cs} | 12 +- APIMatic.Core.Test/RequestBuilderTest.cs | 29 +++ .../Utilities/CoreHelperTest.cs | 220 ++++++++++++++++++ APIMatic.Core/Pagination/AsyncPaginator.cs | 2 +- APIMatic.Core/Pagination/Paginator.cs | 4 +- ...{PagePagination.cs => NumberPagination.cs} | 6 +- APIMatic.Core/Request/RequestBuilder.cs | 7 +- APIMatic.Core/Utilities/CoreHelper.cs | 33 ++- 13 files changed, 420 insertions(+), 48 deletions(-) create mode 100644 APIMatic.Core.Test/Api/Pagination/Paginator.cs create mode 100644 APIMatic.Core.Test/MockTypes/Models/Person.cs rename APIMatic.Core.Test/MockTypes/Pagination/{PageableFactory.cs => PaginatorFactory.cs} (53%) create mode 100644 APIMatic.Core.Test/RequestBuilderTest.cs rename APIMatic.Core/Pagination/Strategies/{PagePagination.cs => NumberPagination.cs} (87%) diff --git a/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs b/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs index 9d1f7af6..4e8f500c 100644 --- a/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs @@ -47,7 +47,7 @@ public async Task PaginateAsync_QueryOffsetPaginationYieldsAllItems_AcrossPages( res => res.Data.Data, OffsetPagedResponseFactory.Create, page => page.Items, - PageableFactory.Create, + PaginatorFactory.Create, new OffsetPagination("$request.query#/offset") ); @@ -61,10 +61,7 @@ public async Task PaginateAsync_QueryOffsetPaginationYieldsAllItems_AcrossPages( // Assert Assert.AreEqual(allTransactions.Count, collected.Count); - foreach (var tx in allTransactions) - { - Assert.IsTrue(collected.Any(t => t.Id == tx.Id)); - } + CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); } [Test] @@ -96,7 +93,7 @@ public async Task PaginateAsync_QueryCursorPaginationYieldsAllItems_AcrossPages( res => res.Data.Data, CursorPagedResponseFactory.Create, page => page.Items, - PageableFactory.Create, + PaginatorFactory.Create, new CursorPagination("$response.body#/nextCursor", "$request.query#/cursor") ); @@ -110,10 +107,7 @@ public async Task PaginateAsync_QueryCursorPaginationYieldsAllItems_AcrossPages( // Assert Assert.AreEqual(allTransactions.Count, collected.Count); - foreach (var tx in allTransactions) - { - Assert.IsTrue(collected.Any(t => t.Id == tx.Id)); - } + CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); } [Test] @@ -145,7 +139,7 @@ public async Task PaginateAsync_QueryCursorPaginationInvalidPointer_AcrossPages( res => res.Data.Data, CursorPagedResponseFactory.Create, page => page.Items, - PageableFactory.Create, + PaginatorFactory.Create, new CursorPagination("$response.body/nextCursor", "$request.query#/cursor") ); @@ -192,7 +186,7 @@ public async Task PaginateAsync_QueryLinkPaginationYieldsAllItems_AcrossPages() res => res.Data.Data, LinkPagedResponseFactory.Create, page => page.Items, - PageableFactory.Create, + PaginatorFactory.Create, new LinkPagination("$response.body#/links/next") ); @@ -238,8 +232,8 @@ public async Task PaginateAsync_QueryPagePaginationYieldsAllItemsAcrossPages() res => res.Data.Data, NumberPagedResponseFactory.Create, page => page.Items, - PageableFactory.Create, - new PagePagination("$request.query#/page") + PaginatorFactory.Create, + new NumberPagination("$request.query#/page") ); var collected = new List(); @@ -284,9 +278,9 @@ public async Task PaginateAsync_QueryMultiPaginationYieldsAllItems_AcrossPages() res => res.Data.Data, PagedResponseFactory.Create, page => page.Items, - PageableFactory.Create, + PaginatorFactory.Create, new LinkPagination("$response.body#/links/next"), - new PagePagination("$request.query#/page") + new NumberPagination("$request.query#/page") ); var collected = new List(); diff --git a/APIMatic.Core.Test/Api/Pagination/Paginator.cs b/APIMatic.Core.Test/Api/Pagination/Paginator.cs new file mode 100644 index 00000000..6991cfef --- /dev/null +++ b/APIMatic.Core.Test/Api/Pagination/Paginator.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.MockTypes.Models.Pagination; +using APIMatic.Core.Test.MockTypes.Pagination; +using APIMatic.Core.Test.MockTypes.Pagination.MetaData; +using APIMatic.Core.Test.Utilities; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Api.Pagination +{ + [TestFixture] + public class Paginator : ApiCallTest + { + [Test] + public void Paginate_QueryOffsetPaginationYieldsAllItems_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions/offset"; + + var allTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + var transactionOffsets = PaginationHelper.ChunkTransactions(allTransactions, limit); + handlerMock.MockOffsetPaginatedResponses(transactionOffsets, GetCompleteUrl(url)); + + var paginator = CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("offset", 0)) + .Query(q => q.Setup("limit", limit)))) + .Paginate( + res => res.Data.Data, + OffsetPagedResponseFactory.Create, + page => page.Items, + PaginatorFactory.Create, + new OffsetPagination("$request.query#/offset") + ); + + var enumerator = ((IEnumerable)paginator).GetEnumerator(); + var collected = new List(); + while (enumerator.MoveNext()) + { + collected.Add((Transaction)enumerator.Current); + } + + // Assert + Assert.AreEqual(allTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + + [Test] + public void Paginate_QueryLinkPaginationYieldsAllItems_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions"; + + var allTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + var linkedPages = PaginationHelper.ChunkTransactionsWithLinks(allTransactions, limit, url); + handlerMock.MockLinkedPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty)); + + var paginator = CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))) + .Paginate( + res => res.Data.Data, + LinkPagedResponseFactory.Create, + page => page.Items, + PaginatorFactory.Create, + new LinkPagination("$response.body#/links/next") + ); + + var collected = new List(); + + // Act + foreach (var item in paginator) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(allTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Models/Person.cs b/APIMatic.Core.Test/MockTypes/Models/Person.cs new file mode 100644 index 00000000..b4c2a61c --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Models/Person.cs @@ -0,0 +1,13 @@ +using Newtonsoft.Json; + +namespace APIMatic.Core.Test.MockTypes.Models +{ + public class Person + { + [JsonProperty("name")] + public string Name { get; init; } + + [JsonProperty("age")] + public int Age { get; init; } + } +} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs index f43aab41..f3c48bd4 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs @@ -6,7 +6,7 @@ namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData { public class NumberPagedResponse : PagedResponse { - public NumberPagedResponse(ApiResponse pageData, PagePagination manager, IEnumerable pageItems) : base( + public NumberPagedResponse(ApiResponse pageData, NumberPagination manager, IEnumerable pageItems) : base( pageData, pageItems) { Type = PaginationTypes.Page; @@ -22,6 +22,6 @@ public static NumberPagedResponse Create( ApiResponse pageData, IPaginationStrategy manager, IEnumerable pageItems) => - new NumberPagedResponse(pageData, manager as PagePagination, pageItems); + new NumberPagedResponse(pageData, manager as NumberPagination, pageItems); } } diff --git a/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs index 47b311fa..d4abd061 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs @@ -40,7 +40,7 @@ public static PagedResponse Create( offsetPagination, pageItems), CursorPagination cursorPagination => CursorPagedResponseFactory.Create(pageData, cursorPagination, pageItems), - PagePagination pagePagination => NumberPagedResponseFactory.Create(pageData, + NumberPagination pagePagination => NumberPagedResponseFactory.Create(pageData, pagePagination, pageItems), LinkPagination linkPagination => LinkPagedResponseFactory.Create(pageData, linkPagination, pageItems), diff --git a/APIMatic.Core.Test/MockTypes/Pagination/PageableFactory.cs b/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs similarity index 53% rename from APIMatic.Core.Test/MockTypes/Pagination/PageableFactory.cs rename to APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs index 5c2ba291..7bbd1949 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/PageableFactory.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs @@ -8,14 +8,22 @@ namespace APIMatic.Core.Test.MockTypes.Pagination { - internal static class PageableFactory + internal static class PaginatorFactory { public static AsyncPaginator Create( Func>> apiCallExecutor, RequestBuilder requestBuilder, Func> pagedResponseItemConverter, - params IPaginationStrategy[] dataManagers) => new AsyncPaginator(apiCallExecutor, + params IPaginationStrategy[] dataManagers) => new(apiCallExecutor, + requestBuilder, pagedResponseItemConverter, dataManagers); + + public static Paginator Create( + Func>> + apiCallExecutor, + RequestBuilder requestBuilder, + Func> pagedResponseItemConverter, + params IPaginationStrategy[] dataManagers) => new(apiCallExecutor, requestBuilder, pagedResponseItemConverter, dataManagers); } } diff --git a/APIMatic.Core.Test/RequestBuilderTest.cs b/APIMatic.Core.Test/RequestBuilderTest.cs new file mode 100644 index 00000000..5e86b247 --- /dev/null +++ b/APIMatic.Core.Test/RequestBuilderTest.cs @@ -0,0 +1,29 @@ +using System; +using System.Net.Http; +using System.Threading.Tasks; +using APIMatic.Core.Request; +using NUnit.Framework; + +namespace APIMatic.Core.Test +{ + [TestFixture] + public class RequestBuilderTest : TestBase + { + readonly Action setupRequest = rb => + rb.Setup(HttpMethod.Get, "https://api.example.com/users"); + + [Test] + public async Task UpdateByReference_HeaderParam_UpdatesCorrectly() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value); + setupRequest(builder); + builder.Parameters(p => p.Header(h => h.Setup("id", 0))); + await builder.Build(); + + builder.UpdateByReference("$request.headers#/id", old => "999"); + + Assert.IsTrue(builder.headers.ContainsKey("id")); + Assert.IsTrue(builder.headers.ContainsValue("999")); + } + } +} diff --git a/APIMatic.Core.Test/Utilities/CoreHelperTest.cs b/APIMatic.Core.Test/Utilities/CoreHelperTest.cs index b2b267a1..0c40507f 100644 --- a/APIMatic.Core.Test/Utilities/CoreHelperTest.cs +++ b/APIMatic.Core.Test/Utilities/CoreHelperTest.cs @@ -11,6 +11,7 @@ using APIMatic.Core.Utilities; using APIMatic.Core.Utilities.Converters; using APIMatic.Core.Utilities.Date; +using Microsoft.Json.Pointer; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -808,7 +809,226 @@ private IEnumerable> GetParameters(List key } #endregion + + #region ExtractQueryParameters + [Test] + public void ExtractQueryParametersForUrl_WithValidUrl_ReturnsCorrectDictionary() + { + // Arrange + var url = "https://api.example.com/items?limit=10&page=2&search=hello%20world"; + + // Act + var result = CoreHelper.ExtractQueryParametersForUrl(url); + + // Assert + Assert.AreEqual(3, result.Count); + Assert.AreEqual("10", result["limit"]); + Assert.AreEqual("2", result["page"]); + Assert.AreEqual("hello world", result["search"]); + } + + [Test] + public void ExtractQueryParametersForUrl_WithNoQuery_ReturnsEmptyDictionary() + { + // Arrange + var url = "https://api.example.com/items"; + + // Act + var result = CoreHelper.ExtractQueryParametersForUrl(url); + + // Assert + Assert.IsEmpty(result); + } + + [Test] + public void ExtractQueryParametersForUrl_WithEmptyValue_ReturnsEmptyString() + { + // Arrange + var url = "https://api.example.com/items?foo="; + + // Act + var result = CoreHelper.ExtractQueryParametersForUrl(url); + + // Assert + Assert.AreEqual(1, result.Count); + Assert.AreEqual(string.Empty, result["foo"]); + } + + [Test] + public void ExtractQueryParametersForUrl_WithMissingValuePart_IgnoresIt() + { + // Arrange + var url = "https://api.example.com/items?onlykey"; + + // Act + var result = CoreHelper.ExtractQueryParametersForUrl(url); + + // Assert + Assert.AreEqual(1, result.Count); + Assert.AreEqual(string.Empty, result["onlykey"]); + } + #endregion + + #region JasonUpdatealueByPointer + [Test] + public void UpdateValueByPointer_ValidPointer_UpdatesValue() + { + // Arrange + var person = new Person { Name = "Alice", Age = 30 }; + var pointer = new JsonPointer("/age"); + + // Act + var updated = CoreHelper.UpdateValueByPointer( + person, + pointer, + old => int.Parse(old.ToString() ?? "") + 5 + ); + + // Assert + Assert.AreEqual(35, updated.Age); + Assert.AreEqual("Alice", updated.Name); + } + + [Test] + public void UpdateValueByPointer_NullValue_ReturnsOriginal() + { + // Act + var result = CoreHelper.UpdateValueByPointer( + null, + new JsonPointer("/name"), + old => "Bob" + ); + + // Assert + Assert.IsNull(result); + } + + [Test] + public void UpdateValueByPointer_NullPointer_ReturnsOriginal() + { + var person = new Person { Name = "Charlie", Age = 40 }; + var result = CoreHelper.UpdateValueByPointer( + person, + null, + old => "David" + ); + + Assert.AreEqual("Charlie", result.Name); + } + + [Test] + public void UpdateValueByPointer_NullUpdater_ReturnsOriginal() + { + var person = new Person { Name = "Charlie", Age = 40 }; + var result = CoreHelper.UpdateValueByPointer( + person, + new JsonPointer("/name"), + null + ); + + Assert.AreEqual("Charlie", result.Name); + } + + [Test] + public void UpdateValueByPointer_UpdaterReturnsNull_ReturnsOriginal() + { + var person = new Person { Name = "Eva", Age = 50 }; + var pointer = new JsonPointer("/name"); + + var result = CoreHelper.UpdateValueByPointer( + person, + pointer, + old => null + ); + + Assert.AreEqual("Eva", result.Name); + } + + [Test] + public void UpdateValueByPointer_InvalidPointer_ReturnsOriginal() + { + var person = new Person { Name = "Frank", Age = 60 }; + var invalidPointer = new JsonPointer("/nonexistent"); + + var result = CoreHelper.UpdateValueByPointer( + person, + invalidPointer, + old => "ShouldNotApply" + ); + + Assert.AreEqual("Frank", result.Name); + } + + [Test] + public void GetValueByReference_NullPointer_ReturnsNull() + { + var result = CoreHelper.GetValueByReference(null, "{}", "{}"); + Assert.IsNull(result); + } + + [Test] + public void GetValueByReference_InvalidFormat_ReturnsNull() + { + var result = CoreHelper.GetValueByReference("$response.body", "{}", "{}"); + Assert.IsNull(result); + } + + [Test] + public void GetValueByReference_ValidBodyPointer_ReturnsValue() + { + var json = @"{""name"":""alice"", ""age"":30}"; + var pointer = "$response.body#/name"; + + var result = CoreHelper.GetValueByReference(pointer, json, null); + + Assert.AreEqual("alice", result); + } + [Test] + public void GetValueByReference_ValidHeadersPointer_ReturnsValue() + { + var headers = @"{""content-type"":""application/json""}"; + var pointer = "$response.headers#/content-type"; + + var result = CoreHelper.GetValueByReference(pointer, null, headers); + + Assert.AreEqual("application/json", result); + } + + [Test] + public void GetValueByReference_PointerNotFound_ReturnsNull() + { + var json = @"{""name"":""alice""}"; + var pointer = "$response.body#/nonexistent"; + + var result = CoreHelper.GetValueByReference(pointer, json, null); + + Assert.IsNull(result); + } + + [Test] + public void GetValueByReference_UnsupportedPrefix_ReturnsNull() + { + var pointer = "$request.body#/name"; + var json = @"{""name"":""alice""}"; + + var result = CoreHelper.GetValueByReference(pointer, json, null); + + Assert.IsNull(result); + } + + [Test] + public void GetValueByReference_BooleanToken_ReturnsTokenToString() + { + var jsonBody = @"{ ""isActive"": true }"; + var pointerString = "$response.body#/isActive"; + + var result = CoreHelper.GetValueByReference(pointerString, jsonBody, null); + + Assert.AreEqual("True", result); + } + #endregion + #region PrepareFormFieldsFromObject [Test] diff --git a/APIMatic.Core/Pagination/AsyncPaginator.cs b/APIMatic.Core/Pagination/AsyncPaginator.cs index 453773fa..80d21b12 100644 --- a/APIMatic.Core/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core/Pagination/AsyncPaginator.cs @@ -58,7 +58,7 @@ public async IAsyncEnumerable GetPagesAsync(CancellationToken can var items = result?.Items; if (items == null || !items.Any()) - break; + yield break; paginationContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); diff --git a/APIMatic.Core/Pagination/Paginator.cs b/APIMatic.Core/Pagination/Paginator.cs index 4622a496..1cc402a9 100644 --- a/APIMatic.Core/Pagination/Paginator.cs +++ b/APIMatic.Core/Pagination/Paginator.cs @@ -16,7 +16,7 @@ public class Paginator : IEnumerable private readonly IPaginationStrategy[] _paginationStrategies; private readonly Func> _pagedResponseItemConverter; - protected Paginator( + public Paginator( Func>> apiCallExecutor, RequestBuilder requestBuilder, Func> pagedResponseItemConverter, IPaginationStrategy[] paginationStrategies) @@ -55,7 +55,7 @@ public IEnumerable GetPages() var items = result?.Items; if (items == null || !items.Any()) - break; + yield break; paginationContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); diff --git a/APIMatic.Core/Pagination/Strategies/PagePagination.cs b/APIMatic.Core/Pagination/Strategies/NumberPagination.cs similarity index 87% rename from APIMatic.Core/Pagination/Strategies/PagePagination.cs rename to APIMatic.Core/Pagination/Strategies/NumberPagination.cs index d7890538..46809581 100644 --- a/APIMatic.Core/Pagination/Strategies/PagePagination.cs +++ b/APIMatic.Core/Pagination/Strategies/NumberPagination.cs @@ -2,17 +2,17 @@ namespace APIMatic.Core.Pagination.Strategies { - public class PagePagination : IPaginationStrategy + public class NumberPagination : IPaginationStrategy { private readonly string _input; public int CurrentPage { get; private set; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// JsonPointer of a field in the request, representing the page number. - public PagePagination(string input) + public NumberPagination(string input) { _input = input; } diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 5cd41627..9feda92b 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -185,9 +185,12 @@ private void UpdateHeaderParams(Func setter, JsonPointer pointer var updatedHeaders = CoreHelper.UpdateValueByPointer(new Dictionary(headers), pointer, setter); - foreach (var entry in updatedHeaders) + foreach (var kvp in updatedHeaders) { - headers[entry.Key] = entry.Value; + if (!headers.TryGetValue(kvp.Key, out var existingValue) || existingValue != kvp.Value) + { + headers[kvp.Key] = kvp.Value; + } } } diff --git a/APIMatic.Core/Utilities/CoreHelper.cs b/APIMatic.Core/Utilities/CoreHelper.cs index 7840f357..85e22ca3 100644 --- a/APIMatic.Core/Utilities/CoreHelper.cs +++ b/APIMatic.Core/Utilities/CoreHelper.cs @@ -96,7 +96,7 @@ public static T JsonDeserialize(string json, JsonConverter converter = null) } } - public static IDictionary ExtractQueryParametersForUrl(string queryUrl) + internal static IDictionary ExtractQueryParametersForUrl(string queryUrl) { var queryParams = queryUrl.Split('?'); @@ -151,15 +151,12 @@ public static string GetValueByReference(string pointerString, string jsonBody, var jsonPointer = new JsonPointer(parts[1]); - switch (parts[0]) + return parts[0] switch { - case "$response.body": - return GetJsonValueUsingPointer(jsonPointer, jsonBody); - case "$response.headers": - return GetJsonValueUsingPointer(jsonPointer, jsonHeaders); - default: - return null; - } + "$response.body" => GetJsonValueUsingPointer(jsonPointer, jsonBody), + "$response.headers" => GetJsonValueUsingPointer(jsonPointer, jsonHeaders), + _ => null + }; } private static string GetJsonValueUsingPointer(JsonPointer pointer, string json) @@ -178,30 +175,28 @@ private static string GetJsonValueUsingPointer(JsonPointer pointer, string json) return null; } - if (jsonToken.Type == JTokenType.Null) - return null; - - if (jsonToken.Type == JTokenType.String) - return jsonToken.Value(); - - return jsonToken.ToString(); + return jsonToken.Type switch + { + JTokenType.Null => null, + JTokenType.String => jsonToken.Value(), + _ => jsonToken.ToString() + }; } internal static T UpdateValueByPointer(T value, JsonPointer pointer, Func updater) { - if (value == null || pointer == null || updater == null) + if (pointer == null || updater == null) return value; try { // Step 1: Serialize the object to JSON - var json = CoreHelper.JsonSerialize(value); + var json = JsonSerialize(value); if (string.IsNullOrWhiteSpace(json)) return value; // Step 2: Parse JSON and evaluate pointer var jsonObject = JObject.Parse(json); var jsonToken = pointer.Evaluate(jsonObject); - if (jsonToken == null) return value; // Step 3: Apply updater var oldValue = jsonToken.ToObject(); From 1a84fdb1b7843e9c8cbd660f1bab4ac05150f80c Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Wed, 11 Jun 2025 21:00:10 +0500 Subject: [PATCH 08/24] - add unit tests for HeaderParam and TemplateParam updateByReference - RefactorTemplate parameter and header to populate map instead of directly applying to buidler obejcts --- .../MockTypes/Models/Pagination/User.cs | 8 + APIMatic.Core.Test/ParametersTest.cs | 10 +- APIMatic.Core.Test/RequestBuilderTest.cs | 29 - .../Utilities/CoreHelperTest.cs | 534 +++++------------- .../Utilities/Json/JsonPointerAccessorTest.cs | 318 +++++++++++ APIMatic.Core/GlobalConfiguration.cs | 5 +- .../Pagination/Strategies/CursorPagination.cs | 3 +- .../Pagination/Strategies/LinkPagination.cs | 3 +- .../Request/Parameters/HeaderParam.cs | 8 +- .../Request/Parameters/TemplateParam.cs | 29 +- APIMatic.Core/Request/RequestBuilder.cs | 120 ++-- APIMatic.Core/Utilities/CoreHelper.cs | 98 +--- .../Utilities/Json/JsonPointerAccessor.cs | 141 +++++ 13 files changed, 679 insertions(+), 627 deletions(-) create mode 100644 APIMatic.Core.Test/MockTypes/Models/Pagination/User.cs delete mode 100644 APIMatic.Core.Test/RequestBuilderTest.cs create mode 100644 APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs create mode 100644 APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs diff --git a/APIMatic.Core.Test/MockTypes/Models/Pagination/User.cs b/APIMatic.Core.Test/MockTypes/Models/Pagination/User.cs new file mode 100644 index 00000000..292a35ef --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Models/Pagination/User.cs @@ -0,0 +1,8 @@ +namespace APIMatic.Core.Test.MockTypes.Models.Pagination +{ + internal sealed class User + { + public int Id { get; init; } + public string Name { get; init; } + } +} diff --git a/APIMatic.Core.Test/ParametersTest.cs b/APIMatic.Core.Test/ParametersTest.cs index 717d204a..166b283c 100644 --- a/APIMatic.Core.Test/ParametersTest.cs +++ b/APIMatic.Core.Test/ParametersTest.cs @@ -28,7 +28,7 @@ public void Parameters_Apply_WithoutValidation() Assert.False(requestBuilder.queryParameters.ContainsKey("query")); Assert.False(requestBuilder.formParameters.Exists(kv => kv.Key == "form")); Assert.False(requestBuilder.headers.ContainsKey("header")); - Assert.True(requestBuilder.QueryUrl.ToString().Contains("{template}")); + Assert.True(requestBuilder.GetQueryUrl().Contains("{template}")); new Parameter.Builder() @@ -74,7 +74,7 @@ public void TemplateParameter_Null() new Parameter.Builder().Template(p => p.Setup("template", null)).Validate().Apply(requestBuilder); - Assert.AreEqual("http://my/path:3000/v1/path/", requestBuilder.QueryUrl.ToString()); + Assert.AreEqual("http://my/path:3000/v1/path/", requestBuilder.GetQueryUrl()); } [Test] @@ -89,7 +89,7 @@ public void TemplateParameter_Collection() "gamma" })).Validate().Apply(requestBuilder); - Assert.AreEqual("http://my/path:3000/v1/path/asad/alpha/gamma", requestBuilder.QueryUrl.ToString()); + Assert.AreEqual("http://my/path:3000/v1/path/asad/alpha/gamma", requestBuilder.GetQueryUrl()); } [Test] @@ -99,7 +99,7 @@ public void TemplateParameter_DateTime() new Parameter.Builder().Template(p => p.Setup("template", new DateTime(2022, 12, 14))).Validate().Apply(requestBuilder); - Assert.AreEqual("http://my/path:3000/v1/path/2022-12-14T00:00:00", requestBuilder.QueryUrl.ToString()); + Assert.AreEqual("http://my/path:3000/v1/path/2022-12-14T00:00:00", requestBuilder.GetQueryUrl()); } [Test] @@ -111,7 +111,7 @@ public void TemplateParameter_DateTimeOffset() .Validate() .Apply(requestBuilder); - Assert.AreEqual("http://my/path:3000/v1/path/2022-12-14T02:01:04+00:00", requestBuilder.QueryUrl.ToString()); + Assert.AreEqual("http://my/path:3000/v1/path/2022-12-14T02:01:04+00:00", requestBuilder.GetQueryUrl()); } [Test] diff --git a/APIMatic.Core.Test/RequestBuilderTest.cs b/APIMatic.Core.Test/RequestBuilderTest.cs deleted file mode 100644 index 5e86b247..00000000 --- a/APIMatic.Core.Test/RequestBuilderTest.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading.Tasks; -using APIMatic.Core.Request; -using NUnit.Framework; - -namespace APIMatic.Core.Test -{ - [TestFixture] - public class RequestBuilderTest : TestBase - { - readonly Action setupRequest = rb => - rb.Setup(HttpMethod.Get, "https://api.example.com/users"); - - [Test] - public async Task UpdateByReference_HeaderParam_UpdatesCorrectly() - { - var builder = new RequestBuilder(LazyGlobalConfiguration.Value); - setupRequest(builder); - builder.Parameters(p => p.Header(h => h.Setup("id", 0))); - await builder.Build(); - - builder.UpdateByReference("$request.headers#/id", old => "999"); - - Assert.IsTrue(builder.headers.ContainsKey("id")); - Assert.IsTrue(builder.headers.ContainsValue("999")); - } - } -} diff --git a/APIMatic.Core.Test/Utilities/CoreHelperTest.cs b/APIMatic.Core.Test/Utilities/CoreHelperTest.cs index 0c40507f..98aba361 100644 --- a/APIMatic.Core.Test/Utilities/CoreHelperTest.cs +++ b/APIMatic.Core.Test/Utilities/CoreHelperTest.cs @@ -11,7 +11,6 @@ using APIMatic.Core.Utilities; using APIMatic.Core.Utilities.Converters; using APIMatic.Core.Utilities.Date; -using Microsoft.Json.Pointer; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using NUnit.Framework; @@ -22,6 +21,7 @@ namespace APIMatic.Core.Test.Utilities public class CoreHelperTest : TestBase { internal const string SERVER_URL = "http://my/path:3000/v1"; + #region CleanUrl [Test] @@ -61,19 +61,20 @@ public void CleanUrl_InvalidUrl() stringBuilder.Append("://localhost:3000//api/body"); Assert.Throws(() => CoreHelper.CleanUrl(stringBuilder)); } + #endregion #region Serialize + [Test] public void JsonSerialize_ServerResponse() { ServerResponse serverResponse = new ServerResponse() { - Message = "Pass", - Passed = true, - Input = "Test is pass" + Message = "Pass", Passed = true, Input = "Test is pass" }; - string expected = "{\"passed\":true,\"Message\":\"Pass\",\"input\":\"Test is pass\",\"AdditionalProperties\":null}"; + string expected = + "{\"passed\":true,\"Message\":\"Pass\",\"input\":\"Test is pass\",\"AdditionalProperties\":null}"; string actual = CoreHelper.JsonSerialize(serverResponse); Assert.AreEqual(expected, actual); } @@ -81,10 +82,7 @@ public void JsonSerialize_ServerResponse() [Test] public void JsonSerialize_CustomAttributeClass() { - TestModelC testModelC = new TestModelC() - { - Name = "Test", - }; + TestModelC testModelC = new TestModelC() { Name = "Test", }; string expected = "{\"$id\":\"1\",\"Name\":\"Test\"}"; string actual = CoreHelper.JsonSerialize(testModelC); Assert.AreEqual(expected, actual); @@ -102,10 +100,7 @@ public void JsonSerialize_Null_Objects() [Test] public void JsonSerialize_DateTime_RFCFormat() { - List list = new List - { - DateTime.Parse("2018-4-12") - }; + List list = new List { DateTime.Parse("2018-4-12") }; string expected = "[\"Thu, 12 Apr 2018 00:00:00 GMT\"]"; string actual = CoreHelper.JsonSerialize(list, new CoreCustomDateTimeConverter("r")); Assert.AreEqual(expected, actual); @@ -132,13 +127,15 @@ public void JsonSerialize_JsonString_NotAString() [Test] public void JsonSerialize_EnumStringAllowUnknownEnumValues() { - Assert.Throws(() => CoreHelper.JsonSerialize(WorkingDaysAllowAdditionalValues._Unknown)); + Assert.Throws(() => + CoreHelper.JsonSerialize(WorkingDaysAllowAdditionalValues._Unknown)); } [Test] public void JsonSerialize_EnumNumberAllowUnknownEnumValues() { - Assert.Throws(() => CoreHelper.JsonSerialize(MonthNumberAllowAdditionalValues._Unknown)); + Assert.Throws(() => + CoreHelper.JsonSerialize(MonthNumberAllowAdditionalValues._Unknown)); } [Test] @@ -158,13 +155,9 @@ public void JsonSerialize_EnumNumber() [Test] public void JsonDeserialize_ServerResponse() { - string serverResponseJson = "{\"passed\":true,\"Message\":\"Pass\",\"input\":\"Test is pass\",\"AdditionalProperties\":null}"; - ServerResponse expected = new ServerResponse() - { - Message = "Pass", - Passed = true, - Input = "Test is pass" - }; + string serverResponseJson = + "{\"passed\":true,\"Message\":\"Pass\",\"input\":\"Test is pass\",\"AdditionalProperties\":null}"; + ServerResponse expected = new ServerResponse() { Message = "Pass", Passed = true, Input = "Test is pass" }; ServerResponse actual = CoreHelper.JsonDeserialize(serverResponseJson); Assert.AreEqual(expected, actual); @@ -175,13 +168,11 @@ public void JsonDeserialize_ServerResponse() public void JsonDeserialize_DateTime() { DateTime dateTime = new DateTime(2022, 12, 12); - List expected = new List - { - dateTime - }; + List expected = new List { dateTime }; string dateTimeString = CoreHelper.JsonSerialize(expected, new CoreCustomDateTimeConverter("r")); - List actual = CoreHelper.JsonDeserialize>(dateTimeString, new CoreCustomDateTimeConverter("r")); + List actual = + CoreHelper.JsonDeserialize>(dateTimeString, new CoreCustomDateTimeConverter("r")); Assert.AreEqual(expected, actual); } @@ -277,12 +268,10 @@ public void AppendQueryParameter_ValidParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var parametersKeys = new List() - { - "service", "api" - }; + var parametersKeys = new List() { "service", "api" }; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, "messagingService", "testApi")); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, + GetParameters(parametersKeys, "messagingService", "testApi")); string expected = $"{SERVER_URL}?service=messagingService&api=testApi"; string actual = queryBuilder.ToString(); @@ -307,13 +296,11 @@ public void AppendQueryParameter_DateTimeParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime=2022-12-14T00:00:00"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, new DateTime(2022, 12, 14))); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, + GetParameters(parametersKeys, new DateTime(2022, 12, 14))); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } @@ -323,10 +310,7 @@ public void AppendQueryParameter_DateTimeOffsetParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; var dateTimeOffset = new DateTimeOffset(new DateTime(2022, 12, 14)); string dateTimeFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.FFFFFFFK"; @@ -342,10 +326,7 @@ public void AppendQueryParameter_NullParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; object obj = null; string expected = $"{SERVER_URL}"; @@ -359,10 +340,7 @@ public void AppendQueryParameter_NullListParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - List parametersKeys = new List() - { - "list" - }; + List parametersKeys = new List() { "list" }; List obj = null; string expected = $"{SERVER_URL}"; @@ -376,10 +354,7 @@ public void AppendQueryParameter_EmptyListParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - List parametersKeys = new List() - { - "list" - }; + List parametersKeys = new List() { "list" }; List obj = new List(); string expected = $"{SERVER_URL}"; @@ -393,10 +368,7 @@ public void AppendQueryParameter_NullDictionaryParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - List parametersKeys = new List() - { - "dictionary" - }; + List parametersKeys = new List() { "dictionary" }; Dictionary dictionary = null; string expected = $"{SERVER_URL}"; @@ -410,10 +382,7 @@ public void AppendQueryParameter_EmptyDictionaryParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - List parametersKeys = new List() - { - "dictionary" - }; + List parametersKeys = new List() { "dictionary" }; Dictionary dictionary = new Dictionary(); string expected = $"{SERVER_URL}"; @@ -427,15 +396,9 @@ public void AppendQueryParameter_UnIndexedCollectionParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var stringCollection = new List() - { - "test", "collection" - }; + var stringCollection = new List() { "test", "collection" }; - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime[]=test&dateTime[]=collection"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection)); @@ -449,18 +412,13 @@ public void AppendQueryParameter_IndexedCollectionParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var stringCollection = new List() - { - "test", "collection" - }; + var stringCollection = new List() { "test", "collection" }; - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime[0]=test&dateTime[1]=collection"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), ArraySerialization.Indexed); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), + ArraySerialization.Indexed); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } @@ -470,18 +428,13 @@ public void AppendQueryParameter_PlainCollectionParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var stringCollection = new List() - { - "test", "collection" - }; + var stringCollection = new List() { "test", "collection" }; - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime=test&dateTime=collection"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), ArraySerialization.Plain); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), + ArraySerialization.Plain); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } @@ -491,18 +444,13 @@ public void AppendQueryParameter_TabSeparatedCollectionParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var stringCollection = new List() - { - "test", "collection" - }; + var stringCollection = new List() { "test", "collection" }; - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime=test\tcollection"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), ArraySerialization.TSV); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), + ArraySerialization.TSV); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } @@ -512,18 +460,13 @@ public void AppendQueryParameter_CommaSeparatedCollectionParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var stringCollection = new List() - { - "test", "collection" - }; + var stringCollection = new List() { "test", "collection" }; - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime=test,collection"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), ArraySerialization.CSV); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), + ArraySerialization.CSV); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } @@ -535,10 +478,7 @@ public void AppendQueryParameter_CustomParameter() queryBuilder.Append(SERVER_URL); var serverResponse = new ServerResponse(true); - var parametersKeys = new List() - { - "serverResponse" - }; + var parametersKeys = new List() { "serverResponse" }; string expected = $"{SERVER_URL}?serverResponse%5Bpassed%5D=true"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, serverResponse)); @@ -552,19 +492,13 @@ public void AppendQueryParameter_IndexedCustomDictionaryParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var dictionaryData = new Dictionary() - { - {"key1", "value1" }, - {"key2", "value2" } - }; + var dictionaryData = new Dictionary() { { "key1", "value1" }, { "key2", "value2" } }; - var parametersKeys = new List() - { - "dictionary" - }; + var parametersKeys = new List() { "dictionary" }; string expected = $"{SERVER_URL}?dictionary[0]=%5Bkey1%2C%20value1%5D&dictionary[1]=%5Bkey2%2C%20value2%5D"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, dictionaryData), ArraySerialization.Indexed); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, dictionaryData), + ArraySerialization.Indexed); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } @@ -575,14 +509,8 @@ public void AppendQueryParameter_CustomDateTimeParameter() StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); DateTime dateTime = new DateTime(2022, 12, 15); - TestModel testModel = new TestModel() - { - TestDateTime = dateTime - }; - var parametersKeys = new List() - { - "dateTime" - }; + TestModel testModel = new TestModel() { TestDateTime = dateTime }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime%5BTestDateTime%5D=2022-12-15T00%3A00%3A00"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, testModel)); @@ -596,14 +524,8 @@ public void AppendQueryParameter_CustomDateTimeConvertorParameter() StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); DateTime dateTime = new DateTime(2022, 12, 15); - TestModelB testModel = new TestModelB() - { - TestDateTime = dateTime - }; - var parametersKeys = new List() - { - "dateTime" - }; + TestModelB testModel = new TestModelB() { TestDateTime = dateTime }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime%5BTestDateTime%5D=2022-12-15"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, testModel)); @@ -619,16 +541,11 @@ public void AppendQueryParameter_CustomStreamParameter() queryBuilder.Append(SERVER_URL); byte[] buffer = Encoding.ASCII.GetBytes("test"); Stream memoryStream = new MemoryStream(buffer); - TestModel testModel = new TestModel() - { - DateStream = memoryStream - }; - var parametersKeys = new List() - { - "stream" - }; + TestModel testModel = new TestModel() { DateStream = memoryStream }; + var parametersKeys = new List() { "stream" }; - string expected = $"{SERVER_URL}?stream%5BDateStream%5D=System.IO.MemoryStream&stream%5BTestDateTime%5D=0001-01-01T00%3A00%3A00"; + string expected = + $"{SERVER_URL}?stream%5BDateStream%5D=System.IO.MemoryStream&stream%5BTestDateTime%5D=0001-01-01T00%3A00%3A00"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, testModel)); memoryStream.Dispose(); string actual = queryBuilder.ToString(); @@ -645,10 +562,7 @@ public void AppendQueryParameter_CustomJObjectParameter() jObject.Album = "alpha"; jObject.Artist = "beta"; - var parametersKeys = new List() - { - "jObject" - }; + var parametersKeys = new List() { "jObject" }; string expected = $"{SERVER_URL}?jObject%5BAlbum%5D=alpha&jObject%5BArtist%5D=beta"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, jObject)); @@ -662,13 +576,11 @@ public void AppendQueryParameter_CustomEnumParameter() StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var parametersKeys = new List() - { - "enum" - }; + var parametersKeys = new List() { "enum" }; string expected = $"{SERVER_URL}?enum=5"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, ArraySerialization.PSV)); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, + GetParameters(parametersKeys, ArraySerialization.PSV)); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } @@ -678,18 +590,13 @@ public void AppendQueryParameter_PipeSeparatedCollectionParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var stringCollection = new List() - { - "test", "collection" - }; + var stringCollection = new List() { "test", "collection" }; - var parametersKeys = new List() - { - "dateTime" - }; + var parametersKeys = new List() { "dateTime" }; string expected = $"{SERVER_URL}?dateTime=test|collection"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), ArraySerialization.PSV); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), + ArraySerialization.PSV); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } @@ -699,15 +606,9 @@ public void AppendQueryParameter_CustomCollectionParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var stringCollection = new List() - { - new ServerResponse(true) - }; + var stringCollection = new List() { new ServerResponse(true) }; - var parametersKeys = new List() - { - "serverResponse" - }; + var parametersKeys = new List() { "serverResponse" }; string expected = $"{SERVER_URL}?serverResponse%5B0%5D%5Bpassed%5D=true"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection)); @@ -721,10 +622,7 @@ public void AppendQueryParameter_CustomNullCollectionParameter() StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); IList stringCollection = null; - var parametersKeys = new List() - { - "serverResponse" - }; + var parametersKeys = new List() { "serverResponse" }; string expected = $"{SERVER_URL}"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection)); @@ -738,10 +636,7 @@ public void AppendQueryParameter_WithAlreadyAppendedParameter() StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL + "?x=9"); IList stringCollection = null; - var parametersKeys = new List() - { - "serverResponse" - }; + var parametersKeys = new List() { "serverResponse" }; string expected = $"{SERVER_URL}?x=9"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection)); @@ -761,16 +656,11 @@ public void AppendQueryParameter_IntegerArrayParameter() integers[2] = 5; integers[3] = 6; integers[4] = 7; - TestModel testModel = new TestModel - { - Integers = integers - }; - var parametersKeys = new List() - { - "req" - }; + TestModel testModel = new TestModel { Integers = integers }; + var parametersKeys = new List() { "req" }; - string expected = $"{SERVER_URL}?req%5BIntegers%5D[]=9&req%5BIntegers%5D[]=4&req%5BIntegers%5D[]=5&req%5BIntegers%5D[]=6&req%5BIntegers%5D[]=7&req%5BTestDateTime%5D=0001-01-01T00%3A00%3A00"; + string expected = + $"{SERVER_URL}?req%5BIntegers%5D[]=9&req%5BIntegers%5D[]=4&req%5BIntegers%5D[]=5&req%5BIntegers%5D[]=6&req%5BIntegers%5D[]=7&req%5BTestDateTime%5D=0001-01-01T00%3A00%3A00"; CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, testModel)); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); @@ -784,21 +674,21 @@ public void AppendQueryParameter_CustomCollectionOfCollectionParameter() queryBuilder.Append(SERVER_URL); var stringCollection = new List>() { - new List(){ new ServerResponse(true) } + new List() { new ServerResponse(true) } }; - var parametersKeys = new List() - { - "serverResponse" - }; + var parametersKeys = new List() { "serverResponse" }; - string expected = $"{SERVER_URL}?serverResponse=System.Collections.Generic.List%601%5BAPIMatic.Core.Test.MockTypes.Models.ServerResponse%5D"; - CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), ArraySerialization.Plain); + string expected = + $"{SERVER_URL}?serverResponse=System.Collections.Generic.List%601%5BAPIMatic.Core.Test.MockTypes.Models.ServerResponse%5D"; + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, GetParameters(parametersKeys, stringCollection), + ArraySerialization.Plain); string actual = queryBuilder.ToString(); Assert.AreEqual(expected, actual); } - private IEnumerable> GetParameters(List keys = null, params object[] parameters) + private IEnumerable> GetParameters(List keys = null, + params object[] parameters) { int index = 0; foreach (object parameter in parameters) @@ -809,8 +699,9 @@ private IEnumerable> GetParameters(List key } #endregion - + #region ExtractQueryParameters + [Test] public void ExtractQueryParametersForUrl_WithValidUrl_ReturnsCorrectDictionary() { @@ -867,168 +758,9 @@ public void ExtractQueryParametersForUrl_WithMissingValuePart_IgnoresIt() Assert.AreEqual(1, result.Count); Assert.AreEqual(string.Empty, result["onlykey"]); } - #endregion - - #region JasonUpdatealueByPointer - [Test] - public void UpdateValueByPointer_ValidPointer_UpdatesValue() - { - // Arrange - var person = new Person { Name = "Alice", Age = 30 }; - var pointer = new JsonPointer("/age"); - - // Act - var updated = CoreHelper.UpdateValueByPointer( - person, - pointer, - old => int.Parse(old.ToString() ?? "") + 5 - ); - - // Assert - Assert.AreEqual(35, updated.Age); - Assert.AreEqual("Alice", updated.Name); - } - [Test] - public void UpdateValueByPointer_NullValue_ReturnsOriginal() - { - // Act - var result = CoreHelper.UpdateValueByPointer( - null, - new JsonPointer("/name"), - old => "Bob" - ); - - // Assert - Assert.IsNull(result); - } - - [Test] - public void UpdateValueByPointer_NullPointer_ReturnsOriginal() - { - var person = new Person { Name = "Charlie", Age = 40 }; - var result = CoreHelper.UpdateValueByPointer( - person, - null, - old => "David" - ); - - Assert.AreEqual("Charlie", result.Name); - } - - [Test] - public void UpdateValueByPointer_NullUpdater_ReturnsOriginal() - { - var person = new Person { Name = "Charlie", Age = 40 }; - var result = CoreHelper.UpdateValueByPointer( - person, - new JsonPointer("/name"), - null - ); - - Assert.AreEqual("Charlie", result.Name); - } - - [Test] - public void UpdateValueByPointer_UpdaterReturnsNull_ReturnsOriginal() - { - var person = new Person { Name = "Eva", Age = 50 }; - var pointer = new JsonPointer("/name"); - - var result = CoreHelper.UpdateValueByPointer( - person, - pointer, - old => null - ); - - Assert.AreEqual("Eva", result.Name); - } - - [Test] - public void UpdateValueByPointer_InvalidPointer_ReturnsOriginal() - { - var person = new Person { Name = "Frank", Age = 60 }; - var invalidPointer = new JsonPointer("/nonexistent"); - - var result = CoreHelper.UpdateValueByPointer( - person, - invalidPointer, - old => "ShouldNotApply" - ); - - Assert.AreEqual("Frank", result.Name); - } - - [Test] - public void GetValueByReference_NullPointer_ReturnsNull() - { - var result = CoreHelper.GetValueByReference(null, "{}", "{}"); - Assert.IsNull(result); - } - - [Test] - public void GetValueByReference_InvalidFormat_ReturnsNull() - { - var result = CoreHelper.GetValueByReference("$response.body", "{}", "{}"); - Assert.IsNull(result); - } - - [Test] - public void GetValueByReference_ValidBodyPointer_ReturnsValue() - { - var json = @"{""name"":""alice"", ""age"":30}"; - var pointer = "$response.body#/name"; - - var result = CoreHelper.GetValueByReference(pointer, json, null); - - Assert.AreEqual("alice", result); - } - - [Test] - public void GetValueByReference_ValidHeadersPointer_ReturnsValue() - { - var headers = @"{""content-type"":""application/json""}"; - var pointer = "$response.headers#/content-type"; - - var result = CoreHelper.GetValueByReference(pointer, null, headers); - - Assert.AreEqual("application/json", result); - } - - [Test] - public void GetValueByReference_PointerNotFound_ReturnsNull() - { - var json = @"{""name"":""alice""}"; - var pointer = "$response.body#/nonexistent"; - - var result = CoreHelper.GetValueByReference(pointer, json, null); - - Assert.IsNull(result); - } - - [Test] - public void GetValueByReference_UnsupportedPrefix_ReturnsNull() - { - var pointer = "$request.body#/name"; - var json = @"{""name"":""alice""}"; - - var result = CoreHelper.GetValueByReference(pointer, json, null); - - Assert.IsNull(result); - } - - [Test] - public void GetValueByReference_BooleanToken_ReturnsTokenToString() - { - var jsonBody = @"{ ""isActive"": true }"; - var pointerString = "$response.body#/isActive"; - - var result = CoreHelper.GetValueByReference(pointerString, jsonBody, null); - - Assert.AreEqual("True", result); - } #endregion - + #region PrepareFormFieldsFromObject [Test] @@ -1036,19 +768,15 @@ public void PrepareFormFieldsFromObject_IndexesCustomDictionaryParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var dictionaryData = new Dictionary() - { - {"key1", "value1" }, - {"key2", "value2" } - }; + var dictionaryData = new Dictionary() { { "key1", "value1" }, { "key2", "value2" } }; List> expected = new List>() { - new KeyValuePair("dictionary[key1]", "value1"), - new KeyValuePair("dictionary[key2]", "value2") + new("dictionary[key1]", "value1"), new("dictionary[key2]", "value2") }; - List> actual = CoreHelper.PrepareFormFieldsFromObject("dictionary", dictionaryData, ArraySerialization.Indexed); + List> actual = + CoreHelper.PrepareFormFieldsFromObject("dictionary", dictionaryData, ArraySerialization.Indexed); Assert.AreEqual(expected, actual); } @@ -1057,19 +785,18 @@ public void PrepareFormFieldsFromObject_PlainCustomListParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var arraySerializations = new List() - { - "alpha", "beta", "gamma" - }; + var arraySerializations = new List() { "alpha", "beta", "gamma" }; List> expected = new List>() { - new KeyValuePair("arraySerialization", "alpha"), - new KeyValuePair("arraySerialization", "beta"), - new KeyValuePair("arraySerialization", "gamma") + new("arraySerialization", "alpha"), + new("arraySerialization", "beta"), + new("arraySerialization", "gamma") }; - List> actual = CoreHelper.PrepareFormFieldsFromObject("arraySerialization", arraySerializations, ArraySerialization.Plain); + List> actual = + CoreHelper.PrepareFormFieldsFromObject("arraySerialization", arraySerializations, + ArraySerialization.Plain); Assert.AreEqual(expected, actual); } @@ -1078,19 +805,17 @@ public void PrepareFormFieldsFromObject_UnIndexedCustomListParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var arraySerializations = new List() - { - "alpha", "beta", "gamma" - }; + var arraySerializations = new List() { "alpha", "beta", "gamma" }; List> expected = new List>() { - new KeyValuePair("arraySerialization[]", "alpha"), - new KeyValuePair("arraySerialization[]", "beta"), - new KeyValuePair("arraySerialization[]", "gamma") + new("arraySerialization[]", "alpha"), + new("arraySerialization[]", "beta"), + new("arraySerialization[]", "gamma") }; - List> actual = CoreHelper.PrepareFormFieldsFromObject("arraySerialization", arraySerializations, ArraySerialization.UnIndexed); + List> actual = CoreHelper.PrepareFormFieldsFromObject("arraySerialization", + arraySerializations, ArraySerialization.UnIndexed); Assert.AreEqual(expected, actual); } @@ -1099,19 +824,17 @@ public void PrepareFormFieldsFromObject_UnIndexedCustomListNullParameter() { StringBuilder queryBuilder = new StringBuilder(); queryBuilder.Append(SERVER_URL); - var arraySerializations = new List() - { - "alpha", "beta", "gamma", null - }; + var arraySerializations = new List() { "alpha", "beta", "gamma", null }; List> expected = new List>() { - new KeyValuePair("arraySerialization[]", "alpha"), - new KeyValuePair("arraySerialization[]", "beta"), - new KeyValuePair("arraySerialization[]", "gamma") + new("arraySerialization[]", "alpha"), + new("arraySerialization[]", "beta"), + new("arraySerialization[]", "gamma") }; - List> actual = CoreHelper.PrepareFormFieldsFromObject("arraySerialization", arraySerializations, ArraySerialization.UnIndexed); + List> actual = CoreHelper.PrepareFormFieldsFromObject("arraySerialization", + arraySerializations, ArraySerialization.UnIndexed); Assert.AreEqual(expected, actual); } @@ -1122,19 +845,18 @@ public void PrepareFormFieldsFromObject_PlainCustomNestedListParameter() queryBuilder.Append(SERVER_URL); var arraySerializations = new List>() { - new List() - { - ArraySerialization.Indexed, ArraySerialization.PSV - } + new() { ArraySerialization.Indexed, ArraySerialization.PSV } }; List> expected = new List>() { - new KeyValuePair("arraySerialization[0][0]", "0"), - new KeyValuePair("arraySerialization[0][1]", "5") + new("arraySerialization[0][0]", "0"), + new("arraySerialization[0][1]", "5") }; - List> actual = CoreHelper.PrepareFormFieldsFromObject("arraySerialization", arraySerializations, ArraySerialization.Plain); + List> actual = + CoreHelper.PrepareFormFieldsFromObject("arraySerialization", arraySerializations, + ArraySerialization.Plain); Assert.AreEqual(expected, actual); } @@ -1148,9 +870,10 @@ public void PrepareFormFieldsFromObject_IndexedCoreJsonObjectParameter() List> expected = new List>() { - new KeyValuePair("jsonObject[message]", "TestCase") + new("jsonObject[message]", "TestCase") }; - List> actual = CoreHelper.PrepareFormFieldsFromObject("jsonObject", jsonObject, ArraySerialization.Indexed); + List> actual = + CoreHelper.PrepareFormFieldsFromObject("jsonObject", jsonObject, ArraySerialization.Indexed); Assert.AreEqual(expected, actual); } @@ -1161,9 +884,9 @@ public void PrepareFormFieldsFromObject_IndexedCoreJsonObjectNullValuesParameter queryBuilder.Append(SERVER_URL); JsonObject jsonObject = JsonObject.FromJsonString("{\"message\" : {\"prop\" : null}}"); - List> expected = new List>(); - List> actual = CoreHelper.PrepareFormFieldsFromObject("jsonObject", jsonObject, ArraySerialization.Indexed); + List> actual = + CoreHelper.PrepareFormFieldsFromObject("jsonObject", jsonObject, ArraySerialization.Indexed); Assert.AreEqual(expected, actual); } @@ -1174,12 +897,12 @@ public void PrepareFormFieldsFromObject_IndexedCoreJsonValueParameter() queryBuilder.Append(SERVER_URL); JsonValue jsonValue = JsonValue.FromString("TestCase"); - List> expected = new List>() { - new KeyValuePair("jsonValue", "TestCase") + new("jsonValue", "TestCase") }; - List> actual = CoreHelper.PrepareFormFieldsFromObject("jsonValue", jsonValue, ArraySerialization.Indexed); + List> actual = + CoreHelper.PrepareFormFieldsFromObject("jsonValue", jsonValue, ArraySerialization.Indexed); Assert.AreEqual(expected, actual); } @@ -1222,7 +945,8 @@ public void PrepareFormFieldsFromObject_WithAdditionalPropertiesBaseModel() List> expected = new List>() { new("simpleModelWithAdditionalPropertiesBaseModel[requiredProperty]", "Required Field"), - new("simpleModelWithAdditionalPropertiesBaseModel[additionalPropertyKey]", "additionalPropertyValue") + new("simpleModelWithAdditionalPropertiesBaseModel[additionalPropertyKey]", + "additionalPropertyValue") }; List> actual = CoreHelper.PrepareFormFieldsFromObject( @@ -1232,12 +956,12 @@ public void PrepareFormFieldsFromObject_WithAdditionalPropertiesBaseModel() // Assert that all items in 'expected' are found within 'actual' bool containsAllExpectedEntries = expected.All(item => actual.Contains(item)); Assert.IsTrue(containsAllExpectedEntries, "Not all expected entries are present in actual."); - } #endregion #region DeepCloneObject + [Test] public void DeepCloneObject_ServerResponse() { @@ -1245,6 +969,7 @@ public void DeepCloneObject_ServerResponse() ServerResponse actual = CoreHelper.DeepCloneObject(expected); Assert.AreEqual(expected, actual); } + #endregion #region Converters @@ -1296,6 +1021,7 @@ public void IsNullableType_AllTypes() Assert.That(CoreHelper.IsNullableType(typeof(List)), Is.True); Assert.That(CoreHelper.IsNullableType(typeof(Dictionary)), Is.True); } + #endregion } } diff --git a/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs b/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs new file mode 100644 index 00000000..17b40e53 --- /dev/null +++ b/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs @@ -0,0 +1,318 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using APIMatic.Core.Request; +using APIMatic.Core.Test.MockTypes.Models; +using APIMatic.Core.Test.MockTypes.Models.Pagination; +using APIMatic.Core.Utilities.Json; +using Microsoft.Json.Pointer; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Utilities.Json +{ + [TestFixture] + public class JsonPointerAccessorTest : TestBase + { + private const string ServerUrl = "https://api.example.com/users"; + + readonly Action setupRequest = rb => + rb.Setup(HttpMethod.Get, ServerUrl); + + [Test] + public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesCorrectly() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + builder.Parameters(p => p.Header(h => h.Setup("id", 0))); + await builder.Build(); + + // Act + builder.UpdateByReference("$request.headers#/id", old => 999); + + // Assert + Assert.IsTrue(builder.headers.ContainsKey("id")); + Assert.AreEqual("999", builder.headers["id"].ToString()); + } + + [Test] + public async Task UpdateByReference_HeaderParam_IncorrectJsonPointer_UpdatesPrevious() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + builder.Parameters(p => p.Header(h => h.Setup("id", 0))); + await builder.Build(); + + var previousHeaders = builder.headers; + + // Act + builder.UpdateByReference("$request.headers", old => 999); + + // Assert + Assert.AreEqual(previousHeaders, builder.headers); + } + + [Test] + public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesWithEmptyPointer() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + builder.Parameters(p => p.Header(h => h.Setup("id", 0))); + await builder.Build(); + + var previousHeaders = builder.headers; + + // Act + builder.UpdateByReference("$request.headers#/nonexistent", old => 999); + + // Assert + Assert.AreEqual(previousHeaders, builder.headers); + } + + [Test] + public async Task UpdateByReference_HeaderParam_ObjectValue_UpdatesObjectCorrectly() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + var initialUser = new User { Id = 1, Name = "User1" }; + builder.Parameters(p => p.Header(h => h.Setup("user", initialUser))); + await builder.Build(); + + // Act + builder.UpdateByReference("$request.headers#/user/Id", old => 456); + + // Assert + Assert.IsTrue(builder.headers.ContainsKey("user")); + var updatedUser = builder.headers["user"] as User; + Assert.IsNotNull(updatedUser); + Assert.AreEqual(456, updatedUser.Id); + Assert.AreEqual("User1", updatedUser.Name); + } + + [Test] + public async Task UpdateByReference_QueryParam_ObjectValue_UpdatesObjectCorrectly() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + var initialUser = new User { Id = 1, Name = "User1" }; + builder.Parameters(p => p.Query(q => q.Setup("user", initialUser))); + await builder.Build(); + + // Act + builder.UpdateByReference("$request.query#/user/Id", old => 456); + + // Assert + Assert.IsTrue(builder.queryParameters.ContainsKey("user")); + var updatedUser = builder.queryParameters["user"] as User; + Assert.IsNotNull(updatedUser); + Assert.AreEqual(456, updatedUser.Id); + Assert.AreEqual("User1", updatedUser.Name); + } + + [Test] + public async Task UpdateByReference_TemplateParam_SingleValue_UpdatesCorrectly() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + builder.Parameters(p => p.Template(t => t.Setup("userId", 1))); + await builder.Build(); + + // Act + builder.UpdateByReference("$request.path#/userId", old => 456); + + // Assert + Assert.IsTrue(builder.templateParameters.ContainsKey("userId")); + Assert.AreEqual(456, builder.templateParameters["userId"]); + } + + [Test] + public async Task UpdateByReference_TemplateParam_Collection_UpdatesCorrectly() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + builder.Parameters(p => p.Template(t => t.Setup("ids", new List { 1, 2, 3 }))); + await builder.Build(); + + // Act + builder.UpdateByReference("$request.path#/ids/1", old => 999); + + // Assert + Assert.IsTrue(builder.templateParameters.ContainsKey("ids")); + var updated = builder.templateParameters["ids"] as IList; + Assert.IsNotNull(updated); + CollectionAssert.AreEqual(new List { 1, 999, 3 }, updated); + } + + [Test] + public void UpdateValueByPointer_ValidPointer_UpdatesValue() + { + // Arrange + var person = new Person { Name = "Alice", Age = 30 }; + var pointer = new JsonPointer("/age"); + + // Act + var updated = JsonPointerAccessor.UpdateValueByPointer( + person, + pointer, + old => int.Parse(old.ToString() ?? "") + 5 + ); + + // Assert + Assert.AreEqual(35, updated.Age); + Assert.AreEqual("Alice", updated.Name); + } + + [Test] + public void UpdateValueByPointer_NullValue_ReturnsOriginal() + { + // Act + var result = JsonPointerAccessor.UpdateValueByPointer( + null, + new JsonPointer("/name"), + old => "Bob" + ); + + // Assert + Assert.IsNull(result); + } + + [Test] + public void UpdateValueByPointer_NullPointer_ReturnsOriginal() + { + var person = new Person { Name = "Charlie", Age = 40 }; + var result = JsonPointerAccessor.UpdateValueByPointer( + person, + null, + old => "David" + ); + + Assert.AreEqual("Charlie", result.Name); + } + + [Test] + public void UpdateValueByPointer_NullUpdater_ReturnsOriginal() + { + var person = new Person { Name = "Charlie", Age = 40 }; + var result = JsonPointerAccessor.UpdateValueByPointer( + person, + new JsonPointer("/name"), + null + ); + + Assert.AreEqual("Charlie", result.Name); + } + + [Test] + public void UpdateValueByPointer_UpdaterReturnsNull_ReturnsOriginal() + { + var person = new Person { Name = "Eva", Age = 50 }; + var pointer = new JsonPointer("/name"); + + var result = JsonPointerAccessor.UpdateValueByPointer( + person, + pointer, + old => null + ); + + Assert.AreEqual("Eva", result.Name); + } + + [Test] + public void UpdateValueByPointer_InvalidPointer_ReturnsOriginal() + { + var person = new Person { Name = "Frank", Age = 60 }; + var invalidPointer = new JsonPointer("/nonexistent"); + + var result = JsonPointerAccessor.UpdateValueByPointer( + person, + invalidPointer, + old => "ShouldNotApply" + ); + + Assert.AreEqual("Frank", result.Name); + } + + [Test] + public void ResolveJsonValueByReference_NullPointer_ReturnsNull() + { + var result = JsonPointerAccessor.ResolveJsonValueByReference(null, "{}", "{}"); + Assert.IsNull(result); + } + + [Test] + public void ResolveJsonValueByReference_InvalidFormat_ReturnsNull() + { + var result = JsonPointerAccessor.ResolveJsonValueByReference("$response.body", "{}", "{}"); + Assert.IsNull(result); + } + + [Test] + public void ResolveJsonValueByReference_NullJsonPointer_ReturnsNull() + { + var result = JsonPointerAccessor.ResolveJsonValueByReference("$response.body#", "{}", "{}"); + Assert.IsNull(result); + } + + [Test] + public void ResolveJsonValueByReference_ValidBodyPointer_ReturnsValue() + { + var json = @"{""name"":""alice"", ""age"":30}"; + var pointer = "$response.body#/name"; + + var result = JsonPointerAccessor.ResolveJsonValueByReference(pointer, json, null); + + Assert.AreEqual("alice", result); + } + + [Test] + public void ResolveJsonValueByReference_ValidHeadersPointer_ReturnsValue() + { + var headers = @"{""content-type"":""application/json""}"; + var pointer = "$response.headers#/content-type"; + + var result = JsonPointerAccessor.ResolveJsonValueByReference(pointer, null, headers); + + Assert.AreEqual("application/json", result); + } + + [Test] + public void ResolveJsonValueByReference_PointerNotFound_ReturnsNull() + { + var json = @"{""name"":""alice""}"; + var pointer = "$response.body#/nonexistent"; + + var result = JsonPointerAccessor.ResolveJsonValueByReference(pointer, json, null); + + Assert.IsNull(result); + } + + [Test] + public void ResolveJsonValueByReference_UnsupportedPrefix_ReturnsNull() + { + var pointer = "$request.body#/name"; + var json = @"{""name"":""alice""}"; + + var result = JsonPointerAccessor.ResolveJsonValueByReference(pointer, json, null); + + Assert.IsNull(result); + } + + [Test] + public void ResolveJsonValueByReference_BooleanToken_ReturnsTokenToString() + { + var jsonBody = @"{ ""isActive"": true }"; + var pointerString = "$response.body#/isActive"; + + var result = JsonPointerAccessor.ResolveJsonValueByReference(pointerString, jsonBody, null); + + Assert.AreEqual("True", result); + } + } +} diff --git a/APIMatic.Core/GlobalConfiguration.cs b/APIMatic.Core/GlobalConfiguration.cs index 1636f823..1895e276 100644 --- a/APIMatic.Core/GlobalConfiguration.cs +++ b/APIMatic.Core/GlobalConfiguration.cs @@ -50,7 +50,7 @@ private GlobalConfiguration(ICoreHttpClientConfiguration httpConfiguration, Dict /// /// /// - public string ServerUrl(Enum server = null) => GlobalRequestBuilder(server).QueryUrl.ToString(); + public string ServerUrl(Enum server = null) => GlobalRequestBuilder(server).GetQueryUrl(); /// /// Returns the global request builder after applying the global configurations @@ -59,8 +59,7 @@ private GlobalConfiguration(ICoreHttpClientConfiguration httpConfiguration, Dict /// public RequestBuilder GlobalRequestBuilder(Enum server = null) { - RequestBuilder requestBuilder = new RequestBuilder(this); - requestBuilder.QueryUrl.Append(_serverUrls[server ?? _defaultServer]); + RequestBuilder requestBuilder = new RequestBuilder(this, _serverUrls[server ?? _defaultServer]); _parameters.Validate().Apply(requestBuilder); return requestBuilder; } diff --git a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs index a07f02fc..ff5770be 100644 --- a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs @@ -1,5 +1,6 @@ using APIMatic.Core.Request; using APIMatic.Core.Utilities; +using APIMatic.Core.Utilities.Json; namespace APIMatic.Core.Pagination.Strategies { @@ -36,7 +37,7 @@ public RequestBuilder Apply(PaginationContext paginationContext) return oldValue; } - var cursorValue = CoreHelper.GetValueByReference( + var cursorValue = JsonPointerAccessor.ResolveJsonValueByReference( _output, paginationContext.ResponseBody, paginationContext.ResponseHeaders diff --git a/APIMatic.Core/Pagination/Strategies/LinkPagination.cs b/APIMatic.Core/Pagination/Strategies/LinkPagination.cs index f94199e3..b4c943cc 100644 --- a/APIMatic.Core/Pagination/Strategies/LinkPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/LinkPagination.cs @@ -1,5 +1,6 @@ using APIMatic.Core.Utilities; using APIMatic.Core.Request; +using APIMatic.Core.Utilities.Json; namespace APIMatic.Core.Pagination.Strategies { @@ -20,7 +21,7 @@ public LinkPagination(string next) public RequestBuilder Apply(PaginationContext paginationContext) { - CurrentLinkValue = CoreHelper.GetValueByReference( + CurrentLinkValue = JsonPointerAccessor.ResolveJsonValueByReference( _next, paginationContext.ResponseBody, paginationContext.ResponseHeaders diff --git a/APIMatic.Core/Request/Parameters/HeaderParam.cs b/APIMatic.Core/Request/Parameters/HeaderParam.cs index c9c1a9f4..a248364e 100644 --- a/APIMatic.Core/Request/Parameters/HeaderParam.cs +++ b/APIMatic.Core/Request/Parameters/HeaderParam.cs @@ -18,12 +18,8 @@ internal override void Apply(RequestBuilder requestBuilder) { return; } - var headerValue = CoreHelper.JsonSerialize(value)?.TrimStart('"').TrimEnd('"'); - if (headerValue == null) - { - return; - } - requestBuilder.headers[key] = headerValue; + + requestBuilder.headers[key] = value; } } } diff --git a/APIMatic.Core/Request/Parameters/TemplateParam.cs b/APIMatic.Core/Request/Parameters/TemplateParam.cs index 2f808362..767be531 100644 --- a/APIMatic.Core/Request/Parameters/TemplateParam.cs +++ b/APIMatic.Core/Request/Parameters/TemplateParam.cs @@ -15,31 +15,6 @@ public class TemplateParam : Parameter { internal TemplateParam() => typeName = "template"; - private string GetReplacerValueForCollection(ICollection collection) - { - var replacedValues = new List(); - var enumerator = collection.GetEnumerator(); - while (enumerator.MoveNext()) - { - replacedValues.Add(GetReplacerValue(enumerator.Current)); - } - return string.Join("/", replacedValues); - } - - private string GetReplacerValue(object value) - { - if (value == null) - { - return string.Empty; - } - if (value is ICollection collection) - { - return GetReplacerValueForCollection(collection); - } - - return CoreHelper.JsonSerialize(value).TrimStart('"').TrimEnd('"'); - } - internal override void Apply(RequestBuilder requestBuilder) { if (!validated) @@ -47,8 +22,8 @@ internal override void Apply(RequestBuilder requestBuilder) return; } CoreHelper.TryGetInnerValueForContainer(value, out var innerValue); - string replacerValue = Uri.EscapeUriString(GetReplacerValue(innerValue ?? value)); - requestBuilder.QueryUrl.Replace(string.Format("{{{0}}}", key), replacerValue); + requestBuilder.templateParameters.Add(key, innerValue ?? value); } } } + diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 9feda92b..2bf1eec7 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -15,9 +15,8 @@ using APIMatic.Core.Request.Parameters; using APIMatic.Core.Types.Sdk; using APIMatic.Core.Utilities; +using APIMatic.Core.Utilities.Json; using Microsoft.Json.Pointer; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace APIMatic.Core.Request { @@ -33,23 +32,24 @@ public class RequestBuilder private readonly Parameter.Builder parameters = new Parameter.Builder(); private bool contentTypeAllowed = true; private bool xmlRequest = false; + private readonly StringBuilder queryUrl; private Func bodySerializer = PrepareBody; - internal readonly Dictionary headers = new Dictionary(); + internal readonly Dictionary headers = new Dictionary(); internal dynamic body; internal Type bodyType; + internal readonly Dictionary templateParameters = new Dictionary(); internal readonly Dictionary bodyParameters = new Dictionary(); internal readonly List> formParameters = new List>(); internal readonly Dictionary queryParameters = new Dictionary(); - internal RequestBuilder(GlobalConfiguration configuration) + internal RequestBuilder(GlobalConfiguration configuration, string serverUrl) { this.configuration = configuration; + queryUrl = new StringBuilder(serverUrl); authGroup = new AuthGroupBuilder(configuration.AuthManagers); } - internal StringBuilder QueryUrl { get; private set; } = new StringBuilder(); - internal ContentType AcceptHeader { get; set; } = ContentType.SCALAR; internal ArraySerialization ArraySerialization { get; set; } @@ -67,7 +67,7 @@ internal RequestBuilder(GlobalConfiguration configuration) public RequestBuilder Setup(HttpMethod httpMethod, string path) { this.httpMethod = httpMethod; - QueryUrl.Append(path); + queryUrl.Append(path); return this; } @@ -173,17 +173,24 @@ public RequestBuilder UpdateByReference(string pointerString, Func setter, JsonPointer jsonPointer) + private void UpdateTemplateParams(Func setter, JsonPointer pointer) { - var template = QueryUrl.ToString().ToTemplateParameters(); - QueryUrl = new StringBuilder(CoreHelper.UpdateValueByPointer(template, jsonPointer, setter) - .ToQueryString()); + var updatedTemplateParameters = + JsonPointerAccessor.UpdateValueByPointer(templateParameters, pointer, setter); + + foreach (var kvp in updatedTemplateParameters) + { + if (!templateParameters.TryGetValue(kvp.Key, out var existingValue) || existingValue != kvp.Value) + { + templateParameters[kvp.Key] = kvp.Value; + } + } } private void UpdateHeaderParams(Func setter, JsonPointer pointer) { var updatedHeaders = - CoreHelper.UpdateValueByPointer(new Dictionary(headers), pointer, setter); + JsonPointerAccessor.UpdateValueByPointer(headers, pointer, setter); foreach (var kvp in updatedHeaders) { @@ -197,12 +204,25 @@ private void UpdateHeaderParams(Func setter, JsonPointer pointer private void UpdateQueryParams(Func setter, JsonPointer pointer) { var updatedQueryParams = - CoreHelper.UpdateValueByPointer(new Dictionary(queryParameters), pointer, setter); + JsonPointerAccessor.UpdateValueByPointer(queryParameters, pointer, setter); + + foreach (var kvp in updatedQueryParams) + { + if (!queryParameters.TryGetValue(kvp.Key, out var existingValue) || existingValue != kvp.Value) + { + queryParameters[kvp.Key] = kvp.Value; + } + } + } - foreach (var entry in updatedQueryParams) + internal string GetQueryUrl() + { + var queryBuilder = new StringBuilder(this.queryUrl.ToString()); + foreach (var kvp in templateParameters) { - queryParameters[entry.Key] = entry.Value; + queryBuilder.Replace($"{{{kvp.Key}}}", CoreHelper.GetTemplateReplacerValue(kvp.Value)); } + return queryBuilder.ToString(); } /// @@ -214,18 +234,15 @@ public async Task Build() parameters.Validate().Apply(this); configuration.RuntimeParameters.Validate().Apply(this); await authGroup.Apply(this).ConfigureAwait(false); - var queryUrl = new StringBuilder(QueryUrl.ToString()); - CoreHelper.AppendUrlWithQueryParameters(queryUrl, queryParameters, ArraySerialization); + var queryBuilder = new StringBuilder(GetQueryUrl()); + CoreHelper.AppendUrlWithQueryParameters(queryBuilder, queryParameters, ArraySerialization); body = bodyParameters.Any() ? bodyParameters : body; AppendContentTypeHeader(); AppendAcceptHeader(); - return new CoreRequest(httpMethod, CoreHelper.CleanUrl(queryUrl), headers, bodySerializer(body), formParameters, queryParameters) - { - RetryOption = retryOption, - HasBinaryResponse = HasBinaryResponse - }; + return new CoreRequest(httpMethod, CoreHelper.CleanUrl(queryBuilder), headers.ToCoreRequestHeaders(), bodySerializer(body), + formParameters, queryParameters) { RetryOption = retryOption, HasBinaryResponse = HasBinaryResponse }; } - + private void AppendContentTypeHeader() { if (!ContentHeaderKeyRequired("content-type")) @@ -303,7 +320,7 @@ public RequestBuilder(RequestBuilder source) this.retryOption = source.retryOption; this.contentTypeAllowed = source.contentTypeAllowed; this.httpMethod = source.httpMethod; - this.QueryUrl = new StringBuilder(source.QueryUrl.ToString()); + this.queryUrl = new StringBuilder(source.queryUrl.ToString()); this.AcceptHeader = source.AcceptHeader; this.ArraySerialization = source.ArraySerialization; this.HasBinaryResponse = source.HasBinaryResponse; @@ -312,7 +329,8 @@ public RequestBuilder(RequestBuilder source) this.body = source.body; this.bodyType = source.bodyType; - this.headers = new Dictionary(source.headers); + this.headers = new Dictionary(source.headers); + this.templateParameters = new Dictionary(source.templateParameters); this.bodyParameters = new Dictionary(source.bodyParameters); this.formParameters = new List>(source.formParameters); this.queryParameters = new Dictionary(source.queryParameters); @@ -327,59 +345,23 @@ public static RequestBuilder RequestBuilderWithParameters(RequestBuilder source) } } - internal static class QueryUrlParser + internal static class RequestBuilderExtensions { - public static Dictionary ToTemplateParameters(this string queryUrl) + public static Dictionary ToCoreRequestHeaders(this Dictionary headers) { - var uri = new Uri(queryUrl); - var query = HttpUtility.ParseQueryString(uri.Query); + var result = new Dictionary(); - var paramDict = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (string key in query.AllKeys.Where(k => k != null)) + foreach (var kvp in headers) { - var values = query.GetValues(key); - if (values.Length > 1) - { - paramDict[key] = values.Cast().ToList(); - } - else + var serialized = CoreHelper.JsonSerialize(kvp.Value)?.TrimStart('"').TrimEnd('"'); + if (serialized != null) { - paramDict[key] = values[0]; + result[kvp.Key] = serialized; } } - return paramDict; + return result; } - - public static string ToQueryString(this Dictionary parameters) - { - if (parameters == null || parameters.Count == 0) - return string.Empty; - - var builder = new StringBuilder(); - foreach (var kvp in parameters) - { - var key = WebUtility.UrlEncode(kvp.Key); - if (kvp.Value is IEnumerable list && !(kvp.Value is string)) - { - foreach (var val in list) - { - builder.Append($"{key}={WebUtility.UrlEncode(val?.ToString())}&"); - } - } - else - { - builder.Append($"{key}={WebUtility.UrlEncode(kvp.Value?.ToString())}&"); - } - } - - // Remove trailing '&' if present - if (builder.Length > 0) - builder.Length--; - - return builder.ToString(); - } } } diff --git a/APIMatic.Core/Utilities/CoreHelper.cs b/APIMatic.Core/Utilities/CoreHelper.cs index 85e22ca3..13d1446c 100644 --- a/APIMatic.Core/Utilities/CoreHelper.cs +++ b/APIMatic.Core/Utilities/CoreHelper.cs @@ -81,19 +81,9 @@ public static string JsonSerialize(object obj, JsonConverter converter = null) /// The deserialized object. public static T JsonDeserialize(string json, JsonConverter converter = null) { - if (string.IsNullOrWhiteSpace(json)) - { - return default; - } - - if (converter == null) - { - return JsonConvert.DeserializeObject(json, new IsoDateTimeConverter()); - } - else - { - return JsonConvert.DeserializeObject(json, converter); - } + return string.IsNullOrWhiteSpace(json) + ? default + : JsonConvert.DeserializeObject(json, converter ?? new IsoDateTimeConverter()); } internal static IDictionary ExtractQueryParametersForUrl(string queryUrl) @@ -139,83 +129,27 @@ public static void AppendUrlWithQueryParameters(StringBuilder queryBuilder, IEnu } } - public static string GetValueByReference(string pointerString, string jsonBody, string jsonHeaders) - { - if (pointerString == null) - return null; - - var parts = pointerString.Split('#'); - - if (parts.Length <= 1) - return null; - - var jsonPointer = new JsonPointer(parts[1]); - - return parts[0] switch - { - "$response.body" => GetJsonValueUsingPointer(jsonPointer, jsonBody), - "$response.headers" => GetJsonValueUsingPointer(jsonPointer, jsonHeaders), - _ => null - }; - } - - private static string GetJsonValueUsingPointer(JsonPointer pointer, string json) + private static string GetTemplateReplacerValueForCollection(ICollection collection) { - if (pointer == null || json == null) - return null; - - var jsonObject = JObject.Parse(json); - JToken jsonToken; - try - { - jsonToken = pointer.Evaluate(jsonObject); - } - catch + var replacedValues = new List(); + var enumerator = collection.GetEnumerator(); + while (enumerator.MoveNext()) { - return null; + replacedValues.Add(GetTemplateReplacerValue(enumerator.Current)); } - - return jsonToken.Type switch - { - JTokenType.Null => null, - JTokenType.String => jsonToken.Value(), - _ => jsonToken.ToString() - }; + return string.Join("/", replacedValues); } - internal static T UpdateValueByPointer(T value, JsonPointer pointer, Func updater) + public static string GetTemplateReplacerValue(object value) { - if (pointer == null || updater == null) - return value; - - try + return value switch { - // Step 1: Serialize the object to JSON - var json = JsonSerialize(value); - if (string.IsNullOrWhiteSpace(json)) return value; - - // Step 2: Parse JSON and evaluate pointer - var jsonObject = JObject.Parse(json); - var jsonToken = pointer.Evaluate(jsonObject); - - // Step 3: Apply updater - var oldValue = jsonToken.ToObject(); - var newValue = updater(oldValue); - if (newValue == null) return value; - - // Step 4: Replace token - jsonToken.Replace(JToken.FromObject(newValue)); - - // Step 5: Deserialize back to T - var updatedJson = jsonObject.ToString(); - return CoreHelper.JsonDeserialize(updatedJson); - } - catch - { - return value; - } + null => string.Empty, + ICollection collection => GetTemplateReplacerValueForCollection(collection), + _ => CoreHelper.JsonSerialize(value).TrimStart('"').TrimEnd('"') + }; } - + private static void AppendParameters(StringBuilder queryBuilder, ArraySerialization arraySerialization, KeyValuePair pair) { string paramKeyValPair; diff --git a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs new file mode 100644 index 00000000..365e1bab --- /dev/null +++ b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Json.Pointer; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace APIMatic.Core.Utilities.Json +{ + internal static class JsonPointerAccessor + { + public static string ResolveJsonValueByReference(string pointerString, string jsonBody, string jsonHeaders) + { + if (string.IsNullOrEmpty(pointerString) || !pointerString.Contains('#')) + return null; + + var (scope, path) = (pointerString.Split('#')[0], pointerString.Split('#')[1]); + if (string.IsNullOrEmpty(path)) + return null; + + var jsonPointer = new JsonPointer(path); + + return scope switch + { + "$response.body" => GetJsonValueByPointer(jsonPointer, jsonBody), + "$response.headers" => GetJsonValueByPointer(jsonPointer, jsonHeaders), + _ => null + }; + } + + private static string GetJsonValueByPointer(JsonPointer jsonPointer, string json) + { + if (json == null) return null; + + var jsonObject = JObject.Parse(json); + JToken jsonToken; + try + { + jsonToken = jsonPointer.Evaluate(jsonObject); + } + catch + { + return null; + } + + return jsonToken.Type switch + { + JTokenType.Null => null, + JTokenType.String => jsonToken.Value(), + _ => jsonToken.ToString() + }; + } + + public static T UpdateValueByPointer(T value, JsonPointer pointer, Func updater) + { + if (value == null || pointer == null || updater == null) + return value; + + try + { + // Step 1: Serialize the object to JSON + var json = CoreHelper.JsonSerialize(value); + + // Step 2: Parse JSON and evaluate pointer + var jsonObject = JObject.Parse(json); + var jsonToken = pointer.Evaluate(jsonObject); + + // Step 3: Apply updater + var oldValue = jsonToken.ToObject(); + var newValue = updater(oldValue); + if (newValue == null) return value; + + // Step 4: Replace token + jsonToken.Replace(JToken.FromObject(newValue)); + + // Step 5: Deserialize back to T + var updatedJson = jsonObject.ToString(); + return CoreHelper.JsonDeserialize(updatedJson); + } + catch + { + return value; + } + } + + public static Dictionary UpdateValueByPointer( + Dictionary value, + JsonPointer pointer, + Func updater) + { + try + { + // 0. Capture original types of root dictionary values + var typeMap = value.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.GetType() ?? typeof(object)); + + // 1. Serialize the input dictionary to JToken + var json = JsonConvert.SerializeObject(value); + var root = JToken.Parse(json); + + // 2. Locate the token to update + var tokenToUpdate = pointer.Evaluate(root); + + // 3. Apply updater + var currentValue = tokenToUpdate.ToObject(); + var updatedValue = updater(currentValue); + var newToken = updatedValue == null ? JValue.CreateNull() : JToken.FromObject(updatedValue); + + switch (tokenToUpdate.Parent) + { + // 4. Replace token + case JProperty prop: + prop.Value = newToken; + break; + case JArray array: + { + int index = array.IndexOf(tokenToUpdate); + array[index] = newToken; + break; + } + } + + // 5. Deserialize each value using its original type + var result = new Dictionary(); + foreach (var kvp in ((JObject)root).Properties()) + { + var key = kvp.Name; + var token = kvp.Value; + var targetType = typeMap.TryGetValue(key, out var type) ? type : typeof(object); + var obj = token.ToObject(targetType); + result[key] = obj!; + } + + return result; + } + catch + { + return value; + } + } + } +} From a6ddd411d72a509012b1df07522eab0eda08c6a4 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 11:07:55 +0500 Subject: [PATCH 09/24] refactor code --- APIMatic.Core.Test/ParametersTest.cs | 2 +- .../Utilities/Json/JsonPointerAccessorTest.cs | 28 ++-- .../Request/Parameters/HeaderParam.cs | 2 +- APIMatic.Core/Request/RequestBuilder.cs | 156 +++++++----------- APIMatic.Core/Utilities/CoreHelper.cs | 9 +- .../Utilities/Json/JsonPointerAccessor.cs | 2 +- 6 files changed, 82 insertions(+), 117 deletions(-) diff --git a/APIMatic.Core.Test/ParametersTest.cs b/APIMatic.Core.Test/ParametersTest.cs index 166b283c..f52fda40 100644 --- a/APIMatic.Core.Test/ParametersTest.cs +++ b/APIMatic.Core.Test/ParametersTest.cs @@ -27,7 +27,7 @@ public void Parameters_Apply_WithoutValidation() .Apply(requestBuilder); Assert.False(requestBuilder.queryParameters.ContainsKey("query")); Assert.False(requestBuilder.formParameters.Exists(kv => kv.Key == "form")); - Assert.False(requestBuilder.headers.ContainsKey("header")); + Assert.False(requestBuilder.headersParameters.ContainsKey("header")); Assert.True(requestBuilder.GetQueryUrl().Contains("{template}")); diff --git a/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs b/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs index 17b40e53..412a54df 100644 --- a/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs +++ b/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs @@ -32,8 +32,8 @@ public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesCorrectly() builder.UpdateByReference("$request.headers#/id", old => 999); // Assert - Assert.IsTrue(builder.headers.ContainsKey("id")); - Assert.AreEqual("999", builder.headers["id"].ToString()); + Assert.IsTrue(builder.headersParameters.ContainsKey("id")); + Assert.AreEqual("999", builder.headersParameters["id"].ToString()); } [Test] @@ -45,13 +45,13 @@ public async Task UpdateByReference_HeaderParam_IncorrectJsonPointer_UpdatesPrev builder.Parameters(p => p.Header(h => h.Setup("id", 0))); await builder.Build(); - var previousHeaders = builder.headers; + var previousHeaders = builder.headersParameters; // Act builder.UpdateByReference("$request.headers", old => 999); // Assert - Assert.AreEqual(previousHeaders, builder.headers); + Assert.AreEqual(previousHeaders, builder.headersParameters); } [Test] @@ -63,13 +63,13 @@ public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesWithEmptyPoin builder.Parameters(p => p.Header(h => h.Setup("id", 0))); await builder.Build(); - var previousHeaders = builder.headers; + var previousHeaders = builder.headersParameters; // Act builder.UpdateByReference("$request.headers#/nonexistent", old => 999); // Assert - Assert.AreEqual(previousHeaders, builder.headers); + Assert.AreEqual(previousHeaders, builder.headersParameters); } [Test] @@ -86,8 +86,8 @@ public async Task UpdateByReference_HeaderParam_ObjectValue_UpdatesObjectCorrect builder.UpdateByReference("$request.headers#/user/Id", old => 456); // Assert - Assert.IsTrue(builder.headers.ContainsKey("user")); - var updatedUser = builder.headers["user"] as User; + Assert.IsTrue(builder.headersParameters.ContainsKey("user")); + var updatedUser = builder.headersParameters["user"] as User; Assert.IsNotNull(updatedUser); Assert.AreEqual(456, updatedUser.Id); Assert.AreEqual("User1", updatedUser.Name); @@ -158,7 +158,7 @@ public void UpdateValueByPointer_ValidPointer_UpdatesValue() var pointer = new JsonPointer("/age"); // Act - var updated = JsonPointerAccessor.UpdateValueByPointer( + var updated = JsonPointerAccessor.UpdateBodyValueByPointer( person, pointer, old => int.Parse(old.ToString() ?? "") + 5 @@ -173,7 +173,7 @@ public void UpdateValueByPointer_ValidPointer_UpdatesValue() public void UpdateValueByPointer_NullValue_ReturnsOriginal() { // Act - var result = JsonPointerAccessor.UpdateValueByPointer( + var result = JsonPointerAccessor.UpdateBodyValueByPointer( null, new JsonPointer("/name"), old => "Bob" @@ -187,7 +187,7 @@ public void UpdateValueByPointer_NullValue_ReturnsOriginal() public void UpdateValueByPointer_NullPointer_ReturnsOriginal() { var person = new Person { Name = "Charlie", Age = 40 }; - var result = JsonPointerAccessor.UpdateValueByPointer( + var result = JsonPointerAccessor.UpdateBodyValueByPointer( person, null, old => "David" @@ -200,7 +200,7 @@ public void UpdateValueByPointer_NullPointer_ReturnsOriginal() public void UpdateValueByPointer_NullUpdater_ReturnsOriginal() { var person = new Person { Name = "Charlie", Age = 40 }; - var result = JsonPointerAccessor.UpdateValueByPointer( + var result = JsonPointerAccessor.UpdateBodyValueByPointer( person, new JsonPointer("/name"), null @@ -215,7 +215,7 @@ public void UpdateValueByPointer_UpdaterReturnsNull_ReturnsOriginal() var person = new Person { Name = "Eva", Age = 50 }; var pointer = new JsonPointer("/name"); - var result = JsonPointerAccessor.UpdateValueByPointer( + var result = JsonPointerAccessor.UpdateBodyValueByPointer( person, pointer, old => null @@ -230,7 +230,7 @@ public void UpdateValueByPointer_InvalidPointer_ReturnsOriginal() var person = new Person { Name = "Frank", Age = 60 }; var invalidPointer = new JsonPointer("/nonexistent"); - var result = JsonPointerAccessor.UpdateValueByPointer( + var result = JsonPointerAccessor.UpdateBodyValueByPointer( person, invalidPointer, old => "ShouldNotApply" diff --git a/APIMatic.Core/Request/Parameters/HeaderParam.cs b/APIMatic.Core/Request/Parameters/HeaderParam.cs index a248364e..d0664a38 100644 --- a/APIMatic.Core/Request/Parameters/HeaderParam.cs +++ b/APIMatic.Core/Request/Parameters/HeaderParam.cs @@ -19,7 +19,7 @@ internal override void Apply(RequestBuilder requestBuilder) return; } - requestBuilder.headers[key] = value; + requestBuilder.headersParameters[key] = value; } } } diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 2bf1eec7..31f33509 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -35,7 +35,7 @@ public class RequestBuilder private readonly StringBuilder queryUrl; private Func bodySerializer = PrepareBody; - internal readonly Dictionary headers = new Dictionary(); + internal readonly Dictionary headersParameters = new Dictionary(); internal dynamic body; internal Type bodyType; internal readonly Dictionary templateParameters = new Dictionary(); @@ -49,15 +49,55 @@ internal RequestBuilder(GlobalConfiguration configuration, string serverUrl) queryUrl = new StringBuilder(serverUrl); authGroup = new AuthGroupBuilder(configuration.AuthManagers); } + + internal RequestBuilder(RequestBuilder source) + { + this.configuration = source.configuration; + this.retryOption = source.retryOption; + this.contentTypeAllowed = source.contentTypeAllowed; + this.httpMethod = source.httpMethod; + this.queryUrl = new StringBuilder(source.queryUrl.ToString()); + this.AcceptHeader = source.AcceptHeader; + this.ArraySerialization = source.ArraySerialization; + this.HasBinaryResponse = source.HasBinaryResponse; + this.xmlRequest = source.xmlRequest; + this.bodySerializer = source.bodySerializer; + this.body = source.body; + this.bodyType = source.bodyType; + this.headersParameters = new Dictionary(source.headersParameters); + this.templateParameters = new Dictionary(source.templateParameters); + this.bodyParameters = new Dictionary(source.bodyParameters); + this.formParameters = new List>(source.formParameters); + this.queryParameters = new Dictionary(source.queryParameters); + this.authGroup = new AuthGroupBuilder(source.configuration.AuthManagers); + } + + internal static RequestBuilder RequestBuilderWithParameters(RequestBuilder source) + { + var requestBuilder = new RequestBuilder(source); + source.parameters.Validate().Apply(requestBuilder); + return requestBuilder; + } + internal ContentType AcceptHeader { get; set; } = ContentType.SCALAR; internal ArraySerialization ArraySerialization { get; set; } internal bool HasBinaryResponse { get; set; } + internal string GetQueryUrl() + { + var queryBuilder = new StringBuilder(this.queryUrl.ToString()); + foreach (var kvp in templateParameters) + { + queryBuilder.Replace($"{{{kvp.Key}}}", CoreHelper.GetTemplateReplacerValue(kvp.Value)); + } + return queryBuilder.ToString(); + } + private Type BodyType => bodyParameters.Any() ? typeof(object) : bodyType; - + /// /// Required: Sets the API route and http method /// @@ -159,72 +199,20 @@ public RequestBuilder UpdateByReference(string pointerString, Func setter, JsonPointer pointer) - { - var updatedTemplateParameters = - JsonPointerAccessor.UpdateValueByPointer(templateParameters, pointer, setter); - - foreach (var kvp in updatedTemplateParameters) - { - if (!templateParameters.TryGetValue(kvp.Key, out var existingValue) || existingValue != kvp.Value) - { - templateParameters[kvp.Key] = kvp.Value; - } - } - } - - private void UpdateHeaderParams(Func setter, JsonPointer pointer) - { - var updatedHeaders = - JsonPointerAccessor.UpdateValueByPointer(headers, pointer, setter); - - foreach (var kvp in updatedHeaders) - { - if (!headers.TryGetValue(kvp.Key, out var existingValue) || existingValue != kvp.Value) - { - headers[kvp.Key] = kvp.Value; - } - } - } - - private void UpdateQueryParams(Func setter, JsonPointer pointer) - { - var updatedQueryParams = - JsonPointerAccessor.UpdateValueByPointer(queryParameters, pointer, setter); - - foreach (var kvp in updatedQueryParams) - { - if (!queryParameters.TryGetValue(kvp.Key, out var existingValue) || existingValue != kvp.Value) - { - queryParameters[kvp.Key] = kvp.Value; - } - } - } - - internal string GetQueryUrl() - { - var queryBuilder = new StringBuilder(this.queryUrl.ToString()); - foreach (var kvp in templateParameters) - { - queryBuilder.Replace($"{{{kvp.Key}}}", CoreHelper.GetTemplateReplacerValue(kvp.Value)); - } - return queryBuilder.ToString(); - } - /// /// This applies all the configuration and build an instance of CoreRequest /// @@ -239,7 +227,7 @@ public async Task Build() body = bodyParameters.Any() ? bodyParameters : body; AppendContentTypeHeader(); AppendAcceptHeader(); - return new CoreRequest(httpMethod, CoreHelper.CleanUrl(queryBuilder), headers.ToCoreRequestHeaders(), bodySerializer(body), + return new CoreRequest(httpMethod, CoreHelper.CleanUrl(queryBuilder), headersParameters.ToCoreRequestHeaders(), bodySerializer(body), formParameters, queryParameters) { RetryOption = retryOption, HasBinaryResponse = HasBinaryResponse }; } @@ -255,20 +243,20 @@ private void AppendContentTypeHeader() } if (xmlRequest) { - headers.Add("content-type", ContentType.XML.GetValue()); + headersParameters.Add("content-type", ContentType.XML.GetValue()); return; } if (body is Stream || body is byte[]) { - headers.Add("content-type", ContentType.BINARY.GetValue()); + headersParameters.Add("content-type", ContentType.BINARY.GetValue()); return; } if (CoreHelper.IsScalarType(BodyType)) { - headers.Add("content-type", ContentType.SCALAR.GetValue()); + headersParameters.Add("content-type", ContentType.SCALAR.GetValue()); return; } - headers.Add("content-type", ContentType.JSON_UTF8.GetValue()); + headersParameters.Add("content-type", ContentType.JSON_UTF8.GetValue()); } private void AppendAcceptHeader() @@ -281,7 +269,7 @@ private void AppendAcceptHeader() { return; } - headers.Add("accept", AcceptHeader.GetValue()); + headersParameters.Add("accept", AcceptHeader.GetValue()); } private bool ContentHeaderKeyRequired(string key) @@ -290,7 +278,7 @@ private bool ContentHeaderKeyRequired(string key) { return false; } - if (headers.Any(p => p.Key.EqualsIgnoreCase(key))) + if (headersParameters.Any(p => p.Key.EqualsIgnoreCase(key))) { return false; } @@ -314,34 +302,18 @@ private static object PrepareBody(dynamic value) return CoreHelper.JsonSerialize(value); } - public RequestBuilder(RequestBuilder source) + private static void UpdateValuesByPointer(Dictionary requestParameters, Func setter, + JsonPointer pointer) { - this.configuration = source.configuration; - this.retryOption = source.retryOption; - this.contentTypeAllowed = source.contentTypeAllowed; - this.httpMethod = source.httpMethod; - this.queryUrl = new StringBuilder(source.queryUrl.ToString()); - this.AcceptHeader = source.AcceptHeader; - this.ArraySerialization = source.ArraySerialization; - this.HasBinaryResponse = source.HasBinaryResponse; - this.xmlRequest = source.xmlRequest; - this.bodySerializer = source.bodySerializer; - this.body = source.body; - this.bodyType = source.bodyType; + var updated = JsonPointerAccessor.UpdateValueByPointer(requestParameters, pointer, setter); - this.headers = new Dictionary(source.headers); - this.templateParameters = new Dictionary(source.templateParameters); - this.bodyParameters = new Dictionary(source.bodyParameters); - this.formParameters = new List>(source.formParameters); - this.queryParameters = new Dictionary(source.queryParameters); - this.authGroup = new AuthGroupBuilder(source.configuration.AuthManagers); - } - - public static RequestBuilder RequestBuilderWithParameters(RequestBuilder source) - { - var requestBuilder = new RequestBuilder(source); - source.parameters.Validate().Apply(requestBuilder); - return requestBuilder; + foreach (var kvp in updated) + { + if (!requestParameters.TryGetValue(kvp.Key, out var existingValue) || !Equals(existingValue, kvp.Value)) + { + requestParameters[kvp.Key] = kvp.Value; + } + } } } diff --git a/APIMatic.Core/Utilities/CoreHelper.cs b/APIMatic.Core/Utilities/CoreHelper.cs index 13d1446c..400bd67b 100644 --- a/APIMatic.Core/Utilities/CoreHelper.cs +++ b/APIMatic.Core/Utilities/CoreHelper.cs @@ -60,14 +60,7 @@ public static string JsonSerialize(object obj, JsonConverter converter = null) settings.PreserveReferencesHandling = PreserveReferencesHandling.Objects; } - if (converter == null) - { - settings.Converters.Add(new IsoDateTimeConverter()); - } - else - { - settings.Converters.Add(converter); - } + settings.Converters.Add(converter ?? new IsoDateTimeConverter()); return JsonConvert.SerializeObject(obj, Formatting.None, settings); } diff --git a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs index 365e1bab..64c3cab7 100644 --- a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs +++ b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs @@ -51,7 +51,7 @@ private static string GetJsonValueByPointer(JsonPointer jsonPointer, string json }; } - public static T UpdateValueByPointer(T value, JsonPointer pointer, Func updater) + public static T UpdateBodyValueByPointer(T value, JsonPointer pointer, Func updater) { if (value == null || pointer == null || updater == null) return value; From e975d6bf81f39cbfb9e202e1a4fdf2366c3c62bb Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 11:18:22 +0500 Subject: [PATCH 10/24] reactor code to fix code climate errors --- .../Pagination/Strategies/CursorPagination.cs | 52 ++++++++++--------- .../Pagination/Strategies/LinkPagination.cs | 11 ++++ .../Pagination/Strategies/NumberPagination.cs | 11 ++++ .../Pagination/Strategies/OffsetPagination.cs | 12 +++++ .../Request/Parameters/HeaderParam.cs | 6 +-- .../Request/Parameters/QueryParam.cs | 5 +- .../Utilities/Json/JsonPointerAccessor.cs | 47 +++++------------ 7 files changed, 76 insertions(+), 68 deletions(-) diff --git a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs index ff5770be..57a8ac3b 100644 --- a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs @@ -22,38 +22,40 @@ public CursorPagination(string output, string input) _input = input; } + /// + /// Applies pagination logic by updating the request builder with a new cursor value + /// extracted from the response, if available. + /// + /// The context containing the current request builder, response data, and headers. + /// + /// A new instance with updated cursor parameter if the response was processed; + /// otherwise, null if no update occurred. + /// public RequestBuilder Apply(PaginationContext paginationContext) { var isUpdated = false; - var currentRequestBuilder = RequestBuilder.RequestBuilderWithParameters(paginationContext.RequestBuilder) - .UpdateByReference( - _input, old => + + var updatedBuilder = RequestBuilder + .RequestBuilderWithParameters(paginationContext.RequestBuilder) + .UpdateByReference(_input, old => + { + var oldValue = old?.ToString(); + if (!paginationContext.HasResponse) { - var oldValue = old?.ToString(); - if (!paginationContext.HasResponse) - { - CursorValue = oldValue; - isUpdated = true; - return oldValue; - } + CursorValue = oldValue; + isUpdated = true; + return oldValue; + } - var cursorValue = JsonPointerAccessor.ResolveJsonValueByReference( - _output, - paginationContext.ResponseBody, - paginationContext.ResponseHeaders - ); - - if (cursorValue == null) - { - return oldValue; - } + CursorValue = JsonPointerAccessor.ResolveJsonValueByReference( + _output, paginationContext.ResponseBody, paginationContext.ResponseHeaders + ) ?? oldValue; - CursorValue = cursorValue; - isUpdated = true; - return CursorValue; - }); + isUpdated |= CursorValue != oldValue; + return CursorValue; + }); - return isUpdated ? currentRequestBuilder : null; + return isUpdated ? updatedBuilder : null; } } } diff --git a/APIMatic.Core/Pagination/Strategies/LinkPagination.cs b/APIMatic.Core/Pagination/Strategies/LinkPagination.cs index b4c943cc..cfddfb79 100644 --- a/APIMatic.Core/Pagination/Strategies/LinkPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/LinkPagination.cs @@ -19,6 +19,17 @@ public LinkPagination(string next) _next = next; } + /// + /// Applies link-based pagination by extracting the next page URL from the response + /// and updating the request builder with query parameters from that URL. + /// + /// + /// The context containing the current request builder and response data (body and headers). + /// + /// + /// A new with updated query parameters if a next link is found; + /// the original request builder if no response is available; otherwise, null if no link is present. + /// public RequestBuilder Apply(PaginationContext paginationContext) { CurrentLinkValue = JsonPointerAccessor.ResolveJsonValueByReference( diff --git a/APIMatic.Core/Pagination/Strategies/NumberPagination.cs b/APIMatic.Core/Pagination/Strategies/NumberPagination.cs index 46809581..de9b9d25 100644 --- a/APIMatic.Core/Pagination/Strategies/NumberPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/NumberPagination.cs @@ -17,6 +17,17 @@ public NumberPagination(string input) _input = input; } + /// + /// Applies page-based pagination by incrementing the current page number + /// and updating the request builder accordingly. + /// + /// + /// The context containing the current request builder and response metadata (body and headers). + /// + /// + /// A new with the page number incremented if pagination is applied; + /// otherwise, null if no changes are made. + /// public RequestBuilder Apply(PaginationContext paginationContext) { var isUpdated = false; diff --git a/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs b/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs index b3133d56..40372524 100644 --- a/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs @@ -17,6 +17,18 @@ public OffsetPagination(string input) _input = input; } + /// + /// Applies offset-based pagination by updating the request builder with a new offset value. + /// If the response is not available, it uses the existing offset value from the request. + /// Otherwise, it increments the offset by the size of the data returned in the previous response. + /// + /// + /// The context containing the current request builder, response data, and data size for pagination. + /// + /// + /// A new with the updated offset value if pagination was applied; + /// otherwise, null if no update occurred. + /// public RequestBuilder Apply(PaginationContext paginationContext) { var isUpdated = false; diff --git a/APIMatic.Core/Request/Parameters/HeaderParam.cs b/APIMatic.Core/Request/Parameters/HeaderParam.cs index d0664a38..4d206bb8 100644 --- a/APIMatic.Core/Request/Parameters/HeaderParam.cs +++ b/APIMatic.Core/Request/Parameters/HeaderParam.cs @@ -14,11 +14,7 @@ public class HeaderParam : Parameter internal override void Apply(RequestBuilder requestBuilder) { - if (!validated) - { - return; - } - + if (!validated) return; requestBuilder.headersParameters[key] = value; } } diff --git a/APIMatic.Core/Request/Parameters/QueryParam.cs b/APIMatic.Core/Request/Parameters/QueryParam.cs index 11529b42..0f0231ff 100644 --- a/APIMatic.Core/Request/Parameters/QueryParam.cs +++ b/APIMatic.Core/Request/Parameters/QueryParam.cs @@ -12,10 +12,7 @@ public class QueryParam : Parameter internal override void Apply(RequestBuilder requestBuilder) { - if (!validated) - { - return; - } + if (!validated) return; requestBuilder.queryParameters[key] = value; } } diff --git a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs index 64c3cab7..5aeeb47c 100644 --- a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs +++ b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs @@ -83,54 +83,33 @@ public static T UpdateBodyValueByPointer(T value, JsonPointer pointer, Func UpdateValueByPointer( - Dictionary value, - JsonPointer pointer, - Func updater) + public static Dictionary UpdateValueByPointer(Dictionary value, + JsonPointer pointer, Func updater) { try { - // 0. Capture original types of root dictionary values var typeMap = value.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.GetType() ?? typeof(object)); - - // 1. Serialize the input dictionary to JToken - var json = JsonConvert.SerializeObject(value); - var root = JToken.Parse(json); - - // 2. Locate the token to update + var root = JToken.Parse(JsonConvert.SerializeObject(value)); var tokenToUpdate = pointer.Evaluate(root); - // 3. Apply updater - var currentValue = tokenToUpdate.ToObject(); - var updatedValue = updater(currentValue); + var updatedValue = updater(tokenToUpdate.ToObject()); var newToken = updatedValue == null ? JValue.CreateNull() : JToken.FromObject(updatedValue); switch (tokenToUpdate.Parent) { - // 4. Replace token - case JProperty prop: - prop.Value = newToken; - break; + case JProperty prop: prop.Value = newToken; break; case JArray array: - { - int index = array.IndexOf(tokenToUpdate); - array[index] = newToken; + array[array.IndexOf(tokenToUpdate)] = newToken; break; - } } - // 5. Deserialize each value using its original type - var result = new Dictionary(); - foreach (var kvp in ((JObject)root).Properties()) - { - var key = kvp.Name; - var token = kvp.Value; - var targetType = typeMap.TryGetValue(key, out var type) ? type : typeof(object); - var obj = token.ToObject(targetType); - result[key] = obj!; - } - - return result; + return ((JObject)root).Properties().ToDictionary( + prop => prop.Name, + prop => + { + var type = typeMap.TryGetValue(prop.Name, out var t) ? t : typeof(object); + return prop.Value.ToObject(type)!; + }); } catch { From 524993c45b174efbeeb8a9e814940811f04b9f27 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 11:26:29 +0500 Subject: [PATCH 11/24] Update JsonPointerAccessor.cs --- .../Utilities/Json/JsonPointerAccessor.cs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs index 5aeeb47c..4446f723 100644 --- a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs +++ b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs @@ -91,7 +91,6 @@ public static Dictionary UpdateValueByPointer(Dictionary kvp.Key, kvp => kvp.Value?.GetType() ?? typeof(object)); var root = JToken.Parse(JsonConvert.SerializeObject(value)); var tokenToUpdate = pointer.Evaluate(root); - var updatedValue = updater(tokenToUpdate.ToObject()); var newToken = updatedValue == null ? JValue.CreateNull() : JToken.FromObject(updatedValue); @@ -103,18 +102,10 @@ public static Dictionary UpdateValueByPointer(Dictionary prop.Name, - prop => - { - var type = typeMap.TryGetValue(prop.Name, out var t) ? t : typeof(object); - return prop.Value.ToObject(type)!; - }); - } - catch - { - return value; + return ((JObject)root).Properties().ToDictionary(prop => prop.Name, + prop => prop.Value.ToObject(typeMap.TryGetValue(prop.Name, out var t) ? t : typeof(object))); } + catch { return value; } } } } From 09feb61627064750f30a56c115bf65a5de0285c4 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 11:31:37 +0500 Subject: [PATCH 12/24] Update JsonPointerAccessor.cs --- .../Utilities/Json/JsonPointerAccessor.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs index 4446f723..88a8dd6a 100644 --- a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs +++ b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs @@ -53,34 +53,23 @@ private static string GetJsonValueByPointer(JsonPointer jsonPointer, string json public static T UpdateBodyValueByPointer(T value, JsonPointer pointer, Func updater) { - if (value == null || pointer == null || updater == null) + if (EqualityComparer.Default.Equals(value, default) || pointer == null || updater == null) return value; try { - // Step 1: Serialize the object to JSON var json = CoreHelper.JsonSerialize(value); - - // Step 2: Parse JSON and evaluate pointer var jsonObject = JObject.Parse(json); var jsonToken = pointer.Evaluate(jsonObject); - // Step 3: Apply updater var oldValue = jsonToken.ToObject(); var newValue = updater(oldValue); if (newValue == null) return value; - // Step 4: Replace token jsonToken.Replace(JToken.FromObject(newValue)); - - // Step 5: Deserialize back to T - var updatedJson = jsonObject.ToString(); - return CoreHelper.JsonDeserialize(updatedJson); - } - catch - { - return value; + return CoreHelper.JsonDeserialize(jsonObject.ToString()); } + catch { return value; } } public static Dictionary UpdateValueByPointer(Dictionary value, From 7929eb6df715389ff11edc98ba538cd6ca92b3ad Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 13:38:33 +0500 Subject: [PATCH 13/24] adds body support in pagination updateByReference --- .../Utilities/Json/JsonPointerAccessorTest.cs | 42 ++++++++---- APIMatic.Core/Request/RequestBuilder.cs | 68 ++++++++++++++++--- .../Utilities/Json/JsonPointerAccessor.cs | 13 ++-- 3 files changed, 97 insertions(+), 26 deletions(-) diff --git a/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs b/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs index 412a54df..0bae3769 100644 --- a/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs +++ b/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs @@ -66,12 +66,30 @@ public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesWithEmptyPoin var previousHeaders = builder.headersParameters; // Act - builder.UpdateByReference("$request.headers#/nonexistent", old => 999); + builder.UpdateByReference("$request.headers#", old => 999); // Assert Assert.AreEqual(previousHeaders, builder.headersParameters); } + [Test] + public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesWithNonExistentPointer() + { + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + builder.Parameters(p => p.Header(h => h.Setup("id", 0))); + await builder.Build(); + + var previousHeaders = builder.headersParameters; + + // Act + builder.UpdateByReference("$request.headers#/nonexistent", old => 999); + + // Assert + Assert.AreEqual(previousHeaders, builder.headersParameters); + } + [Test] public async Task UpdateByReference_HeaderParam_ObjectValue_UpdatesObjectCorrectly() { @@ -151,11 +169,11 @@ public async Task UpdateByReference_TemplateParam_Collection_UpdatesCorrectly() } [Test] - public void UpdateValueByPointer_ValidPointer_UpdatesValue() + public void UpdateBodyValueByPointer_ValidPointer_UpdatesValue() { // Arrange var person = new Person { Name = "Alice", Age = 30 }; - var pointer = new JsonPointer("/age"); + var pointer = "/age"; // Act var updated = JsonPointerAccessor.UpdateBodyValueByPointer( @@ -170,12 +188,12 @@ public void UpdateValueByPointer_ValidPointer_UpdatesValue() } [Test] - public void UpdateValueByPointer_NullValue_ReturnsOriginal() + public void UpdateBodyValueByPointer_NullValue_ReturnsOriginal() { // Act var result = JsonPointerAccessor.UpdateBodyValueByPointer( null, - new JsonPointer("/name"), + "/name", old => "Bob" ); @@ -184,7 +202,7 @@ public void UpdateValueByPointer_NullValue_ReturnsOriginal() } [Test] - public void UpdateValueByPointer_NullPointer_ReturnsOriginal() + public void UpdateBodyValueByPointer_NullPointer_ReturnsOriginal() { var person = new Person { Name = "Charlie", Age = 40 }; var result = JsonPointerAccessor.UpdateBodyValueByPointer( @@ -197,12 +215,12 @@ public void UpdateValueByPointer_NullPointer_ReturnsOriginal() } [Test] - public void UpdateValueByPointer_NullUpdater_ReturnsOriginal() + public void UpdateBodyValueByPointer_NullUpdater_ReturnsOriginal() { var person = new Person { Name = "Charlie", Age = 40 }; var result = JsonPointerAccessor.UpdateBodyValueByPointer( person, - new JsonPointer("/name"), + "/name", null ); @@ -210,10 +228,10 @@ public void UpdateValueByPointer_NullUpdater_ReturnsOriginal() } [Test] - public void UpdateValueByPointer_UpdaterReturnsNull_ReturnsOriginal() + public void UpdateBodyValueByPointer_UpdaterReturnsNull_ReturnsOriginal() { var person = new Person { Name = "Eva", Age = 50 }; - var pointer = new JsonPointer("/name"); + var pointer = "/name"; var result = JsonPointerAccessor.UpdateBodyValueByPointer( person, @@ -225,10 +243,10 @@ public void UpdateValueByPointer_UpdaterReturnsNull_ReturnsOriginal() } [Test] - public void UpdateValueByPointer_InvalidPointer_ReturnsOriginal() + public void UpdateBodyValueByPointer_InvalidPointer_ReturnsOriginal() { var person = new Person { Name = "Frank", Age = 60 }; - var invalidPointer = new JsonPointer("/nonexistent"); + var invalidPointer = "/nonexistent"; var result = JsonPointerAccessor.UpdateBodyValueByPointer( person, diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 31f33509..5d94267e 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -34,10 +34,10 @@ public class RequestBuilder private bool xmlRequest = false; private readonly StringBuilder queryUrl; private Func bodySerializer = PrepareBody; - - internal readonly Dictionary headersParameters = new Dictionary(); + internal dynamic body; internal Type bodyType; + internal readonly Dictionary headersParameters = new Dictionary(); internal readonly Dictionary templateParameters = new Dictionary(); internal readonly Dictionary bodyParameters = new Dictionary(); internal readonly List> formParameters = new List>(); @@ -194,25 +194,60 @@ public RequestBuilder UpdateByReference(string pointerString, Func setter, string pointer) + { + if (bodyParameters.Any()) + { + UpdateValuesByPointer(bodyParameters, pointer, setter); + return; + } + + if (body != null) + { + if (body is CoreFileStreamInfo) + { + return; // file data not supported + } + + if (bodySerializer != null && string.IsNullOrEmpty(pointer)) + { + bodySerializer = _ => setter(body); + return; + } + + if (body is string && string.IsNullOrEmpty(pointer) || string.IsNullOrEmpty(pointer)) + { + body = setter(body); + return; + } + + body = JsonPointerAccessor.UpdateBodyValueByPointer(body, pointer, setter); + } + + UpdateFormParameter(formParameters, pointer, setter); + } + /// /// This applies all the configuration and build an instance of CoreRequest /// @@ -302,8 +337,7 @@ private static object PrepareBody(dynamic value) return CoreHelper.JsonSerialize(value); } - private static void UpdateValuesByPointer(Dictionary requestParameters, Func setter, - JsonPointer pointer) + private static void UpdateValuesByPointer(Dictionary requestParameters, string pointer, Func setter) { var updated = JsonPointerAccessor.UpdateValueByPointer(requestParameters, pointer, setter); @@ -315,6 +349,22 @@ private static void UpdateValuesByPointer(Dictionary requestPara } } } + + private static void UpdateFormParameter(List> formParameters, string pointer, + Func setter) + { + // Convert to dictionary for update + var formParameterDictionary = formParameters + .GroupBy(p => p.Key) + .ToDictionary(g => g.Key, g => g.Last().Value); // Handles duplicate keys safely + + // Apply update + var updated = JsonPointerAccessor.UpdateValueByPointer(formParameterDictionary, pointer, setter); + + // Rebuild list from updated dictionary + formParameters.Clear(); + formParameters.AddRange(updated.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value))); + } } internal static class RequestBuilderExtensions diff --git a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs index 88a8dd6a..bcb9b540 100644 --- a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs +++ b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs @@ -51,16 +51,17 @@ private static string GetJsonValueByPointer(JsonPointer jsonPointer, string json }; } - public static T UpdateBodyValueByPointer(T value, JsonPointer pointer, Func updater) + public static T UpdateBodyValueByPointer(T value, string pointer, Func updater) { - if (EqualityComparer.Default.Equals(value, default) || pointer == null || updater == null) + if (EqualityComparer.Default.Equals(value, default) || string.IsNullOrEmpty(pointer) || updater == null) return value; try { var json = CoreHelper.JsonSerialize(value); var jsonObject = JObject.Parse(json); - var jsonToken = pointer.Evaluate(jsonObject); + var jsonPointer = new JsonPointer(pointer); + var jsonToken = jsonPointer.Evaluate(jsonObject); var oldValue = jsonToken.ToObject(); var newValue = updater(oldValue); @@ -73,13 +74,15 @@ public static T UpdateBodyValueByPointer(T value, JsonPointer pointer, Func UpdateValueByPointer(Dictionary value, - JsonPointer pointer, Func updater) + string pointer, Func updater) { + if (value is null || string.IsNullOrEmpty(pointer) || updater == null) return value; try { var typeMap = value.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.GetType() ?? typeof(object)); var root = JToken.Parse(JsonConvert.SerializeObject(value)); - var tokenToUpdate = pointer.Evaluate(root); + var jsonPointer = new JsonPointer(pointer); + var tokenToUpdate = jsonPointer.Evaluate(root); var updatedValue = updater(tokenToUpdate.ToObject()); var newToken = updatedValue == null ? JValue.CreateNull() : JToken.FromObject(updatedValue); From 2ad2da3d06bf8b597d17275fc43f1bed2eb98c4a Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 14:01:27 +0500 Subject: [PATCH 14/24] Update RequestBuilder.cs --- APIMatic.Core/Request/RequestBuilder.cs | 46 ++++++++++++++----------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 5d94267e..766f4e0a 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -222,32 +222,36 @@ private void UpdateBodyByPointer(Func setter, string pointer) UpdateValuesByPointer(bodyParameters, pointer, setter); return; } - - if (body != null) + + if (body is null) { - if (body is CoreFileStreamInfo) - { - return; // file data not supported - } + UpdateFormParameter(formParameters, pointer, setter); + return; + } - if (bodySerializer != null && string.IsNullOrEmpty(pointer)) - { - bodySerializer = _ => setter(body); - return; - } + UpdateDynamicBodyContent(setter, pointer); + } + + private void UpdateDynamicBodyContent(Func setter, string pointer) + { + if (body is CoreFileStreamInfo || setter == null) + return; - if (body is string && string.IsNullOrEmpty(pointer) || string.IsNullOrEmpty(pointer)) - { - body = setter(body); - return; - } - - body = JsonPointerAccessor.UpdateBodyValueByPointer(body, pointer, setter); + if (bodySerializer != null && string.IsNullOrEmpty(pointer)) + { + bodySerializer = _ => setter(body); + return; } - - UpdateFormParameter(formParameters, pointer, setter); - } + if (body is string || string.IsNullOrEmpty(pointer)) + { + body = setter(body); + return; + } + + body = JsonPointerAccessor.UpdateBodyValueByPointer(body, pointer, setter); + } + /// /// This applies all the configuration and build an instance of CoreRequest /// From 6557fdd7ea86f9ed76e45a40dbd6739713de6ec6 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 17:33:56 +0500 Subject: [PATCH 15/24] Add body UpdateByReference Tests and refactor code --- .../Request/RequestBuilderExtensionsTest.cs | 99 +++++++++ .../RequestBuilderUpdateByReferenceTest.cs} | 193 ++++++------------ .../Utilities/Json/JsonPointerResolverTest.cs | 85 ++++++++ .../Pagination/Strategies/CursorPagination.cs | 2 +- .../Pagination/Strategies/LinkPagination.cs | 2 +- APIMatic.Core/Request/RequestBuilder.cs | 73 +------ .../Request/RequestBuilderExtensions.cs | 112 ++++++++++ .../Utilities/Json/JsonPointerAccessor.cs | 103 ---------- .../Utilities/Json/JsonPointerResolver.cs | 51 +++++ 9 files changed, 418 insertions(+), 302 deletions(-) create mode 100644 APIMatic.Core.Test/Request/RequestBuilderExtensionsTest.cs rename APIMatic.Core.Test/{Utilities/Json/JsonPointerAccessorTest.cs => Request/RequestBuilderUpdateByReferenceTest.cs} (53%) create mode 100644 APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs create mode 100644 APIMatic.Core/Request/RequestBuilderExtensions.cs delete mode 100644 APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs create mode 100644 APIMatic.Core/Utilities/Json/JsonPointerResolver.cs diff --git a/APIMatic.Core.Test/Request/RequestBuilderExtensionsTest.cs b/APIMatic.Core.Test/Request/RequestBuilderExtensionsTest.cs new file mode 100644 index 00000000..74635a23 --- /dev/null +++ b/APIMatic.Core.Test/Request/RequestBuilderExtensionsTest.cs @@ -0,0 +1,99 @@ +using APIMatic.Core.Request; +using APIMatic.Core.Test.MockTypes.Models; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Request +{ + [TestFixture] + public class RequestBuilderExtensionsTest + { + [Test] + public void UpdateBodyValueByPointer_ValidPointer_UpdatesValue() + { + // Arrange + var person = new Person { Name = "Alice", Age = 30 }; + var pointer = "/age"; + + // Act + var updated = RequestBuilderExtensions.UpdateValueByPointer( + person, + pointer, + old => int.Parse(old.ToString() ?? "") + 5 + ); + + // Assert + Assert.AreEqual(35, updated.Age); + Assert.AreEqual("Alice", updated.Name); + } + + [Test] + public void UpdateBodyValueByPointer_NullValue_ReturnsOriginal() + { + // Act + var result = RequestBuilderExtensions.UpdateValueByPointer( + null, + "/name", + old => "Bob" + ); + + // Assert + Assert.IsNull(result); + } + + [Test] + public void UpdateBodyValueByPointer_NullPointer_ReturnsOriginal() + { + var person = new Person { Name = "Charlie", Age = 40 }; + var result = RequestBuilderExtensions.UpdateValueByPointer( + person, + null, + old => "David" + ); + + Assert.AreEqual("Charlie", result.Name); + } + + [Test] + public void UpdateBodyValueByPointer_NullUpdater_ReturnsOriginal() + { + var person = new Person { Name = "Charlie", Age = 40 }; + var result = RequestBuilderExtensions.UpdateValueByPointer( + person, + "/name", + null + ); + + Assert.AreEqual("Charlie", result.Name); + } + + [Test] + public void UpdateBodyValueByPointer_UpdaterReturnsNull_ReturnsOriginal() + { + var person = new Person { Name = "Eva", Age = 50 }; + var pointer = "/name"; + + var result = RequestBuilderExtensions.UpdateValueByPointer( + person, + pointer, + old => null + ); + + Assert.AreEqual("Eva", result.Name); + } + + [Test] + public void UpdateBodyValueByPointer_InvalidPointer_ReturnsOriginal() + { + var person = new Person { Name = "Frank", Age = 60 }; + var invalidPointer = "/nonexistent"; + + var result = RequestBuilderExtensions.UpdateValueByPointer( + person, + invalidPointer, + old => "ShouldNotApply" + ); + + Assert.AreEqual("Frank", result.Name); + } + } +} diff --git a/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs b/APIMatic.Core.Test/Request/RequestBuilderUpdateByReferenceTest.cs similarity index 53% rename from APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs rename to APIMatic.Core.Test/Request/RequestBuilderUpdateByReferenceTest.cs index 0bae3769..7ba7c658 100644 --- a/APIMatic.Core.Test/Utilities/Json/JsonPointerAccessorTest.cs +++ b/APIMatic.Core.Test/Request/RequestBuilderUpdateByReferenceTest.cs @@ -1,20 +1,20 @@ -using System; +using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net.Http; using System.Threading.Tasks; using APIMatic.Core.Request; -using APIMatic.Core.Test.MockTypes.Models; using APIMatic.Core.Test.MockTypes.Models.Pagination; -using APIMatic.Core.Utilities.Json; -using Microsoft.Json.Pointer; +using APIMatic.Core.Types.Sdk; using NUnit.Framework; -namespace APIMatic.Core.Test.Utilities.Json +namespace APIMatic.Core.Test.Request { [TestFixture] - public class JsonPointerAccessorTest : TestBase + public class RequestBuilderUpdateByReferenceTest : TestBase { - private const string ServerUrl = "https://api.example.com/users"; + private const string ServerUrl = "https://api.example.com/users"; readonly Action setupRequest = rb => rb.Setup(HttpMethod.Get, ServerUrl); @@ -167,170 +167,101 @@ public async Task UpdateByReference_TemplateParam_Collection_UpdatesCorrectly() Assert.IsNotNull(updated); CollectionAssert.AreEqual(new List { 1, 999, 3 }, updated); } - + [Test] - public void UpdateBodyValueByPointer_ValidPointer_UpdatesValue() + public async Task UpdateByReference_BodyParameters_UpdatesCorrectly() { - // Arrange - var person = new Person { Name = "Alice", Age = 30 }; - var pointer = "/age"; - - // Act - var updated = JsonPointerAccessor.UpdateBodyValueByPointer( - person, - pointer, - old => int.Parse(old.ToString() ?? "") + 5 - ); + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); - // Assert - Assert.AreEqual(35, updated.Age); - Assert.AreEqual("Alice", updated.Name); - } + builder.Parameters(p => p.Body(b => b.Setup("age", 25))); + await builder.Build(); - [Test] - public void UpdateBodyValueByPointer_NullValue_ReturnsOriginal() - { - // Act - var result = JsonPointerAccessor.UpdateBodyValueByPointer( - null, - "/name", - old => "Bob" - ); + builder.UpdateByReference("$request.body#/age", old => 30); - // Assert - Assert.IsNull(result); + Assert.IsTrue(builder.bodyParameters.ContainsKey("age")); + Assert.AreEqual(30, builder.bodyParameters["age"]); } [Test] - public void UpdateBodyValueByPointer_NullPointer_ReturnsOriginal() + public async Task UpdateByReference_FormParameters_UpdatesCorrectly() { - var person = new Person { Name = "Charlie", Age = 40 }; - var result = JsonPointerAccessor.UpdateBodyValueByPointer( - person, - null, - old => "David" - ); - - Assert.AreEqual("Charlie", result.Name); - } + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); - [Test] - public void UpdateBodyValueByPointer_NullUpdater_ReturnsOriginal() - { - var person = new Person { Name = "Charlie", Age = 40 }; - var result = JsonPointerAccessor.UpdateBodyValueByPointer( - person, - "/name", - null - ); - - Assert.AreEqual("Charlie", result.Name); - } + builder.Parameters(p => p.Form(f => + { + f.Setup("username", "john"); + f.Setup("email", "john@example.com"); + })); - [Test] - public void UpdateBodyValueByPointer_UpdaterReturnsNull_ReturnsOriginal() - { - var person = new Person { Name = "Eva", Age = 50 }; - var pointer = "/name"; + await builder.Build(); - var result = JsonPointerAccessor.UpdateBodyValueByPointer( - person, - pointer, - old => null - ); + builder.UpdateByReference("$request.body#/email", old => "jane@example.com"); - Assert.AreEqual("Eva", result.Name); + var updated = builder.formParameters.ToDictionary(p => p.Key, p => p.Value); + Assert.AreEqual("jane@example.com", updated["email"]); } [Test] - public void UpdateBodyValueByPointer_InvalidPointer_ReturnsOriginal() + public async Task UpdateByReference_DynamicBody_UpdatesCorrectly() { - var person = new Person { Name = "Frank", Age = 60 }; - var invalidPointer = "/nonexistent"; - - var result = JsonPointerAccessor.UpdateBodyValueByPointer( - person, - invalidPointer, - old => "ShouldNotApply" - ); - - Assert.AreEqual("Frank", result.Name); - } + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); - [Test] - public void ResolveJsonValueByReference_NullPointer_ReturnsNull() - { - var result = JsonPointerAccessor.ResolveJsonValueByReference(null, "{}", "{}"); - Assert.IsNull(result); - } + builder.Parameters(p => p.Body( b => b.Setup(new { name = "Alice", age = 30 }))); + await builder.Build(); - [Test] - public void ResolveJsonValueByReference_InvalidFormat_ReturnsNull() - { - var result = JsonPointerAccessor.ResolveJsonValueByReference("$response.body", "{}", "{}"); - Assert.IsNull(result); - } + builder.UpdateByReference("$request.body#/age", old => 35); - [Test] - public void ResolveJsonValueByReference_NullJsonPointer_ReturnsNull() - { - var result = JsonPointerAccessor.ResolveJsonValueByReference("$response.body#", "{}", "{}"); - Assert.IsNull(result); + Assert.AreEqual(35, builder.body.age); } [Test] - public void ResolveJsonValueByReference_ValidBodyPointer_ReturnsValue() + public async Task UpdateByReference_StringBody_UpdatesCorrectly() { - var json = @"{""name"":""alice"", ""age"":30}"; - var pointer = "$response.body#/name"; - - var result = JsonPointerAccessor.ResolveJsonValueByReference(pointer, json, null); - - Assert.AreEqual("alice", result); - } + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); - [Test] - public void ResolveJsonValueByReference_ValidHeadersPointer_ReturnsValue() - { - var headers = @"{""content-type"":""application/json""}"; - var pointer = "$response.headers#/content-type"; + builder.Parameters(p => p.Body( b => b.Setup("InitialValue"))); + await builder.Build(); - var result = JsonPointerAccessor.ResolveJsonValueByReference(pointer, null, headers); + builder.UpdateByReference("$request.body#/", old => "UpdatedValue"); - Assert.AreEqual("application/json", result); + Assert.AreEqual("UpdatedValue", builder.body); } [Test] - public void ResolveJsonValueByReference_PointerNotFound_ReturnsNull() + public async Task UpdateByReference_ObjectBody_UpdatesCorrectly() { - var json = @"{""name"":""alice""}"; - var pointer = "$response.body#/nonexistent"; + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); + + builder.Parameters(p => p.Body( b => b.Setup(new User { Id = 1, Name = "Alice" }))); + await builder.Build(); - var result = JsonPointerAccessor.ResolveJsonValueByReference(pointer, json, null); + builder.UpdateByReference("$request.body#/Id", old => 35); - Assert.IsNull(result); + var updatedBody = builder.body as User; + Assert.IsNotNull(updatedBody); + Assert.AreEqual(35, updatedBody.Id); + Assert.AreEqual("Alice", updatedBody.Name); // Ensure other values remain intact } - + [Test] - public void ResolveJsonValueByReference_UnsupportedPrefix_ReturnsNull() + public async Task UpdateByReference_Body_CoreFileStreamInfo_DoesNotUpdate() { - var pointer = "$request.body#/name"; - var json = @"{""name"":""alice""}"; - - var result = JsonPointerAccessor.ResolveJsonValueByReference(pointer, json, null); + var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); + setupRequest(builder); - Assert.IsNull(result); - } + var originalBody = new CoreFileStreamInfo(new MemoryStream(), "text/plain", "file.txt"); + builder.Parameters(p => p.Body( b => b.Setup(originalBody))); - [Test] - public void ResolveJsonValueByReference_BooleanToken_ReturnsTokenToString() - { - var jsonBody = @"{ ""isActive"": true }"; - var pointerString = "$response.body#/isActive"; + await builder.Build(); - var result = JsonPointerAccessor.ResolveJsonValueByReference(pointerString, jsonBody, null); + builder.UpdateByReference("$request.body#/", _ => "should-not-apply"); - Assert.AreEqual("True", result); + Assert.AreSame(originalBody, builder.body); // Still the same object } } } diff --git a/APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs b/APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs new file mode 100644 index 00000000..9cdfbed0 --- /dev/null +++ b/APIMatic.Core.Test/Utilities/Json/JsonPointerResolverTest.cs @@ -0,0 +1,85 @@ +using APIMatic.Core.Utilities.Json; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Utilities.Json +{ + [TestFixture] + public class JsonPointerResolverTest + { + [Test] + public void ResolveScopedJsonValue_NullPointer_ReturnsNull() + { + var result = JsonPointerResolver.ResolveScopedJsonValue(null, "{}", "{}"); + Assert.IsNull(result); + } + + [Test] + public void ResolveScopedJsonValue_InvalidFormat_ReturnsNull() + { + var result = JsonPointerResolver.ResolveScopedJsonValue("$response.body", "{}", "{}"); + Assert.IsNull(result); + } + + [Test] + public void ResolveScopedJsonValue_NullJsonPointer_ReturnsNull() + { + var result = JsonPointerResolver.ResolveScopedJsonValue("$response.body#", "{}", "{}"); + Assert.IsNull(result); + } + + [Test] + public void ResolveScopedJsonValue_ValidBodyPointer_ReturnsValue() + { + var json = @"{""name"":""alice"", ""age"":30}"; + var pointer = "$response.body#/name"; + + var result = JsonPointerResolver.ResolveScopedJsonValue(pointer, json, null); + + Assert.AreEqual("alice", result); + } + + [Test] + public void ResolveScopedJsonValue_ValidHeadersPointer_ReturnsValue() + { + var headers = @"{""content-type"":""application/json""}"; + var pointer = "$response.headers#/content-type"; + + var result = JsonPointerResolver.ResolveScopedJsonValue(pointer, null, headers); + + Assert.AreEqual("application/json", result); + } + + [Test] + public void ResolveScopedJsonValue_PointerNotFound_ReturnsNull() + { + var json = @"{""name"":""alice""}"; + var pointer = "$response.body#/nonexistent"; + + var result = JsonPointerResolver.ResolveScopedJsonValue(pointer, json, null); + + Assert.IsNull(result); + } + + [Test] + public void ResolveScopedJsonValue_UnsupportedPrefix_ReturnsNull() + { + var pointer = "$request.body#/name"; + var json = @"{""name"":""alice""}"; + + var result = JsonPointerResolver.ResolveScopedJsonValue(pointer, json, null); + + Assert.IsNull(result); + } + + [Test] + public void ResolveScopedJsonValue_BooleanToken_ReturnsTokenToString() + { + var jsonBody = @"{ ""isActive"": true }"; + var pointerString = "$response.body#/isActive"; + + var result = JsonPointerResolver.ResolveScopedJsonValue(pointerString, jsonBody, null); + + Assert.AreEqual("True", result); + } + } +} diff --git a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs index 57a8ac3b..6d6622e7 100644 --- a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs @@ -47,7 +47,7 @@ public RequestBuilder Apply(PaginationContext paginationContext) return oldValue; } - CursorValue = JsonPointerAccessor.ResolveJsonValueByReference( + CursorValue = JsonPointerResolver.ResolveScopedJsonValue( _output, paginationContext.ResponseBody, paginationContext.ResponseHeaders ) ?? oldValue; diff --git a/APIMatic.Core/Pagination/Strategies/LinkPagination.cs b/APIMatic.Core/Pagination/Strategies/LinkPagination.cs index cfddfb79..f19ea187 100644 --- a/APIMatic.Core/Pagination/Strategies/LinkPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/LinkPagination.cs @@ -32,7 +32,7 @@ public LinkPagination(string next) /// public RequestBuilder Apply(PaginationContext paginationContext) { - CurrentLinkValue = JsonPointerAccessor.ResolveJsonValueByReference( + CurrentLinkValue = JsonPointerResolver.ResolveScopedJsonValue( _next, paginationContext.ResponseBody, paginationContext.ResponseHeaders diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 766f4e0a..4730c251 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -5,18 +5,14 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using System.Web; using APIMatic.Core.Authentication; using APIMatic.Core.Http.Configuration; using APIMatic.Core.Request.Parameters; using APIMatic.Core.Types.Sdk; using APIMatic.Core.Utilities; -using APIMatic.Core.Utilities.Json; -using Microsoft.Json.Pointer; namespace APIMatic.Core.Request { @@ -187,7 +183,7 @@ public RequestBuilder XmlBodySerializer(Func xmlSerializer) return this; } - public RequestBuilder UpdateByReference(string pointerString, Func setter) + internal RequestBuilder UpdateByReference(string pointerString, Func setter) { var index = pointerString.IndexOf('#'); if (index < 0) @@ -199,13 +195,13 @@ public RequestBuilder UpdateByReference(string pointerString, Func setter, string pointer) { if (bodyParameters.Any()) { - UpdateValuesByPointer(bodyParameters, pointer, setter); + RequestBuilderExtensions.UpdateRequestParametersByPointer(bodyParameters, pointer, setter); return; } if (body is null) { - UpdateFormParameter(formParameters, pointer, setter); + RequestBuilderExtensions.UpdateFormParameterValueByPointer(formParameters, pointer, setter); return; } @@ -237,19 +233,13 @@ private void UpdateDynamicBodyContent(Func setter, string pointe if (body is CoreFileStreamInfo || setter == null) return; - if (bodySerializer != null && string.IsNullOrEmpty(pointer)) - { - bodySerializer = _ => setter(body); - return; - } - if (body is string || string.IsNullOrEmpty(pointer)) { body = setter(body); return; } - body = JsonPointerAccessor.UpdateBodyValueByPointer(body, pointer, setter); + body = RequestBuilderExtensions.UpdateValueByPointer(body, pointer, setter); } /// @@ -340,54 +330,5 @@ private static object PrepareBody(dynamic value) } return CoreHelper.JsonSerialize(value); } - - private static void UpdateValuesByPointer(Dictionary requestParameters, string pointer, Func setter) - { - var updated = JsonPointerAccessor.UpdateValueByPointer(requestParameters, pointer, setter); - - foreach (var kvp in updated) - { - if (!requestParameters.TryGetValue(kvp.Key, out var existingValue) || !Equals(existingValue, kvp.Value)) - { - requestParameters[kvp.Key] = kvp.Value; - } - } - } - - private static void UpdateFormParameter(List> formParameters, string pointer, - Func setter) - { - // Convert to dictionary for update - var formParameterDictionary = formParameters - .GroupBy(p => p.Key) - .ToDictionary(g => g.Key, g => g.Last().Value); // Handles duplicate keys safely - - // Apply update - var updated = JsonPointerAccessor.UpdateValueByPointer(formParameterDictionary, pointer, setter); - - // Rebuild list from updated dictionary - formParameters.Clear(); - formParameters.AddRange(updated.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value))); - } - } - - internal static class RequestBuilderExtensions - { - public static Dictionary ToCoreRequestHeaders(this Dictionary headers) - { - var result = new Dictionary(); - - foreach (var kvp in headers) - { - var serialized = CoreHelper.JsonSerialize(kvp.Value)?.TrimStart('"').TrimEnd('"'); - if (serialized != null) - { - result[kvp.Key] = serialized; - } - } - - return result; - } - } } diff --git a/APIMatic.Core/Request/RequestBuilderExtensions.cs b/APIMatic.Core/Request/RequestBuilderExtensions.cs new file mode 100644 index 00000000..81a767b5 --- /dev/null +++ b/APIMatic.Core/Request/RequestBuilderExtensions.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using APIMatic.Core.Utilities; +using Microsoft.Json.Pointer; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace APIMatic.Core.Request +{ + internal static class RequestBuilderExtensions + { + public static Dictionary ToCoreRequestHeaders(this Dictionary headers) + { + var result = new Dictionary(); + + foreach (var kvp in headers) + { + var serialized = CoreHelper.JsonSerialize(kvp.Value)?.TrimStart('"').TrimEnd('"'); + if (serialized != null) + { + result[kvp.Key] = serialized; + } + } + + return result; + } + + public static void UpdateRequestParametersByPointer(Dictionary requestParameters, + string pointer, + Func setter) + { + var updated = + UpdateValueByPointer(requestParameters, pointer, setter); + + foreach (var kvp in updated) + { + if (!requestParameters.TryGetValue(kvp.Key, out var existingValue) || !Equals(existingValue, kvp.Value)) + { + requestParameters[kvp.Key] = kvp.Value; + } + } + } + + public static void UpdateFormParameterValueByPointer(List> formParameters, + string pointer, + Func setter) + { + var formParameterDictionary = formParameters + .GroupBy(p => p.Key) + .ToDictionary(g => g.Key, g => g.Last().Value); // Handles duplicate keys safely + + var updated = + UpdateValueByPointer(formParameterDictionary, pointer, setter); + + formParameters.Clear(); + formParameters.AddRange(updated.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value))); + } + + public static T UpdateValueByPointer(T value, string pointer, Func updater) + { + if (value == null || string.IsNullOrEmpty(pointer) || updater == null) + return value; + + try + { + var type = value.GetType(); + var rootToken = JToken.Parse(JsonConvert.SerializeObject(value)); + var tokenToUpdate = new JsonPointer(pointer).Evaluate(rootToken); + + var updatedValue = updater(tokenToUpdate.ToObject()); + if (updatedValue == null) return value; + + ReplaceToken(tokenToUpdate, updatedValue); + + return typeof(T) == typeof(Dictionary) + ? (T)(object)ReconstructDictionary((Dictionary)(object)value, (JObject)rootToken) + : (T)JsonConvert.DeserializeObject(rootToken.ToString(), type); + } + catch + { + return value; + } + } + + private static void ReplaceToken(JToken token, object newValue) + { + var newToken = JToken.FromObject(newValue); + switch (token.Parent) + { + case JProperty prop: + prop.Value = newToken; + break; + case JArray array: + array[array.IndexOf(token)] = newToken; + break; + } + } + + private static Dictionary ReconstructDictionary( + Dictionary original, + JObject updatedRoot) + { + var typeMap = original.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.GetType() ?? typeof(object)); + return updatedRoot.Properties().ToDictionary( + prop => prop.Name, + prop => prop.Value.ToObject(typeMap.TryGetValue(prop.Name, out var t) ? t : typeof(object)) + ); + } + } +} + diff --git a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs b/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs deleted file mode 100644 index bcb9b540..00000000 --- a/APIMatic.Core/Utilities/Json/JsonPointerAccessor.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.Json.Pointer; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace APIMatic.Core.Utilities.Json -{ - internal static class JsonPointerAccessor - { - public static string ResolveJsonValueByReference(string pointerString, string jsonBody, string jsonHeaders) - { - if (string.IsNullOrEmpty(pointerString) || !pointerString.Contains('#')) - return null; - - var (scope, path) = (pointerString.Split('#')[0], pointerString.Split('#')[1]); - if (string.IsNullOrEmpty(path)) - return null; - - var jsonPointer = new JsonPointer(path); - - return scope switch - { - "$response.body" => GetJsonValueByPointer(jsonPointer, jsonBody), - "$response.headers" => GetJsonValueByPointer(jsonPointer, jsonHeaders), - _ => null - }; - } - - private static string GetJsonValueByPointer(JsonPointer jsonPointer, string json) - { - if (json == null) return null; - - var jsonObject = JObject.Parse(json); - JToken jsonToken; - try - { - jsonToken = jsonPointer.Evaluate(jsonObject); - } - catch - { - return null; - } - - return jsonToken.Type switch - { - JTokenType.Null => null, - JTokenType.String => jsonToken.Value(), - _ => jsonToken.ToString() - }; - } - - public static T UpdateBodyValueByPointer(T value, string pointer, Func updater) - { - if (EqualityComparer.Default.Equals(value, default) || string.IsNullOrEmpty(pointer) || updater == null) - return value; - - try - { - var json = CoreHelper.JsonSerialize(value); - var jsonObject = JObject.Parse(json); - var jsonPointer = new JsonPointer(pointer); - var jsonToken = jsonPointer.Evaluate(jsonObject); - - var oldValue = jsonToken.ToObject(); - var newValue = updater(oldValue); - if (newValue == null) return value; - - jsonToken.Replace(JToken.FromObject(newValue)); - return CoreHelper.JsonDeserialize(jsonObject.ToString()); - } - catch { return value; } - } - - public static Dictionary UpdateValueByPointer(Dictionary value, - string pointer, Func updater) - { - if (value is null || string.IsNullOrEmpty(pointer) || updater == null) return value; - try - { - var typeMap = value.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.GetType() ?? typeof(object)); - var root = JToken.Parse(JsonConvert.SerializeObject(value)); - var jsonPointer = new JsonPointer(pointer); - var tokenToUpdate = jsonPointer.Evaluate(root); - var updatedValue = updater(tokenToUpdate.ToObject()); - var newToken = updatedValue == null ? JValue.CreateNull() : JToken.FromObject(updatedValue); - - switch (tokenToUpdate.Parent) - { - case JProperty prop: prop.Value = newToken; break; - case JArray array: - array[array.IndexOf(tokenToUpdate)] = newToken; - break; - } - - return ((JObject)root).Properties().ToDictionary(prop => prop.Name, - prop => prop.Value.ToObject(typeMap.TryGetValue(prop.Name, out var t) ? t : typeof(object))); - } - catch { return value; } - } - } -} diff --git a/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs b/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs new file mode 100644 index 00000000..e1dd6b78 --- /dev/null +++ b/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Microsoft.Json.Pointer; +using Newtonsoft.Json.Linq; + +namespace APIMatic.Core.Utilities.Json +{ + internal static class JsonPointerResolver + { + public static string ResolveScopedJsonValue(string pointerString, string jsonBody, string jsonHeaders) + { + if (string.IsNullOrEmpty(pointerString) || !pointerString.Contains('#')) + return null; + + var (scope, path) = (pointerString.Split('#')[0], pointerString.Split('#')[1]); + if (string.IsNullOrEmpty(path)) + return null; + + var jsonPointer = new JsonPointer(path); + + return scope switch + { + "$response.body" => ExtractValueByPointer(jsonPointer, jsonBody), + "$response.headers" => ExtractValueByPointer(jsonPointer, jsonHeaders), + _ => null + }; + } + + private static string ExtractValueByPointer(JsonPointer jsonPointer, string json) + { + if (json == null) return null; + + var jsonObject = JObject.Parse(json); + JToken jsonToken; + try + { + jsonToken = jsonPointer.Evaluate(jsonObject); + } + catch + { + return null; + } + + return jsonToken.Type switch + { + JTokenType.Null => null, + JTokenType.String => jsonToken.Value(), + _ => jsonToken.ToString() + }; + } + } +} From 43f4810ee9c4a379a3ef3a754f28a215005daddb Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 18:05:19 +0500 Subject: [PATCH 16/24] Update RequestBuilderExtensions.cs --- APIMatic.Core/Request/RequestBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/APIMatic.Core/Request/RequestBuilderExtensions.cs b/APIMatic.Core/Request/RequestBuilderExtensions.cs index 81a767b5..ff654083 100644 --- a/APIMatic.Core/Request/RequestBuilderExtensions.cs +++ b/APIMatic.Core/Request/RequestBuilderExtensions.cs @@ -59,7 +59,7 @@ public static void UpdateFormParameterValueByPointer(List(T value, string pointer, Func updater) { - if (value == null || string.IsNullOrEmpty(pointer) || updater == null) + if (EqualityComparer.Default.Equals(value, default) || string.IsNullOrEmpty(pointer) || updater == null) return value; try From e0ae094d6786f0794a20ebb5e1f72feb829c4787 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Thu, 12 Jun 2025 19:33:12 +0500 Subject: [PATCH 17/24] incorporate refactoring by @haseeb-mhr - update the unit tests --- .../Api/Pagination/AsyncPaginator.cs | 205 +++++++++++------- .../Api/Pagination/Paginator.cs | 61 +++--- .../Pagination/AsyncPaginatorFactory.cs | 20 ++ ...{PagedResponse.cs => BasePagedResponse.cs} | 34 ++- .../MetaData/CursorPagedResponse.cs | 21 +- .../Pagination/MetaData/LinkPagedResponse.cs | 21 +- .../MetaData/NumberPagedResponse.cs | 18 +- .../MetaData/OffsetPagedResponse.cs | 19 +- .../MockTypes/Pagination/PaginatorFactory.cs | 17 +- APIMatic.Core/ApiCall.cs | 63 +----- APIMatic.Core/Pagination/AsyncPaginator.cs | 6 +- APIMatic.Core/Pagination/PaginatedResult.cs | 6 +- APIMatic.Core/Pagination/Paginator.cs | 9 +- APIMatic.Core/PaginationHandler.cs | 65 ++++++ APIMatic.Core/Response/ResponseHandler.cs | 2 + 15 files changed, 313 insertions(+), 254 deletions(-) create mode 100644 APIMatic.Core.Test/MockTypes/Pagination/AsyncPaginatorFactory.cs rename APIMatic.Core.Test/MockTypes/Pagination/{PagedResponse.cs => BasePagedResponse.cs} (58%) create mode 100644 APIMatic.Core/PaginationHandler.cs diff --git a/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs b/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs index 4e8f500c..667516c2 100644 --- a/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net; using System.Net.Http; -using System.Net.Http.Json; using System.Threading.Tasks; using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Test.MockTypes.Models.Pagination; @@ -11,7 +9,6 @@ using APIMatic.Core.Test.MockTypes.Pagination.MetaData; using APIMatic.Core.Test.Utilities; using NUnit.Framework; -using RichardSzalay.MockHttp; namespace APIMatic.Core.Test.Api.Pagination { @@ -37,19 +34,28 @@ public async Task PaginateAsync_QueryOffsetPaginationYieldsAllItems_AcrossPages( var transactionOffsets = PaginationHelper.ChunkTransactions(allTransactions, limit); handlerMock.MockOffsetPaginatedResponses(transactionOffsets, GetCompleteUrl(url)); - var asyncPageable = CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("offset", 0)) - .Query(q => q.Setup("limit", limit)))) - .PaginateAsync( - res => res.Data.Data, - OffsetPagedResponseFactory.Create, - page => page.Items, - PaginatorFactory.Create, - new OffsetPagination("$request.query#/offset") - ); + var asyncPageable = PaginationFactory.CreateHandler( + CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("offset", 0)) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + OffsetPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + AsyncPaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new OffsetPagination("$request.query#/offset") + ) + ) + .Paginate(); var collected = new List(); @@ -83,19 +89,26 @@ public async Task PaginateAsync_QueryCursorPaginationYieldsAllItems_AcrossPages( var transactionsWithCursor = PaginationHelper.ChunkTransactionsWithCursor(allTransactions, limit); handlerMock.MockCursorPaginatedResponses(transactionsWithCursor, $"{GetCompleteUrl(url)}?limit={limit}"); - var asyncPageable = CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("cursor", "cursor0")) - .Query(q => q.Setup("limit", limit)))) - .PaginateAsync( - res => res.Data.Data, - CursorPagedResponseFactory.Create, - page => page.Items, - PaginatorFactory.Create, - new CursorPagination("$response.body#/nextCursor", "$request.query#/cursor") - ); + var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("cursor", "cursor0")) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + CursorPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + AsyncPaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new CursorPagination("$response.body#/nextCursor", "$request.query#/cursor") + ) + ).Paginate(); var collected = new List(); @@ -129,19 +142,26 @@ public async Task PaginateAsync_QueryCursorPaginationInvalidPointer_AcrossPages( var transactionsWithCursor = PaginationHelper.ChunkTransactionsWithCursor(allTransactions, limit); handlerMock.MockCursorPaginatedResponses(transactionsWithCursor, $"{GetCompleteUrl(url)}?limit={limit}"); - var asyncPageable = CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("cursor", "cursor0")) - .Query(q => q.Setup("limit", limit)))) - .PaginateAsync( - res => res.Data.Data, - CursorPagedResponseFactory.Create, - page => page.Items, - PaginatorFactory.Create, - new CursorPagination("$response.body/nextCursor", "$request.query#/cursor") - ); + var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("cursor", "cursor0")) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + CursorPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + AsyncPaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new CursorPagination("$response.body/nextCursor", "$request.query#/cursor") + ) + ).Paginate(); var collected = new List(); @@ -176,19 +196,26 @@ public async Task PaginateAsync_QueryLinkPaginationYieldsAllItems_AcrossPages() var linkedPages = PaginationHelper.ChunkTransactionsWithLinks(allTransactions, limit, url); handlerMock.MockLinkedPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty)); - var asyncPageable = CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("page", "1")) - .Query(q => q.Setup("limit", limit)))) - .PaginateAsync( - res => res.Data.Data, - LinkPagedResponseFactory.Create, - page => page.Items, - PaginatorFactory.Create, - new LinkPagination("$response.body#/links/next") - ); + var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + LinkPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + AsyncPaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new LinkPagination("$response.body#/links/next") + ) + ).Paginate(); var collected = new List(); @@ -222,19 +249,26 @@ public async Task PaginateAsync_QueryPagePaginationYieldsAllItemsAcrossPages() var pages = PaginationHelper.ChunkTransactions(allTransactions, limit); handlerMock.MockPagePaginatedResponse(pages, GetCompleteUrl(url), limit); - var asyncPageable = CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("page", 0)) // Initial page - .Query(q => q.Setup("limit", limit)))) - .PaginateAsync( - res => res.Data.Data, - NumberPagedResponseFactory.Create, - page => page.Items, - PaginatorFactory.Create, - new NumberPagination("$request.query#/page") - ); + var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", 0)) // Initial page + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + NumberPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + AsyncPaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new NumberPagination("$request.query#/page") + ) + ).Paginate(); var collected = new List(); @@ -268,20 +302,27 @@ public async Task PaginateAsync_QueryMultiPaginationYieldsAllItems_AcrossPages() var linkedPages = PaginationHelper.ChunkTransactionsWithLinksAndPages(allTransactions, limit, url); handlerMock.MockMultiPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty), limit); - var asyncPageable = CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("page", "1")) - .Query(q => q.Setup("limit", limit)))) - .PaginateAsync( - res => res.Data.Data, - PagedResponseFactory.Create, - page => page.Items, - PaginatorFactory.Create, - new LinkPagination("$response.body#/links/next"), - new NumberPagination("$request.query#/page") - ); + var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + BasePagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + AsyncPaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new LinkPagination("$response.body#/links/next"), + new NumberPagination("$request.query#/page") + ) + ).Paginate(); var collected = new List(); diff --git a/APIMatic.Core.Test/Api/Pagination/Paginator.cs b/APIMatic.Core.Test/Api/Pagination/Paginator.cs index 6991cfef..98a12724 100644 --- a/APIMatic.Core.Test/Api/Pagination/Paginator.cs +++ b/APIMatic.Core.Test/Api/Pagination/Paginator.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Threading.Tasks; using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Test.MockTypes.Models.Pagination; using APIMatic.Core.Test.MockTypes.Pagination; @@ -35,19 +34,24 @@ public void Paginate_QueryOffsetPaginationYieldsAllItems_AcrossPages() var transactionOffsets = PaginationHelper.ChunkTransactions(allTransactions, limit); handlerMock.MockOffsetPaginatedResponses(transactionOffsets, GetCompleteUrl(url)); - var paginator = CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("offset", 0)) - .Query(q => q.Setup("limit", limit)))) - .Paginate( - res => res.Data.Data, - OffsetPagedResponseFactory.Create, - page => page.Items, - PaginatorFactory.Create, + var paginator = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("offset", 0)) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => OffsetPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => PaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, new OffsetPagination("$request.query#/offset") - ); + ) + ).Paginate(); var enumerator = ((IEnumerable)paginator).GetEnumerator(); var collected = new List(); @@ -60,7 +64,7 @@ public void Paginate_QueryOffsetPaginationYieldsAllItems_AcrossPages() Assert.AreEqual(allTransactions.Count, collected.Count); CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); } - + [Test] public void Paginate_QueryLinkPaginationYieldsAllItems_AcrossPages() { @@ -80,19 +84,24 @@ public void Paginate_QueryLinkPaginationYieldsAllItems_AcrossPages() var linkedPages = PaginationHelper.ChunkTransactionsWithLinks(allTransactions, limit, url); handlerMock.MockLinkedPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty)); - var paginator = CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("page", "1")) - .Query(q => q.Setup("limit", limit)))) - .Paginate( - res => res.Data.Data, - LinkPagedResponseFactory.Create, - page => page.Items, - PaginatorFactory.Create, + var paginator = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => LinkPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => PaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, new LinkPagination("$response.body#/links/next") - ); + ) + ).Paginate(); var collected = new List(); diff --git a/APIMatic.Core.Test/MockTypes/Pagination/AsyncPaginatorFactory.cs b/APIMatic.Core.Test/MockTypes/Pagination/AsyncPaginatorFactory.cs new file mode 100644 index 00000000..5ced6dc1 --- /dev/null +++ b/APIMatic.Core.Test/MockTypes/Pagination/AsyncPaginatorFactory.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Pagination; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Request; + +namespace APIMatic.Core.Test.MockTypes.Pagination +{ + internal static class AsyncPaginatorFactory + { + public static AsyncPaginator Create( + Func>> apiCallExecutor, + RequestBuilder requestBuilder, + Func> pagedResponseItemConverter, + params IPaginationStrategy[] dataManagers) + => new AsyncPaginator(apiCallExecutor, requestBuilder, pagedResponseItemConverter, dataManagers); + } +} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/BasePagedResponse.cs similarity index 58% rename from APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs rename to APIMatic.Core.Test/MockTypes/Pagination/BasePagedResponse.cs index d4abd061..01365ecd 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/PagedResponse.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/BasePagedResponse.cs @@ -6,44 +6,38 @@ namespace APIMatic.Core.Test.MockTypes.Pagination { - public class PagedResponse : ApiResponse + public class BasePagedResponse : ApiResponse { - protected PagedResponse(ApiResponse pageData, IEnumerable pageItems) : base(pageData.StatusCode, + protected BasePagedResponse(ApiResponse pageData, Func> pageToItems) : base( + pageData.StatusCode, pageData.Headers, pageData.Data) { - this.Items = pageItems; + this.Data = pageData.Data; + this.Items = pageToItems(pageData.Data); } - public IEnumerable Items { get; } - - public PaginationTypes Type { get; protected set; } - } + public TPage Data { get; } - public enum PaginationTypes - { - Offset, - Cursor, - Page, - Link + public IEnumerable Items { get; } } - internal static class PagedResponseFactory + internal static class BasePagedResponseFactory { - public static PagedResponse Create( + public static BasePagedResponse Create( ApiResponse pageData, IPaginationStrategy manager, - IEnumerable pageItems) + Func> pageToItems) { return manager switch { OffsetPagination offsetPagination => OffsetPagedResponseFactory.Create(pageData, - offsetPagination, pageItems), + offsetPagination, pageToItems), CursorPagination cursorPagination => CursorPagedResponseFactory.Create(pageData, - cursorPagination, pageItems), + cursorPagination, pageToItems), NumberPagination pagePagination => NumberPagedResponseFactory.Create(pageData, - pagePagination, pageItems), + pagePagination, pageToItems), LinkPagination linkPagination => LinkPagedResponseFactory.Create(pageData, linkPagination, - pageItems), + pageToItems), _ => throw new NotImplementedException("Unknown pagination type") }; } diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/CursorPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/CursorPagedResponse.cs index 6760ce3a..96400dd8 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/CursorPagedResponse.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/CursorPagedResponse.cs @@ -1,32 +1,27 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Test.MockTypes.Http.Response; namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData { - public class CursorPagedResponse : PagedResponse + public class CursorPagedResponse : BasePagedResponse { - public CursorPagedResponse(ApiResponse pageData, CursorPagination manager, IEnumerable pageItems) : base( - pageData, pageItems) + public CursorPagedResponse(ApiResponse pageData, CursorPagination manager, + Func> pageToItems) : base( + pageData, pageToItems) { - Type = PaginationTypes.Cursor; NextCursor = manager.CursorValue; } public string NextCursor { get; } } - + internal static class CursorPagedResponseFactory { public static CursorPagedResponse Create( ApiResponse pageData, IPaginationStrategy manager, - IEnumerable pageItems) - { - return new CursorPagedResponse( - pageData, - manager as CursorPagination, - pageItems); - } + Func> pageToItems) => new(pageData, manager as CursorPagination, pageToItems); } } diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/LinkPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/LinkPagedResponse.cs index 718feff7..697c021d 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/LinkPagedResponse.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/LinkPagedResponse.cs @@ -1,32 +1,27 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Test.MockTypes.Http.Response; namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData { - public class LinkPagedResponse : PagedResponse + public class LinkPagedResponse : BasePagedResponse { - public LinkPagedResponse(ApiResponse pageData, LinkPagination manager, IEnumerable pageItems) : base( - pageData, pageItems) + public LinkPagedResponse(ApiResponse pageData, LinkPagination manager, + Func> pageToItems) : base( + pageData, pageToItems) { - Type = PaginationTypes.Link; NextLink = manager.CurrentLinkValue; } public string NextLink { get; } } - + internal static class LinkPagedResponseFactory { public static LinkPagedResponse Create( ApiResponse pageData, IPaginationStrategy manager, - IEnumerable pageItems) - { - return new LinkPagedResponse( - pageData, - manager as LinkPagination, - pageItems); - } + Func> pageToItems) => new(pageData, manager as LinkPagination, pageToItems); } } diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs index f3c48bd4..f36abca9 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/NumberPagedResponse.cs @@ -1,15 +1,15 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Test.MockTypes.Http.Response; namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData { - public class NumberPagedResponse : PagedResponse + public class NumberPagedResponse : BasePagedResponse { - public NumberPagedResponse(ApiResponse pageData, NumberPagination manager, IEnumerable pageItems) : base( - pageData, pageItems) + public NumberPagedResponse(ApiResponse pageData, NumberPagination manager, Func> pageToItems) : base( + pageData, pageToItems) { - Type = PaginationTypes.Page; PageNumber = manager.CurrentPage; } @@ -18,10 +18,8 @@ public NumberPagedResponse(ApiResponse pageData, NumberPagination manager internal static class NumberPagedResponseFactory { - public static NumberPagedResponse Create( - ApiResponse pageData, - IPaginationStrategy manager, - IEnumerable pageItems) => - new NumberPagedResponse(pageData, manager as NumberPagination, pageItems); + public static NumberPagedResponse Create(ApiResponse pageData, + IPaginationStrategy manager, Func> pageToItems) => + new(pageData, manager as NumberPagination, pageToItems); } } diff --git a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/OffsetPagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/OffsetPagedResponse.cs index 35c2eeea..6d0eece8 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/MetaData/OffsetPagedResponse.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/MetaData/OffsetPagedResponse.cs @@ -1,16 +1,17 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Test.MockTypes.Http.Response; namespace APIMatic.Core.Test.MockTypes.Pagination.MetaData { - public class OffsetPagedResponse : PagedResponse + public class OffsetPagedResponse : BasePagedResponse { - public OffsetPagedResponse(ApiResponse pageData, OffsetPagination manager, IEnumerable pageItems) : base( - pageData, pageItems) + public OffsetPagedResponse(ApiResponse pageData, OffsetPagination manager, + Func> pageToItems) : base( + pageData, pageToItems) { - Type = PaginationTypes.Offset; Offset = manager.OffsetValue; } @@ -19,10 +20,8 @@ public OffsetPagedResponse(ApiResponse pageData, OffsetPagination manager internal static class OffsetPagedResponseFactory { - public static OffsetPagedResponse Create( - ApiResponse pageData, - IPaginationStrategy manager, - IEnumerable pageItems) => - new OffsetPagedResponse(pageData, manager as OffsetPagination, pageItems); + public static OffsetPagedResponse Create(ApiResponse pageData, + IPaginationStrategy manager, Func> pageToItems) => + new(pageData, manager as OffsetPagination, pageToItems); } } diff --git a/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs b/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs index 7bbd1949..096eab28 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs @@ -10,20 +10,11 @@ namespace APIMatic.Core.Test.MockTypes.Pagination { internal static class PaginatorFactory { - public static AsyncPaginator Create( - Func>> - apiCallExecutor, - RequestBuilder requestBuilder, - Func> pagedResponseItemConverter, - params IPaginationStrategy[] dataManagers) => new(apiCallExecutor, - requestBuilder, pagedResponseItemConverter, dataManagers); - public static Paginator Create( - Func>> - apiCallExecutor, + Func>> apiCallExecutor, RequestBuilder requestBuilder, - Func> pagedResponseItemConverter, - params IPaginationStrategy[] dataManagers) => new(apiCallExecutor, - requestBuilder, pagedResponseItemConverter, dataManagers); + Func> responseToItems, + params IPaginationStrategy[] dataManagers) + => new Paginator(apiCallExecutor, requestBuilder, responseToItems, dataManagers); } } diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index cafa62a4..7b4a8c17 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -90,6 +90,8 @@ public ApiCall requestBuilder; + /// /// Setup the response handler /// @@ -108,73 +110,22 @@ public ApiCall /// public async Task ExecuteAsync(CancellationToken cancellationToken = default) - { - requestBuilder.AcceptHeader = responseHandler.AcceptHeader; - CoreRequest request = await requestBuilder.Build().ConfigureAwait(false); - globalConfiguration.ApiCallback?.OnBeforeHttpRequestEventHandler(request); - _sdkLogger.LogRequest(request); - CoreResponse response = await globalConfiguration.HttpClient.ExecuteAsync(request, cancellationToken).ConfigureAwait(false); - globalConfiguration.ApiCallback?.OnAfterHttpResponseEventHandler(response); - _sdkLogger.LogResponse(response); - var context = new CoreContext(request, response); - return responseHandler.Result(context, returnTypeCreator); - } + => (await ExecuteRequestAsync(cancellationToken)).result; - private async Task> ExecutePaginationAsync( - IPaginationStrategy manager, - Func> converter, - Func, TPageMetadata> pageResponseConverter, + public async Task<(ReturnType result, CoreResponse response)> ExecuteRequestAsync( CancellationToken cancellationToken = default) { requestBuilder.AcceptHeader = responseHandler.AcceptHeader; CoreRequest request = await requestBuilder.Build().ConfigureAwait(false); globalConfiguration.ApiCallback?.OnBeforeHttpRequestEventHandler(request); _sdkLogger.LogRequest(request); - var response = await globalConfiguration.HttpClient.ExecuteAsync(request, cancellationToken) + CoreResponse response = await globalConfiguration.HttpClient.ExecuteAsync(request, cancellationToken) .ConfigureAwait(false); globalConfiguration.ApiCallback?.OnAfterHttpResponseEventHandler(response); _sdkLogger.LogResponse(response); var context = new CoreContext(request, response); - var page = responseHandler.Result(context, returnTypeCreator); - var pageItems = converter(page); - var pageMeta = pageResponseConverter(page, manager, pageItems); - return new PaginatedResult(response, pageItems, pageMeta); - } - - public TEnumerable Paginate( - Func> converter, - Func, TPageMetadata> pageResponseConverter, - Func> pagedResponseItemConverter, - Func< - Func>>, - RequestBuilder, Func>, IPaginationStrategy[], - TEnumerable> returnTypeGetter, - params IPaginationStrategy[] dataManagers) - { - return returnTypeGetter( - (reqBuilder, manager) => RequestBuilder(reqBuilder) - .ExecutePaginationAsync(manager, converter, pageResponseConverter), - requestBuilder, - pagedResponseItemConverter, - dataManagers); - } - - public TEnumerable PaginateAsync( - Func> converter, - Func, TPageMetadata> pageResponseConverter, - Func> pagedResponseItemConverter, - Func< - Func>>, - RequestBuilder, Func>, IPaginationStrategy[], - TEnumerable> returnTypeGetter, - params IPaginationStrategy[] dataManagers) - { - return returnTypeGetter(async (reqBuilder, manager, cancellationToken) => - await RequestBuilder(reqBuilder).ExecutePaginationAsync(manager, converter, pageResponseConverter, - cancellationToken).ConfigureAwait(false), - requestBuilder, - pagedResponseItemConverter, - dataManagers); + var result = responseHandler.Result(context, returnTypeCreator); + return (result, response); } } } diff --git a/APIMatic.Core/Pagination/AsyncPaginator.cs b/APIMatic.Core/Pagination/AsyncPaginator.cs index 80d21b12..ad886291 100644 --- a/APIMatic.Core/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core/Pagination/AsyncPaginator.cs @@ -10,13 +10,13 @@ namespace APIMatic.Core.Pagination { public class AsyncPaginator : IAsyncEnumerable { - private readonly Func>> _apiCallExecutor; + private readonly Func>> _apiCallExecutor; private readonly RequestBuilder _requestBuilder; private readonly IPaginationStrategy[] _paginationStrategies; private readonly Func> _pagedResponseItemConverter; public AsyncPaginator( - Func>> apiCallExecutor, + Func>> apiCallExecutor, RequestBuilder requestBuilder, Func> pagedResponseItemConverter, IPaginationStrategy[] paginationStrategies) @@ -56,7 +56,7 @@ public async IAsyncEnumerable GetPagesAsync(CancellationToken can var result = await _apiCallExecutor(requestBuilder, paginationStrategy, cancellationToken) .ConfigureAwait(false); - var items = result?.Items; + var items = _pagedResponseItemConverter(result.PageMetadata); if (items == null || !items.Any()) yield break; diff --git a/APIMatic.Core/Pagination/PaginatedResult.cs b/APIMatic.Core/Pagination/PaginatedResult.cs index 4bd1dc7d..b38c5450 100644 --- a/APIMatic.Core/Pagination/PaginatedResult.cs +++ b/APIMatic.Core/Pagination/PaginatedResult.cs @@ -3,16 +3,14 @@ namespace APIMatic.Core.Pagination { - public class PaginatedResult + public class PaginatedResult { public CoreResponse Response { get; } - public IEnumerable Items { get; } public TPage PageMetadata { get; } - public PaginatedResult(CoreResponse response, IEnumerable items, TPage pageMetadata) + public PaginatedResult(CoreResponse response, TPage pageMetadata) { Response = response; - Items = items; PageMetadata = pageMetadata; } } diff --git a/APIMatic.Core/Pagination/Paginator.cs b/APIMatic.Core/Pagination/Paginator.cs index 1cc402a9..d884dec0 100644 --- a/APIMatic.Core/Pagination/Paginator.cs +++ b/APIMatic.Core/Pagination/Paginator.cs @@ -6,18 +6,19 @@ using System.Linq; using System.Threading.Tasks; using APIMatic.Core.Pagination.Strategies; +using System.Threading; namespace APIMatic.Core.Pagination { public class Paginator : IEnumerable { - private readonly Func>> _apiCallExecutor; + private readonly Func>> _apiCallExecutor; private readonly RequestBuilder _requestBuilder; private readonly IPaginationStrategy[] _paginationStrategies; private readonly Func> _pagedResponseItemConverter; public Paginator( - Func>> apiCallExecutor, RequestBuilder requestBuilder, + Func>> apiCallExecutor, RequestBuilder requestBuilder, Func> pagedResponseItemConverter, IPaginationStrategy[] paginationStrategies) { @@ -51,9 +52,9 @@ public IEnumerable GetPages() if (requestBuilder == null) break; - var result = CoreHelper.RunTask(_apiCallExecutor(requestBuilder, paginationStrategy)); + var result = CoreHelper.RunTask(_apiCallExecutor(requestBuilder, paginationStrategy, default)); - var items = result?.Items; + var items = _pagedResponseItemConverter(result.PageMetadata); if (items == null || !items.Any()) yield break; diff --git a/APIMatic.Core/PaginationHandler.cs b/APIMatic.Core/PaginationHandler.cs new file mode 100644 index 00000000..9973cb19 --- /dev/null +++ b/APIMatic.Core/PaginationHandler.cs @@ -0,0 +1,65 @@ +using APIMatic.Core.Types.Sdk; +using System; +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Pagination; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Request; +namespace APIMatic.Core +{ + public class PaginationHandler + { + private readonly Func> + _requestExecutor; + + private readonly RequestBuilder _requestBuilder; + private readonly Func _createPageResponse; + + private readonly Func>>, + RequestBuilder, TPageableType> _createPageable; + + public PaginationHandler( + Func> requestExecutor, + RequestBuilder requestBuilder, + Func createPageResponse, + Func>>, + RequestBuilder, TPageableType> createPageable) + { + _requestExecutor = requestExecutor; + _requestBuilder = requestBuilder; + _createPageResponse = createPageResponse; + _createPageable = createPageable; + } + + public TPageableType Paginate() + { + return _createPageable(async (reqBuilder, strategy, cancellationToken) => + { + var (result, response) = + await _requestExecutor(reqBuilder, cancellationToken).ConfigureAwait(false); + var pageMeta = _createPageResponse(result, strategy); + return new PaginatedResult(response, pageMeta); + }, + _requestBuilder); + } + } + + public static class PaginationFactory + { + public static PaginationHandler CreateHandler( + ApiCall apiCall, + Func createPageResponse, + Func>>, + RequestBuilder, TPageableType> createPageable) + where Request : CoreRequest + where Response : CoreResponse + where Context : CoreContext + where ApiException : CoreApiException + { + return new PaginationHandler((builder, token) => + apiCall.RequestBuilder(builder).ExecuteRequestAsync(token), + apiCall.GetRequestBuilder(), createPageResponse, createPageable); + } + } +} diff --git a/APIMatic.Core/Response/ResponseHandler.cs b/APIMatic.Core/Response/ResponseHandler.cs index b27881b5..130bc4c7 100644 --- a/APIMatic.Core/Response/ResponseHandler.cs +++ b/APIMatic.Core/Response/ResponseHandler.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using APIMatic.Core.Http.Configuration; +using APIMatic.Core.Pagination; +using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Types; using APIMatic.Core.Types.Sdk; using APIMatic.Core.Utilities; From 5940db5b4d7625e6b0fbf98387ea191ee67f612d Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Fri, 13 Jun 2025 14:40:23 +0500 Subject: [PATCH 18/24] add support and unit test for narrow down pagination strategy - refactor code --- .../Api/Pagination/Paginator.cs | 119 ---------- .../Pagination/AsyncPaginatorFactory.cs | 2 +- .../{Api => }/Pagination/AsyncPaginator.cs | 210 ++++++++++-------- APIMatic.Core.Test/Pagination/Paginator.cs | 191 ++++++++++++++++ .../Utilities/PaginationHelper.cs | 36 +-- APIMatic.Core/APIMatic.Core.csproj | 2 +- APIMatic.Core/ApiCall.cs | 2 - APIMatic.Core/Pagination/AsyncPaginator.cs | 97 ++++++-- APIMatic.Core/Pagination/PaginatedResult.cs | 1 - APIMatic.Core/Pagination/Paginator.cs | 93 ++++++-- .../Pagination/Strategies/CursorPagination.cs | 1 - 11 files changed, 487 insertions(+), 267 deletions(-) delete mode 100644 APIMatic.Core.Test/Api/Pagination/Paginator.cs rename APIMatic.Core.Test/{Api => }/Pagination/AsyncPaginator.cs (59%) create mode 100644 APIMatic.Core.Test/Pagination/Paginator.cs diff --git a/APIMatic.Core.Test/Api/Pagination/Paginator.cs b/APIMatic.Core.Test/Api/Pagination/Paginator.cs deleted file mode 100644 index 98a12724..00000000 --- a/APIMatic.Core.Test/Api/Pagination/Paginator.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using APIMatic.Core.Pagination.Strategies; -using APIMatic.Core.Test.MockTypes.Models.Pagination; -using APIMatic.Core.Test.MockTypes.Pagination; -using APIMatic.Core.Test.MockTypes.Pagination.MetaData; -using APIMatic.Core.Test.Utilities; -using NUnit.Framework; - -namespace APIMatic.Core.Test.Api.Pagination -{ - [TestFixture] - public class Paginator : ApiCallTest - { - [Test] - public void Paginate_QueryOffsetPaginationYieldsAllItems_AcrossPages() - { - // Arrange - const int limit = 2; - const string url = "/transactions/offset"; - - var allTransactions = new List - { - new("id1", 100, DateTime.UtcNow), - new("id2", 200, DateTime.UtcNow), - new("id3", 300, DateTime.UtcNow), - new("id4", 400, DateTime.UtcNow), - new("id5", 500, DateTime.UtcNow) - }; - - var transactionOffsets = PaginationHelper.ChunkTransactions(allTransactions, limit); - handlerMock.MockOffsetPaginatedResponses(transactionOffsets, GetCompleteUrl(url)); - - var paginator = PaginationFactory.CreateHandler(CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("offset", 0)) - .Query(q => q.Setup("limit", limit)))), - (page, strategy) => OffsetPagedResponseFactory.Create( - page, - strategy, - tPage => tPage.Data - ), - (func, builder) => PaginatorFactory.Create( - func, - builder, - pagedResponse => pagedResponse.Items, - new OffsetPagination("$request.query#/offset") - ) - ).Paginate(); - - var enumerator = ((IEnumerable)paginator).GetEnumerator(); - var collected = new List(); - while (enumerator.MoveNext()) - { - collected.Add((Transaction)enumerator.Current); - } - - // Assert - Assert.AreEqual(allTransactions.Count, collected.Count); - CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); - } - - [Test] - public void Paginate_QueryLinkPaginationYieldsAllItems_AcrossPages() - { - // Arrange - const int limit = 2; - const string url = "/transactions"; - - var allTransactions = new List - { - new("id1", 100, DateTime.UtcNow), - new("id2", 200, DateTime.UtcNow), - new("id3", 300, DateTime.UtcNow), - new("id4", 400, DateTime.UtcNow), - new("id5", 500, DateTime.UtcNow) - }; - - var linkedPages = PaginationHelper.ChunkTransactionsWithLinks(allTransactions, limit, url); - handlerMock.MockLinkedPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty)); - - var paginator = PaginationFactory.CreateHandler(CreateApiCall() - .RequestBuilder(rb => rb - .Setup(HttpMethod.Get, url) - .Parameters(p => p - .Query(q => q.Setup("page", "1")) - .Query(q => q.Setup("limit", limit)))), - (page, strategy) => LinkPagedResponseFactory.Create( - page, - strategy, - tPage => tPage.Data - ), - (func, builder) => PaginatorFactory.Create( - func, - builder, - pagedResponse => pagedResponse.Items, - new LinkPagination("$response.body#/links/next") - ) - ).Paginate(); - - var collected = new List(); - - // Act - foreach (var item in paginator) - { - collected.Add(item); - } - - // Assert - Assert.AreEqual(allTransactions.Count, collected.Count); - CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); - } - } -} diff --git a/APIMatic.Core.Test/MockTypes/Pagination/AsyncPaginatorFactory.cs b/APIMatic.Core.Test/MockTypes/Pagination/AsyncPaginatorFactory.cs index 5ced6dc1..d0bb04ba 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/AsyncPaginatorFactory.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/AsyncPaginatorFactory.cs @@ -15,6 +15,6 @@ public static AsyncPaginator Create( RequestBuilder requestBuilder, Func> pagedResponseItemConverter, params IPaginationStrategy[] dataManagers) - => new AsyncPaginator(apiCallExecutor, requestBuilder, pagedResponseItemConverter, dataManagers); + => new(apiCallExecutor, requestBuilder, pagedResponseItemConverter, dataManagers); } } diff --git a/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs b/APIMatic.Core.Test/Pagination/AsyncPaginator.cs similarity index 59% rename from APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs rename to APIMatic.Core.Test/Pagination/AsyncPaginator.cs index 667516c2..feaf84cf 100644 --- a/APIMatic.Core.Test/Api/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core.Test/Pagination/AsyncPaginator.cs @@ -1,16 +1,16 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.Api; using APIMatic.Core.Test.MockTypes.Models.Pagination; using APIMatic.Core.Test.MockTypes.Pagination; using APIMatic.Core.Test.MockTypes.Pagination.MetaData; using APIMatic.Core.Test.Utilities; using NUnit.Framework; -namespace APIMatic.Core.Test.Api.Pagination +namespace APIMatic.Core.Test.Pagination { [TestFixture] public class AsyncPaginator : ApiCallTest @@ -22,19 +22,10 @@ public async Task PaginateAsync_QueryOffsetPaginationYieldsAllItems_AcrossPages( const int limit = 2; const string url = "/transactions/offset"; - var allTransactions = new List - { - new("id1", 100, DateTime.UtcNow), - new("id2", 200, DateTime.UtcNow), - new("id3", 300, DateTime.UtcNow), - new("id4", 400, DateTime.UtcNow), - new("id5", 500, DateTime.UtcNow) - }; - - var transactionOffsets = PaginationHelper.ChunkTransactions(allTransactions, limit); + var transactionOffsets = PaginationHelper.ChunkTransactions(PaginationHelper.AllTransactions, limit); handlerMock.MockOffsetPaginatedResponses(transactionOffsets, GetCompleteUrl(url)); - var asyncPageable = PaginationFactory.CreateHandler( + var asyncPaginator = PaginationFactory.CreateHandler( CreateApiCall() .RequestBuilder(rb => rb .Setup(HttpMethod.Get, url) @@ -60,14 +51,59 @@ public async Task PaginateAsync_QueryOffsetPaginationYieldsAllItems_AcrossPages( var collected = new List(); // Act - await foreach (var item in asyncPageable) + await foreach (var item in asyncPaginator) { collected.Add(item); } // Assert - Assert.AreEqual(allTransactions.Count, collected.Count); - CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + Assert.AreEqual(PaginationHelper.AllTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(PaginationHelper.AllTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + + [Test] + public async Task PaginateAsync_QueryOffsetPagination_YieldsNoItems() + { + // Arrange + const int limit = 2; + const string url = "/transactions/offset/noitems"; + + var emptyTransactionOffsets = new List { new() }; + handlerMock.MockOffsetPaginatedResponses(emptyTransactionOffsets, GetCompleteUrl(url)); + + var asyncPaginator = PaginationFactory.CreateHandler( + CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("offset", 0)) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + OffsetPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + AsyncPaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new OffsetPagination("$request.query#/offset") + ) + ) + .Paginate(); + + var collected = new List(); + + // Act + await foreach (var item in asyncPaginator) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(0, collected.Count); } [Test] @@ -77,19 +113,10 @@ public async Task PaginateAsync_QueryCursorPaginationYieldsAllItems_AcrossPages( const int limit = 2; const string url = "/transactions"; - var allTransactions = new List - { - new("id1", 100, DateTime.UtcNow), - new("id2", 200, DateTime.UtcNow), - new("id3", 300, DateTime.UtcNow), - new("id4", 400, DateTime.UtcNow), - new("id5", 500, DateTime.UtcNow) - }; - - var transactionsWithCursor = PaginationHelper.ChunkTransactionsWithCursor(allTransactions, limit); + var transactionsWithCursor = PaginationHelper.ChunkTransactionsWithCursor(PaginationHelper.AllTransactions, limit); handlerMock.MockCursorPaginatedResponses(transactionsWithCursor, $"{GetCompleteUrl(url)}?limit={limit}"); - var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + var asyncPaginator = PaginationFactory.CreateHandler(CreateApiCall() .RequestBuilder(rb => rb .Setup(HttpMethod.Get, url) .Parameters(p => p @@ -113,14 +140,14 @@ public async Task PaginateAsync_QueryCursorPaginationYieldsAllItems_AcrossPages( var collected = new List(); // Act - await foreach (var item in asyncPageable) + await foreach (var item in asyncPaginator) { collected.Add(item); } // Assert - Assert.AreEqual(allTransactions.Count, collected.Count); - CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + Assert.AreEqual(PaginationHelper.AllTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(PaginationHelper.AllTransactions.Select(t => t.Id), collected.Select(t => t.Id)); } [Test] @@ -130,19 +157,10 @@ public async Task PaginateAsync_QueryCursorPaginationInvalidPointer_AcrossPages( const int limit = 2; const string url = "/transactions"; - var allTransactions = new List - { - new("id1", 100, DateTime.UtcNow), - new("id2", 200, DateTime.UtcNow), - new("id3", 300, DateTime.UtcNow), - new("id4", 400, DateTime.UtcNow), - new("id5", 500, DateTime.UtcNow) - }; - - var transactionsWithCursor = PaginationHelper.ChunkTransactionsWithCursor(allTransactions, limit); + var transactionsWithCursor = PaginationHelper.ChunkTransactionsWithCursor(PaginationHelper.AllTransactions, limit); handlerMock.MockCursorPaginatedResponses(transactionsWithCursor, $"{GetCompleteUrl(url)}?limit={limit}"); - var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + var asyncPaginator = PaginationFactory.CreateHandler(CreateApiCall() .RequestBuilder(rb => rb .Setup(HttpMethod.Get, url) .Parameters(p => p @@ -166,14 +184,14 @@ public async Task PaginateAsync_QueryCursorPaginationInvalidPointer_AcrossPages( var collected = new List(); // Act - await foreach (var item in asyncPageable) + await foreach (var item in asyncPaginator) { collected.Add(item); } // Assert Assert.AreEqual(limit, collected.Count); - var expected = allTransactions.Take(limit).ToList(); + var expected = PaginationHelper.AllTransactions.Take(limit).ToList(); CollectionAssert.AreEquivalent(expected.Select(t => t.Id), collected.Select(t => t.Id)); } @@ -184,19 +202,10 @@ public async Task PaginateAsync_QueryLinkPaginationYieldsAllItems_AcrossPages() const int limit = 2; const string url = "/transactions"; - var allTransactions = new List - { - new("id1", 100, DateTime.UtcNow), - new("id2", 200, DateTime.UtcNow), - new("id3", 300, DateTime.UtcNow), - new("id4", 400, DateTime.UtcNow), - new("id5", 500, DateTime.UtcNow) - }; - - var linkedPages = PaginationHelper.ChunkTransactionsWithLinks(allTransactions, limit, url); + var linkedPages = PaginationHelper.ChunkTransactionsWithLinks(PaginationHelper.AllTransactions, limit, url); handlerMock.MockLinkedPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty)); - var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + var asyncPaginator = PaginationFactory.CreateHandler(CreateApiCall() .RequestBuilder(rb => rb .Setup(HttpMethod.Get, url) .Parameters(p => p @@ -220,14 +229,14 @@ public async Task PaginateAsync_QueryLinkPaginationYieldsAllItems_AcrossPages() var collected = new List(); // Act - await foreach (var item in asyncPageable) + await foreach (var item in asyncPaginator) { collected.Add(item); } // Assert - Assert.AreEqual(allTransactions.Count, collected.Count); - CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + Assert.AreEqual(PaginationHelper.AllTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(PaginationHelper.AllTransactions.Select(t => t.Id), collected.Select(t => t.Id)); } [Test] @@ -237,19 +246,10 @@ public async Task PaginateAsync_QueryPagePaginationYieldsAllItemsAcrossPages() const int limit = 2; const string url = "/transactions/page"; - var allTransactions = new List - { - new("id1", 100, DateTime.UtcNow), - new("id2", 200, DateTime.UtcNow), - new("id3", 300, DateTime.UtcNow), - new("id4", 400, DateTime.UtcNow), - new("id5", 500, DateTime.UtcNow) - }; - - var pages = PaginationHelper.ChunkTransactions(allTransactions, limit); + var pages = PaginationHelper.ChunkTransactions(PaginationHelper.AllTransactions, limit); handlerMock.MockPagePaginatedResponse(pages, GetCompleteUrl(url), limit); - var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + var asyncPaginator = PaginationFactory.CreateHandler(CreateApiCall() .RequestBuilder(rb => rb .Setup(HttpMethod.Get, url) .Parameters(p => p @@ -273,14 +273,14 @@ public async Task PaginateAsync_QueryPagePaginationYieldsAllItemsAcrossPages() var collected = new List(); // Act - await foreach (var tx in asyncPageable) + await foreach (var tx in asyncPaginator) { collected.Add(tx); } // Assert - Assert.AreEqual(allTransactions.Count, collected.Count); - CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + Assert.AreEqual(PaginationHelper.AllTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(PaginationHelper.AllTransactions.Select(t => t.Id), collected.Select(t => t.Id)); } [Test] @@ -288,21 +288,12 @@ public async Task PaginateAsync_QueryMultiPaginationYieldsAllItems_AcrossPages() { // Arrange const int limit = 2; - const string url = "/transactions"; + const string url = "/multi/transactions/multi/pagination/strategy"; - var allTransactions = new List - { - new("id1", 100, DateTime.UtcNow), - new("id2", 200, DateTime.UtcNow), - new("id3", 300, DateTime.UtcNow), - new("id4", 400, DateTime.UtcNow), - new("id5", 500, DateTime.UtcNow) - }; - - var linkedPages = PaginationHelper.ChunkTransactionsWithLinksAndPages(allTransactions, limit, url); - handlerMock.MockMultiPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty), limit); + var linkedPages = PaginationHelper.ChunkTransactionsWithLinksAndPages(PaginationHelper.AllTransactions, limit, url); + handlerMock.MockMultiPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty), url, limit); - var asyncPageable = PaginationFactory.CreateHandler(CreateApiCall() + var asyncPaginator = PaginationFactory.CreateHandler(CreateApiCall() .RequestBuilder(rb => rb .Setup(HttpMethod.Get, url) .Parameters(p => p @@ -327,14 +318,59 @@ public async Task PaginateAsync_QueryMultiPaginationYieldsAllItems_AcrossPages() var collected = new List(); // Act - await foreach (var item in asyncPageable) + await foreach (var item in asyncPaginator) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(PaginationHelper.AllTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(PaginationHelper.AllTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + + [Test] + public async Task PaginateAsync_QuerySinglePagination_YieldsFirstPageItemsOnly() + { + // Arrange + const int limit = 2; + const string url = "/multi/transactions/single/pagination/strategy"; + + var linkedPages = PaginationHelper.ChunkTransactionsWithLinksAndPages(PaginationHelper.AllTransactions, limit, url); + handlerMock.MockMultiPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty), url, limit); + + var asyncPaginator = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + BasePagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + AsyncPaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new LinkPagination("$response.body#/links/next") + ) + ).Paginate(); + + var collected = new List(); + + // Act + await foreach (var item in asyncPaginator) { collected.Add(item); } // Assert - Assert.AreEqual(allTransactions.Count, collected.Count); - CollectionAssert.AreEquivalent(allTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + var firstPageItems = PaginationHelper.AllTransactions.Take(2).ToList(); + Assert.AreEqual(firstPageItems.Count, collected.Count); + CollectionAssert.AreEquivalent(firstPageItems.Select(t => t.Id), collected.Select(t => t.Id)); } } } diff --git a/APIMatic.Core.Test/Pagination/Paginator.cs b/APIMatic.Core.Test/Pagination/Paginator.cs new file mode 100644 index 00000000..4ffd7731 --- /dev/null +++ b/APIMatic.Core.Test/Pagination/Paginator.cs @@ -0,0 +1,191 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Test.Api; +using APIMatic.Core.Test.MockTypes.Models.Pagination; +using APIMatic.Core.Test.MockTypes.Pagination; +using APIMatic.Core.Test.MockTypes.Pagination.MetaData; +using APIMatic.Core.Test.Utilities; +using NUnit.Framework; + +namespace APIMatic.Core.Test.Pagination +{ + [TestFixture] + public class Paginator : ApiCallTest + { + [Test] + public void Paginate_QueryOffsetPaginationYieldsAllItems_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions/offset"; + + var transactionOffsets = PaginationHelper.ChunkTransactions(PaginationHelper.AllTransactions, limit); + handlerMock.MockOffsetPaginatedResponses(transactionOffsets, GetCompleteUrl(url)); + + var paginator = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("offset", 0)) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => OffsetPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => PaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new OffsetPagination("$request.query#/offset") + ) + ).Paginate(); + + var enumerator = ((IEnumerable)paginator).GetEnumerator(); + var collected = new List(); + while (enumerator.MoveNext()) + { + collected.Add((Transaction)enumerator.Current); + } + + // Assert + Assert.AreEqual(PaginationHelper.AllTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(PaginationHelper.AllTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + + [Test] + public void Paginate_QueryOffsetPagination_YieldsNoItems() + { + // Arrange + const int limit = 2; + const string url = "/transactions/offset/noitems"; + + var emptyTransactionOffsets = new List { new() }; + handlerMock.MockOffsetPaginatedResponses(emptyTransactionOffsets, GetCompleteUrl(url)); + + var paginator = PaginationFactory.CreateHandler( + CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("offset", 0)) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + OffsetPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + PaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new OffsetPagination("$request.query#/offset") + ) + ) + .Paginate(); + + var collected = new List(); + + // Act + foreach (var item in paginator) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(0, collected.Count); + } + + [Test] + public void Paginate_QueryLinkPaginationYieldsAllItems_AcrossPages() + { + // Arrange + const int limit = 2; + const string url = "/transactions"; + + var linkedPages = PaginationHelper.ChunkTransactionsWithLinks(PaginationHelper.AllTransactions, limit, url); + handlerMock.MockLinkedPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty)); + + var paginator = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => LinkPagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => PaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new LinkPagination("$response.body#/links/next") + ) + ).Paginate(); + + var collected = new List(); + + // Act + foreach (var item in paginator) + { + collected.Add(item); + } + + // Assert + Assert.AreEqual(PaginationHelper.AllTransactions.Count, collected.Count); + CollectionAssert.AreEquivalent(PaginationHelper.AllTransactions.Select(t => t.Id), collected.Select(t => t.Id)); + } + + [Test] + public void Paginate_QuerySinglePagination_YieldsFirstPageItemsOnly() + { + // Arrange + const int limit = 2; + const string url = "/multi/transactions/single/pagination/strategy"; + + var linkedPages = PaginationHelper.ChunkTransactionsWithLinksAndPages(PaginationHelper.AllTransactions, limit, url); + handlerMock.MockMultiPaginatedResponses(linkedPages, GetCompleteUrl(string.Empty), url, limit); + + var paginator = PaginationFactory.CreateHandler(CreateApiCall() + .RequestBuilder(rb => rb + .Setup(HttpMethod.Get, url) + .Parameters(p => p + .Query(q => q.Setup("page", "1")) + .Query(q => q.Setup("limit", limit)))), + (page, strategy) => + BasePagedResponseFactory.Create( + page, + strategy, + tPage => tPage.Data + ), + (func, builder) => + PaginatorFactory.Create( + func, + builder, + pagedResponse => pagedResponse.Items, + new LinkPagination("$response.body#/links/next") + ) + ).Paginate(); + + var collected = new List(); + + // Act + foreach (var item in paginator) + { + collected.Add(item); + } + + // Assert + var firstPageItems = PaginationHelper.AllTransactions.Take(2).ToList(); + Assert.AreEqual(firstPageItems.Count, collected.Count); + CollectionAssert.AreEquivalent(firstPageItems.Select(t => t.Id), collected.Select(t => t.Id)); + } + } +} diff --git a/APIMatic.Core.Test/Utilities/PaginationHelper.cs b/APIMatic.Core.Test/Utilities/PaginationHelper.cs index 0b50b127..e29316b1 100644 --- a/APIMatic.Core.Test/Utilities/PaginationHelper.cs +++ b/APIMatic.Core.Test/Utilities/PaginationHelper.cs @@ -1,9 +1,9 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http.Json; using APIMatic.Core.Test.MockTypes.Models.Pagination; -using APIMatic.Core.Utilities; using NUnit.Framework; using RichardSzalay.MockHttp; @@ -11,7 +11,16 @@ namespace APIMatic.Core.Test.Utilities { internal static class PaginationHelper { - public static List ChunkTransactions(List transactions, int limit) + internal static readonly IReadOnlyCollection AllTransactions = new List + { + new("id1", 100, DateTime.UtcNow), + new("id2", 200, DateTime.UtcNow), + new("id3", 300, DateTime.UtcNow), + new("id4", 400, DateTime.UtcNow), + new("id5", 500, DateTime.UtcNow) + }; + + public static List ChunkTransactions(IReadOnlyCollection transactions, int limit) { var chunks = transactions .Select((tx, index) => new { tx, index }) @@ -62,7 +71,7 @@ public static void MockPagePaginatedResponse(this MockHttpMessageHandler handler } } - public static List ChunkTransactionsWithCursor(List transactions, int limit) + public static List ChunkTransactionsWithCursor(IReadOnlyCollection transactions, int limit) { var chunks = transactions .Select((tx, index) => new { tx, index }) @@ -97,7 +106,7 @@ public static void MockCursorPaginatedResponses(this MockHttpMessageHandler hand } } - public static List ChunkTransactionsWithLinks(List transactions, int limit, + public static List ChunkTransactionsWithLinks(IReadOnlyCollection transactions, int limit, string urlPath) { var chunks = transactions @@ -138,7 +147,7 @@ public static void MockLinkedPaginatedResponses(this MockHttpMessageHandler hand } } - public static List ChunkTransactionsWithLinksAndPages(List transactions, + public static List ChunkTransactionsWithLinksAndPages(IReadOnlyCollection transactions, int limit, string urlPath) { var chunks = transactions @@ -146,12 +155,7 @@ public static List ChunkTransactionsWithLinksAndPages(List x.index / limit) .Select((g, i) => { - var pageNumber = i + 1; - var nextLink = (i + 1) * limit < transactions.Count - ? $"{urlPath}?page={pageNumber + 1}&limit={limit}" - : null; - - var links = i >= 1 ? new Links(next: null) : new Links(next: nextLink); + var links = new Links(next: null); return new TransactionsLinked(g.Select(x => x.tx).ToList(), links); }) .ToList(); @@ -162,10 +166,10 @@ public static List ChunkTransactionsWithLinksAndPages(List pages, string baseUrl, int limit) + List pages, string baseUrl, string basePathUrl, int limit) { - var currentLink = "/transactions?page=1&limit=2"; - var linkPaginationPages = pages.Take(2).ToList(); + var currentLink = $"{basePathUrl}?page=1&limit={limit}"; + var linkPaginationPages = pages.Take(1).ToList(); for (int i = 0; i < linkPaginationPages.Count; i++) { @@ -182,10 +186,10 @@ public static void MockMultiPaginatedResponses(this MockHttpMessageHandler handl currentLink = page.Links?.Next; } - for (int i = 2; i < pages.Count; i++) + for (int i = 1; i < pages.Count; i++) { var content = JsonContent.Create(pages[i]); - var requestUrl = $"{baseUrl}/transactions?page={i + 1}&limit={limit}"; + var requestUrl = $"{baseUrl}{basePathUrl}?page={i + 1}&limit={limit}"; handlerMock.When(requestUrl) .With(req => diff --git a/APIMatic.Core/APIMatic.Core.csproj b/APIMatic.Core/APIMatic.Core.csproj index 77f8288b..396b5631 100644 --- a/APIMatic.Core/APIMatic.Core.csproj +++ b/APIMatic.Core/APIMatic.Core.csproj @@ -30,7 +30,7 @@ - + diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index 7b4a8c17..10c541f8 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -7,8 +7,6 @@ using System.Threading; using System.Threading.Tasks; using APIMatic.Core.Http.Configuration; -using APIMatic.Core.Pagination; -using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Request; using APIMatic.Core.Response; using APIMatic.Core.Types.Sdk; diff --git a/APIMatic.Core/Pagination/AsyncPaginator.cs b/APIMatic.Core/Pagination/AsyncPaginator.cs index ad886291..9bd856fe 100644 --- a/APIMatic.Core/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core/Pagination/AsyncPaginator.cs @@ -2,21 +2,35 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using APIMatic.Core.Pagination.Strategies; namespace APIMatic.Core.Pagination { + /// + /// Provides asynchronous iteration over a paginated data source by fetching items across multiple pages. + /// + /// + /// The type of individual items being enumerated. + /// + /// + /// The type of metadata associated with each page of results (e.g., pagination tokens, total counts). + /// public class AsyncPaginator : IAsyncEnumerable { - private readonly Func>> _apiCallExecutor; + private readonly + Func>> + _apiCallExecutor; + private readonly RequestBuilder _requestBuilder; private readonly IPaginationStrategy[] _paginationStrategies; private readonly Func> _pagedResponseItemConverter; public AsyncPaginator( - Func>> apiCallExecutor, + Func>> + apiCallExecutor, RequestBuilder requestBuilder, Func> pagedResponseItemConverter, IPaginationStrategy[] paginationStrategies) @@ -27,6 +41,19 @@ public AsyncPaginator( _paginationStrategies = paginationStrategies; } + /// + /// Returns an asynchronous enumerator that iterates through all items across multiple paged responses. + /// + /// + /// A cancellation token that can be used to cancel the asynchronous iteration. + /// + /// + /// An asynchronous enumerator that yields items of type from all retrieved pages. + /// + /// + /// This method uses to retrieve pages and applies a converter function to extract items from each page. + /// Supports cancellation via the provided . + /// public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default) { await foreach (var pageMetaData in GetPagesAsync(cancellationToken)) @@ -38,33 +65,65 @@ public async IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancel } } - public async IAsyncEnumerable GetPagesAsync(CancellationToken cancellationToken = default) + /// + /// Asynchronously retrieves a sequence of page metadata objects, one page at a time. + /// + /// + /// A cancellation token that can be used to cancel the asynchronous operation. + /// + /// + /// An asynchronous stream of representing metadata for each page. + /// + /// + /// Use await foreach to iterate over the returned pages asynchronously. + /// + public async IAsyncEnumerable GetPagesAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); + var paginationContext = PaginationContext.CreateDefault(_requestBuilder); + var initialStrategy = _paginationStrategies.First(); + var requestBuilder = initialStrategy.Apply(paginationContext); - foreach (var paginationStrategy in _paginationStrategies) - { - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); + var result = await ExecuteAndYieldAsync(initialStrategy, requestBuilder, cancellationToken) + .ConfigureAwait(false); + if (result == null) yield break; - var requestBuilder = paginationStrategy.Apply(paginationContext); - if (requestBuilder == null) - break; + yield return result.Value.PageMetadata; + paginationContext = result.Value.Context; - var result = await _apiCallExecutor(requestBuilder, paginationStrategy, cancellationToken) - .ConfigureAwait(false); + var strategy = _paginationStrategies + .Select(s => new { Strategy = s, Builder = s.Apply(paginationContext) }) + .FirstOrDefault(x => x.Builder != null)?.Strategy; + if (strategy == null) yield break; - var items = _pagedResponseItemConverter(result.PageMetadata); - if (items == null || !items.Any()) - yield break; + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); - paginationContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); + requestBuilder = strategy.Apply(paginationContext); + if (requestBuilder == null) yield break; - yield return result.PageMetadata; - } + result = await ExecuteAndYieldAsync(strategy, requestBuilder, cancellationToken).ConfigureAwait(false); + if (result == null) yield break; + + yield return result.Value.PageMetadata; + paginationContext = result.Value.Context; } } + + private async Task<(TPageMetadata PageMetadata, PaginationContext Context)?> ExecuteAndYieldAsync( + IPaginationStrategy strategy, + RequestBuilder requestBuilder, + CancellationToken cancellationToken) + { + var result = await _apiCallExecutor(requestBuilder, strategy, cancellationToken).ConfigureAwait(false); + var items = _pagedResponseItemConverter(result.PageMetadata); + if (items == null || !items.Any()) return null; + + var updatedContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); + return (result.PageMetadata, updatedContext); + } } } diff --git a/APIMatic.Core/Pagination/PaginatedResult.cs b/APIMatic.Core/Pagination/PaginatedResult.cs index b38c5450..c023b469 100644 --- a/APIMatic.Core/Pagination/PaginatedResult.cs +++ b/APIMatic.Core/Pagination/PaginatedResult.cs @@ -1,5 +1,4 @@ using APIMatic.Core.Types.Sdk; -using System.Collections.Generic; namespace APIMatic.Core.Pagination { diff --git a/APIMatic.Core/Pagination/Paginator.cs b/APIMatic.Core/Pagination/Paginator.cs index d884dec0..79d63c8c 100644 --- a/APIMatic.Core/Pagination/Paginator.cs +++ b/APIMatic.Core/Pagination/Paginator.cs @@ -10,16 +10,25 @@ namespace APIMatic.Core.Pagination { - public class Paginator : IEnumerable + /// + /// Provides synchronous iteration over a paginated data source by fetching items across multiple pages. + /// + /// + /// The type of individual items to be iterated over. + /// + /// + /// The type of metadata associated with each page of results, such as pagination state or response headers. + /// + public class Paginator : IEnumerable { - private readonly Func>> _apiCallExecutor; + private readonly Func>> _apiCallExecutor; private readonly RequestBuilder _requestBuilder; private readonly IPaginationStrategy[] _paginationStrategies; - private readonly Func> _pagedResponseItemConverter; + private readonly Func> _pagedResponseItemConverter; public Paginator( - Func>> apiCallExecutor, RequestBuilder requestBuilder, - Func> pagedResponseItemConverter, + Func>> apiCallExecutor, RequestBuilder requestBuilder, + Func> pagedResponseItemConverter, IPaginationStrategy[] paginationStrategies) { _apiCallExecutor = apiCallExecutor; @@ -28,6 +37,16 @@ public Paginator( _paginationStrategies = paginationStrategies; } + /// + /// Returns an enumerator that iterates through all items retrieved from a paginated data source. + /// + /// + /// An that iterates through individual items across all pages. + /// + /// + /// This method synchronously retrieves pages using and extracts items from each page + /// using the configured response-to-item converter. + /// public IEnumerator GetEnumerator() { foreach (var pageMetaData in GetPages()) @@ -39,30 +58,64 @@ public IEnumerator GetEnumerator() } } + /// + /// Returns a non-generic enumerator that iterates through the collection. + /// + /// + /// An that iterates through the collection. + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - public IEnumerable GetPages() + /// + /// Retrieves metadata for each page of results from a paginated data source synchronously. + /// + /// + /// An that yields metadata for each page retrieved using the configured pagination strategies. + /// + /// + /// This method applies pagination strategies to generate requests and yields metadata for each page until no further pages are available. + /// It is the synchronous counterpart to GetPagesAsync. + /// + public IEnumerable GetPages() { var paginationContext = PaginationContext.CreateDefault(_requestBuilder); - foreach (var paginationStrategy in _paginationStrategies) - { - while (true) - { - var requestBuilder = paginationStrategy.Apply(paginationContext); - if (requestBuilder == null) - break; + var initialStrategy = _paginationStrategies.First(); + var requestBuilder = initialStrategy.Apply(paginationContext); - var result = CoreHelper.RunTask(_apiCallExecutor(requestBuilder, paginationStrategy, default)); + var result = ExecuteAndYield(initialStrategy, requestBuilder); + if (result == null) yield break; - var items = _pagedResponseItemConverter(result.PageMetadata); - if (items == null || !items.Any()) - yield break; + yield return result.Value.PageMetadata; + paginationContext = result.Value.Context; - paginationContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); + var strategy = _paginationStrategies + .Select(s => new { Strategy = s, Builder = s.Apply(paginationContext) }) + .FirstOrDefault(x => x.Builder != null)?.Strategy; + if (strategy == null) yield break; - yield return result.PageMetadata; - } + while (true) + { + requestBuilder = strategy.Apply(paginationContext); + if (requestBuilder == null) yield break; + + result = ExecuteAndYield(strategy, requestBuilder); + if (result == null) yield break; + + yield return result.Value.PageMetadata; + paginationContext = result.Value.Context; } } + + private (TPageMetadata PageMetadata, PaginationContext Context)? ExecuteAndYield( + IPaginationStrategy strategy, + RequestBuilder requestBuilder) + { + var result = CoreHelper.RunTask(_apiCallExecutor(requestBuilder, strategy, CancellationToken.None)); + var items = _pagedResponseItemConverter(result.PageMetadata); + if (items == null || !items.Any()) return null; + + var updatedContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); + return (result.PageMetadata, updatedContext); + } } } diff --git a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs index 6d6622e7..2612c747 100644 --- a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs @@ -1,5 +1,4 @@ using APIMatic.Core.Request; -using APIMatic.Core.Utilities; using APIMatic.Core.Utilities.Json; namespace APIMatic.Core.Pagination.Strategies From 5f7046c3c7587a33329b157848ea235948a7f5cf Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Fri, 13 Jun 2025 14:56:33 +0500 Subject: [PATCH 19/24] refactor code to remove duplicate logic --- APIMatic.Core/Pagination/AsyncPaginator.cs | 24 ++++--------- APIMatic.Core/Pagination/Paginator.cs | 30 ++++++---------- .../Pagination/PaginatorExtension.cs | 36 +++++++++++++++++++ 3 files changed, 53 insertions(+), 37 deletions(-) create mode 100644 APIMatic.Core/Pagination/PaginatorExtension.cs diff --git a/APIMatic.Core/Pagination/AsyncPaginator.cs b/APIMatic.Core/Pagination/AsyncPaginator.cs index 9bd856fe..82bf6f58 100644 --- a/APIMatic.Core/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core/Pagination/AsyncPaginator.cs @@ -86,16 +86,15 @@ public async IAsyncEnumerable GetPagesAsync( var initialStrategy = _paginationStrategies.First(); var requestBuilder = initialStrategy.Apply(paginationContext); - var result = await ExecuteAndYieldAsync(initialStrategy, requestBuilder, cancellationToken) + var result = await initialStrategy + .ExecuteAndYieldAsync(requestBuilder, _apiCallExecutor, _pagedResponseItemConverter, cancellationToken) .ConfigureAwait(false); if (result == null) yield break; yield return result.Value.PageMetadata; paginationContext = result.Value.Context; - var strategy = _paginationStrategies - .Select(s => new { Strategy = s, Builder = s.Apply(paginationContext) }) - .FirstOrDefault(x => x.Builder != null)?.Strategy; + var strategy = _paginationStrategies.SelectFirstApplicableStrategy(paginationContext); if (strategy == null) yield break; while (true) @@ -105,25 +104,14 @@ public async IAsyncEnumerable GetPagesAsync( requestBuilder = strategy.Apply(paginationContext); if (requestBuilder == null) yield break; - result = await ExecuteAndYieldAsync(strategy, requestBuilder, cancellationToken).ConfigureAwait(false); + result = await strategy + .ExecuteAndYieldAsync(requestBuilder, _apiCallExecutor, _pagedResponseItemConverter, + cancellationToken).ConfigureAwait(false); if (result == null) yield break; yield return result.Value.PageMetadata; paginationContext = result.Value.Context; } } - - private async Task<(TPageMetadata PageMetadata, PaginationContext Context)?> ExecuteAndYieldAsync( - IPaginationStrategy strategy, - RequestBuilder requestBuilder, - CancellationToken cancellationToken) - { - var result = await _apiCallExecutor(requestBuilder, strategy, cancellationToken).ConfigureAwait(false); - var items = _pagedResponseItemConverter(result.PageMetadata); - if (items == null || !items.Any()) return null; - - var updatedContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); - return (result.PageMetadata, updatedContext); - } } } diff --git a/APIMatic.Core/Pagination/Paginator.cs b/APIMatic.Core/Pagination/Paginator.cs index 79d63c8c..c5f1b397 100644 --- a/APIMatic.Core/Pagination/Paginator.cs +++ b/APIMatic.Core/Pagination/Paginator.cs @@ -21,13 +21,17 @@ namespace APIMatic.Core.Pagination /// public class Paginator : IEnumerable { - private readonly Func>> _apiCallExecutor; + private readonly + Func>> + _apiCallExecutor; + private readonly RequestBuilder _requestBuilder; private readonly IPaginationStrategy[] _paginationStrategies; private readonly Func> _pagedResponseItemConverter; public Paginator( - Func>> apiCallExecutor, RequestBuilder requestBuilder, + Func>> + apiCallExecutor, RequestBuilder requestBuilder, Func> pagedResponseItemConverter, IPaginationStrategy[] paginationStrategies) { @@ -82,15 +86,14 @@ public IEnumerable GetPages() var initialStrategy = _paginationStrategies.First(); var requestBuilder = initialStrategy.Apply(paginationContext); - var result = ExecuteAndYield(initialStrategy, requestBuilder); + var result = CoreHelper.RunTask(initialStrategy.ExecuteAndYieldAsync(requestBuilder, _apiCallExecutor, + _pagedResponseItemConverter, CancellationToken.None)); if (result == null) yield break; yield return result.Value.PageMetadata; paginationContext = result.Value.Context; - var strategy = _paginationStrategies - .Select(s => new { Strategy = s, Builder = s.Apply(paginationContext) }) - .FirstOrDefault(x => x.Builder != null)?.Strategy; + var strategy = _paginationStrategies.SelectFirstApplicableStrategy(paginationContext); if (strategy == null) yield break; while (true) @@ -98,24 +101,13 @@ public IEnumerable GetPages() requestBuilder = strategy.Apply(paginationContext); if (requestBuilder == null) yield break; - result = ExecuteAndYield(strategy, requestBuilder); + result = CoreHelper.RunTask(strategy.ExecuteAndYieldAsync(requestBuilder, _apiCallExecutor, + _pagedResponseItemConverter, CancellationToken.None)); if (result == null) yield break; yield return result.Value.PageMetadata; paginationContext = result.Value.Context; } } - - private (TPageMetadata PageMetadata, PaginationContext Context)? ExecuteAndYield( - IPaginationStrategy strategy, - RequestBuilder requestBuilder) - { - var result = CoreHelper.RunTask(_apiCallExecutor(requestBuilder, strategy, CancellationToken.None)); - var items = _pagedResponseItemConverter(result.PageMetadata); - if (items == null || !items.Any()) return null; - - var updatedContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); - return (result.PageMetadata, updatedContext); - } } } diff --git a/APIMatic.Core/Pagination/PaginatorExtension.cs b/APIMatic.Core/Pagination/PaginatorExtension.cs new file mode 100644 index 00000000..3683215c --- /dev/null +++ b/APIMatic.Core/Pagination/PaginatorExtension.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Request; + +namespace APIMatic.Core.Pagination +{ + internal static class PaginatorExtension + { + public static IPaginationStrategy SelectFirstApplicableStrategy( + this IEnumerable strategies, PaginationContext context) => strategies + .Select(s => new { Strategy = s, Builder = s.Apply(context) }) + .FirstOrDefault(x => x.Builder != null) + ?.Strategy; + + public static async Task<(TPageMetadata PageMetadata, PaginationContext Context)?> ExecuteAndYieldAsync( + this IPaginationStrategy strategy, + RequestBuilder requestBuilder, + Func>> + apiCallExecutor, + Func> pagedResponseItemConverter, + CancellationToken cancellationToken = default) + { + var result = await apiCallExecutor(requestBuilder, strategy, cancellationToken).ConfigureAwait(false); + var items = pagedResponseItemConverter(result.PageMetadata); + if (items == null || !items.Any()) return null; + + var updatedContext = PaginationContext.Create(items.Count(), result.Response, requestBuilder); + return (result.PageMetadata, updatedContext); + } + } +} From e5742f2c126c43abb2a4932a0ffea5caa6d2079f Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Fri, 13 Jun 2025 15:00:08 +0500 Subject: [PATCH 20/24] Update AsyncPaginator.cs --- APIMatic.Core/Pagination/AsyncPaginator.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/APIMatic.Core/Pagination/AsyncPaginator.cs b/APIMatic.Core/Pagination/AsyncPaginator.cs index 82bf6f58..49e6a79d 100644 --- a/APIMatic.Core/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core/Pagination/AsyncPaginator.cs @@ -86,9 +86,9 @@ public async IAsyncEnumerable GetPagesAsync( var initialStrategy = _paginationStrategies.First(); var requestBuilder = initialStrategy.Apply(paginationContext); - var result = await initialStrategy - .ExecuteAndYieldAsync(requestBuilder, _apiCallExecutor, _pagedResponseItemConverter, cancellationToken) - .ConfigureAwait(false); + var result = await initialStrategy.ExecuteAndYieldAsync(requestBuilder, + _apiCallExecutor, _pagedResponseItemConverter, cancellationToken).ConfigureAwait(false); + if (result == null) yield break; yield return result.Value.PageMetadata; @@ -104,9 +104,8 @@ public async IAsyncEnumerable GetPagesAsync( requestBuilder = strategy.Apply(paginationContext); if (requestBuilder == null) yield break; - result = await strategy - .ExecuteAndYieldAsync(requestBuilder, _apiCallExecutor, _pagedResponseItemConverter, - cancellationToken).ConfigureAwait(false); + result = await strategy.ExecuteAndYieldAsync(requestBuilder, _apiCallExecutor, + _pagedResponseItemConverter, cancellationToken).ConfigureAwait(false); if (result == null) yield break; yield return result.Value.PageMetadata; From 71e5010c71a9548a87bf8e6d6fa58a231733ae40 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Mon, 16 Jun 2025 10:58:33 +0500 Subject: [PATCH 21/24] revert pattern matching to switch case for backward compatibility --- APIMatic.Core/Utilities/CoreHelper.cs | 13 ++++++---- .../Utilities/Json/JsonPointerResolver.cs | 26 ++++++++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/APIMatic.Core/Utilities/CoreHelper.cs b/APIMatic.Core/Utilities/CoreHelper.cs index 400bd67b..0ba74290 100644 --- a/APIMatic.Core/Utilities/CoreHelper.cs +++ b/APIMatic.Core/Utilities/CoreHelper.cs @@ -135,12 +135,15 @@ private static string GetTemplateReplacerValueForCollection(ICollection collecti public static string GetTemplateReplacerValue(object value) { - return value switch + switch (value) { - null => string.Empty, - ICollection collection => GetTemplateReplacerValueForCollection(collection), - _ => CoreHelper.JsonSerialize(value).TrimStart('"').TrimEnd('"') - }; + case null: + return string.Empty; + case ICollection collection: + return GetTemplateReplacerValueForCollection(collection); + default: + return CoreHelper.JsonSerialize(value).TrimStart('"').TrimEnd('"'); + } } private static void AppendParameters(StringBuilder queryBuilder, ArraySerialization arraySerialization, KeyValuePair pair) diff --git a/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs b/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs index e1dd6b78..c9ab7d08 100644 --- a/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs +++ b/APIMatic.Core/Utilities/Json/JsonPointerResolver.cs @@ -17,12 +17,15 @@ public static string ResolveScopedJsonValue(string pointerString, string jsonBod var jsonPointer = new JsonPointer(path); - return scope switch + switch (scope) { - "$response.body" => ExtractValueByPointer(jsonPointer, jsonBody), - "$response.headers" => ExtractValueByPointer(jsonPointer, jsonHeaders), - _ => null - }; + case "$response.body": + return ExtractValueByPointer(jsonPointer, jsonBody); + case "$response.headers": + return ExtractValueByPointer(jsonPointer, jsonHeaders); + default: + return null; + } } private static string ExtractValueByPointer(JsonPointer jsonPointer, string json) @@ -40,12 +43,15 @@ private static string ExtractValueByPointer(JsonPointer jsonPointer, string json return null; } - return jsonToken.Type switch + switch (jsonToken.Type) { - JTokenType.Null => null, - JTokenType.String => jsonToken.Value(), - _ => jsonToken.ToString() - }; + case JTokenType.Null: + return null; + case JTokenType.String: + return jsonToken.Value(); + default: + return jsonToken.ToString(); + } } } } From 77b237247d94657e140fdd505bbb3c27b57a2366 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Tue, 17 Jun 2025 14:19:32 +0500 Subject: [PATCH 22/24] fix: address PR review comments --- APIMatic.Core.Test/AuthenticationTest.cs | 11 +--- .../MockTypes/Pagination/BasePagedResponse.cs | 2 +- ...yncPaginator.cs => AsyncPaginatorTests.cs} | 2 +- .../{Paginator.cs => PaginatorTests.cs} | 2 +- ...uilderUpdateParameterByJsonPointerTest.cs} | 58 +++++++++---------- APIMatic.Core/APIMatic.Core.csproj | 2 +- APIMatic.Core/ApiCall.cs | 2 +- APIMatic.Core/Pagination/PaginatedResult.cs | 6 +- APIMatic.Core/Pagination/PaginationContext.cs | 2 +- .../Pagination/Strategies/CursorPagination.cs | 2 +- .../Pagination/Strategies/NumberPagination.cs | 2 +- .../Pagination/Strategies/OffsetPagination.cs | 2 +- APIMatic.Core/PaginationHandler.cs | 2 +- APIMatic.Core/Request/RequestBuilder.cs | 2 +- APIMatic.Core/Response/ResponseHandler.cs | 2 - 15 files changed, 44 insertions(+), 55 deletions(-) rename APIMatic.Core.Test/Pagination/{AsyncPaginator.cs => AsyncPaginatorTests.cs} (99%) rename APIMatic.Core.Test/Pagination/{Paginator.cs => PaginatorTests.cs} (99%) rename APIMatic.Core.Test/Request/{RequestBuilderUpdateByReferenceTest.cs => RequestBuilderUpdateParameterByJsonPointerTest.cs} (73%) diff --git a/APIMatic.Core.Test/AuthenticationTest.cs b/APIMatic.Core.Test/AuthenticationTest.cs index eeaf652c..4cf147b9 100644 --- a/APIMatic.Core.Test/AuthenticationTest.cs +++ b/APIMatic.Core.Test/AuthenticationTest.cs @@ -112,7 +112,7 @@ public void Multiple_Authentication_OR_Validation_Failure() } [Test] - public async Task Multiple_Authentication_AND_Validation_Failure() + public void Multiple_Authentication_AND_Validation_Failure() { var globalConfiguration = new GlobalConfiguration.Builder() .ServerUrls(new Dictionary @@ -137,15 +137,6 @@ public async Task Multiple_Authentication_AND_Validation_Failure() Assert.AreEqual("Following authentication credentials are required:\n" + "-> Missing required header field: TOKEN", exp.Message); - - async void AuthCode() - { - await globalConfiguration.GlobalRequestBuilder() - .Setup(HttpMethod.Get, "/auth") - .WithAndAuth(auth => auth.Add("query") - .Add("header")) - .Build(); - } } [Test] diff --git a/APIMatic.Core.Test/MockTypes/Pagination/BasePagedResponse.cs b/APIMatic.Core.Test/MockTypes/Pagination/BasePagedResponse.cs index 01365ecd..abce28a0 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/BasePagedResponse.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/BasePagedResponse.cs @@ -16,7 +16,7 @@ protected BasePagedResponse(ApiResponse pageData, Func Items { get; } } diff --git a/APIMatic.Core.Test/Pagination/AsyncPaginator.cs b/APIMatic.Core.Test/Pagination/AsyncPaginatorTests.cs similarity index 99% rename from APIMatic.Core.Test/Pagination/AsyncPaginator.cs rename to APIMatic.Core.Test/Pagination/AsyncPaginatorTests.cs index feaf84cf..6cf8e0e0 100644 --- a/APIMatic.Core.Test/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core.Test/Pagination/AsyncPaginatorTests.cs @@ -13,7 +13,7 @@ namespace APIMatic.Core.Test.Pagination { [TestFixture] - public class AsyncPaginator : ApiCallTest + public class AsyncPaginatorTests : ApiCallTest { [Test] public async Task PaginateAsync_QueryOffsetPaginationYieldsAllItems_AcrossPages() diff --git a/APIMatic.Core.Test/Pagination/Paginator.cs b/APIMatic.Core.Test/Pagination/PaginatorTests.cs similarity index 99% rename from APIMatic.Core.Test/Pagination/Paginator.cs rename to APIMatic.Core.Test/Pagination/PaginatorTests.cs index 4ffd7731..bfced63f 100644 --- a/APIMatic.Core.Test/Pagination/Paginator.cs +++ b/APIMatic.Core.Test/Pagination/PaginatorTests.cs @@ -13,7 +13,7 @@ namespace APIMatic.Core.Test.Pagination { [TestFixture] - public class Paginator : ApiCallTest + public class PaginatorTests : ApiCallTest { [Test] public void Paginate_QueryOffsetPaginationYieldsAllItems_AcrossPages() diff --git a/APIMatic.Core.Test/Request/RequestBuilderUpdateByReferenceTest.cs b/APIMatic.Core.Test/Request/RequestBuilderUpdateParameterByJsonPointerTest.cs similarity index 73% rename from APIMatic.Core.Test/Request/RequestBuilderUpdateByReferenceTest.cs rename to APIMatic.Core.Test/Request/RequestBuilderUpdateParameterByJsonPointerTest.cs index 7ba7c658..b11a4757 100644 --- a/APIMatic.Core.Test/Request/RequestBuilderUpdateByReferenceTest.cs +++ b/APIMatic.Core.Test/Request/RequestBuilderUpdateParameterByJsonPointerTest.cs @@ -12,7 +12,7 @@ namespace APIMatic.Core.Test.Request { [TestFixture] - public class RequestBuilderUpdateByReferenceTest : TestBase + public class RequestBuilderUpdateParameterByJsonPointerTest : TestBase { private const string ServerUrl = "https://api.example.com/users"; @@ -20,7 +20,7 @@ public class RequestBuilderUpdateByReferenceTest : TestBase rb.Setup(HttpMethod.Get, ServerUrl); [Test] - public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesCorrectly() + public async Task UpdateParameterByJsonPointer_HeaderParam_SingleValue_UpdatesCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -29,7 +29,7 @@ public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesCorrectly() await builder.Build(); // Act - builder.UpdateByReference("$request.headers#/id", old => 999); + builder.UpdateParameterByJsonPointer("$request.headers#/id", old => 999); // Assert Assert.IsTrue(builder.headersParameters.ContainsKey("id")); @@ -37,7 +37,7 @@ public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesCorrectly() } [Test] - public async Task UpdateByReference_HeaderParam_IncorrectJsonPointer_UpdatesPrevious() + public async Task UpdateParameterByJsonPointer_HeaderParam_IncorrectJsonPointer_UpdatesPrevious() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -48,14 +48,14 @@ public async Task UpdateByReference_HeaderParam_IncorrectJsonPointer_UpdatesPrev var previousHeaders = builder.headersParameters; // Act - builder.UpdateByReference("$request.headers", old => 999); + builder.UpdateParameterByJsonPointer("$request.headers", old => 999); // Assert Assert.AreEqual(previousHeaders, builder.headersParameters); } [Test] - public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesWithEmptyPointer() + public async Task UpdateParameterByJsonPointer_HeaderParam_SingleValue_UpdatesWithEmptyPointer() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -66,14 +66,14 @@ public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesWithEmptyPoin var previousHeaders = builder.headersParameters; // Act - builder.UpdateByReference("$request.headers#", old => 999); + builder.UpdateParameterByJsonPointer("$request.headers#", old => 999); // Assert Assert.AreEqual(previousHeaders, builder.headersParameters); } [Test] - public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesWithNonExistentPointer() + public async Task UpdateParameterByJsonPointer_HeaderParam_SingleValue_UpdatesWithNonExistentPointer() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -84,14 +84,14 @@ public async Task UpdateByReference_HeaderParam_SingleValue_UpdatesWithNonExiste var previousHeaders = builder.headersParameters; // Act - builder.UpdateByReference("$request.headers#/nonexistent", old => 999); + builder.UpdateParameterByJsonPointer("$request.headers#/nonexistent", old => 999); // Assert Assert.AreEqual(previousHeaders, builder.headersParameters); } [Test] - public async Task UpdateByReference_HeaderParam_ObjectValue_UpdatesObjectCorrectly() + public async Task UpdateParameterByJsonPointer_HeaderParam_ObjectValue_UpdatesObjectCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -101,7 +101,7 @@ public async Task UpdateByReference_HeaderParam_ObjectValue_UpdatesObjectCorrect await builder.Build(); // Act - builder.UpdateByReference("$request.headers#/user/Id", old => 456); + builder.UpdateParameterByJsonPointer("$request.headers#/user/Id", old => 456); // Assert Assert.IsTrue(builder.headersParameters.ContainsKey("user")); @@ -112,7 +112,7 @@ public async Task UpdateByReference_HeaderParam_ObjectValue_UpdatesObjectCorrect } [Test] - public async Task UpdateByReference_QueryParam_ObjectValue_UpdatesObjectCorrectly() + public async Task UpdateParameterByJsonPointer_QueryParam_ObjectValue_UpdatesObjectCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -122,7 +122,7 @@ public async Task UpdateByReference_QueryParam_ObjectValue_UpdatesObjectCorrectl await builder.Build(); // Act - builder.UpdateByReference("$request.query#/user/Id", old => 456); + builder.UpdateParameterByJsonPointer("$request.query#/user/Id", old => 456); // Assert Assert.IsTrue(builder.queryParameters.ContainsKey("user")); @@ -133,7 +133,7 @@ public async Task UpdateByReference_QueryParam_ObjectValue_UpdatesObjectCorrectl } [Test] - public async Task UpdateByReference_TemplateParam_SingleValue_UpdatesCorrectly() + public async Task UpdateParameterByJsonPointer_TemplateParam_SingleValue_UpdatesCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -142,7 +142,7 @@ public async Task UpdateByReference_TemplateParam_SingleValue_UpdatesCorrectly() await builder.Build(); // Act - builder.UpdateByReference("$request.path#/userId", old => 456); + builder.UpdateParameterByJsonPointer("$request.path#/userId", old => 456); // Assert Assert.IsTrue(builder.templateParameters.ContainsKey("userId")); @@ -150,7 +150,7 @@ public async Task UpdateByReference_TemplateParam_SingleValue_UpdatesCorrectly() } [Test] - public async Task UpdateByReference_TemplateParam_Collection_UpdatesCorrectly() + public async Task UpdateParameterByJsonPointer_TemplateParam_Collection_UpdatesCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -159,7 +159,7 @@ public async Task UpdateByReference_TemplateParam_Collection_UpdatesCorrectly() await builder.Build(); // Act - builder.UpdateByReference("$request.path#/ids/1", old => 999); + builder.UpdateParameterByJsonPointer("$request.path#/ids/1", old => 999); // Assert Assert.IsTrue(builder.templateParameters.ContainsKey("ids")); @@ -169,7 +169,7 @@ public async Task UpdateByReference_TemplateParam_Collection_UpdatesCorrectly() } [Test] - public async Task UpdateByReference_BodyParameters_UpdatesCorrectly() + public async Task UpdateParameterByJsonPointer_BodyParameters_UpdatesCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -177,14 +177,14 @@ public async Task UpdateByReference_BodyParameters_UpdatesCorrectly() builder.Parameters(p => p.Body(b => b.Setup("age", 25))); await builder.Build(); - builder.UpdateByReference("$request.body#/age", old => 30); + builder.UpdateParameterByJsonPointer("$request.body#/age", old => 30); Assert.IsTrue(builder.bodyParameters.ContainsKey("age")); Assert.AreEqual(30, builder.bodyParameters["age"]); } [Test] - public async Task UpdateByReference_FormParameters_UpdatesCorrectly() + public async Task UpdateParameterByJsonPointer_FormParameters_UpdatesCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -197,14 +197,14 @@ public async Task UpdateByReference_FormParameters_UpdatesCorrectly() await builder.Build(); - builder.UpdateByReference("$request.body#/email", old => "jane@example.com"); + builder.UpdateParameterByJsonPointer("$request.body#/email", old => "jane@example.com"); var updated = builder.formParameters.ToDictionary(p => p.Key, p => p.Value); Assert.AreEqual("jane@example.com", updated["email"]); } [Test] - public async Task UpdateByReference_DynamicBody_UpdatesCorrectly() + public async Task UpdateParameterByJsonPointer_DynamicBody_UpdatesCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -212,13 +212,13 @@ public async Task UpdateByReference_DynamicBody_UpdatesCorrectly() builder.Parameters(p => p.Body( b => b.Setup(new { name = "Alice", age = 30 }))); await builder.Build(); - builder.UpdateByReference("$request.body#/age", old => 35); + builder.UpdateParameterByJsonPointer("$request.body#/age", old => 35); Assert.AreEqual(35, builder.body.age); } [Test] - public async Task UpdateByReference_StringBody_UpdatesCorrectly() + public async Task UpdateParameterByJsonPointer_StringBody_UpdatesCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -226,13 +226,13 @@ public async Task UpdateByReference_StringBody_UpdatesCorrectly() builder.Parameters(p => p.Body( b => b.Setup("InitialValue"))); await builder.Build(); - builder.UpdateByReference("$request.body#/", old => "UpdatedValue"); + builder.UpdateParameterByJsonPointer("$request.body#/", old => "UpdatedValue"); Assert.AreEqual("UpdatedValue", builder.body); } [Test] - public async Task UpdateByReference_ObjectBody_UpdatesCorrectly() + public async Task UpdateParameterByJsonPointer_ObjectBody_UpdatesCorrectly() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -240,7 +240,7 @@ public async Task UpdateByReference_ObjectBody_UpdatesCorrectly() builder.Parameters(p => p.Body( b => b.Setup(new User { Id = 1, Name = "Alice" }))); await builder.Build(); - builder.UpdateByReference("$request.body#/Id", old => 35); + builder.UpdateParameterByJsonPointer("$request.body#/Id", old => 35); var updatedBody = builder.body as User; Assert.IsNotNull(updatedBody); @@ -249,7 +249,7 @@ public async Task UpdateByReference_ObjectBody_UpdatesCorrectly() } [Test] - public async Task UpdateByReference_Body_CoreFileStreamInfo_DoesNotUpdate() + public async Task UpdateParameterByJsonPointer_Body_CoreFileStreamInfo_DoesNotUpdate() { var builder = new RequestBuilder(LazyGlobalConfiguration.Value, ServerUrl); setupRequest(builder); @@ -259,7 +259,7 @@ public async Task UpdateByReference_Body_CoreFileStreamInfo_DoesNotUpdate() await builder.Build(); - builder.UpdateByReference("$request.body#/", _ => "should-not-apply"); + builder.UpdateParameterByJsonPointer("$request.body#/", _ => "should-not-apply"); Assert.AreSame(originalBody, builder.body); // Still the same object } diff --git a/APIMatic.Core/APIMatic.Core.csproj b/APIMatic.Core/APIMatic.Core.csproj index 396b5631..3ecebcbd 100644 --- a/APIMatic.Core/APIMatic.Core.csproj +++ b/APIMatic.Core/APIMatic.Core.csproj @@ -30,7 +30,7 @@ - + diff --git a/APIMatic.Core/ApiCall.cs b/APIMatic.Core/ApiCall.cs index 10c541f8..17a86aee 100644 --- a/APIMatic.Core/ApiCall.cs +++ b/APIMatic.Core/ApiCall.cs @@ -88,7 +88,7 @@ public ApiCall requestBuilder; + internal RequestBuilder GetRequestBuilder() => requestBuilder; /// /// Setup the response handler diff --git a/APIMatic.Core/Pagination/PaginatedResult.cs b/APIMatic.Core/Pagination/PaginatedResult.cs index c023b469..935f6746 100644 --- a/APIMatic.Core/Pagination/PaginatedResult.cs +++ b/APIMatic.Core/Pagination/PaginatedResult.cs @@ -2,12 +2,12 @@ namespace APIMatic.Core.Pagination { - public class PaginatedResult + public class PaginatedResult { public CoreResponse Response { get; } - public TPage PageMetadata { get; } + public TPageMetadata PageMetadata { get; } - public PaginatedResult(CoreResponse response, TPage pageMetadata) + internal PaginatedResult(CoreResponse response, TPageMetadata pageMetadata) { Response = response; PageMetadata = pageMetadata; diff --git a/APIMatic.Core/Pagination/PaginationContext.cs b/APIMatic.Core/Pagination/PaginationContext.cs index ad967259..257f222e 100644 --- a/APIMatic.Core/Pagination/PaginationContext.cs +++ b/APIMatic.Core/Pagination/PaginationContext.cs @@ -24,7 +24,7 @@ private PaginationContext(RequestBuilder requestBuilder, int dataSize = 0, CoreR internal static PaginationContext CreateDefault(RequestBuilder requestBuilder) => new PaginationContext(RequestBuilder.RequestBuilderWithParameters(requestBuilder)); - + internal static PaginationContext Create(int dataSize, CoreResponse response, RequestBuilder requestBuilder) => new PaginationContext(requestBuilder, dataSize, response); } diff --git a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs index 2612c747..feed2647 100644 --- a/APIMatic.Core/Pagination/Strategies/CursorPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/CursorPagination.cs @@ -36,7 +36,7 @@ public RequestBuilder Apply(PaginationContext paginationContext) var updatedBuilder = RequestBuilder .RequestBuilderWithParameters(paginationContext.RequestBuilder) - .UpdateByReference(_input, old => + .UpdateParameterByJsonPointer(_input, old => { var oldValue = old?.ToString(); if (!paginationContext.HasResponse) diff --git a/APIMatic.Core/Pagination/Strategies/NumberPagination.cs b/APIMatic.Core/Pagination/Strategies/NumberPagination.cs index de9b9d25..7ea58c3d 100644 --- a/APIMatic.Core/Pagination/Strategies/NumberPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/NumberPagination.cs @@ -33,7 +33,7 @@ public RequestBuilder Apply(PaginationContext paginationContext) var isUpdated = false; var currentRequestBuilder = RequestBuilder.RequestBuilderWithParameters(paginationContext.RequestBuilder) - .UpdateByReference( + .UpdateParameterByJsonPointer( _input, old => { var oldValue = int.TryParse(old?.ToString(), out var value) ? value : 0; diff --git a/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs b/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs index 40372524..36d5e1ac 100644 --- a/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs +++ b/APIMatic.Core/Pagination/Strategies/OffsetPagination.cs @@ -34,7 +34,7 @@ public RequestBuilder Apply(PaginationContext paginationContext) var isUpdated = false; var currentRequestBuilder = RequestBuilder.RequestBuilderWithParameters(paginationContext.RequestBuilder) - .UpdateByReference( + .UpdateParameterByJsonPointer( _input, old => { var oldValue = int.Parse(old?.ToString() ?? "0"); diff --git a/APIMatic.Core/PaginationHandler.cs b/APIMatic.Core/PaginationHandler.cs index 9973cb19..2bd2c45c 100644 --- a/APIMatic.Core/PaginationHandler.cs +++ b/APIMatic.Core/PaginationHandler.cs @@ -19,7 +19,7 @@ public class PaginationHandler Task>>, RequestBuilder, TPageableType> _createPageable; - public PaginationHandler( + internal PaginationHandler( Func> requestExecutor, RequestBuilder requestBuilder, Func createPageResponse, diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 4730c251..809217f6 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -183,7 +183,7 @@ public RequestBuilder XmlBodySerializer(Func xmlSerializer) return this; } - internal RequestBuilder UpdateByReference(string pointerString, Func setter) + internal RequestBuilder UpdateParameterByJsonPointer(string pointerString, Func setter) { var index = pointerString.IndexOf('#'); if (index < 0) diff --git a/APIMatic.Core/Response/ResponseHandler.cs b/APIMatic.Core/Response/ResponseHandler.cs index 130bc4c7..b27881b5 100644 --- a/APIMatic.Core/Response/ResponseHandler.cs +++ b/APIMatic.Core/Response/ResponseHandler.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using APIMatic.Core.Http.Configuration; -using APIMatic.Core.Pagination; -using APIMatic.Core.Pagination.Strategies; using APIMatic.Core.Types; using APIMatic.Core.Types.Sdk; using APIMatic.Core.Utilities; From 1a5dd9dccfbfcca848f8d21fbd88fadafaa68184 Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Tue, 17 Jun 2025 16:38:45 +0500 Subject: [PATCH 23/24] fix(request-builder): ensure AuthGroup is correctly copied in copy constructor --- APIMatic.Core/Authentication/AuthGroupBuilder.cs | 8 ++++++++ APIMatic.Core/Request/RequestBuilder.cs | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/APIMatic.Core/Authentication/AuthGroupBuilder.cs b/APIMatic.Core/Authentication/AuthGroupBuilder.cs index 9913b4b7..f94cce26 100644 --- a/APIMatic.Core/Authentication/AuthGroupBuilder.cs +++ b/APIMatic.Core/Authentication/AuthGroupBuilder.cs @@ -21,6 +21,14 @@ internal AuthGroupBuilder(Dictionary authManagers) { authManagersMap = authManagers ?? new Dictionary(); } + + internal AuthGroupBuilder(AuthGroupBuilder other) + { + authManagersMap = new Dictionary(other.authManagersMap); + authManagers = new List(other.authManagers); + validatedAuthManagers = new List(other.validatedAuthManagers); + isAndGroup = other.isAndGroup; + } /// /// Add an authorization scheme to this group, using its name. diff --git a/APIMatic.Core/Request/RequestBuilder.cs b/APIMatic.Core/Request/RequestBuilder.cs index 809217f6..bd1b9d6f 100644 --- a/APIMatic.Core/Request/RequestBuilder.cs +++ b/APIMatic.Core/Request/RequestBuilder.cs @@ -66,7 +66,7 @@ internal RequestBuilder(RequestBuilder source) this.bodyParameters = new Dictionary(source.bodyParameters); this.formParameters = new List>(source.formParameters); this.queryParameters = new Dictionary(source.queryParameters); - this.authGroup = new AuthGroupBuilder(source.configuration.AuthManagers); + this.authGroup = new AuthGroupBuilder(source.authGroup); } internal static RequestBuilder RequestBuilderWithParameters(RequestBuilder source) From aa340c99868b96064ce5fa534f388af88b7e9c1e Mon Sep 17 00:00:00 2001 From: Hamza Mahmood Date: Tue, 17 Jun 2025 18:56:24 +0500 Subject: [PATCH 24/24] fix: validate empty strategies in constructors and add PolySharp for polyfills --- .../MockTypes/Pagination/PaginatorFactory.cs | 2 +- .../Pagination/AsyncPaginatorTests.cs | 30 +++++++++++++++++ .../Pagination/PaginatorTests.cs | 33 ++++++++++++++++++- APIMatic.Core/APIMatic.Core.csproj | 4 +++ APIMatic.Core/Pagination/AsyncPaginator.cs | 9 +++-- APIMatic.Core/Pagination/Paginator.cs | 9 +++-- 6 files changed, 79 insertions(+), 8 deletions(-) diff --git a/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs b/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs index 096eab28..385d2ac8 100644 --- a/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs +++ b/APIMatic.Core.Test/MockTypes/Pagination/PaginatorFactory.cs @@ -15,6 +15,6 @@ public static Paginator Create( RequestBuilder requestBuilder, Func> responseToItems, params IPaginationStrategy[] dataManagers) - => new Paginator(apiCallExecutor, requestBuilder, responseToItems, dataManagers); + => new(apiCallExecutor, requestBuilder, responseToItems, dataManagers); } } diff --git a/APIMatic.Core.Test/Pagination/AsyncPaginatorTests.cs b/APIMatic.Core.Test/Pagination/AsyncPaginatorTests.cs index 6cf8e0e0..2c9f55d0 100644 --- a/APIMatic.Core.Test/Pagination/AsyncPaginatorTests.cs +++ b/APIMatic.Core.Test/Pagination/AsyncPaginatorTests.cs @@ -1,8 +1,12 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; +using APIMatic.Core.Pagination; using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Request; using APIMatic.Core.Test.Api; using APIMatic.Core.Test.MockTypes.Models.Pagination; using APIMatic.Core.Test.MockTypes.Pagination; @@ -15,6 +19,32 @@ namespace APIMatic.Core.Test.Pagination [TestFixture] public class AsyncPaginatorTests : ApiCallTest { + [Test] + public void Constructor_ShouldThrowArgumentException_WhenPaginationStrategiesIsEmpty() + { + // Arrange + Func>> + apiCallExecutor = + (rb, s, ct) => Task.FromResult>(null); + + Func> pagedResponseItemConverter = _ => Enumerable.Empty(); + var requestBuilder = new RequestBuilder(LazyGlobalConfiguration.Value, string.Empty); + var emptyStrategies = Array.Empty(); + + // Act & Assert + var ex = Assert.Throws(() => + { + new AsyncPaginator( + apiCallExecutor, + requestBuilder, + pagedResponseItemConverter, + emptyStrategies); + }); + + Assert.That(ex?.ParamName, Is.EqualTo("paginationStrategies")); + Assert.That(ex.Message, Does.StartWith("At least one pagination strategy must be provided.")); + } + [Test] public async Task PaginateAsync_QueryOffsetPaginationYieldsAllItems_AcrossPages() { diff --git a/APIMatic.Core.Test/Pagination/PaginatorTests.cs b/APIMatic.Core.Test/Pagination/PaginatorTests.cs index bfced63f..3a7cab86 100644 --- a/APIMatic.Core.Test/Pagination/PaginatorTests.cs +++ b/APIMatic.Core.Test/Pagination/PaginatorTests.cs @@ -1,8 +1,13 @@ -using System.Collections; +using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using APIMatic.Core.Pagination; using APIMatic.Core.Pagination.Strategies; +using APIMatic.Core.Request; using APIMatic.Core.Test.Api; using APIMatic.Core.Test.MockTypes.Models.Pagination; using APIMatic.Core.Test.MockTypes.Pagination; @@ -15,6 +20,32 @@ namespace APIMatic.Core.Test.Pagination [TestFixture] public class PaginatorTests : ApiCallTest { + [Test] + public void Constructor_ShouldThrowArgumentException_WhenPaginationStrategiesIsEmpty() + { + // Arrange + Func>> + apiCallExecutor = + (rb, s, ct) => Task.FromResult>(null); + + Func> pagedResponseItemConverter = _ => Enumerable.Empty(); + var requestBuilder = new RequestBuilder(LazyGlobalConfiguration.Value, string.Empty); + var emptyStrategies = Array.Empty(); + + // Act & Assert + var ex = Assert.Throws(() => + { + new Paginator( + apiCallExecutor, + requestBuilder, + pagedResponseItemConverter, + emptyStrategies); + }); + + Assert.That(ex?.ParamName, Is.EqualTo("paginationStrategies")); + Assert.That(ex.Message, Does.StartWith("At least one pagination strategy must be provided.")); + } + [Test] public void Paginate_QueryOffsetPaginationYieldsAllItems_AcrossPages() { diff --git a/APIMatic.Core/APIMatic.Core.csproj b/APIMatic.Core/APIMatic.Core.csproj index 3ecebcbd..a7549d72 100644 --- a/APIMatic.Core/APIMatic.Core.csproj +++ b/APIMatic.Core/APIMatic.Core.csproj @@ -35,6 +35,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/APIMatic.Core/Pagination/AsyncPaginator.cs b/APIMatic.Core/Pagination/AsyncPaginator.cs index 49e6a79d..f6445d93 100644 --- a/APIMatic.Core/Pagination/AsyncPaginator.cs +++ b/APIMatic.Core/Pagination/AsyncPaginator.cs @@ -25,7 +25,7 @@ private readonly _apiCallExecutor; private readonly RequestBuilder _requestBuilder; - private readonly IPaginationStrategy[] _paginationStrategies; + private readonly IReadOnlyCollection _paginationStrategies; private readonly Func> _pagedResponseItemConverter; public AsyncPaginator( @@ -33,12 +33,15 @@ public AsyncPaginator( apiCallExecutor, RequestBuilder requestBuilder, Func> pagedResponseItemConverter, - IPaginationStrategy[] paginationStrategies) + IReadOnlyCollection paginationStrategies) { _apiCallExecutor = apiCallExecutor; _requestBuilder = requestBuilder; _pagedResponseItemConverter = pagedResponseItemConverter; - _paginationStrategies = paginationStrategies; + _paginationStrategies = paginationStrategies?.Count > 0 + ? paginationStrategies + : throw new ArgumentException("At least one pagination strategy must be provided.", + nameof(paginationStrategies)); } /// diff --git a/APIMatic.Core/Pagination/Paginator.cs b/APIMatic.Core/Pagination/Paginator.cs index c5f1b397..d586c9bc 100644 --- a/APIMatic.Core/Pagination/Paginator.cs +++ b/APIMatic.Core/Pagination/Paginator.cs @@ -26,19 +26,22 @@ private readonly _apiCallExecutor; private readonly RequestBuilder _requestBuilder; - private readonly IPaginationStrategy[] _paginationStrategies; + private readonly IReadOnlyCollection _paginationStrategies; private readonly Func> _pagedResponseItemConverter; public Paginator( Func>> apiCallExecutor, RequestBuilder requestBuilder, Func> pagedResponseItemConverter, - IPaginationStrategy[] paginationStrategies) + IReadOnlyCollection paginationStrategies) { _apiCallExecutor = apiCallExecutor; _requestBuilder = requestBuilder; _pagedResponseItemConverter = pagedResponseItemConverter; - _paginationStrategies = paginationStrategies; + _paginationStrategies = paginationStrategies?.Count > 0 + ? paginationStrategies + : throw new ArgumentException("At least one pagination strategy must be provided.", + nameof(paginationStrategies)); } ///