From 3bc06ebde464019ce63e6c0cad2f2dcd629f0ed5 Mon Sep 17 00:00:00 2001 From: Constantine Nathanson Date: Mon, 31 Jul 2023 14:32:40 +0300 Subject: [PATCH] Add support for `SearchFolders` API --- .../SearchApi/SearchFoldersTest.cs | 35 +++++ .../{ => SearchApi}/SearchTest.cs | 31 +++-- .../Actions/Search/SearchFolder.cs | 35 +++++ .../Actions/Search/SearchFoldersResult.cs | 18 +++ .../Actions/Search/SearchResource.cs | 12 +- .../Actions/Search/SearchResult.cs | 21 +-- .../Actions/Search/SearchResultBase.cs | 29 +++++ CloudinaryDotNet/Cloudinary.AdminApi.cs | 9 ++ CloudinaryDotNet/Search/Search.cs | 18 +++ .../{Search.cs => Search/SearchBaseFluent.cs} | 121 +++++------------- CloudinaryDotNet/Search/SearchFluent.cs | 88 +++++++++++++ CloudinaryDotNet/Search/SearchFolders.cs | 18 +++ .../Search/SearchFoldersFluent.cs | 58 +++++++++ 13 files changed, 363 insertions(+), 130 deletions(-) create mode 100644 CloudinaryDotNet.Tests/SearchApi/SearchFoldersTest.cs rename CloudinaryDotNet.Tests/{ => SearchApi}/SearchTest.cs (82%) create mode 100644 CloudinaryDotNet/Actions/Search/SearchFolder.cs create mode 100644 CloudinaryDotNet/Actions/Search/SearchFoldersResult.cs create mode 100644 CloudinaryDotNet/Actions/Search/SearchResultBase.cs create mode 100644 CloudinaryDotNet/Search/Search.cs rename CloudinaryDotNet/{Search.cs => Search/SearchBaseFluent.cs} (58%) create mode 100644 CloudinaryDotNet/Search/SearchFluent.cs create mode 100644 CloudinaryDotNet/Search/SearchFolders.cs create mode 100644 CloudinaryDotNet/Search/SearchFoldersFluent.cs diff --git a/CloudinaryDotNet.Tests/SearchApi/SearchFoldersTest.cs b/CloudinaryDotNet.Tests/SearchApi/SearchFoldersTest.cs new file mode 100644 index 00000000..5177ca39 --- /dev/null +++ b/CloudinaryDotNet.Tests/SearchApi/SearchFoldersTest.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; +using SystemHttp = System.Net.Http; + +namespace CloudinaryDotNet.Tests.SearchApi +{ + public class SearchFoldersTest + { + private MockedCloudinary _cloudinary = new MockedCloudinary(); + + [SetUp] + public void SetUp() + { + _cloudinary = new MockedCloudinary(); + } + + [Test] + public void TestShouldSearchFolders() + { + _cloudinary + .SearchFolders() + .Expression("path:*") + .MaxResults(1) + .Execute(); + + _cloudinary.AssertHttpCall(SystemHttp.HttpMethod.Post, "folders/search"); + + var requestJson = _cloudinary.RequestJson(); + + Assert.IsNotNull(requestJson["expression"]); + Assert.AreEqual("path:*", requestJson["expression"].ToString()); + Assert.IsNotNull(requestJson["max_results"]); + Assert.AreEqual("1", requestJson["max_results"].ToString()); + } + } +} diff --git a/CloudinaryDotNet.Tests/SearchTest.cs b/CloudinaryDotNet.Tests/SearchApi/SearchTest.cs similarity index 82% rename from CloudinaryDotNet.Tests/SearchTest.cs rename to CloudinaryDotNet.Tests/SearchApi/SearchTest.cs index fd483dbc..bb0d2fff 100644 --- a/CloudinaryDotNet.Tests/SearchTest.cs +++ b/CloudinaryDotNet.Tests/SearchApi/SearchTest.cs @@ -3,15 +3,15 @@ using Newtonsoft.Json.Linq; using NUnit.Framework; -namespace CloudinaryDotNet.Tests +namespace CloudinaryDotNet.Tests.SearchApi { public class SearchTest { - private MockedCloudinary cloudinary = new MockedCloudinary(); + private MockedCloudinary _cloudinary = new MockedCloudinary(); - private Search search; + private Search _search; - private string searchExpression = "resource_type:image AND tags=kitten AND uploaded_at>1d AND bytes>1m"; + private const string SearchExpression = "resource_type:image AND tags=kitten AND uploaded_at>1d AND bytes>1m"; private const string B64Query = "eyJleHByZXNzaW9uIjoicmVzb3VyY2VfdHlwZTppbWFnZSBBTkQgdGFncz1raXR0ZW4gQU5EIHV" + "wbG9hZGVkX2F0PjFkIEFORCBieXRlcz4xbSIsIm1heF9yZXN1bHRzIjozMCwic29ydF9ieSI6W3" + @@ -26,7 +26,7 @@ public class SearchTest [SetUp] public void SetUp() { - cloudinary = new MockedCloudinary + _cloudinary = new MockedCloudinary { Api = { @@ -34,8 +34,8 @@ public void SetUp() } }; - search = cloudinary.Search() - .Expression(searchExpression) + _search = _cloudinary.Search() + .Expression(SearchExpression) .SortBy("public_id", "desc") .MaxResults(30); } @@ -44,7 +44,7 @@ public void SetUp() [Test] public void TestSearchUrl() { - Assert.AreEqual($"{SearchUrlPrefix}/{Ttl300Sig}/300/{B64Query}", search.ToUrl()); + Assert.AreEqual($"{SearchUrlPrefix}/{Ttl300Sig}/300/{B64Query}", _search.ToUrl()); } [Test] @@ -52,7 +52,7 @@ public void TestSearchUrlWithNextCursor() { Assert.AreEqual( $"{SearchUrlPrefix}/{Ttl300Sig}/300/{B64Query}/{NextCursor}", - search.ToUrl(null, NextCursor) + _search.ToUrl(null, NextCursor) ); } @@ -61,7 +61,7 @@ public void TestSearchUrlWithCustomTtlAndNextCursor() { Assert.AreEqual( $"{SearchUrlPrefix}/{Ttl1000Sig}/1000/{B64Query}/{NextCursor}", - search.ToUrl(1000, NextCursor) + _search.ToUrl(1000, NextCursor) ); } @@ -70,27 +70,26 @@ public void TestSearchUrlWithCustomTtlAndNextCursorSetFromTheClass() { Assert.AreEqual( $"{SearchUrlPrefix}/{Ttl1000Sig}/1000/{B64Query}/{NextCursor}", - search.Ttl(1000).NextCursor(NextCursor).ToUrl() + _search.Ttl(1000).NextCursor(NextCursor).ToUrl() ); } [Test] public void TestSearchUrlPrivateCdn() { - cloudinary.Api.UsePrivateCdn = true; + _cloudinary.Api.UsePrivateCdn = true; Assert.AreEqual( $"https://test123-res.cloudinary.com/search/{Ttl300Sig}/300/{B64Query}", - cloudinary.Search().Expression(searchExpression).SortBy("public_id", "desc") + _cloudinary.Search().Expression(SearchExpression).SortBy("public_id", "desc") .MaxResults(30).ToUrl() ); } - [Test] public void TestShouldNotDuplicateValues() { - cloudinary + _cloudinary .Search() .SortBy("created_at", "asc") .SortBy("created_at", "desc") @@ -103,7 +102,7 @@ public void TestShouldNotDuplicateValues() .WithField("tags") .Execute(); - AssertCorrectRequest(cloudinary.HttpRequestContent); + AssertCorrectRequest(_cloudinary.HttpRequestContent); } private static void AssertCorrectRequest(string request) diff --git a/CloudinaryDotNet/Actions/Search/SearchFolder.cs b/CloudinaryDotNet/Actions/Search/SearchFolder.cs new file mode 100644 index 00000000..505f4b3c --- /dev/null +++ b/CloudinaryDotNet/Actions/Search/SearchFolder.cs @@ -0,0 +1,35 @@ +namespace CloudinaryDotNet.Actions +{ + using System.Runtime.Serialization; + + /// + /// The details of the folder found. + /// + [DataContract] + public class SearchFolder + { + /// + /// Gets or sets the name of the folder. + /// + [DataMember(Name = "name")] + public string Name; + + /// + /// Gets or sets the path of the folder. + /// + [DataMember(Name = "path")] + public string Path { get; set; } + + /// + /// Gets or sets date when the folder was created. + /// + [DataMember(Name = "created_at")] + public string CreatedAt { get; set; } + + /// + /// Gets or sets the eternal id of the folder. + /// + [DataMember(Name = "external_id")] + public string ExternalId { get; set; } + } +} diff --git a/CloudinaryDotNet/Actions/Search/SearchFoldersResult.cs b/CloudinaryDotNet/Actions/Search/SearchFoldersResult.cs new file mode 100644 index 00000000..92b14507 --- /dev/null +++ b/CloudinaryDotNet/Actions/Search/SearchFoldersResult.cs @@ -0,0 +1,18 @@ +namespace CloudinaryDotNet.Actions +{ + using System.Collections.Generic; + using System.Runtime.Serialization; + + /// + /// Search response with information about the folders matching the search criteria. + /// + [DataContract] + public class SearchFoldersResult : SearchResultBase + { + /// + /// Gets or sets the details of each of the folders found. + /// + [DataMember(Name = "folders")] + public List Folders { get; set; } + } +} diff --git a/CloudinaryDotNet/Actions/Search/SearchResource.cs b/CloudinaryDotNet/Actions/Search/SearchResource.cs index ea04146e..7afe0d18 100644 --- a/CloudinaryDotNet/Actions/Search/SearchResource.cs +++ b/CloudinaryDotNet/Actions/Search/SearchResource.cs @@ -84,8 +84,8 @@ public class SearchResource [Obsolete("Property Created is deprecated, please use CreatedAt instead")] public string Created { - get { return CreatedAt; } - set { CreatedAt = value; } + get => CreatedAt; + set => CreatedAt = value; } /// @@ -100,8 +100,8 @@ public string Created [Obsolete("Property Uploaded is deprecated, please use UploadedAt instead")] public string Uploaded { - get { return UploadedAt; } - set { UploadedAt = value; } + get => UploadedAt; + set => UploadedAt = value; } /// @@ -116,8 +116,8 @@ public string Uploaded [Obsolete("Property Length is deprecated, please use Bytes instead")] public long Length { - get { return Bytes; } - set { Bytes = value; } + get => Bytes; + set => Bytes = value; } /// diff --git a/CloudinaryDotNet/Actions/Search/SearchResult.cs b/CloudinaryDotNet/Actions/Search/SearchResult.cs index 684c4326..e5a719fa 100644 --- a/CloudinaryDotNet/Actions/Search/SearchResult.cs +++ b/CloudinaryDotNet/Actions/Search/SearchResult.cs @@ -7,33 +7,14 @@ /// Search response with information about the assets matching the search criteria. /// [DataContract] - public class SearchResult : BaseResult + public class SearchResult : SearchResultBase { - /// - /// Gets or sets the total count of assets matching the search criteria. - /// - [DataMember(Name = "total_count")] - public int TotalCount { get; set; } - - /// - /// Gets or sets the time taken to process the request. - /// - [DataMember(Name = "time")] - public long Time { get; set; } - /// /// Gets or sets the details of each of the assets (resources) found. /// [DataMember(Name = "resources")] public List Resources { get; set; } - /// - /// Gets or sets when a search request has more results to return than max_results, the next_cursor value is returned as - /// part of the response. - /// - [DataMember(Name = "next_cursor")] - public string NextCursor { get; set; } - /// /// Gets or sets counts of assets, grouped by specified parameters. /// diff --git a/CloudinaryDotNet/Actions/Search/SearchResultBase.cs b/CloudinaryDotNet/Actions/Search/SearchResultBase.cs new file mode 100644 index 00000000..f46c521d --- /dev/null +++ b/CloudinaryDotNet/Actions/Search/SearchResultBase.cs @@ -0,0 +1,29 @@ +namespace CloudinaryDotNet.Actions +{ + using System.Runtime.Serialization; + + /// + /// Search response with information matching the search criteria. + /// + public class SearchResultBase : BaseResult + { + /// + /// Gets or sets the total count of assets matching the search criteria. + /// + [DataMember(Name = "total_count")] + public int TotalCount { get; set; } + + /// + /// Gets or sets the time taken to process the request. + /// + [DataMember(Name = "time")] + public long Time { get; set; } + + /// + /// Gets or sets when a search request has more results to return than max_results, the next_cursor value is returned as + /// part of the response. + /// + [DataMember(Name = "next_cursor")] + public string NextCursor { get; set; } + } +} diff --git a/CloudinaryDotNet/Cloudinary.AdminApi.cs b/CloudinaryDotNet/Cloudinary.AdminApi.cs index 54cadc30..914d4f0e 100644 --- a/CloudinaryDotNet/Cloudinary.AdminApi.cs +++ b/CloudinaryDotNet/Cloudinary.AdminApi.cs @@ -23,6 +23,15 @@ public Search Search() return new Search(m_api); } + /// + /// Gets the advanced search folders provider used by the Cloudinary instance. + /// + /// Instance of the class. + public SearchFolders SearchFolders() + { + return new SearchFolders(m_api); + } + /// /// Lists resource types asynchronously. /// diff --git a/CloudinaryDotNet/Search/Search.cs b/CloudinaryDotNet/Search/Search.cs new file mode 100644 index 00000000..cae886a8 --- /dev/null +++ b/CloudinaryDotNet/Search/Search.cs @@ -0,0 +1,18 @@ +namespace CloudinaryDotNet +{ + /// + /// Advanced search provider. Allows you to retrieve information on all the assets in your account with the help of + /// query expressions in a Lucene-like query language. + /// + public class Search : SearchFluent + { + /// + /// Initializes a new instance of the class. + /// + /// Provider of the API calls. + public Search(ApiShared api) + : base(api) + { + } + } +} diff --git a/CloudinaryDotNet/Search.cs b/CloudinaryDotNet/Search/SearchBaseFluent.cs similarity index 58% rename from CloudinaryDotNet/Search.cs rename to CloudinaryDotNet/Search/SearchBaseFluent.cs index b7dc2722..931b67c1 100644 --- a/CloudinaryDotNet/Search.cs +++ b/CloudinaryDotNet/Search/SearchBaseFluent.cs @@ -2,50 +2,48 @@ { using System.Collections.Generic; using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using CloudinaryDotNet.Actions; /// /// Advanced search provider. Allows you to retrieve information on all the assets in your account with the help of /// query expressions in a Lucene-like query language. /// - public class Search + /// The type. + public abstract class SearchBaseFluent + where T : SearchBaseFluent { + /// + /// The API provider. + /// + protected ApiShared api; + private List> sortByParam; private List aggregateParam; private List withFieldParam; private Dictionary searchParams; - private int urlTtl = 300; - private ApiShared m_api; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Provider of the API calls. - public Search(ApiShared api) + public SearchBaseFluent(ApiShared api) { - m_api = api; + this.api = api; searchParams = new Dictionary(); sortByParam = new List>(); aggregateParam = new List(); withFieldParam = new List(); } - private Url SearchResourcesUrl => m_api?.ApiUrlV? - .Add("resources") - .Add("search"); - /// /// The (Lucene-like) string expression specifying the search query. If this parameter is not provided then all /// resources are listed (up to max_results). /// /// Search query expression. /// The search provider with search query defined. - public Search Expression(string value) + public T Expression(string value) { searchParams.Add("expression", value); - return this; + return (T)this; } /// @@ -53,10 +51,10 @@ public Search Expression(string value) /// /// Number of results to return. /// The search provider with maximum number of results defined. - public Search MaxResults(int value) + public T MaxResults(int value) { searchParams.Add("max_results", value); - return this; + return (T)this; } /// @@ -64,10 +62,10 @@ public Search MaxResults(int value) /// /// The value of NextCursor. /// The search provider with next cursor defined. - public Search NextCursor(string value) + public T NextCursor(string value) { searchParams.Add("next_cursor", value); - return this; + return (T)this; } /// @@ -75,10 +73,10 @@ public Search NextCursor(string value) /// /// The value of Direction. /// The search provider with direction defined. - public Search Direction(string value) + public T Direction(string value) { searchParams.Add("direction", value); - return this; + return (T)this; } /// @@ -87,10 +85,10 @@ public Search Direction(string value) /// /// The name of field. /// The search provider with aggregation field defined. - public Search Aggregate(string field) + public T Aggregate(string field) { aggregateParam.Add(field); - return this; + return (T)this; } /// @@ -99,10 +97,10 @@ public Search Aggregate(string field) /// /// The name of field. /// The search provider with additional asset attribute defined. - public Search WithField(string field) + public T WithField(string field) { withFieldParam.Add(field); - return this; + return (T)this; } /// @@ -112,25 +110,12 @@ public Search WithField(string field) /// The field to sort by. /// The direction. /// The search provider with sort parameter defined. - public Search SortBy(string field, string dir) + public T SortBy(string field, string dir) { - Dictionary sortBucket = new Dictionary(); - sortBucket.Add(field, dir); + var sortBucket = new Dictionary { { field, dir } }; sortByParam.Add(sortBucket); - return this; - } - - /// - /// Sets the time to live of the search URL. - /// - /// The time to live in seconds. - /// The search provider with TTL defined. - public Search Ttl(int ttl) - { - this.urlTtl = ttl; - - return this; + return (T)this; } /// @@ -160,56 +145,16 @@ public Dictionary ToQuery() } /// - /// Execute search request. + /// Prepares search params. /// - /// Search response with information about the assets matching the search criteria. - public SearchResult Execute() + /// Updated search params. + protected SortedDictionary PrepareSearchParams() { - return m_api.CallAndParse( - HttpMethod.POST, - SearchResourcesUrl.BuildUrl(), - PrepareSearchParams(), - null, - Utils.PrepareJsonHeaders()); - } - - /// - /// Execute search request asynchronously. - /// - /// (Optional) Cancellation token. - /// Search response with information about the assets matching the search criteria. - public Task ExecuteAsync(CancellationToken? cancellationToken = null) - { - return m_api.CallAndParseAsync( - HttpMethod.POST, - SearchResourcesUrl.BuildUrl(), - PrepareSearchParams(), - null, - Utils.PrepareJsonHeaders(), - cancellationToken); - } - - /// - /// Creates a signed Search URL that can be used on the client side. - /// - /// The time to live in seconds. - /// Starting position. - /// The resulting search URL. - public string ToUrl(int? ttl = null, string nextCursor = null) - { - if (ttl == null) + var sParams = new SortedDictionary(ToQuery()) { - ttl = urlTtl; - } - - return m_api.Url.BuildSearchUrl(ToQuery(), (int)ttl, nextCursor); - } - - private SortedDictionary PrepareSearchParams() - { - SortedDictionary sParams = new SortedDictionary(ToQuery()); - sParams.Add("unsigned", string.Empty); - sParams.Add("removeUnsignedParam", string.Empty); + { "unsigned", string.Empty }, + { "removeUnsignedParam", string.Empty }, + }; return sParams; } diff --git a/CloudinaryDotNet/Search/SearchFluent.cs b/CloudinaryDotNet/Search/SearchFluent.cs new file mode 100644 index 00000000..9cf14f49 --- /dev/null +++ b/CloudinaryDotNet/Search/SearchFluent.cs @@ -0,0 +1,88 @@ +namespace CloudinaryDotNet +{ + using System.Threading; + using System.Threading.Tasks; + using CloudinaryDotNet.Actions; + + /// + /// Advanced search provider. Allows you to retrieve information on all the assets in your account with the help of + /// query expressions in a Lucene-like query language. + /// + /// The type. + public class SearchFluent : SearchBaseFluent + where T : SearchFluent + { + private int urlTtl = 300; + + /// + /// Initializes a new instance of the class. + /// + /// Provider of the API calls. + public SearchFluent(ApiShared api) + : base(api) + { + } + + private Url SearchResourcesUrl => api?.ApiUrlV? + .Add("resources") + .Add("search"); + + /// + /// Sets the time to live of the search URL. + /// + /// The time to live in seconds. + /// The search provider with TTL defined. + public T Ttl(int ttl) + { + urlTtl = ttl; + + return (T)this; + } + + /// + /// Execute search request. + /// + /// Search response with information about the assets matching the search criteria. + public SearchResult Execute() + { + return api.CallAndParse( + HttpMethod.POST, + SearchResourcesUrl.BuildUrl(), + PrepareSearchParams(), + null, + Utils.PrepareJsonHeaders()); + } + + /// + /// Execute search request asynchronously. + /// + /// (Optional) Cancellation token. + /// Search response with information about the assets matching the search criteria. + public Task ExecuteAsync(CancellationToken? cancellationToken = null) + { + return api.CallAndParseAsync( + HttpMethod.POST, + SearchResourcesUrl.BuildUrl(), + PrepareSearchParams(), + null, + Utils.PrepareJsonHeaders(), + cancellationToken); + } + + /// + /// Creates a signed Search URL that can be used on the client side. + /// + /// The time to live in seconds. + /// Starting position. + /// The resulting search URL. + public string ToUrl(int? ttl = null, string nextCursor = null) + { + if (ttl == null) + { + ttl = urlTtl; + } + + return api.Url.BuildSearchUrl(ToQuery(), (int)ttl, nextCursor); + } + } +} diff --git a/CloudinaryDotNet/Search/SearchFolders.cs b/CloudinaryDotNet/Search/SearchFolders.cs new file mode 100644 index 00000000..2b7092d3 --- /dev/null +++ b/CloudinaryDotNet/Search/SearchFolders.cs @@ -0,0 +1,18 @@ +namespace CloudinaryDotNet +{ + /// + /// Advanced search provider. Allows you to retrieve information on all the folders in your account with the help of + /// query expressions in a Lucene-like query language. + /// + public class SearchFolders : SearchFoldersFluent + { + /// + /// Initializes a new instance of the class. + /// + /// Provider of the API calls. + public SearchFolders(ApiShared api) + : base(api) + { + } + } +} diff --git a/CloudinaryDotNet/Search/SearchFoldersFluent.cs b/CloudinaryDotNet/Search/SearchFoldersFluent.cs new file mode 100644 index 00000000..0a37370b --- /dev/null +++ b/CloudinaryDotNet/Search/SearchFoldersFluent.cs @@ -0,0 +1,58 @@ +namespace CloudinaryDotNet +{ + using System.Threading; + using System.Threading.Tasks; + using CloudinaryDotNet.Actions; + + /// + /// Advanced search provider. Allows you to retrieve information on all the folder in your account with the help of + /// query expressions in a Lucene-like query language. + /// + /// The type. + public class SearchFoldersFluent : SearchBaseFluent + where T : SearchFoldersFluent + { + /// + /// Initializes a new instance of the class. + /// + /// Provider of the API calls. + public SearchFoldersFluent(ApiShared api) + : base(api) + { + } + + private Url SearchFoldersUrl => api?.ApiUrlV? + .Add("folders") + .Add("search"); + + /// + /// Execute search request. + /// + /// Search response with information about the assets matching the search criteria. + public SearchFoldersResult Execute() + { + return api.CallAndParse( + HttpMethod.POST, + SearchFoldersUrl.BuildUrl(), + PrepareSearchParams(), + null, + Utils.PrepareJsonHeaders()); + } + + /// + /// Execute search request asynchronously. + /// + /// (Optional) Cancellation token. + /// Search response with information about the assets matching the search criteria. + public Task ExecuteAsync(CancellationToken? cancellationToken = null) + { + return api.CallAndParseAsync( + HttpMethod.POST, + SearchFoldersUrl.BuildUrl(), + PrepareSearchParams(), + null, + Utils.PrepareJsonHeaders(), + cancellationToken); + } + } +}