diff --git a/CHANGELOG.md b/CHANGELOG.md index a265a5f..037950b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +### Version: 1.1.0 +#### Date: March-24-2026 +- Added `GetVariantAliases` and `GetDataCsvariantsAttribute` for variant alias extraction and `data-csvariants` serialization; Invalid arguments throw `ArgumentException`. + + ### Version: 1.0.7 #### Date: January-12-2026 - Improved error messages diff --git a/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj b/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj index 45ab904..ee46f08 100644 --- a/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj +++ b/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj @@ -37,4 +37,7 @@ + + + diff --git a/Contentstack.Utils.Tests/Resources/variantsEntries.json b/Contentstack.Utils.Tests/Resources/variantsEntries.json new file mode 100644 index 0000000..09cc7a5 --- /dev/null +++ b/Contentstack.Utils.Tests/Resources/variantsEntries.json @@ -0,0 +1,71 @@ +{ + "entries": [ + { + "uid": "entry_uid_1", + "_metadata": {}, + "locale": "en-us", + "_version": 1, + "title": "Sample Movie", + "publish_details": { + "time": "2025-12-11T07:56:17.574Z", + "user": "test_user", + "environment": "test_env", + "locale": "en-us", + "variants": { + "cs_variant_0_0": { + "alias": "cs_personalize_0_0", + "environment": "test_env", + "time": "2025-12-11T07:56:17.574Z", + "locale": "en-us", + "user": "test_user", + "version": 1 + }, + "cs_variant_0_3": { + "alias": "cs_personalize_0_3", + "environment": "test_env", + "time": "2025-12-11T07:56:17.582Z", + "locale": "en-us", + "user": "test_user", + "version": 1 + } + } + } + }, + { + "uid": "entry_uid_2", + "_metadata": {}, + "locale": "en-us", + "_version": 2, + "title": "Another Movie", + "publish_details": { + "time": "2025-12-11T07:10:19.964Z", + "user": "test_user", + "environment": "test_env", + "locale": "en-us", + "variants": { + "cs_variant_0_0": { + "alias": "cs_personalize_0_0", + "environment": "test_env", + "time": "2025-12-11T07:10:19.964Z", + "locale": "en-us", + "user": "test_user", + "version": 2 + } + } + } + }, + { + "uid": "entry_uid_3", + "_metadata": {}, + "locale": "en-us", + "_version": 1, + "title": "Movie No Variants", + "publish_details": { + "time": "2025-11-20T10:00:00.000Z", + "user": "test_user", + "environment": "test_env", + "locale": "en-us" + } + } + ] + } diff --git a/Contentstack.Utils.Tests/Resources/variantsSingleEntry.json b/Contentstack.Utils.Tests/Resources/variantsSingleEntry.json new file mode 100644 index 0000000..ddb0e22 --- /dev/null +++ b/Contentstack.Utils.Tests/Resources/variantsSingleEntry.json @@ -0,0 +1,39 @@ +{ + "entry": { + "uid": "entry_uid_single", + "_metadata": {}, + "locale": "en-us", + "_version": 1, + "ACL": {}, + "_in_progress": false, + "title": "Sample Movie", + "created_at": "2025-11-20T10:00:00.000Z", + "updated_at": "2025-12-11T07:56:17.574Z", + "created_by": "test_user", + "updated_by": "test_user", + "publish_details": { + "time": "2025-12-11T07:56:17.574Z", + "user": "test_user", + "environment": "test_env", + "locale": "en-us", + "variants": { + "cs_variant_0_0": { + "alias": "cs_personalize_0_0", + "environment": "test_env", + "time": "2025-12-11T07:56:17.574Z", + "locale": "en-us", + "user": "test_user", + "version": 1 + }, + "cs_variant_0_3": { + "alias": "cs_personalize_0_3", + "environment": "test_env", + "time": "2025-12-11T07:56:17.582Z", + "locale": "en-us", + "user": "test_user", + "version": 1 + } + } + } + } + } diff --git a/Contentstack.Utils.Tests/VariantAliasesTest.cs b/Contentstack.Utils.Tests/VariantAliasesTest.cs new file mode 100644 index 0000000..09cecf5 --- /dev/null +++ b/Contentstack.Utils.Tests/VariantAliasesTest.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Newtonsoft.Json.Linq; +using Xunit; + +namespace Contentstack.Utils.Tests +{ + public class VariantAliasesTest + { + private static JObject ReadJsonRoot(string fileName) + { + var path = Path.Combine(AppContext.BaseDirectory, "Resources", fileName); + return JObject.Parse(File.ReadAllText(path)); + } + + private static HashSet JsonArrayToStringSet(JArray arr) + { + var set = new HashSet(); + foreach (var t in arr) + { + set.Add(t.ToString()); + } + return set; + } + + [Fact] + public void GetVariantAliases_SingleEntry_ReturnsAliases() + { + JObject full = ReadJsonRoot("variantsSingleEntry.json"); + JObject entry = (JObject)full["entry"]; + const string contentTypeUid = "movie"; + + JObject result = Utils.GetVariantAliases(entry, contentTypeUid); + + Assert.True(result["entry_uid"] != null && !string.IsNullOrEmpty(result["entry_uid"].ToString())); + Assert.Equal(contentTypeUid, result["contenttype_uid"].ToString()); + JArray variants = (JArray)result["variants"]; + Assert.NotNull(variants); + var aliasSet = JsonArrayToStringSet(variants); + Assert.Equal( + new HashSet { "cs_personalize_0_0", "cs_personalize_0_3" }, + aliasSet); + } + + [Fact] + public void GetDataCsvariantsAttribute_SingleEntry_ReturnsJsonArrayString() + { + JObject full = ReadJsonRoot("variantsSingleEntry.json"); + JObject entry = (JObject)full["entry"]; + const string contentTypeUid = "movie"; + + JObject result = Utils.GetDataCsvariantsAttribute(entry, contentTypeUid); + + Assert.True(result["data-csvariants"] != null); + string dataCsvariantsStr = result["data-csvariants"].ToString(); + JArray arr = JArray.Parse(dataCsvariantsStr); + Assert.Single(arr); + JObject first = (JObject)arr[0]; + Assert.True(first["entry_uid"] != null && !string.IsNullOrEmpty(first["entry_uid"].ToString())); + Assert.Equal(contentTypeUid, first["contenttype_uid"].ToString()); + var aliasSet = JsonArrayToStringSet((JArray)first["variants"]); + Assert.Equal( + new HashSet { "cs_personalize_0_0", "cs_personalize_0_3" }, + aliasSet); + } + + [Fact] + public void GetVariantAliases_MultipleEntries_ReturnsOneResultPerEntryWithUid() + { + JObject full = ReadJsonRoot("variantsEntries.json"); + JArray entries = (JArray)full["entries"]; + const string contentTypeUid = "movie"; + + JArray result = Utils.GetVariantAliases(entries, contentTypeUid); + + Assert.NotNull(result); + Assert.Equal(3, result.Count); + + JObject first = (JObject)result[0]; + Assert.True(first["entry_uid"] != null && !string.IsNullOrEmpty(first["entry_uid"].ToString())); + Assert.Equal(contentTypeUid, first["contenttype_uid"].ToString()); + var firstSet = JsonArrayToStringSet((JArray)first["variants"]); + Assert.Equal( + new HashSet { "cs_personalize_0_0", "cs_personalize_0_3" }, + firstSet); + + JObject second = (JObject)result[1]; + Assert.True(second["entry_uid"] != null && !string.IsNullOrEmpty(second["entry_uid"].ToString())); + Assert.Single((JArray)second["variants"]); + Assert.Equal("cs_personalize_0_0", ((JArray)second["variants"])[0].ToString()); + + JObject third = (JObject)result[2]; + Assert.True(third["entry_uid"] != null && !string.IsNullOrEmpty(third["entry_uid"].ToString())); + Assert.Empty((JArray)third["variants"]); + } + + [Fact] + public void GetDataCsvariantsAttribute_MultipleEntries_ReturnsJsonArrayString() + { + JObject full = ReadJsonRoot("variantsEntries.json"); + JArray entries = (JArray)full["entries"]; + const string contentTypeUid = "movie"; + + JObject result = Utils.GetDataCsvariantsAttribute(entries, contentTypeUid); + + Assert.True(result["data-csvariants"] != null); + string dataCsvariantsStr = result["data-csvariants"].ToString(); + JArray arr = JArray.Parse(dataCsvariantsStr); + Assert.Equal(3, arr.Count); + Assert.True(((JObject)arr[0])["entry_uid"] != null && !string.IsNullOrEmpty(((JObject)arr[0])["entry_uid"].ToString())); + Assert.Equal(2, ((JArray)((JObject)arr[0])["variants"]).Count); + Assert.True(((JObject)arr[1])["entry_uid"] != null && !string.IsNullOrEmpty(((JObject)arr[1])["entry_uid"].ToString())); + Assert.Single((JArray)((JObject)arr[1])["variants"]); + Assert.True(((JObject)arr[2])["entry_uid"] != null && !string.IsNullOrEmpty(((JObject)arr[2])["entry_uid"].ToString())); + Assert.Empty((JArray)((JObject)arr[2])["variants"]); + } + + [Fact] + public void GetVariantAliases_ThrowsWhenEntryNull() + { + Assert.Throws(() => Utils.GetVariantAliases((JObject)null, "landing_page")); + } + + [Fact] + public void GetVariantAliases_ThrowsWhenContentTypeUidNull() + { + JObject full = ReadJsonRoot("variantsSingleEntry.json"); + JObject entry = (JObject)full["entry"]; + Assert.Throws(() => Utils.GetVariantAliases(entry, null)); + } + + [Fact] + public void GetVariantAliases_ThrowsWhenContentTypeUidEmpty() + { + JObject full = ReadJsonRoot("variantsSingleEntry.json"); + JObject entry = (JObject)full["entry"]; + Assert.Throws(() => Utils.GetVariantAliases(entry, "")); + } + + [Fact] + public void GetDataCsvariantsAttribute_WhenEntryNull_ReturnsEmptyArrayString() + { + JObject result = Utils.GetDataCsvariantsAttribute((JObject)null, "landing_page"); + Assert.True(result["data-csvariants"] != null); + Assert.Equal("[]", result["data-csvariants"].ToString()); + } + + [Fact] + public void GetVariantAliases_ThrowsWhenUidMissing() + { + var entry = new JObject { ["title"] = "no-uid" }; + Assert.Throws(() => Utils.GetVariantAliases(entry, "movie")); + } + + [Fact] + public void GetVariantAliases_ThrowsWhenUidNull() + { + var entry = new JObject { ["uid"] = JValue.CreateNull() }; + Assert.Throws(() => Utils.GetVariantAliases(entry, "movie")); + } + + [Fact] + public void GetVariantAliases_Batch_ThrowsWhenContentTypeUidNull() + { + var entries = new JArray { new JObject { ["uid"] = "a" } }; + Assert.Throws(() => Utils.GetVariantAliases(entries, null)); + } + + [Fact] + public void GetVariantAliases_Batch_ThrowsWhenContentTypeUidEmpty() + { + var entries = new JArray { new JObject { ["uid"] = "a" } }; + Assert.Throws(() => Utils.GetVariantAliases(entries, "")); + } + + [Fact] + public void GetDataCsvariantsAttribute_WhenEntriesArrayNull_ReturnsEmptyArrayString() + { + JObject result = Utils.GetDataCsvariantsAttribute((JArray)null, "movie"); + Assert.Equal("[]", result["data-csvariants"].ToString()); + } + + [Fact] + public void GetDataCsvariantsAttribute_Batch_ThrowsWhenContentTypeUidNull() + { + var entries = new JArray { new JObject { ["uid"] = "a" } }; + Assert.Throws(() => Utils.GetDataCsvariantsAttribute(entries, null)); + } + + [Fact] + public void GetDataCsvariantsAttribute_Batch_ThrowsWhenContentTypeUidEmpty() + { + var entries = new JArray { new JObject { ["uid"] = "a" } }; + Assert.Throws(() => Utils.GetDataCsvariantsAttribute(entries, "")); + } + + [Fact] + public void GetVariantAliases_ReturnsEmptyVariantsWhenPublishDetailsMissing() + { + var entry = new JObject { ["uid"] = "blt_no_pd" }; + JObject result = Utils.GetVariantAliases(entry, "movie"); + Assert.Equal("blt_no_pd", result["entry_uid"].ToString()); + Assert.Equal("movie", result["contenttype_uid"].ToString()); + Assert.Empty((JArray)result["variants"]); + } + + [Fact] + public void GetVariantAliases_ReturnsEmptyVariantsWhenVariantsObjectEmpty() + { + var entry = new JObject + { + ["uid"] = "blt_empty_v", + ["publish_details"] = new JObject + { + ["variants"] = new JObject() + } + }; + JObject result = Utils.GetVariantAliases(entry, "movie"); + Assert.Empty((JArray)result["variants"]); + } + + [Fact] + public void GetVariantAliases_ReturnsEmptyVariantsWhenVariantsKeyMissing() + { + var entry = new JObject + { + ["uid"] = "blt_no_variants_key", + ["publish_details"] = new JObject { ["time"] = "2025-01-01T00:00:00.000Z" } + }; + JObject result = Utils.GetVariantAliases(entry, "movie"); + Assert.Empty((JArray)result["variants"]); + } + + [Fact] + public void GetVariantAliases_SkipsVariantWhenAliasMissingOrEmpty() + { + var entry = new JObject + { + ["uid"] = "blt_skip", + ["publish_details"] = new JObject + { + ["variants"] = new JObject + { + ["v1"] = new JObject { ["alias"] = "keep_me" }, + ["v2"] = new JObject(), + ["v3"] = new JObject { ["alias"] = "" } + } + } + }; + JObject result = Utils.GetVariantAliases(entry, "page"); + var variants = (JArray)result["variants"]; + Assert.Single(variants); + Assert.Equal("keep_me", variants[0].ToString()); + } + } +} diff --git a/Contentstack.Utils/Utils.cs b/Contentstack.Utils/Utils.cs index 81f4c00..821df2c 100644 --- a/Contentstack.Utils/Utils.cs +++ b/Contentstack.Utils/Utils.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; +using System.Collections.Generic; using Contentstack.Utils.Models; using HtmlAgilityPack; using Contentstack.Utils.Extensions; using Contentstack.Utils.Interfaces; using System; using Contentstack.Utils.Enums; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Contentstack.Utils { @@ -286,5 +288,112 @@ private static object GetParentTagsValue(string dataValue, bool tagsAsObject) return $"data-cslp-parent-field={dataValue}"; } } + + public static JObject GetVariantAliases(JObject entry, string contentTypeUid) + { + if (string.IsNullOrEmpty(contentTypeUid)) + { + throw new ArgumentException("ContentType is required."); + } + if (entry == null) + { + throw new ArgumentException("Entry must not be null."); + } + if (!entry.ContainsKey("uid") || entry["uid"] == null || entry["uid"].Type == JTokenType.Null) + { + throw new ArgumentException("Entry must contain uid."); + } + + string entryUid = entry["uid"]?.ToString() ?? ""; + JArray variantsArray = ExtractVariantAliasesFromEntry(entry); + JObject result = new JObject + { + ["entry_uid"] = entryUid, + ["contenttype_uid"] = contentTypeUid, + ["variants"] = variantsArray + }; + return result; + } + + public static JArray GetVariantAliases(JArray entries, string contentTypeUid) + { + if (string.IsNullOrEmpty(contentTypeUid)) + { + throw new ArgumentException("ContentType is required."); + } + if (entries == null) + { + return new JArray(); + } + JArray variantResults = new JArray(); + foreach (JToken token in entries) + { + JObject entry = token as JObject; + if (entry != null && entry.ContainsKey("uid") && entry["uid"] != null && entry["uid"].Type != JTokenType.Null) + { + variantResults.Add(GetVariantAliases(entry, contentTypeUid)); + } + } + return variantResults; + } + + public static JObject GetDataCsvariantsAttribute(JObject entry, string contentTypeUid) + { + if (entry == null) + { + JObject result = new JObject(); + result["data-csvariants"] = "[]"; + return result; + } + JArray entries = new JArray(); + entries.Add(entry); + return GetDataCsvariantsAttribute(entries, contentTypeUid); + } + + public static JObject GetDataCsvariantsAttribute(JArray entries, string contentTypeUid) + { + JObject result = new JObject(); + if (entries == null) + { + result["data-csvariants"] = "[]"; + return result; + } + if (string.IsNullOrEmpty(contentTypeUid)) + { + throw new ArgumentException("ContentType is required."); + } + + JArray variantResults = GetVariantAliases(entries, contentTypeUid); + result["data-csvariants"] = variantResults.ToString(Formatting.None); + return result; + } + + private static JArray ExtractVariantAliasesFromEntry(JObject entry) + { + JArray variantArray = new JArray(); + JObject publishDetails = entry["publish_details"] as JObject; + if (publishDetails == null) + { + return variantArray; + } + JObject variants = publishDetails["variants"] as JObject; + if (variants == null) + { + return variantArray; + } + + foreach (JProperty prop in variants.Properties()) + { + if (prop.Value is JObject valueObj) + { + string alias = valueObj["alias"]?.ToString(); + if (!string.IsNullOrEmpty(alias)) + { + variantArray.Add(alias.Trim()); + } + } + } + return variantArray; + } } }