diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f47ef2..e870720 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +### Version: 3.2.0 +#### Date: Aug-3-2026 + +##### Feat: +- Taxonomy Publishing CDA support + - `Taxonomies(uid)` scopes to a specific taxonomy: `.Fetch()`, `.Term(termUid)`, `.Terms()` + - `Taxonomies().Find()` — list all published taxonomies (`GET /taxonomies`) + - New `Term` class: `.Fetch()`, `.Locales()`, `.Ancestors()`, `.Descendants()` + - New `TermQuery` class: `.Find()` for listing terms within a taxonomy + - `Depth(int)` and `IncludeBranch()` on `Term`/`TermQuery` for hierarchy traversal control + - `Skip(int)`, `Limit(int)`, `IncludeCount()` on `TermQuery` for paginated term listing + - Added `Internals/TaxonomyRequestHelper` — shared request-building, header-merging, and error-parsing for `Taxonomy`/`Term`/`TermQuery` + +- Taxonomy / Term / TermQuery — localization support + - `SetLocale(string)` filters the taxonomy/term response to a specific locale (e.g. `"fr-fr"`) + - `IncludeFallback()` returns the master-locale (`en-us`) version when a term/taxonomy isn't translated into the requested locale, instead of omitting it + - Both compose correctly with hierarchy traversal — `Term(uid).SetLocale("fr-fr").IncludeFallback().Depth(2).Descendants()` returns a full localized subtree, with untranslated nodes individually falling back to master locale in the same response + - Fallback is per-node, not all-or-nothing: a single hierarchy fetch can return some terms translated and others fallen-back simultaneously — safe to use on partially-translated taxonomies + +--- + ### Version: 3.1.0 #### Date: Jul-20-2026 diff --git a/Contentstack.Core.Tests/Helpers/TestDataHelper.cs b/Contentstack.Core.Tests/Helpers/TestDataHelper.cs index 9499df5..e850af4 100644 --- a/Contentstack.Core.Tests/Helpers/TestDataHelper.cs +++ b/Contentstack.Core.Tests/Helpers/TestDataHelper.cs @@ -127,9 +127,21 @@ static TestDataHelper() /// /// Gets the taxonomy term for India state (e.g., "maharashtra") /// - public static string TaxIndiaState => + public static string TaxIndiaState => GetRequiredConfig("TAX_INDIA_STATE"); - + + /// + /// UID of the published taxonomy to use in localization tests (e.g. "gadgets"). + /// + public static string TaxPublishTaxonomyUid => + GetRequiredConfig("TAX_PUBLISH_TAXONOMY_UID"); + + /// + /// Locale code used for localized taxonomy/term delivery tests (e.g. "fr-fr"). + /// + public static string TaxPublishLocale => + GetRequiredConfig("TAX_PUBLISH_LOCALE"); + #endregion #region Live Preview diff --git a/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs new file mode 100644 index 0000000..82883de --- /dev/null +++ b/Contentstack.Core.Tests/Integration/Taxonomy/TaxonomyLocalisationTest.cs @@ -0,0 +1,565 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Contentstack.Core.Tests.Helpers; + +namespace Contentstack.Core.Tests.Integration.Taxonomy +{ + /// + /// Integration tests for localized taxonomy and term delivery (CDA). + /// Covers: Taxonomy.Fetch(locale), TermQuery.SetLocale/IncludeFallback/Find, + /// Term.Fetch(locale), Term.Locales, Term.Ancestors, Term.Descendants. + /// Stack: gadgets taxonomy, locales en-us / fr-fr. + /// + [Trait("Category", "TaxonomyLocalisation")] + public class TaxonomyLocalisationTest : IntegrationTestBase + { + public TaxonomyLocalisationTest(ITestOutputHelper output) : base(output) { } + + // ── 1. Fetch localized taxonomy ────────────────────────────────────── + + [Fact(DisplayName = "TaxPublish - Fetch taxonomy returns object with uid and name")] + public async Task Fetch_Taxonomy_MasterLocale_ReturnsValidObject() + { + LogArrange("Fetching taxonomy without locale (master)"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + + var client = CreateClient(); + + LogAct("Calling Taxonomies(uid).Fetch()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Fetch(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result["uid"]?.ToString()); + Assert.Equal(TestDataHelper.TaxPublishTaxonomyUid, result["uid"]?.ToString()); + } + + [Fact(DisplayName = "TaxPublish - Fetch taxonomy with locale returns localized name")] + public async Task Fetch_Taxonomy_WithLocale_ReturnsLocalizedName() + { + LogArrange("Fetching taxonomy with locale"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + var client = CreateClient(); + + LogAct("Calling Taxonomies(uid).SetLocale(locale).Fetch()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .SetLocale(TestDataHelper.TaxPublishLocale) + .Fetch(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result["uid"]?.ToString()); + Assert.NotNull(result["name"]?.ToString()); + } + + // ── 2. Find all terms ───────────────────────────────────────────────── + + [Fact(DisplayName = "TaxPublish - Find all terms without locale returns master-locale terms")] + public async Task Find_AllTerms_ReturnsCollection() + { + LogArrange("Fetching all terms in master locale (no locale filter)"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + + var client = CreateClient(); + + LogAct("Calling Taxonomies(uid).Terms().Find()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result.Items); + Assert.True(result.Items.Any()); + } + + // ── 3. Find terms with locale ───────────────────────────────────────── + + [Fact(DisplayName = "TaxPublish - Find terms with locale returns localized terms")] + public async Task Find_Terms_WithLocale_ReturnsLocalizedTerms() + { + LogArrange("Finding terms with locale"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + var client = CreateClient(); + + LogAct("Calling Terms().SetLocale(locale).Find()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .SetLocale(TestDataHelper.TaxPublishLocale) + .Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result.Items); + } + + [Fact(DisplayName = "TaxPublish - Find terms with locale and fallback returns terms")] + public async Task Find_Terms_WithLocaleAndFallback_ReturnsTerms() + { + LogArrange("Finding terms with locale and include_fallback"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + var client = CreateClient(); + + LogAct("Calling Terms().SetLocale(locale).IncludeFallback().Find()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .SetLocale(TestDataHelper.TaxPublishLocale) + .IncludeFallback() + .Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result.Items); + Assert.True(result.Items.Any()); + } + + // ── 4–7. Single term methods ────────────────────────────────────────── + + /// + /// Fetches the first available term UID from the gadgets taxonomy to use in subsequent tests. + /// + private async Task GetFirstTermUidAsync(ContentstackClient client) + { + var terms = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .SetLocale(TestDataHelper.TaxPublishLocale) + .IncludeFallback() + .Find(); + + return terms?.Items?.FirstOrDefault()?["uid"]?.ToString(); + } + + [Fact(DisplayName = "TaxPublish - Fetch single term with locale returns localized term")] + public async Task Fetch_SingleTerm_WithLocale_ReturnsLocalizedTerm() + { + var client = CreateClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching single term with locale"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("TermUid", termUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + LogAct("Calling Term(termUid).SetLocale(locale).IncludeFallback().Fetch()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .SetLocale(TestDataHelper.TaxPublishLocale) + .IncludeFallback() + .Fetch(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result["uid"]?.ToString()); + Assert.Equal(termUid, result["uid"]?.ToString()); + } + + [Fact(DisplayName = "TaxPublish - Term.Locales returns locales collection")] + public async Task Term_Locales_ReturnsLocalesCollection() + { + var client = CreateClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching all locales for a term"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("TermUid", termUid); + + LogAct("Calling Term(termUid).Locales()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .Locales(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + } + + [Fact(DisplayName = "TaxPublish - Term.Ancestors returns ancestors collection")] + public async Task Term_Ancestors_ReturnsAncestorsCollection() + { + var client = CreateClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching ancestors for a term"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("TermUid", termUid); + + LogAct("Calling Term(termUid).Ancestors()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .Ancestors(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + } + + [Fact(DisplayName = "TaxPublish - Term.Descendants returns descendants collection")] + public async Task Term_Descendants_ReturnsDescendantsCollection() + { + var client = CreateClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching descendants for a term"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + LogContext("TermUid", termUid); + + LogAct("Calling Term(termUid).Descendants()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .Descendants(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + } + + // ── 8. Term hierarchy — Depth / IncludeBranch ──────────────────────── + + /// + /// Walks the gadgets taxonomy to find a term with at least one descendant that itself + /// has a descendant — i.e. a parent/child/grandchild chain — for hierarchy-depth tests. + /// Returns (null, null, null) if no such chain exists in the fixture data. + /// + private async Task<(string parentUid, string childUid, string grandchildUid)> GetTermHierarchyAsync(ContentstackClient client) + { + var terms = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .Find(); + + foreach (var candidateParent in terms.Items) + { + var parentUid = candidateParent["uid"]?.ToString(); + if (string.IsNullOrEmpty(parentUid)) continue; + + var children = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(parentUid) + .Descendants(); + var childUid = children?.FirstOrDefault()?["uid"]?.ToString(); + if (string.IsNullOrEmpty(childUid)) continue; + + var grandchildren = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(childUid) + .Descendants(); + var grandchildUid = grandchildren?.FirstOrDefault()?["uid"]?.ToString(); + if (string.IsNullOrEmpty(grandchildUid)) continue; + + return (parentUid, childUid, grandchildUid); + } + return (null, null, null); + } + + [Fact(DisplayName = "TaxPublish - Term.Descendants with depth=1 returns only direct children")] + public async Task Term_Descendants_WithDepth1_ReturnsOnlyDirectChildren() + { + var client = CreateClient(); + var (parentUid, childUid, grandchildUid) = await GetTermHierarchyAsync(client); + + if (string.IsNullOrEmpty(parentUid)) + { + Output.WriteLine("No parent/child/grandchild term chain found — skipping test."); + return; + } + + LogArrange("Fetching direct-child-only descendants (depth=1)"); + LogContext("ParentTermUid", parentUid); + + LogAct("Calling Term(parentUid).Depth(1).Descendants()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(parentUid) + .Depth(1) + .Descendants(); + + LogAssert("Verifying only the direct child is present, not the grandchild"); + Assert.NotNull(result); + var uids = result.Select(t => t["uid"]?.ToString()).ToList(); + Assert.Contains(childUid, uids); + Assert.DoesNotContain(grandchildUid, uids); + } + + [Fact(DisplayName = "TaxPublish - Term.Descendants with depth=2 includes grandchildren")] + public async Task Term_Descendants_WithDepth2_IncludesGrandchildren() + { + var client = CreateClient(); + var (parentUid, childUid, grandchildUid) = await GetTermHierarchyAsync(client); + + if (string.IsNullOrEmpty(parentUid)) + { + Output.WriteLine("No parent/child/grandchild term chain found — skipping test."); + return; + } + + LogArrange("Fetching descendants two levels deep (depth=2)"); + LogContext("ParentTermUid", parentUid); + + LogAct("Calling Term(parentUid).Depth(2).Descendants()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(parentUid) + .Depth(2) + .Descendants(); + + LogAssert("Verifying both the child and grandchild are present"); + Assert.NotNull(result); + var uids = result.Select(t => t["uid"]?.ToString()).ToList(); + Assert.Contains(childUid, uids); + Assert.Contains(grandchildUid, uids); + } + + [Fact(DisplayName = "TaxPublish - Term.Ancestors for a grandchild term returns the full parent chain")] + public async Task Term_Ancestors_ForGrandchildTerm_ReturnsFullChain() + { + var client = CreateClient(); + var (parentUid, childUid, grandchildUid) = await GetTermHierarchyAsync(client); + + if (string.IsNullOrEmpty(grandchildUid)) + { + Output.WriteLine("No parent/child/grandchild term chain found — skipping test."); + return; + } + + LogArrange("Fetching ancestors for the grandchild term"); + LogContext("GrandchildTermUid", grandchildUid); + + LogAct("Calling Term(grandchildUid).Ancestors()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(grandchildUid) + .Ancestors(); + + LogAssert("Verifying both the parent and child (grandparent chain) are present"); + Assert.NotNull(result); + var uids = result.Select(t => t["uid"]?.ToString()).ToList(); + Assert.Contains(childUid, uids); + Assert.Contains(parentUid, uids); + } + + [Fact(DisplayName = "TaxPublish - TermQuery.Find with depth limits the returned hierarchy")] + public async Task TermQuery_Find_WithDepth_LimitsHierarchyDepth() + { + var client = CreateClient(); + + LogArrange("Finding terms with depth=1"); + LogContext("TaxonomyUid", TestDataHelper.TaxPublishTaxonomyUid); + + LogAct("Calling Terms().Depth(1).Find()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Terms() + .Depth(1) + .Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result.Items); + } + + [Fact(DisplayName = "TaxPublish - Term.Fetch with IncludeBranch returns branch info")] + public async Task Term_Fetch_WithIncludeBranch_ReturnsBranchInfo() + { + var client = CreateClient(); + var termUid = await GetFirstTermUidAsync(client); + + if (string.IsNullOrEmpty(termUid)) + { + Output.WriteLine("No term UID found — skipping test."); + return; + } + + LogArrange("Fetching a term with branch info included"); + LogContext("TermUid", termUid); + + LogAct("Calling Term(termUid).IncludeBranch().Fetch()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(termUid) + .IncludeBranch() + .Fetch(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result["uid"]?.ToString()); + } + + [Fact(DisplayName = "TaxPublish - Term.Descendants with locale and fallback returns localized child+grandchild hierarchy")] + public async Task Term_Descendants_WithLocaleAndFallback_ReturnsLocalizedHierarchy() + { + var client = CreateClient(); + var (parentUid, childUid, grandchildUid) = await GetTermHierarchyAsync(client); + + if (string.IsNullOrEmpty(parentUid)) + { + Output.WriteLine("No parent/child/grandchild term chain found — skipping test."); + return; + } + + LogArrange("Fetching localized descendants (depth=2) with fallback, down to grandchild level"); + LogContext("ParentTermUid", parentUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + LogAct("Calling Term(parentUid).SetLocale(locale).IncludeFallback().Depth(2).Descendants()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(parentUid) + .SetLocale(TestDataHelper.TaxPublishLocale) + .IncludeFallback() + .Depth(2) + .Descendants(); + + LogAssert("Verifying both child and grandchild are present, each localized or correctly fallen back"); + Assert.NotNull(result); + var uids = result.Select(t => t["uid"]?.ToString()).ToList(); + Assert.Contains(childUid, uids); + Assert.Contains(grandchildUid, uids); + + foreach (var term in result) + { + var locale = term["locale"]?.ToString(); + Assert.True( + locale == TestDataHelper.TaxPublishLocale || locale == "en-us", + $"Term '{term["uid"]}' returned unexpected locale '{locale}' - expected '{TestDataHelper.TaxPublishLocale}' (translated) or 'en-us' (fallback)."); + } + } + + [Fact(DisplayName = "TaxPublish - Term.Ancestors with locale and fallback returns localized ancestor chain")] + public async Task Term_Ancestors_WithLocaleAndFallback_ReturnsLocalizedChain() + { + var client = CreateClient(); + var (parentUid, childUid, grandchildUid) = await GetTermHierarchyAsync(client); + + if (string.IsNullOrEmpty(grandchildUid)) + { + Output.WriteLine("No parent/child/grandchild term chain found — skipping test."); + return; + } + + LogArrange("Fetching localized ancestors for the grandchild term, with fallback"); + LogContext("GrandchildTermUid", grandchildUid); + LogContext("Locale", TestDataHelper.TaxPublishLocale); + + LogAct("Calling Term(grandchildUid).SetLocale(locale).IncludeFallback().Ancestors()"); + var result = await client + .Taxonomies(TestDataHelper.TaxPublishTaxonomyUid) + .Term(grandchildUid) + .SetLocale(TestDataHelper.TaxPublishLocale) + .IncludeFallback() + .Ancestors(); + + LogAssert("Verifying both parent and child (the ancestor chain) are present and correctly localized/fallen back"); + Assert.NotNull(result); + var uids = result.Select(t => t["uid"]?.ToString()).ToList(); + Assert.Contains(childUid, uids); + Assert.Contains(parentUid, uids); + + foreach (var term in result) + { + var locale = term["locale"]?.ToString(); + Assert.True( + locale == TestDataHelper.TaxPublishLocale || locale == "en-us", + $"Term '{term["uid"]}' returned unexpected locale '{locale}' - expected '{TestDataHelper.TaxPublishLocale}' (translated) or 'en-us' (fallback)."); + } + } + + // ── 9. List all taxonomies ──────────────────────────────────────────── + + [Fact(DisplayName = "TaxPublish - List all taxonomies returns a collection")] + public async Task List_AllTaxonomies_ReturnsCollection() + { + var client = CreateClient(); + + LogArrange("Listing all published taxonomies"); + + LogAct("Calling Taxonomies().Find()"); + var result = await client + .Taxonomies() + .Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + Assert.NotNull(result.Items); + Assert.True(result.Items.Any()); + } + + [Fact(DisplayName = "TaxPublish - List all taxonomies with skip/limit returns a paged subset")] + public async Task List_AllTaxonomies_WithSkipAndLimit_ReturnsPagedSubset() + { + var client = CreateClient(); + + LogArrange("Listing taxonomies with skip/limit"); + + LogAct("Calling Taxonomies().AddParam(\"skip\",\"0\").AddParam(\"limit\",\"1\").Find()"); + var result = await client + .Taxonomies() + .AddParam("skip", "0") + .AddParam("limit", "1") + .Find(); + + LogAssert("Verifying response is limited to at most one item"); + Assert.NotNull(result); + Assert.True(result.Items.Count() <= 1); + } + + [Fact(DisplayName = "TaxPublish - List all taxonomies with include_count returns a count")] + public async Task List_AllTaxonomies_WithIncludeCount_ReturnsCount() + { + var client = CreateClient(); + + LogArrange("Listing taxonomies with include_count"); + + LogAct("Calling Taxonomies().AddParam(\"include_count\",\"true\").Find()"); + var result = await client + .Taxonomies() + .AddParam("include_count", "true") + .Find(); + + LogAssert("Verifying response"); + Assert.NotNull(result); + } + } +} diff --git a/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs b/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs index f4f5e92..9ddbe73 100644 --- a/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs +++ b/Contentstack.Core.Unit.Tests/TaxonomyUnitTests.cs @@ -284,62 +284,55 @@ public void Below_WithDoubleValue_AddsQueryParameter() } [Fact] - public void Taxonomy_GetHeader_WithLocalHeaders_ReturnsMergedHeaders() + public void TaxonomyRequestHelper_GetHeader_WithLocalHeaders_ReturnsMergedHeaders() { // Arrange - var taxonomy = CreateTaxonomy(); - var type = typeof(Taxonomy); - var getHeaderMethod = type.GetMethod("GetHeader", BindingFlags.NonPublic | BindingFlags.Instance); - + var stackHeaders = new Dictionary { { "stack-header", "stack-value" } }; var localHeaders = new Dictionary { { "custom-header", "value1" } }; - + // Act - var result = getHeaderMethod?.Invoke(taxonomy, new object[] { localHeaders }) as Dictionary; + var result = TaxonomyRequestHelper.GetHeader(stackHeaders, localHeaders); // Assert Assert.NotNull(result); } [Fact] - public void Taxonomy_GetHeader_WithNullLocalHeaders_ReturnsStackHeaders() + public void TaxonomyRequestHelper_GetHeader_WithNullLocalHeaders_ReturnsStackHeaders() { // Arrange - var taxonomy = CreateTaxonomy(); - var type = typeof(Taxonomy); - var getHeaderMethod = type.GetMethod("GetHeader", BindingFlags.NonPublic | BindingFlags.Instance); - + var stackHeaders = new Dictionary { { "stack-header", "stack-value" } }; + // Act - var result = getHeaderMethod?.Invoke(taxonomy, new object[] { null }) as Dictionary; + var result = TaxonomyRequestHelper.GetHeader(stackHeaders, null); // Assert Assert.NotNull(result); + Assert.Same(stackHeaders, result); } [Fact] - public void Taxonomy_GetHeader_WithEmptyLocalHeaders_ReturnsStackHeaders() + public void TaxonomyRequestHelper_GetHeader_WithEmptyLocalHeaders_ReturnsStackHeaders() { // Arrange - var taxonomy = CreateTaxonomy(); - var type = typeof(Taxonomy); - var getHeaderMethod = type.GetMethod("GetHeader", BindingFlags.NonPublic | BindingFlags.Instance); - + var stackHeaders = new Dictionary { { "stack-header", "stack-value" } }; + // Act - var result = getHeaderMethod?.Invoke(taxonomy, new object[] { new Dictionary() }) as Dictionary; + var result = TaxonomyRequestHelper.GetHeader(stackHeaders, new Dictionary()); // Assert Assert.NotNull(result); + Assert.Same(stackHeaders, result); } [Fact] - public void Taxonomy_GetContentstackError_WithWebException_ReturnsContentstackException() + public void TaxonomyRequestHelper_GetContentstackError_WithWebException_ReturnsContentstackException() { // Arrange - var type = typeof(Taxonomy); - var getContentstackErrorMethod = type.GetMethod("GetContentstackError", BindingFlags.NonPublic | BindingFlags.Static); var webEx = new System.Net.WebException("Test exception"); // Act - var result = getContentstackErrorMethod?.Invoke(null, new object[] { webEx }) as ContentstackException; + var result = TaxonomyRequestHelper.GetContentstackError(webEx); // Assert Assert.NotNull(result); @@ -347,15 +340,13 @@ public void Taxonomy_GetContentstackError_WithWebException_ReturnsContentstackEx } [Fact] - public void Taxonomy_GetContentstackError_WithGenericException_ReturnsContentstackException() + public void TaxonomyRequestHelper_GetContentstackError_WithGenericException_ReturnsContentstackException() { // Arrange - var type = typeof(Taxonomy); - var getContentstackErrorMethod = type.GetMethod("GetContentstackError", BindingFlags.NonPublic | BindingFlags.Static); var ex = new Exception("Test exception"); // Act - var result = getContentstackErrorMethod?.Invoke(null, new object[] { ex }) as ContentstackException; + var result = TaxonomyRequestHelper.GetContentstackError(ex); // Assert Assert.NotNull(result); @@ -397,21 +388,13 @@ public void Taxonomy_Find_Setup_VerifiesQueryParameters() } [Fact] - public void Taxonomy_GetHeader_WithLocalHeaderAndEmptyStackHeaders_ReturnsLocalHeader() + public void TaxonomyRequestHelper_GetHeader_WithLocalHeaderAndEmptyStackHeaders_ReturnsLocalHeader() { // Arrange - var taxonomy = CreateTaxonomy(); - var getHeaderMethod = typeof(Taxonomy).GetMethod("GetHeader", - BindingFlags.NonPublic | BindingFlags.Instance); var localHeader = new Dictionary { { "custom", "value" } }; - - // Set _StackHeaders to empty dictionary - var stackHeadersField = typeof(Taxonomy).GetField("_StackHeaders", - BindingFlags.NonPublic | BindingFlags.Instance); - stackHeadersField?.SetValue(taxonomy, new Dictionary()); // Act - var result = getHeaderMethod?.Invoke(taxonomy, new object[] { localHeader }) as Dictionary; + var result = TaxonomyRequestHelper.GetHeader(new Dictionary(), localHeader); // Assert Assert.NotNull(result); @@ -419,16 +402,14 @@ public void Taxonomy_GetHeader_WithLocalHeaderAndEmptyStackHeaders_ReturnsLocalH } [Fact] - public void Taxonomy_GetHeader_WithOverlappingKeys_LocalHeaderTakesPrecedence() + public void TaxonomyRequestHelper_GetHeader_WithOverlappingKeys_LocalHeaderTakesPrecedence() { // Arrange - var taxonomy = CreateTaxonomy(); - var getHeaderMethod = typeof(Taxonomy).GetMethod("GetHeader", - BindingFlags.NonPublic | BindingFlags.Instance); + var stackHeaders = new Dictionary { { "custom", "stack_value" } }; var localHeader = new Dictionary { { "custom", "local_value" } }; // Act - var result = getHeaderMethod?.Invoke(taxonomy, new object[] { localHeader }) as Dictionary; + var result = TaxonomyRequestHelper.GetHeader(stackHeaders, localHeader); // Assert Assert.NotNull(result); @@ -436,32 +417,29 @@ public void Taxonomy_GetHeader_WithOverlappingKeys_LocalHeaderTakesPrecedence() } [Fact] - public void Taxonomy_GetHeader_WithBothHeaders_ReturnsMergedHeaders() + public void TaxonomyRequestHelper_GetHeader_WithBothHeaders_ReturnsMergedHeaders() { // Arrange - var taxonomy = CreateTaxonomy(); - var getHeaderMethod = typeof(Taxonomy).GetMethod("GetHeader", - BindingFlags.NonPublic | BindingFlags.Instance); + var stackHeaders = new Dictionary { { "stack_key", "stack_value" } }; var localHeader = new Dictionary { { "local_key", "local_value" } }; // Act - var result = getHeaderMethod?.Invoke(taxonomy, new object[] { localHeader }) as Dictionary; + var result = TaxonomyRequestHelper.GetHeader(stackHeaders, localHeader); // Assert Assert.NotNull(result); Assert.True(result.ContainsKey("local_key")); + Assert.True(result.ContainsKey("stack_key")); } [Fact] - public void Taxonomy_GetContentstackError_WithWebExceptionContainingErrorCode_ExtractsErrorCode() + public void TaxonomyRequestHelper_GetContentstackError_WithWebExceptionContainingErrorCode_ExtractsErrorCode() { // Arrange - var type = typeof(Taxonomy); - var getContentstackErrorMethod = type.GetMethod("GetContentstackError", BindingFlags.NonPublic | BindingFlags.Static); var webEx = new System.Net.WebException("Test exception"); // Act - var result = getContentstackErrorMethod?.Invoke(null, new object[] { webEx }) as ContentstackException; + var result = TaxonomyRequestHelper.GetContentstackError(webEx); // Assert Assert.NotNull(result); @@ -563,6 +541,491 @@ public void Below_WithZeroValue_AddsQueryParameter() } #endregion + + #region Taxonomy UID Constructor Tests + + [Fact] + public void Taxonomies_WithUid_ReturnsScopedTaxonomy() + { + var taxonomy = _client.Taxonomies("gadgets"); + Assert.NotNull(taxonomy); + } + + [Fact] + public void Taxonomies_WithNullUid_ThrowsTaxonomyException() + { + Assert.Throws(() => _client.Taxonomies(null)); + } + + [Fact] + public void Taxonomies_WithEmptyUid_ThrowsTaxonomyException() + { + Assert.Throws(() => _client.Taxonomies("")); + } + + #endregion + + #region Taxonomy.Term() Tests + + [Fact] + public void Taxonomy_Term_WithoutUid_ThrowsTaxonomyException() + { + var taxonomy = _client.Taxonomies(); + Assert.Throws(() => taxonomy.Term("smartwatch")); + } + + [Fact] + public void Taxonomy_Term_WithUid_ReturnsTermInstance() + { + var taxonomy = _client.Taxonomies("gadgets"); + var term = taxonomy.Term("smartwatch"); + Assert.NotNull(term); + Assert.IsType(term); + } + + #endregion + + #region Taxonomy.Terms() Tests + + [Fact] + public void Taxonomy_Terms_WithoutUid_ThrowsTaxonomyException() + { + var taxonomy = _client.Taxonomies(); + Assert.Throws(() => taxonomy.Terms()); + } + + [Fact] + public void Taxonomy_Terms_WithUid_ReturnsTermQueryInstance() + { + var taxonomy = _client.Taxonomies("gadgets"); + var termQuery = taxonomy.Terms(); + Assert.NotNull(termQuery); + Assert.IsType(termQuery); + } + + #endregion + + #region TermQuery Tests + + [Fact] + public void TermQuery_SetLocale_SetsQueryParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.SetLocale("hi-in"); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("locale") ?? false); + Assert.Equal("hi-in", queryParams["locale"]); + } + + [Fact] + public void TermQuery_SetLocale_WithNullOrEmpty_IsNoOp() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.SetLocale(null); + termQuery.SetLocale(""); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.False(queryParams?.ContainsKey("locale") ?? false); + } + + [Fact] + public void TermQuery_IncludeFallback_SetsQueryParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.IncludeFallback(); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("include_fallback") ?? false); + Assert.Equal("true", queryParams["include_fallback"]); + } + + [Fact] + public void TermQuery_AddParam_SetsCustomQueryParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.AddParam("custom_key", "custom_value"); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("custom_key") ?? false); + Assert.Equal("custom_value", queryParams["custom_key"]); + } + + #endregion + + #region TermQuery.Depth / IncludeBranch / Skip / Limit / IncludeCount Tests + + [Fact] + public void TermQuery_Depth_ReturnsSelfForChaining() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + var result = termQuery.Depth(2); + Assert.Same(termQuery, result); + } + + [Fact] + public void TermQuery_Depth_SetsQueryParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.Depth(2); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("depth") ?? false); + Assert.Equal(2, queryParams["depth"]); + } + + [Fact] + public void TermQuery_IncludeBranch_ReturnsSelfForChaining() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + var result = termQuery.IncludeBranch(); + Assert.Same(termQuery, result); + } + + [Fact] + public void TermQuery_IncludeBranch_SetsQueryParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.IncludeBranch(); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("include_branch") ?? false); + Assert.Equal("true", queryParams["include_branch"]); + } + + [Fact] + public void TermQuery_Skip_SetsQueryParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.Skip(10); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("skip") ?? false); + Assert.Equal(10, queryParams["skip"]); + } + + [Fact] + public void TermQuery_Limit_SetsQueryParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.Limit(10); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("limit") ?? false); + Assert.Equal(10, queryParams["limit"]); + } + + [Fact] + public void TermQuery_IncludeCount_SetsQueryParam() + { + var termQuery = _client.Taxonomies("gadgets").Terms(); + termQuery.IncludeCount(); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("include_count") ?? false); + Assert.Equal("true", queryParams["include_count"]); + } + + [Fact] + public void TermQuery_Skip_Limit_IncludeCount_Depth_ChainAllTogether() + { + var termQuery = _client.Taxonomies("gadgets").Terms() + .Skip(0) + .Limit(10) + .IncludeCount() + .Depth(2) + .IncludeBranch(); + + var field = typeof(TermQuery).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(termQuery); + + Assert.True(queryParams?.ContainsKey("skip") ?? false); + Assert.True(queryParams?.ContainsKey("limit") ?? false); + Assert.True(queryParams?.ContainsKey("include_count") ?? false); + Assert.True(queryParams?.ContainsKey("depth") ?? false); + Assert.True(queryParams?.ContainsKey("include_branch") ?? false); + } + + #endregion + + #region Taxonomy.SetLocale / IncludeFallback / AddParam Tests + + [Fact] + public void Taxonomy_SetLocale_SetsUrlQuery() + { + var taxonomy = _client.Taxonomies("gadgets"); + taxonomy.SetLocale("hi-in"); + + var field = typeof(Taxonomy).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var urlQueries = (Dictionary)field?.GetValue(taxonomy); + + Assert.True(urlQueries?.ContainsKey("locale") ?? false); + Assert.Equal("hi-in", urlQueries["locale"]); + } + + [Fact] + public void Taxonomy_SetLocale_WithNullOrEmpty_IsNoOp() + { + var taxonomy = _client.Taxonomies("gadgets"); + taxonomy.SetLocale(null); + taxonomy.SetLocale(""); + + var field = typeof(Taxonomy).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var urlQueries = (Dictionary)field?.GetValue(taxonomy); + + Assert.False(urlQueries?.ContainsKey("locale") ?? false); + } + + [Fact] + public void Taxonomy_IncludeFallback_SetsUrlQuery() + { + var taxonomy = _client.Taxonomies("gadgets"); + taxonomy.IncludeFallback(); + + var field = typeof(Taxonomy).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var urlQueries = (Dictionary)field?.GetValue(taxonomy); + + Assert.True(urlQueries?.ContainsKey("include_fallback") ?? false); + } + + [Fact] + public void Taxonomy_AddParam_SetsUrlQuery() + { + var taxonomy = _client.Taxonomies("gadgets"); + taxonomy.AddParam("include_branch", "true"); + + var field = typeof(Taxonomy).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var urlQueries = (Dictionary)field?.GetValue(taxonomy); + + Assert.True(urlQueries?.ContainsKey("include_branch") ?? false); + } + + [Fact] + public void Taxonomy_SetLocale_Then_IncludeFallback_ChainsBoth() + { + var taxonomy = _client.Taxonomies("gadgets") + .SetLocale("hi-in") + .IncludeFallback(); + + var field = typeof(Taxonomy).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var urlQueries = (Dictionary)field?.GetValue(taxonomy); + + Assert.True(urlQueries?.ContainsKey("locale") ?? false); + Assert.True(urlQueries?.ContainsKey("include_fallback") ?? false); + } + + #endregion + + #region Taxonomy.Find Tests + + [Fact] + public async System.Threading.Tasks.Task Taxonomy_Find_OnScopedInstance_ThrowsTaxonomyException() + { + var taxonomy = _client.Taxonomies("gadgets"); + await Assert.ThrowsAsync(() => taxonomy.Find()); + } + + [Fact] + public void Taxonomy_Find_AddParam_SetsSkipLimitIncludeCountParams() + { + var taxonomy = _client.Taxonomies() + .AddParam("skip", "0") + .AddParam("limit", "10") + .AddParam("include_count", "true"); + + var field = typeof(Taxonomy).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var urlQueries = (Dictionary)field?.GetValue(taxonomy); + + Assert.True(urlQueries?.ContainsKey("skip") ?? false); + Assert.True(urlQueries?.ContainsKey("limit") ?? false); + Assert.True(urlQueries?.ContainsKey("include_count") ?? false); + } + + #endregion + + #region Term.SetLocale / IncludeFallback / AddParam Tests + + [Fact] + public void Term_SetLocale_SetsUrlQuery() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch"); + term.SetLocale("hi-in"); + + var field = typeof(Term).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(term); + + Assert.True(queryParams?.ContainsKey("locale") ?? false); + } + + [Fact] + public void Term_IncludeFallback_SetsUrlQuery() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch"); + term.IncludeFallback(); + + var field = typeof(Term).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(term); + + Assert.True(queryParams?.ContainsKey("include_fallback") ?? false); + } + + [Fact] + public void Term_AddParam_SetsUrlQuery() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch"); + term.AddParam("include_branch", "true"); + + var field = typeof(Term).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(term); + + Assert.True(queryParams?.ContainsKey("include_branch") ?? false); + } + + [Fact] + public void Term_SetLocale_Then_IncludeFallback_ChainsBoth() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch") + .SetLocale("hi-in") + .IncludeFallback(); + + var field = typeof(Term).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(term); + + Assert.True(queryParams?.ContainsKey("locale") ?? false); + Assert.True(queryParams?.ContainsKey("include_fallback") ?? false); + } + + #endregion + + #region Term.Depth / IncludeBranch Tests + + [Fact] + public void Term_Depth_ReturnsSelfForChaining() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch"); + var result = term.Depth(2); + Assert.Same(term, result); + } + + [Fact] + public void Term_Depth_SetsQueryParam() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch"); + term.Depth(2); + + var field = typeof(Term).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(term); + + Assert.True(queryParams?.ContainsKey("depth") ?? false); + Assert.Equal(2, queryParams["depth"]); + } + + [Fact] + public void Term_IncludeBranch_ReturnsSelfForChaining() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch"); + var result = term.IncludeBranch(); + Assert.Same(term, result); + } + + [Fact] + public void Term_IncludeBranch_SetsQueryParam() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch"); + term.IncludeBranch(); + + var field = typeof(Term).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(term); + + Assert.True(queryParams?.ContainsKey("include_branch") ?? false); + Assert.Equal("true", queryParams["include_branch"]); + } + + [Fact] + public void Term_Depth_Then_IncludeBranch_ChainsBoth() + { + var term = _client.Taxonomies("gadgets").Term("smartwatch") + .Depth(2) + .IncludeBranch(); + + var field = typeof(Term).GetField("UrlQueries", + BindingFlags.NonPublic | BindingFlags.Instance); + var queryParams = (Dictionary)field?.GetValue(term); + + Assert.True(queryParams?.ContainsKey("depth") ?? false); + Assert.True(queryParams?.ContainsKey("include_branch") ?? false); + } + + #endregion + + #region Term Constructor Validation Tests + + [Fact] + public void Term_Constructor_WithNullStack_ThrowsTaxonomyException() + { + Assert.Throws(() => new Term(null, "gadgets", "smartwatch")); + } + + [Fact] + public void Term_Constructor_WithNullTaxonomyUid_ThrowsTaxonomyException() + { + Assert.Throws(() => new Term(_client, null, "smartwatch")); + } + + [Fact] + public void Term_Constructor_WithNullTermUid_ThrowsTaxonomyException() + { + Assert.Throws(() => new Term(_client, "gadgets", null)); + } + + [Fact] + public void Taxonomy_Term_WithNullTermUid_ReturnsTermThatThrowsOnConstruction() + { + var taxonomy = _client.Taxonomies("gadgets"); + Assert.Throws(() => taxonomy.Term(null)); + } + + #endregion } } diff --git a/Contentstack.Core/ContentstackClient.cs b/Contentstack.Core/ContentstackClient.cs index 3e2b390..5eb721f 100644 --- a/Contentstack.Core/ContentstackClient.cs +++ b/Contentstack.Core/ContentstackClient.cs @@ -491,6 +491,25 @@ public Taxonomy Taxonomies() return tx; } + /// + /// Returns a instance scoped to the given taxonomy UID, + /// providing access to published taxonomy data, terms, and locale-aware delivery via the CDA. + /// + /// The UID of the published taxonomy. + /// A instance scoped to the given UID. + /// + /// + /// ContentstackClient stack = new ContentstackClient("api_key", "delivery_token", "environment"); + /// var taxonomy = await stack.Taxonomies("gadgets").Fetch<MyTaxonomy>(); + /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").Find<MyTerm>(); + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>(); + /// + /// + public Taxonomy Taxonomies(string uid) + { + return new Taxonomy(this, uid); + } + /// /// Get version. /// diff --git a/Contentstack.Core/Internals/TaxonomyRequestHelper.cs b/Contentstack.Core/Internals/TaxonomyRequestHelper.cs new file mode 100644 index 0000000..0eb4f8e --- /dev/null +++ b/Contentstack.Core/Internals/TaxonomyRequestHelper.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; + +namespace Contentstack.Core.Internals +{ + /// + /// Shared request-building, header-merging, and error-handling logic for the CDA + /// Taxonomy/Term/TermQuery classes. Every taxonomy-related HTTP call goes through + /// so header merging and error parsing behave identically + /// across Taxonomy, Term, and TermQuery. + /// + internal static class TaxonomyRequestHelper + { + /// + /// Merges a call-local header set with the stack's headers, local values taking precedence. + /// Passing null or empty for returns the stack headers unchanged. + /// + internal static Dictionary GetHeader(Dictionary stackHeaders, Dictionary localHeader) + { + if (localHeader != null && localHeader.Count > 0) + { + if (stackHeaders != null && stackHeaders.Count > 0) + { + Dictionary classHeaders = new Dictionary(); + foreach (var entry in localHeader) + classHeaders.Add(entry.Key, entry.Value); + + foreach (var entry in stackHeaders) + if (!classHeaders.ContainsKey(entry.Key)) + classHeaders.Add(entry.Key, entry.Value); + + return classHeaders; + } + return localHeader; + } + return stackHeaders; + } + + /// + /// Builds and sends a taxonomy/term GET request against the CDA and returns the raw response body. + /// + /// The owning client, providing config, headers, and the HTTP handler. + /// The fully-qualified request URL. + /// Query parameters accumulated by the calling class's modifiers (locale, depth, skip, ...). + /// Optional call-local headers, merged with precedence over the stack's headers. + internal static async Task ExecuteRequest( + ContentstackClient stack, + string url, + Dictionary urlQueries, + Dictionary localHeader = null) + { + var headerAll = GetHeader(stack._LocalHeaders, localHeader); + + var mainJson = new Dictionary(); + if (stack.Config?.Environment != null) + mainJson["environment"] = stack.Config.Environment; + + if (urlQueries != null) + foreach (var kvp in urlQueries) + mainJson[kvp.Key] = kvp.Value; + + var handler = new HttpRequestHandler(stack); + return await handler.ProcessRequest( + url, headerAll, mainJson, + Branch: stack.Config.Branch, + timeout: stack.Config.Timeout, + proxy: stack.Config.Proxy + ); + } + + /// + /// Parses a failed taxonomy/term request into a , + /// extracting error_code/error_message/errors from the response body when present. + /// + internal static ContentstackException GetContentstackError(Exception ex) + { + Int32 errorCode = 0; + string errorMessage; + HttpStatusCode statusCode = HttpStatusCode.InternalServerError; + Dictionary errors = null; + + try + { + System.Net.WebException webEx = ex as System.Net.WebException; + + if (webEx != null && webEx.Response != null) + { + using (var exResp = webEx.Response) + { + var stream = exResp.GetResponseStream(); + if (stream != null) + { + using (stream) + using (var reader = new System.IO.StreamReader(stream)) + { + errorMessage = reader.ReadToEnd(); + + if (!string.IsNullOrWhiteSpace(errorMessage)) + { + ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); + } + + var response = exResp as HttpWebResponse; + if (response != null) + statusCode = response.StatusCode; + } + } + else + { + errorMessage = webEx.Message; + } + } + } + else + { + errorMessage = ex.Message; + } + } + catch + { + errorMessage = ex.Message; + } + + return new ContentstackException(errorMessage) + { + ErrorCode = errorCode, + StatusCode = statusCode, + Errors = errors + }; + } + } +} diff --git a/Contentstack.Core/Models/Taxonomy.cs b/Contentstack.Core/Models/Taxonomy.cs index 667915d..8ba9195 100644 --- a/Contentstack.Core/Models/Taxonomy.cs +++ b/Contentstack.Core/Models/Taxonomy.cs @@ -1,19 +1,25 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Net; +using System.Linq; +using System.Threading.Tasks; +using System.Text.Json; +using System.Text.Json.Nodes; using Contentstack.Core.Configuration; using Contentstack.Core.Internals; namespace Contentstack.Core.Models { + /// + /// Represents a published taxonomy from the CDA, providing methods to fetch the taxonomy, + /// list its terms, and navigate to individual terms. + /// Use , , and to + /// configure the request before calling . + /// public class Taxonomy: Query { #region Internal Variables - private Dictionary _ObjectAttributes = new Dictionary(); - private Dictionary _Headers = new Dictionary(); - private Dictionary _StackHeaders = new Dictionary(); private Dictionary UrlQueries = new Dictionary(); + private string _uid = null; protected override string _Url { @@ -28,6 +34,8 @@ protected override string _Url throw new TaxonomyException("Taxonomy Stack Config is null. Please ensure the ContentstackClient is properly configured."); } Config config = this.Stack.Config; + if (_uid != null) + return String.Format("{0}/taxonomies/{1}", config.BaseUrl, _uid); return String.Format("{0}/taxonomies/entries", config.BaseUrl); } } @@ -38,7 +46,7 @@ protected override string _Url set; } - + #region Internal Constructors internal Taxonomy() @@ -51,12 +59,188 @@ internal Taxonomy(ContentstackClient stack): base(stack) throw new TaxonomyException("ContentstackClient instance cannot be null when creating a Taxonomy instance."); } this.Stack = stack; - this._StackHeaders = stack._LocalHeaders; + } + + internal Taxonomy(ContentstackClient stack, string uid) : this(stack) + { + if (string.IsNullOrEmpty(uid)) + throw new TaxonomyException("Taxonomy UID cannot be null or empty."); + _uid = uid; } #endregion #region Public Functions + /// + /// Sets the locale for this taxonomy fetch. + /// Passing null or empty is a no-op. + /// + /// Locale code (e.g. "hi-in", "en-us"). + /// The current for chaining. + /// + /// + /// var taxonomy = await stack.Taxonomies("gadgets").SetLocale("hi-in").Fetch<MyTaxonomy>(); + /// + /// + public new Taxonomy SetLocale(string locale) + { + if (!string.IsNullOrEmpty(locale)) + UrlQueries["locale"] = locale; + return this; + } + + /// + /// Falls back to the master locale when the taxonomy is not published in the requested locale. + /// Can be used with or without . + /// + /// The current for chaining. + /// + /// + /// var taxonomy = await stack.Taxonomies("gadgets").SetLocale("hi-in").IncludeFallback().Fetch<MyTaxonomy>(); + /// + /// + public new Taxonomy IncludeFallback() + { + UrlQueries["include_fallback"] = "true"; + return this; + } + + /// + /// Adds a custom query parameter to the request. + /// + /// Parameter key. + /// Parameter value. + /// The current for chaining. + /// + /// + /// var taxonomy = await stack.Taxonomies("gadgets").AddParam("include_branch", "true").Fetch<MyTaxonomy>(); + /// + /// + public new Taxonomy AddParam(string key, string value) + { + UrlQueries[key] = value; + return this; + } + + /// + /// Returns a Term instance for the given term UID within this taxonomy. + /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid"). + /// + /// The UID of the term to retrieve. + /// A instance scoped to this taxonomy and term. + /// + /// + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>(); + /// + /// + public Term Term(string termUid) + { + if (_uid == null) + throw new TaxonomyException("Term() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy."); + return new Term(Stack, _uid, termUid); + } + + /// + /// Returns a TermQuery for listing all published terms within this taxonomy. + /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid"). + /// + /// A instance for this taxonomy. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").Find<MyTerm>(); + /// + /// + public TermQuery Terms() + { + if (_uid == null) + throw new TaxonomyException("Terms() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy."); + return new TermQuery(Stack, _uid); + } + + /// + /// Fetches the published taxonomy by its UID from the CDA. + /// Requires the Taxonomy to be initialised with a UID via client.Taxonomies("uid"). + /// Use and to set query parameters before calling. + /// + /// The deserialized taxonomy object. + /// + /// + /// ContentstackClient stack = new ContentstackClient("api_key", "delivery_token", "environment"); + /// var taxonomy = await stack.Taxonomies("gadgets").Fetch<MyTaxonomy>(); + /// var localized = await stack.Taxonomies("gadgets").SetLocale("hi-in").Fetch<MyTaxonomy>(); + /// var withFallback = await stack.Taxonomies("gadgets").SetLocale("hi-in").IncludeFallback().Fetch<MyTaxonomy>(); + /// + /// + public async Task Fetch() + { + if (_uid == null) + throw new TaxonomyException("Fetch() requires a taxonomy UID. Use client.Taxonomies(\"uid\") to scope to a specific taxonomy."); + + try + { + var result = await TaxonomyRequestHelper.ExecuteRequest(Stack, _Url, UrlQueries); + + var jObject = JsonNode.Parse(result)!.AsObject(); + var token = jObject["taxonomy"]; + if (token != null) + return JsonSerializer.Deserialize(token.ToJsonString(), Stack.SerializerOptions); + return JsonSerializer.Deserialize(jObject.ToJsonString(), Stack.SerializerOptions); + } + catch (Exception ex) + { + var contentstackError = TaxonomyRequestHelper.GetContentstackError(ex); + throw new TaxonomyException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } + } + + /// + /// Fetches all published taxonomies from the CDA. + /// Only valid on an unscoped instance (client.Taxonomies(), no UID) — + /// throws if called on a UID-scoped instance. + /// + /// A containing the published taxonomies. + /// + /// + /// ContentstackClient stack = new ContentstackClient("api_key", "delivery_token", "environment"); + /// var result = await stack.Taxonomies().Find<MyTaxonomy>(); + /// var page = await stack.Taxonomies().AddParam("skip", "0").AddParam("limit", "10").Find<MyTaxonomy>(); + /// + /// + public new async Task> Find() + { + if (_uid != null) + throw new TaxonomyException("Find() requires an unscoped Taxonomy. Use client.Taxonomies() without a UID."); + + try + { + Config config = this.Stack.Config; + var url = String.Format("{0}/taxonomies", config.BaseUrl); + var result = await TaxonomyRequestHelper.ExecuteRequest(Stack, url, UrlQueries); + + var jObject = JsonNode.Parse(result)!.AsObject(); + var taxonomiesNode = jObject["taxonomies"]?.AsArray(); + IEnumerable taxonomies = taxonomiesNode != null + ? JsonSerializer.Deserialize>(taxonomiesNode.ToJsonString(), Stack.SerializerOptions) ?? Enumerable.Empty() + : Enumerable.Empty(); + return ContentstackCollection.FromDeliveryEnvelope(jObject, taxonomies); + } + catch (Exception ex) + { + var contentstackError = TaxonomyRequestHelper.GetContentstackError(ex); + throw new TaxonomyException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } + } + /// /// Add a constraint to the query that requires a particular key entry to be less than the provided value. /// @@ -212,105 +396,5 @@ public Taxonomy EqualAndBelow(String key, Object value) } #endregion - #region Private Functions - - private Dictionary GetHeader(Dictionary localHeader) - { - Dictionary mainHeader = _StackHeaders; - Dictionary classHeaders = new Dictionary(); - - if (localHeader != null && localHeader.Count > 0) - { - if (mainHeader != null && mainHeader.Count > 0) - { - foreach (var entry in localHeader) - { - String key = entry.Key; - classHeaders.Add(key, entry.Value); - } - - foreach (var entry in mainHeader) - { - String key = entry.Key; - if (!classHeaders.ContainsKey(key)) - { - classHeaders.Add(key, entry.Value); - } - } - - return classHeaders; - - } - else - { - return localHeader; - } - - } - else - { - return _StackHeaders; - } - } - internal static new ContentstackException GetContentstackError(Exception ex) - { - Int32 errorCode = 0; - string errorMessage = string.Empty; - HttpStatusCode statusCode = HttpStatusCode.InternalServerError; - ContentstackException contentstackError = new ContentstackException(ex); - Dictionary errors = null; - - try - { - System.Net.WebException webEx = ex as System.Net.WebException; - - if (webEx != null && webEx.Response != null) - { - using (var exResp = webEx.Response) - { - var stream = exResp.GetResponseStream(); - if (stream != null) - { - using (stream) - using (var reader = new StreamReader(stream)) - { - errorMessage = reader.ReadToEnd(); - - if (!string.IsNullOrWhiteSpace(errorMessage)) - { - ApiErrorBodyParser.TryApply(errorMessage.Replace("\r\n", ""), ref errorCode, ref errorMessage, ref errors); - } - - var response = exResp as HttpWebResponse; - if (response != null) - statusCode = response.StatusCode; - } - } - else - { - errorMessage = webEx.Message; - } - } - } - else - { - errorMessage = ex.Message; - } - } - catch - { - errorMessage = ex.Message; - } - - contentstackError = new ContentstackException(errorMessage) - { - ErrorCode = errorCode, - StatusCode = statusCode, - Errors = errors - }; - - return contentstackError; - } - #endregion } } diff --git a/Contentstack.Core/Models/Term.cs b/Contentstack.Core/Models/Term.cs new file mode 100644 index 0000000..5d92bde --- /dev/null +++ b/Contentstack.Core/Models/Term.cs @@ -0,0 +1,269 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Contentstack.Core.Internals; + +namespace Contentstack.Core.Models +{ + /// + /// Represents a single published term within a taxonomy, providing methods to fetch + /// the term, its localized versions, ancestors, and descendants from the CDA. + /// Use , , and to + /// configure the request before calling . + /// + public class Term + { + private readonly ContentstackClient _stack; + private readonly string _taxonomyUid; + private readonly string _termUid; + private readonly Dictionary UrlQueries = new Dictionary(); + + private string BaseUrlPath => + $"{_stack.Config.BaseUrl}/taxonomies/{_taxonomyUid}/terms/{_termUid}"; + + internal Term(ContentstackClient stack, string taxonomyUid, string termUid) + { + _stack = stack ?? throw new TaxonomyException("ContentstackClient cannot be null when creating a Term instance."); + if (string.IsNullOrEmpty(taxonomyUid)) throw new TaxonomyException("taxonomyUid cannot be null or empty."); + if (string.IsNullOrEmpty(termUid)) throw new TaxonomyException("termUid cannot be null or empty."); + _taxonomyUid = taxonomyUid; + _termUid = termUid; + } + + /// + /// Sets the locale for this term fetch. + /// Passing null or empty is a no-op. + /// + /// Locale code (e.g. "hi-in", "en-us"). + /// The current for chaining. + /// + /// + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").SetLocale("hi-in").Fetch<MyTerm>(); + /// + /// + public Term SetLocale(string locale) + { + if (!string.IsNullOrEmpty(locale)) + UrlQueries["locale"] = locale; + return this; + } + + /// + /// Falls back to the master locale when the term is not published in the requested locale. + /// Can be used with or without . + /// + /// The current for chaining. + /// + /// + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").SetLocale("hi-in").IncludeFallback().Fetch<MyTerm>(); + /// + /// + public Term IncludeFallback() + { + UrlQueries["include_fallback"] = "true"; + return this; + } + + /// + /// Adds a custom query parameter to the request. + /// + /// Parameter key. + /// Parameter value. + /// The current for chaining. + /// + /// + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").AddParam("include_branch", "true").Fetch<MyTerm>(); + /// + /// + public Term AddParam(string key, string value) + { + UrlQueries[key] = value; + return this; + } + + /// + /// Limits the depth of the term hierarchy returned by /. + /// Depth(1) returns only immediate parents/children; higher values include deeper levels. + /// + /// The maximum number of hierarchy levels to include. + /// The current for chaining. + /// + /// + /// var children = await stack.Taxonomies("gadgets").Term("laptops").Depth(1).Descendants<JsonArray>(); + /// + /// + public Term Depth(int depth) + { + UrlQueries["depth"] = depth; + return this; + } + + /// + /// Includes branch information in the response for this term. + /// + /// The current for chaining. + /// + /// + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").IncludeBranch().Fetch<MyTerm>(); + /// + /// + public Term IncludeBranch() + { + UrlQueries["include_branch"] = "true"; + return this; + } + + /// + /// Fetches the published term from the CDA. + /// Maps to GET /v3/taxonomies/{uid}/terms/{termUid} with any configured query parameters. + /// + /// The deserialized term object. + /// + /// + /// var term = await stack.Taxonomies("gadgets").Term("smartwatch").Fetch<MyTerm>(); + /// var localized = await stack.Taxonomies("gadgets").Term("smartwatch").SetLocale("hi-in").Fetch<MyTerm>(); + /// var withFallback = await stack.Taxonomies("gadgets").Term("smartwatch").SetLocale("hi-in").IncludeFallback().Fetch<MyTerm>(); + /// + /// + public async Task Fetch() + { + try + { + var result = await TaxonomyRequestHelper.ExecuteRequest(_stack, BaseUrlPath, UrlQueries); + var jObject = JsonNode.Parse(result)!.AsObject(); + var token = jObject["term"]; + if (token != null) + return JsonSerializer.Deserialize(token.ToJsonString(), _stack.SerializerOptions); + return JsonSerializer.Deserialize(jObject.ToJsonString(), _stack.SerializerOptions); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + var contentstackError = TaxonomyRequestHelper.GetContentstackError(ex); + throw new TaxonomyException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } + } + + /// + /// Fetches all published localized versions of this term across every locale. + /// Maps to GET /v3/taxonomies/{uid}/terms/{termUid}/locales. + /// + /// The deserialized locales collection. + /// + /// + /// var locales = await stack.Taxonomies("gadgets").Term("smartwatch").Locales<JsonNode>(); + /// + /// + public async Task Locales() + { + try + { + var result = await TaxonomyRequestHelper.ExecuteRequest(_stack, $"{BaseUrlPath}/locales", null); + var jObject = JsonNode.Parse(result)!.AsObject(); + var token = jObject["terms"]; + if (token != null) + return JsonSerializer.Deserialize(token.ToJsonString(), _stack.SerializerOptions); + return JsonSerializer.Deserialize(jObject.ToJsonString(), _stack.SerializerOptions); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + var contentstackError = TaxonomyRequestHelper.GetContentstackError(ex); + throw new TaxonomyException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } + } + + /// + /// Fetches all ancestors of this term up to the taxonomy root. + /// Maps to GET /v3/taxonomies/{uid}/terms/{termUid}/ancestors. + /// + /// The deserialized ancestors collection. + /// + /// + /// var ancestors = await stack.Taxonomies("gadgets").Term("smartwatch").Ancestors<JsonNode>(); + /// + /// + public async Task Ancestors() + { + try + { + var result = await TaxonomyRequestHelper.ExecuteRequest(_stack, $"{BaseUrlPath}/ancestors", UrlQueries); + var jObject = JsonNode.Parse(result)!.AsObject(); + var token = jObject["terms"]; + if (token != null) + return JsonSerializer.Deserialize(token.ToJsonString(), _stack.SerializerOptions); + return JsonSerializer.Deserialize(jObject.ToJsonString(), _stack.SerializerOptions); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + var contentstackError = TaxonomyRequestHelper.GetContentstackError(ex); + throw new TaxonomyException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } + } + + /// + /// Fetches all descendants of this term. + /// Maps to GET /v3/taxonomies/{uid}/terms/{termUid}/descendants. + /// + /// The deserialized descendants collection. + /// + /// + /// var descendants = await stack.Taxonomies("gadgets").Term("smartwatch").Descendants<JsonNode>(); + /// + /// + public async Task Descendants() + { + try + { + var result = await TaxonomyRequestHelper.ExecuteRequest(_stack, $"{BaseUrlPath}/descendants", UrlQueries); + var jObject = JsonNode.Parse(result)!.AsObject(); + var token = jObject["terms"]; + if (token != null) + return JsonSerializer.Deserialize(token.ToJsonString(), _stack.SerializerOptions); + return JsonSerializer.Deserialize(jObject.ToJsonString(), _stack.SerializerOptions); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + var contentstackError = TaxonomyRequestHelper.GetContentstackError(ex); + throw new TaxonomyException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } + } + + } +} diff --git a/Contentstack.Core/Models/TermQuery.cs b/Contentstack.Core/Models/TermQuery.cs new file mode 100644 index 0000000..d00331f --- /dev/null +++ b/Contentstack.Core/Models/TermQuery.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Contentstack.Core.Internals; + +namespace Contentstack.Core.Models +{ + /// + /// Provides a fluent query builder for listing published terms within a taxonomy from the CDA. + /// Use , , and to + /// configure the request before calling . + /// + public class TermQuery + { + private readonly ContentstackClient _stack; + private readonly string _taxonomyUid; + private readonly Dictionary UrlQueries = new Dictionary(); + + private string Url => + $"{_stack.Config.BaseUrl}/taxonomies/{_taxonomyUid}/terms"; + + internal TermQuery(ContentstackClient stack, string taxonomyUid) + { + _stack = stack ?? throw new TaxonomyException("ContentstackClient cannot be null when creating a TermQuery instance."); + if (string.IsNullOrEmpty(taxonomyUid)) throw new TaxonomyException("taxonomyUid cannot be null or empty."); + _taxonomyUid = taxonomyUid; + } + + /// + /// Filters terms to those published in the specified locale. + /// Passing null or empty is a no-op. + /// + /// Locale code (e.g. "hi-in", "en-us"). + /// The current for chaining. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").Find<MyTerm>(); + /// + /// + public TermQuery SetLocale(string locale) + { + if (!string.IsNullOrEmpty(locale)) + UrlQueries["locale"] = locale; + return this; + } + + /// + /// Falls back to the master locale when a term is not published in the requested locale. + /// Can be used with or without . + /// + /// The current for chaining. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").IncludeFallback().Find<MyTerm>(); + /// + /// + public TermQuery IncludeFallback() + { + UrlQueries["include_fallback"] = "true"; + return this; + } + + /// + /// Adds a custom query parameter to the request. + /// + /// Parameter key. + /// Parameter value. + /// The current for chaining. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().AddParam("depth", "2").Find<MyTerm>(); + /// + /// + public TermQuery AddParam(string key, string value) + { + UrlQueries[key] = value; + return this; + } + + /// + /// Limits the depth of the term hierarchy returned in the response. + /// Depth(1) returns only root-level terms; higher values include deeper levels. + /// + /// The maximum number of hierarchy levels to include. + /// The current for chaining. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().Depth(1).Find<MyTerm>(); + /// + /// + public TermQuery Depth(int depth) + { + UrlQueries["depth"] = depth; + return this; + } + + /// + /// Includes branch information in the response for each term. + /// + /// The current for chaining. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().IncludeBranch().Find<MyTerm>(); + /// + /// + public TermQuery IncludeBranch() + { + UrlQueries["include_branch"] = "true"; + return this; + } + + /// + /// Sets the number of terms to skip in the result set (pagination offset). + /// + /// The number of terms to skip. Must be >= 0. + /// The current for chaining. + /// + /// + /// var page2 = await stack.Taxonomies("gadgets").Terms().Skip(10).Limit(10).Find<MyTerm>(); + /// + /// + public TermQuery Skip(int skip) + { + UrlQueries["skip"] = skip; + return this; + } + + /// + /// Sets the maximum number of terms to return. + /// + /// The maximum result count. + /// The current for chaining. + /// + /// + /// var terms = await stack.Taxonomies("gadgets").Terms().Limit(5).Find<MyTerm>(); + /// + /// + public TermQuery Limit(int limit) + { + UrlQueries["limit"] = limit; + return this; + } + + /// + /// Includes the total count of matching terms in the response. + /// Access it via on the result. + /// + /// The current for chaining. + /// + /// + /// var result = await stack.Taxonomies("gadgets").Terms().IncludeCount().Find<MyTerm>(); + /// Console.WriteLine($"Total terms: {result.Count}"); + /// + /// + public TermQuery IncludeCount() + { + UrlQueries["include_count"] = "true"; + return this; + } + + /// + /// Executes the query and returns all matching published terms. + /// Maps to GET /v3/taxonomies/{uid}/terms with any configured query parameters. + /// + /// A containing the matched terms. + /// + /// + /// var result = await stack.Taxonomies("gadgets").Terms().SetLocale("hi-in").IncludeFallback().Find<MyTerm>(); + /// foreach (var term in result.Items) { ... } + /// + /// + public async Task> Find() + { + try + { + var result = await TaxonomyRequestHelper.ExecuteRequest(_stack, Url, UrlQueries); + + var jObject = JsonNode.Parse(result)!.AsObject(); + var termsNode = jObject["terms"]?.AsArray(); + IEnumerable terms = termsNode != null + ? JsonSerializer.Deserialize>(termsNode.ToJsonString(), _stack.SerializerOptions) ?? new List() + : new List(); + return ContentstackCollection.FromDeliveryEnvelope(jObject, terms); + } + catch (TaxonomyException) + { + throw; + } + catch (Exception ex) + { + var contentstackError = TaxonomyRequestHelper.GetContentstackError(ex); + throw new TaxonomyException(contentstackError.Message, ex) + { + ErrorCode = contentstackError.ErrorCode, + StatusCode = contentstackError.StatusCode, + Errors = contentstackError.Errors + }; + } + } + } +} diff --git a/Directory.Build.props b/Directory.Build.props index 4bac429..5791b94 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 3.1.0 + 3.2.0