diff --git a/.gitignore b/.gitignore index 000500d..353cc9c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ packages/ *.trx */TestResults/ */app.config -.dccache \ No newline at end of file +.dccache +*/Assets/regions.json \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 72dbee7..08ce83c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,20 @@ +### Version: 2.0.0-beta.2 +#### Date: June-22-2026 +- **New:** Multi-region endpoint resolution via `Endpoint.GetContentstackEndpoint(region, service)` — resolves Contentstack service URLs for all 7 supported regions (NA, EU, AU, Azure-NA, Azure-EU, GCP-NA, GCP-EU) and 18 service keys (contentDelivery, contentManagement, auth, graphqlDelivery, preview, images, assets, automate, launch, developerHub, brandKit, genAI, personalizeManagement, personalizeEdge, composableStudio, assetManagement, and more). +- **New:** `Utils.GetContentstackEndpoint(region, service)` proxy — access endpoint resolution directly from the `Utils` class without importing `Contentstack.Utils.Endpoints`. +- **New:** `omitHttps` flag strips the `https://` scheme from returned URLs — pass directly to SDK host configuration (e.g. `new ContentstackOptions { Host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true) }`). +- **New:** Case-insensitive region alias support — `"us"`, `"NA"`, `"AWS-NA"`, `"azure_na"` all resolve correctly to the same region. +- **New:** `regions.json` registry auto-downloaded from `artifacts.contentstack.com` on first use and cached on disk — no setup required. The SDK self-heals if the file is missing. +- **New:** `Scripts/refresh-region.py` bundled inside the NuGet package — automatically placed in your project's `Scripts/` folder on first `dotnet build`. Run `python3 Scripts/refresh-region.py` (Mac/Linux) or `python Scripts/refresh-region.py` (Windows) anytime to pull the latest regions from CDN. + +### Version: 2.0.0-beta.1 +#### Date: April-27-2026 +- **Breaking:** Replaced **Newtonsoft.Json** with **System.Text.Json** across the package. The `Newtonsoft.Json` package reference is removed; add `System.Text.Json` (or rely on the BCL on supported runtimes) as needed in consuming projects. +- **Breaking:** Variant metadata APIs that previously took `JObject` / `JArray` now use `System.Text.Json.Nodes.JsonObject` and `JsonArray` (`GetVariantAliases`, `GetVariantMetadataTags`, and obsolete `GetDataCsvariantsAttribute` overloads). +- JSON serialization uses the same model attributes with `System.Text.Json.Serialization` (`JsonPropertyName`, `JsonConverter`), including custom converters for RTE/GQL-shaped JSON and **path-mapped** embedded models (`PathMappedJsonConverter`). +- RTE JSON deserialization tolerates **trailing commas** when using the documented test/helper patterns (`AllowTrailingCommas`); attribute dictionaries may surface **`JsonElement`** values instead of boxed strings—use helpers or unwrap explicitly if you access `Node.attrs` directly. +- Internal: `LangVersion` set to **latest** for multi-target builds; utilities normalize attribute values where the HTML pipeline expects strings. + ### Version: 1.2.0 #### Date: March-31-2026 - Added `GetVariantMetadataTags(JObject, string)` and `GetVariantMetadataTags(JArray, string)` as the canonical API for building the `data-csvariants` payload (same behavior as the previous helpers). diff --git a/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj b/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj index 4d4e3d4..f8e4299 100644 --- a/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj +++ b/Contentstack.Utils.Tests/Contentstack.Utils.Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 false $(Version) @@ -9,7 +9,6 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Contentstack.Utils.Tests/DefaultRenderTest.cs b/Contentstack.Utils.Tests/DefaultRenderTest.cs index 6f44ee3..96d1155 100644 --- a/Contentstack.Utils.Tests/DefaultRenderTest.cs +++ b/Contentstack.Utils.Tests/DefaultRenderTest.cs @@ -5,6 +5,7 @@ using Contentstack.Utils.Tests.Helpers; using Contentstack.Utils.Tests.Constants; using System; +using Contentstack.Utils; namespace Contentstack.Utils.Tests { @@ -178,9 +179,9 @@ public void testLinkhDocument() string result = defaultRender.RenderNode("a", nodeLink, (nodes) => { return text; }); - string url = nodeLink.attrs.ContainsKey("url") ? (string)nodeLink.attrs["url"] : ""; - string target = nodeLink.attrs.ContainsKey("target") ? (string)nodeLink.attrs["target"] : ""; - string title = nodeLink.attrs.ContainsKey("title") ? (string)nodeLink.attrs["title"] : ""; + string url = nodeLink.attrs.ContainsKey("url") ? JsonAttrValue.AsString(nodeLink.attrs["url"]) : ""; + string target = nodeLink.attrs.ContainsKey("target") ? JsonAttrValue.AsString(nodeLink.attrs["target"]) : ""; + string title = nodeLink.attrs.ContainsKey("title") ? JsonAttrValue.AsString(nodeLink.attrs["title"]) : ""; Assert.Equal($"Text To set Link", result); } diff --git a/Contentstack.Utils.Tests/EndpointTest.cs b/Contentstack.Utils.Tests/EndpointTest.cs new file mode 100644 index 0000000..2bb5ef8 --- /dev/null +++ b/Contentstack.Utils.Tests/EndpointTest.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Contentstack.Utils.Endpoints; +using Xunit; + +namespace Contentstack.Utils.Tests +{ + public class EndpointTest : IDisposable + { + // Reset cache before each test so tests are isolated. + public EndpointTest() + { + Endpoint.ResetCache(); + } + + public void Dispose() + { + Endpoint.ResetCache(); + } + + // ------------------------------------------------------------------ + // Basic resolution + // ------------------------------------------------------------------ + + [Fact] + public void GetContentstackEndpoint_Na_ReturnsCorrectCdnUrl() + { + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Theory] + [InlineData("na")] + [InlineData("eu")] + [InlineData("au")] + [InlineData("azure-na")] + [InlineData("azure-eu")] + [InlineData("gcp-na")] + [InlineData("gcp-eu")] + public void GetContentstackEndpoint_AllRegionIds_Resolve(string regionId) + { + string url = Endpoint.GetContentstackEndpoint(regionId, "contentDelivery"); + Assert.False(string.IsNullOrEmpty(url)); + Assert.StartsWith("https://", url); + } + + // ------------------------------------------------------------------ + // Alias resolution (case-insensitive, dash/underscore variants) + // ------------------------------------------------------------------ + + [Theory] + [InlineData("na")] + [InlineData("us")] + [InlineData("NA")] + [InlineData("US")] + [InlineData("AWS-NA")] + [InlineData("aws_na")] + [InlineData("AWS_NA")] + public void GetContentstackEndpoint_NaAliasVariants_AllResolveToSameCdn(string alias) + { + string url = Endpoint.GetContentstackEndpoint(alias, "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Theory] + [InlineData("azure-na")] + [InlineData("azure_na")] + [InlineData("AZURE-NA")] + [InlineData("AZURE_NA")] + public void GetContentstackEndpoint_AzureNaAliasVariants_AllResolveToSameUrl(string alias) + { + string expected = Endpoint.GetContentstackEndpoint("azure-na", "contentDelivery"); + string result = Endpoint.GetContentstackEndpoint(alias, "contentDelivery"); + Assert.Equal(expected, result); + } + + // ------------------------------------------------------------------ + // omitHttps flag + // ------------------------------------------------------------------ + + [Fact] + public void GetContentstackEndpoint_OmitHttps_StripsScheme() + { + string host = Endpoint.GetContentstackEndpoint("na", "contentDelivery", omitHttps: true); + Assert.Equal("cdn.contentstack.io", host); + } + + [Fact] + public void GetContentstackEndpoint_OmitHttps_EuRegion_StripsScheme() + { + string host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + Assert.Equal("eu-cdn.contentstack.com", host); + } + + [Fact] + public void GetContentstackEndpoint_OmitHttpsFalse_ReturnsFullUrl() + { + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery", omitHttps: false); + Assert.StartsWith("https://", url); + } + + // ------------------------------------------------------------------ + // All-endpoints (dict) overload + // ------------------------------------------------------------------ + + [Theory] + [InlineData("na")] + [InlineData("eu")] + [InlineData("au")] + [InlineData("azure-na")] + [InlineData("azure-eu")] + [InlineData("gcp-na")] + [InlineData("gcp-eu")] + public void GetContentstackEndpoint_AllEndpoints_ContainsExpectedKeys(string regionId) + { + var endpoints = Endpoint.GetContentstackEndpoint(regionId); + Assert.True(endpoints.Count > 0); + Assert.True(endpoints.ContainsKey("contentDelivery")); + Assert.True(endpoints.ContainsKey("contentManagement")); + Assert.True(endpoints.ContainsKey("auth")); + } + + [Fact] + public void GetContentstackEndpoint_NaAllEndpoints_Has18Keys() + { + var endpoints = Endpoint.GetContentstackEndpoint("na"); + Assert.Equal(18, endpoints.Count); + } + + [Fact] + public void GetContentstackEndpoint_AllEndpoints_OmitHttps_AllValuesStripped() + { + var endpoints = Endpoint.GetContentstackEndpoint("na", omitHttps: true); + foreach (var url in endpoints.Values) + Assert.DoesNotContain("https://", url); + } + + // ------------------------------------------------------------------ + // Error handling + // ------------------------------------------------------------------ + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void GetContentstackEndpoint_EmptyRegion_ThrowsArgumentException(string emptyRegion) + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint(emptyRegion, "contentDelivery")); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public void GetContentstackEndpoint_EmptyRegion_DictOverload_ThrowsArgumentException(string emptyRegion) + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint(emptyRegion)); + } + + [Fact] + public void GetContentstackEndpoint_UnknownRegion_ThrowsKeyNotFoundException() + { + Assert.Throws(() => + Endpoint.GetContentstackEndpoint("xyz", "contentDelivery")); + } + + [Fact] + public void GetContentstackEndpoint_UnknownService_ThrowsKeyNotFoundException() + { + var ex = Assert.Throws(() => + Endpoint.GetContentstackEndpoint("na", "nonExistentService")); + Assert.Contains("nonExistentService", ex.Message); + } + + // ------------------------------------------------------------------ + // Cache behaviour + // ------------------------------------------------------------------ + + [Fact] + public void ResetCache_AllowsReload_NoError() + { + // First call populates cache + Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + + // Reset and call again — should reload without error + Endpoint.ResetCache(); + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Fact] + public void GetContentstackEndpoint_ConsecutiveCalls_ReturnConsistentResults() + { + string first = Endpoint.GetContentstackEndpoint("eu", "contentManagement"); + string second = Endpoint.GetContentstackEndpoint("eu", "contentManagement"); + Assert.Equal(first, second); + } + + // ------------------------------------------------------------------ + // Utils proxy parity + // ------------------------------------------------------------------ + + [Fact] + public void Utils_Proxy_String_MatchesEndpoint() + { + string direct = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + string proxy = Utils.GetContentstackEndpoint("na", "contentDelivery"); + Assert.Equal(direct, proxy); + } + + [Fact] + public void Utils_Proxy_Dict_MatchesEndpoint() + { + var direct = Endpoint.GetContentstackEndpoint("eu"); + var proxy = Utils.GetContentstackEndpoint("eu"); + Assert.Equal(direct.Count, proxy.Count); + foreach (var kvp in direct) + Assert.Equal(kvp.Value, proxy[kvp.Key]); + } + + [Fact] + public void Utils_Proxy_OmitHttps_MatchesEndpoint() + { + string direct = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + string proxy = Utils.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + Assert.Equal(direct, proxy); + } + + // ------------------------------------------------------------------ + // Local file self-heal. + // ------------------------------------------------------------------ + + [Fact] + public void LocalFile_WhenWritten_IsReadOnNextCacheReset() + { + // First call: either reads from disk or downloads from CDN and writes to disk. + Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + + string localPath = Endpoint.GetLocalFilePath(); + + // If CDN download failed (no network in CI), skip this assertion — the + // self-heal path is covered by the CDN download succeeding at call time. + if (!File.Exists(localPath)) + return; + + // Reset cache — next call must read from local file (step 2 in LoadRegions), + // not re-download. Mirrors Python: os.path.exists(_REGIONS_FILE) → open(). + Endpoint.ResetCache(); + string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + + Assert.Equal("https://cdn.contentstack.io", url); + } + + [Fact] + public void LocalFilePath_IsNextToDll_NotSourceDirectory() + { + string localPath = Endpoint.GetLocalFilePath(); + Assert.Contains("Assets", localPath); + Assert.EndsWith("regions.json", localPath); + } + } +} diff --git a/Contentstack.Utils.Tests/Helpers/NodeParser.cs b/Contentstack.Utils.Tests/Helpers/NodeParser.cs index 4ee6ac7..6648754 100644 --- a/Contentstack.Utils.Tests/Helpers/NodeParser.cs +++ b/Contentstack.Utils.Tests/Helpers/NodeParser.cs @@ -1,31 +1,35 @@ -using Contentstack.Utils.Interfaces; +using System.Text.Json; +using Contentstack.Utils.Interfaces; using Contentstack.Utils.Models; using Contentstack.Utils.Tests.Constants; using Contentstack.Utils.Tests.Mocks; -using Newtonsoft.Json; namespace Contentstack.Utils.Tests.Helpers { public class NodeParser { - public static Node parse(string jsonNode) + private static readonly JsonSerializerOptions SerializerSettings = new JsonSerializerOptions { - JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - JsonSerializer Serializer = JsonSerializer.Create(SerializerSettings); + AllowTrailingCommas = true, + }; - return JsonConvert.DeserializeObject(jsonNode, SerializerSettings); + public static Node parse(string jsonNode) + { + return JsonSerializer.Deserialize(jsonNode, SerializerSettings); } } + public class GQLParser { - public static GQLModel parse(string jsonNode, string embedConnection = null) where T: IEmbeddedObject + private static readonly JsonSerializerOptions SerializerSettings = new JsonSerializerOptions + { + AllowTrailingCommas = true, + }; + + public static GQLModel parse(string jsonNode, string embedConnection = null) where T : IEmbeddedObject { var data = JsonToHtmlConstants.KGQLModel(jsonNode, embedConnection); - JsonSerializerSettings SerializerSettings = new JsonSerializerSettings(); - JsonSerializer Serializer = JsonSerializer.Create(SerializerSettings); - return JsonConvert.DeserializeObject>(data, SerializerSettings); + return JsonSerializer.Deserialize>(data, SerializerSettings); } } - } - diff --git a/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs b/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs index 53b37da..1983150 100644 --- a/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs +++ b/Contentstack.Utils.Tests/Mocks/CustomRenderOptionMock.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Contentstack.Utils; using Contentstack.Utils.Interfaces; using Contentstack.Utils.Models; using HtmlAgilityPack; @@ -17,9 +18,9 @@ public override string RenderNode(string nodeType, Node node, NodeChildrenCallBa case "a": if (node.attrs.ContainsKey("target")) { - return $"{callBack(node.children)}"; + return $"{callBack(node.children)}"; } - return $"{callBack(node.children)}"; + return $"{callBack(node.children)}"; } return base.RenderNode(nodeType, node, callBack); } diff --git a/Contentstack.Utils.Tests/Mocks/GQLModel.cs b/Contentstack.Utils.Tests/Mocks/GQLModel.cs index 586ff39..d4cc33d 100644 --- a/Contentstack.Utils.Tests/Mocks/GQLModel.cs +++ b/Contentstack.Utils.Tests/Mocks/GQLModel.cs @@ -2,33 +2,32 @@ using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; using Contentstack.Utils.Models; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Tests.Mocks { - public class GQLModel where T: IEmbeddedObject + public class GQLModel where T : IEmbeddedObject { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] public JsonRTENodes multiplerte { get; set; } public JsonRTENode singlerte { get; set; } } - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(PathMappedJsonConverter))] public class EntryModel : IEmbeddedEntry { - [JsonProperty("system.uid")] + [JsonPropertyName("system.uid")] public string Uid { get; set; } - [JsonProperty("system.content_type_uid")] + [JsonPropertyName("system.content_type_uid")] public string ContentTypeUid { get; set; } - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; @@ -36,34 +35,34 @@ public string Title } } - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(PathMappedJsonConverter))] public class AssetModel : IEmbeddedAsset { - [JsonProperty("system.uid")] + [JsonPropertyName("system.uid")] public string Uid { get; set; } - [JsonProperty("system.content_type_uid")] + [JsonPropertyName("system.content_type_uid")] public string ContentTypeUid { get; set; } - [JsonProperty("title")] + [JsonPropertyName("title")] public string Title { get; set; } - [JsonProperty("filename")] + [JsonPropertyName("filename")] public string FileName { get; set; } - [JsonProperty("url")] + [JsonPropertyName("url")] public string Url { get; @@ -71,4 +70,3 @@ public string Url } } } - diff --git a/Contentstack.Utils.Tests/VariantAliasesTest.cs b/Contentstack.Utils.Tests/VariantAliasesTest.cs index 868f60e..1260b03 100644 --- a/Contentstack.Utils.Tests/VariantAliasesTest.cs +++ b/Contentstack.Utils.Tests/VariantAliasesTest.cs @@ -1,20 +1,20 @@ using System; using System.Collections.Generic; using System.IO; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; using Xunit; namespace Contentstack.Utils.Tests { public class VariantAliasesTest { - private static JObject ReadJsonRoot(string fileName) + private static JsonObject ReadJsonRoot(string fileName) { var path = Path.Combine(AppContext.BaseDirectory, "Resources", fileName); - return JObject.Parse(File.ReadAllText(path)); + return JsonNode.Parse(File.ReadAllText(path)).AsObject(); } - private static HashSet JsonArrayToStringSet(JArray arr) + private static HashSet JsonArrayToStringSet(JsonArray arr) { var set = new HashSet(); foreach (var t in arr) @@ -27,15 +27,15 @@ private static HashSet JsonArrayToStringSet(JArray arr) [Fact] public void GetVariantAliases_SingleEntry_ReturnsAliases() { - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); const string contentTypeUid = "movie"; - JObject result = Utils.GetVariantAliases(entry, contentTypeUid); + JsonObject 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"]; + JsonArray variants = result["variants"].AsArray(); Assert.NotNull(variants); var aliasSet = JsonArrayToStringSet(variants); Assert.Equal( @@ -46,20 +46,20 @@ public void GetVariantAliases_SingleEntry_ReturnsAliases() [Fact] public void GetVariantMetadataTags_SingleEntry_ReturnsJsonArrayString() { - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); const string contentTypeUid = "movie"; - JObject result = Utils.GetVariantMetadataTags(entry, contentTypeUid); + JsonObject result = Utils.GetVariantMetadataTags(entry, contentTypeUid); Assert.True(result["data-csvariants"] != null); string dataCsvariantsStr = result["data-csvariants"].ToString(); - JArray arr = JArray.Parse(dataCsvariantsStr); + JsonArray arr = JsonNode.Parse(dataCsvariantsStr).AsArray(); Assert.Single(arr); - JObject first = (JObject)arr[0]; + JsonObject first = arr[0].AsObject(); 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"]); + var aliasSet = JsonArrayToStringSet(first["variants"].AsArray()); Assert.Equal( new HashSet { "cs_personalize_0_0", "cs_personalize_0_3" }, aliasSet); @@ -68,80 +68,80 @@ public void GetVariantMetadataTags_SingleEntry_ReturnsJsonArrayString() [Fact] public void GetVariantAliases_MultipleEntries_ReturnsOneResultPerEntryWithUid() { - JObject full = ReadJsonRoot("variantsEntries.json"); - JArray entries = (JArray)full["entries"]; + JsonObject full = ReadJsonRoot("variantsEntries.json"); + JsonArray entries = full["entries"].AsArray(); const string contentTypeUid = "movie"; - JArray result = Utils.GetVariantAliases(entries, contentTypeUid); + JsonArray result = Utils.GetVariantAliases(entries, contentTypeUid); Assert.NotNull(result); Assert.Equal(3, result.Count); - JObject first = (JObject)result[0]; + JsonObject first = result[0].AsObject(); 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"]); + var firstSet = JsonArrayToStringSet(first["variants"].AsArray()); Assert.Equal( new HashSet { "cs_personalize_0_0", "cs_personalize_0_3" }, firstSet); - JObject second = (JObject)result[1]; + JsonObject second = result[1].AsObject(); 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()); + Assert.Single(second["variants"].AsArray()); + Assert.Equal("cs_personalize_0_0", second["variants"].AsArray()[0].ToString()); - JObject third = (JObject)result[2]; + JsonObject third = result[2].AsObject(); Assert.True(third["entry_uid"] != null && !string.IsNullOrEmpty(third["entry_uid"].ToString())); - Assert.Empty((JArray)third["variants"]); + Assert.Empty(third["variants"].AsArray()); } [Fact] public void GetVariantMetadataTags_MultipleEntries_ReturnsJsonArrayString() { - JObject full = ReadJsonRoot("variantsEntries.json"); - JArray entries = (JArray)full["entries"]; + JsonObject full = ReadJsonRoot("variantsEntries.json"); + JsonArray entries = full["entries"].AsArray(); const string contentTypeUid = "movie"; - JObject result = Utils.GetVariantMetadataTags(entries, contentTypeUid); + JsonObject result = Utils.GetVariantMetadataTags(entries, contentTypeUid); Assert.True(result["data-csvariants"] != null); string dataCsvariantsStr = result["data-csvariants"].ToString(); - JArray arr = JArray.Parse(dataCsvariantsStr); + JsonArray arr = JsonNode.Parse(dataCsvariantsStr).AsArray(); 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"]); + Assert.True(arr[0].AsObject()["entry_uid"] != null && !string.IsNullOrEmpty(arr[0].AsObject()["entry_uid"].ToString())); + Assert.Equal(2, arr[0].AsObject()["variants"].AsArray().Count); + Assert.True(arr[1].AsObject()["entry_uid"] != null && !string.IsNullOrEmpty(arr[1].AsObject()["entry_uid"].ToString())); + Assert.Single(arr[1].AsObject()["variants"].AsArray()); + Assert.True(arr[2].AsObject()["entry_uid"] != null && !string.IsNullOrEmpty(arr[2].AsObject()["entry_uid"].ToString())); + Assert.Empty(arr[2].AsObject()["variants"].AsArray()); } [Fact] public void GetVariantAliases_ThrowsWhenEntryNull() { - Assert.Throws(() => Utils.GetVariantAliases((JObject)null, "landing_page")); + Assert.Throws(() => Utils.GetVariantAliases((JsonObject)null, "landing_page")); } [Fact] public void GetVariantAliases_ThrowsWhenContentTypeUidNull() { - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); Assert.Throws(() => Utils.GetVariantAliases(entry, null)); } [Fact] public void GetVariantAliases_ThrowsWhenContentTypeUidEmpty() { - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); Assert.Throws(() => Utils.GetVariantAliases(entry, "")); } [Fact] public void GetVariantMetadataTags_WhenEntryNull_ReturnsEmptyArrayString() { - JObject result = Utils.GetVariantMetadataTags((JObject)null, "landing_page"); + JsonObject result = Utils.GetVariantMetadataTags((JsonObject)null, "landing_page"); Assert.True(result["data-csvariants"] != null); Assert.Equal("[]", result["data-csvariants"].ToString()); } @@ -149,49 +149,49 @@ public void GetVariantMetadataTags_WhenEntryNull_ReturnsEmptyArrayString() [Fact] public void GetVariantAliases_ThrowsWhenUidMissing() { - var entry = new JObject { ["title"] = "no-uid" }; + var entry = new JsonObject { ["title"] = "no-uid" }; Assert.Throws(() => Utils.GetVariantAliases(entry, "movie")); } [Fact] public void GetVariantAliases_ThrowsWhenUidNull() { - var entry = new JObject { ["uid"] = JValue.CreateNull() }; + var entry = new JsonObject { ["uid"] = JsonNode.Parse("null") }; Assert.Throws(() => Utils.GetVariantAliases(entry, "movie")); } [Fact] public void GetVariantAliases_Batch_ThrowsWhenContentTypeUidNull() { - var entries = new JArray { new JObject { ["uid"] = "a" } }; + var entries = new JsonArray { new JsonObject { ["uid"] = "a" } }; Assert.Throws(() => Utils.GetVariantAliases(entries, null)); } [Fact] public void GetVariantAliases_Batch_ThrowsWhenContentTypeUidEmpty() { - var entries = new JArray { new JObject { ["uid"] = "a" } }; + var entries = new JsonArray { new JsonObject { ["uid"] = "a" } }; Assert.Throws(() => Utils.GetVariantAliases(entries, "")); } [Fact] public void GetVariantMetadataTags_WhenEntriesArrayNull_ReturnsEmptyArrayString() { - JObject result = Utils.GetVariantMetadataTags((JArray)null, "movie"); + JsonObject result = Utils.GetVariantMetadataTags((JsonArray)null, "movie"); Assert.Equal("[]", result["data-csvariants"].ToString()); } [Fact] public void GetVariantMetadataTags_Batch_ThrowsWhenContentTypeUidNull() { - var entries = new JArray { new JObject { ["uid"] = "a" } }; + var entries = new JsonArray { new JsonObject { ["uid"] = "a" } }; Assert.Throws(() => Utils.GetVariantMetadataTags(entries, null)); } [Fact] public void GetVariantMetadataTags_Batch_ThrowsWhenContentTypeUidEmpty() { - var entries = new JArray { new JObject { ["uid"] = "a" } }; + var entries = new JsonArray { new JsonObject { ["uid"] = "a" } }; Assert.Throws(() => Utils.GetVariantMetadataTags(entries, "")); } @@ -199,85 +199,85 @@ public void GetVariantMetadataTags_Batch_ThrowsWhenContentTypeUidEmpty() public void GetDataCsvariantsAttribute_DelegatesToGetVariantMetadataTags() { #pragma warning disable CS0618 // Type or member is obsolete — intentional coverage of backward-compatible alias - JObject full = ReadJsonRoot("variantsSingleEntry.json"); - JObject entry = (JObject)full["entry"]; + JsonObject full = ReadJsonRoot("variantsSingleEntry.json"); + JsonObject entry = full["entry"].AsObject(); const string contentTypeUid = "movie"; - JObject canonical = Utils.GetVariantMetadataTags(entry, contentTypeUid); - JObject legacy = Utils.GetDataCsvariantsAttribute(entry, contentTypeUid); - Assert.True(JToken.DeepEquals(canonical, legacy)); + JsonObject canonical = Utils.GetVariantMetadataTags(entry, contentTypeUid); + JsonObject legacy = Utils.GetDataCsvariantsAttribute(entry, contentTypeUid); + Assert.True(JsonNode.DeepEquals(canonical, legacy)); - JObject fullMulti = ReadJsonRoot("variantsEntries.json"); - JArray entries = (JArray)fullMulti["entries"]; - JObject canonicalBatch = Utils.GetVariantMetadataTags(entries, contentTypeUid); - JObject legacyBatch = Utils.GetDataCsvariantsAttribute(entries, contentTypeUid); - Assert.True(JToken.DeepEquals(canonicalBatch, legacyBatch)); + JsonObject fullMulti = ReadJsonRoot("variantsEntries.json"); + JsonArray entries = fullMulti["entries"].AsArray(); + JsonObject canonicalBatch = Utils.GetVariantMetadataTags(entries, contentTypeUid); + JsonObject legacyBatch = Utils.GetDataCsvariantsAttribute(entries, contentTypeUid); + Assert.True(JsonNode.DeepEquals(canonicalBatch, legacyBatch)); - JObject nullEntryLegacy = Utils.GetDataCsvariantsAttribute((JObject)null, "x"); - JObject nullEntryCanonical = Utils.GetVariantMetadataTags((JObject)null, "x"); - Assert.True(JToken.DeepEquals(nullEntryCanonical, nullEntryLegacy)); + JsonObject nullEntryLegacy = Utils.GetDataCsvariantsAttribute((JsonObject)null, "x"); + JsonObject nullEntryCanonical = Utils.GetVariantMetadataTags((JsonObject)null, "x"); + Assert.True(JsonNode.DeepEquals(nullEntryCanonical, nullEntryLegacy)); - JObject nullArrLegacy = Utils.GetDataCsvariantsAttribute((JArray)null, "x"); - JObject nullArrCanonical = Utils.GetVariantMetadataTags((JArray)null, "x"); - Assert.True(JToken.DeepEquals(nullArrCanonical, nullArrLegacy)); + JsonObject nullArrLegacy = Utils.GetDataCsvariantsAttribute((JsonArray)null, "x"); + JsonObject nullArrCanonical = Utils.GetVariantMetadataTags((JsonArray)null, "x"); + Assert.True(JsonNode.DeepEquals(nullArrCanonical, nullArrLegacy)); #pragma warning restore CS0618 } [Fact] public void GetVariantAliases_ReturnsEmptyVariantsWhenPublishDetailsMissing() { - var entry = new JObject { ["uid"] = "blt_no_pd" }; - JObject result = Utils.GetVariantAliases(entry, "movie"); + var entry = new JsonObject { ["uid"] = "blt_no_pd" }; + JsonObject 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"]); + Assert.Empty(result["variants"].AsArray()); } [Fact] public void GetVariantAliases_ReturnsEmptyVariantsWhenVariantsObjectEmpty() { - var entry = new JObject + var entry = new JsonObject { ["uid"] = "blt_empty_v", - ["publish_details"] = new JObject + ["publish_details"] = new JsonObject { - ["variants"] = new JObject() + ["variants"] = new JsonObject() } }; - JObject result = Utils.GetVariantAliases(entry, "movie"); - Assert.Empty((JArray)result["variants"]); + JsonObject result = Utils.GetVariantAliases(entry, "movie"); + Assert.Empty(result["variants"].AsArray()); } [Fact] public void GetVariantAliases_ReturnsEmptyVariantsWhenVariantsKeyMissing() { - var entry = new JObject + var entry = new JsonObject { ["uid"] = "blt_no_variants_key", - ["publish_details"] = new JObject { ["time"] = "2025-01-01T00:00:00.000Z" } + ["publish_details"] = new JsonObject { ["time"] = "2025-01-01T00:00:00.000Z" } }; - JObject result = Utils.GetVariantAliases(entry, "movie"); - Assert.Empty((JArray)result["variants"]); + JsonObject result = Utils.GetVariantAliases(entry, "movie"); + Assert.Empty(result["variants"].AsArray()); } [Fact] public void GetVariantAliases_SkipsVariantWhenAliasMissingOrEmpty() { - var entry = new JObject + var entry = new JsonObject { ["uid"] = "blt_skip", - ["publish_details"] = new JObject + ["publish_details"] = new JsonObject { - ["variants"] = new JObject + ["variants"] = new JsonObject { - ["v1"] = new JObject { ["alias"] = "keep_me" }, - ["v2"] = new JObject(), - ["v3"] = new JObject { ["alias"] = "" } + ["v1"] = new JsonObject { ["alias"] = "keep_me" }, + ["v2"] = new JsonObject(), + ["v3"] = new JsonObject { ["alias"] = "" } } } }; - JObject result = Utils.GetVariantAliases(entry, "page"); - var variants = (JArray)result["variants"]; + JsonObject result = Utils.GetVariantAliases(entry, "page"); + var variants = result["variants"].AsArray(); Assert.Single(variants); Assert.Equal("keep_me", variants[0].ToString()); } diff --git a/Contentstack.Utils/Contentstack.Utils.csproj b/Contentstack.Utils/Contentstack.Utils.csproj index 6eaeb87..8fb1830 100644 --- a/Contentstack.Utils/Contentstack.Utils.csproj +++ b/Contentstack.Utils/Contentstack.Utils.csproj @@ -1,7 +1,8 @@ - netstandard2.0;net47;net472; + net10.0 + latest contentstack.utils Contentstack.Utils Contentstack @@ -37,9 +38,24 @@ + - + + + + + true + contentFiles/cs/any/Scripts/refresh-region.py + Content + false + + + true + build/contentstack.utils.targets + + + diff --git a/Contentstack.Utils/Converters/NodeJsonConverter.cs b/Contentstack.Utils/Converters/NodeJsonConverter.cs index 88d22a7..2619eae 100644 --- a/Contentstack.Utils/Converters/NodeJsonConverter.cs +++ b/Contentstack.Utils/Converters/NodeJsonConverter.cs @@ -1,31 +1,91 @@ using System; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; using Contentstack.Utils.Models; namespace Contentstack.Utils.Converters { public class NodeJsonConverter : JsonConverter { - public override Node ReadJson(JsonReader reader, Type objectType, Node existingValue, bool hasExistingValue, JsonSerializer serializer) + public override Node Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - Node node = null; - JObject jObject = JObject.Load(reader); - if (jObject["type"] == null) + using (JsonDocument doc = JsonDocument.ParseValue(ref reader)) { - node = new TextNode(); - node.type = "text"; - }else - { - node = new Node(); + JsonElement root = doc.RootElement; + bool hasType = root.TryGetProperty("type", out JsonElement typeEl) && typeEl.ValueKind != JsonValueKind.Null; + + if (!hasType) + { + TextNode textNode = new TextNode(); + textNode.type = "text"; + PopulateTextNode(root, textNode, options); + return textNode; + } + + Node node = new Node(); + PopulateNode(root, node, options); + return node; } - serializer.Populate(jObject.CreateReader(), node); - return node; } - public override void WriteJson(JsonWriter writer, Node value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, Node value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, value?.GetType() ?? typeof(Node), CloneWithoutNodeConverter(options)); + } + + private static void PopulateNode(JsonElement root, Node node, JsonSerializerOptions options) + { + if (root.TryGetProperty("type", out JsonElement typeProp)) + node.type = typeProp.GetString(); + + if (root.TryGetProperty("attrs", out JsonElement attrs)) + node.attrs = JsonSerializer.Deserialize>(attrs.GetRawText(), options); + + if (root.TryGetProperty("children", out JsonElement children)) + node.children = JsonSerializer.Deserialize>(children.GetRawText(), options); + } + + private static void PopulateTextNode(JsonElement root, TextNode node, JsonSerializerOptions options) + { + PopulateNode(root, node, options); + + if (root.TryGetProperty("text", out JsonElement text)) + node.text = text.GetString(); + + TryBindBool(root, "bold", v => node.bold = v); + TryBindBool(root, "italic", v => node.italic = v); + TryBindBool(root, "underline", v => node.underline = v); + TryBindBool(root, "strikethrough", v => node.strikethrough = v); + TryBindBool(root, "inlineCode", v => node.inlineCode = v); + TryBindBool(root, "subscript", v => node.subscript = v); + TryBindBool(root, "superscript", v => node.superscript = v); + + if (root.TryGetProperty("classname", out JsonElement cn)) + node.classname = cn.ValueKind == JsonValueKind.Null ? null : cn.GetString(); + + if (root.TryGetProperty("id", out JsonElement id)) + node.id = id.ValueKind == JsonValueKind.Null ? null : id.GetString(); + } + + private static void TryBindBool(JsonElement root, string name, Action set) { - + if (!root.TryGetProperty(name, out JsonElement el)) + return; + if (el.ValueKind == JsonValueKind.True || el.ValueKind == JsonValueKind.False) + set(el.GetBoolean()); + } + + private static JsonSerializerOptions CloneWithoutNodeConverter(JsonSerializerOptions options) + { + JsonSerializerOptions inner = new JsonSerializerOptions(options); + for (int i = inner.Converters.Count - 1; i >= 0; i--) + { + if (inner.Converters[i] is NodeJsonConverter) + inner.Converters.RemoveAt(i); + } + + return inner; } } } diff --git a/Contentstack.Utils/Converters/RTEJsonConverter.cs b/Contentstack.Utils/Converters/RTEJsonConverter.cs index ecc198b..6b57279 100644 --- a/Contentstack.Utils/Converters/RTEJsonConverter.cs +++ b/Contentstack.Utils/Converters/RTEJsonConverter.cs @@ -1,50 +1,169 @@ using System; using System.Linq; using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Contentstack.Utils.Constants; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Contentstack.Utils.Interfaces; namespace Contentstack.Utils.Converters { - public class RTEJsonConverter : JsonConverter + /// + /// Factory for JSON path-based deserialization used by and . + /// + public sealed class RTEJsonConverterFactory : JsonConverterFactory { - public override bool CanConvert(Type objectType) + public override bool CanConvert(Type typeToConvert) { - throw new InvalidOperationException(ErrorMessages.InvalidRteJson); + if (!typeToConvert.IsGenericType) + return false; + + Type def = typeToConvert.GetGenericTypeDefinition(); + return def == typeof(Models.JsonRTENode<>) || def == typeof(Models.JsonRTENodes<>); } - public override object ReadJson(JsonReader reader, Type objectType, - object existingValue, JsonSerializer serializer) + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - JObject jo = JObject.Load(reader); - object targetObj = Activator.CreateInstance(objectType); + Type embeddedArg = typeToConvert.GetGenericArguments()[0]; + Type def = typeToConvert.GetGenericTypeDefinition(); + + if (def == typeof(Models.JsonRTENode<>)) + { + Type converterType = typeof(RTEJsonConverterImpl<>).MakeGenericType(embeddedArg); + return (JsonConverter)Activator.CreateInstance(converterType); + } - foreach (PropertyInfo prop in objectType.GetProperties() - .Where(p => p.CanRead && p.CanWrite)) + if (def == typeof(Models.JsonRTENodes<>)) { - JsonPropertyAttribute att = prop.GetCustomAttributes(true) - .OfType() - .FirstOrDefault(); + Type converterType = typeof(RTEJsonNodesConverterImpl<>).MakeGenericType(embeddedArg); + return (JsonConverter)Activator.CreateInstance(converterType); + } - string jsonPath = (att != null ? att.PropertyName : prop.Name); - JToken token = jo.SelectToken(jsonPath); + throw new InvalidOperationException($"Unsupported type for RTE JSON converter: {typeToConvert}"); + } + } - if (token != null && token.Type != JTokenType.Null) + /// + /// Deserializes objects whose properties map to dotted JSON paths (same idea as Newtonsoft SelectToken with ). + /// + public sealed class PathMappedJsonConverter : JsonConverter + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonObject jo = JsonNode.Parse(ref reader) as JsonObject ?? new JsonObject(); + T targetObj = Activator.CreateInstance(); + + foreach (PropertyInfo prop in typeof(T).GetProperties().Where(p => p.CanRead && p.CanWrite)) + { + JsonPropertyNameAttribute att = prop.GetCustomAttribute(); + string jsonPath = att?.Name ?? prop.Name; + JsonNode token = RTEJsonPath.SelectPath(jo, jsonPath); + + if (token != null && !RTEJsonPath.IsJsonNull(token)) { - object value = token.ToObject(prop.PropertyType, serializer); - prop.SetValue(targetObj, value, null); + object value = RTEJsonPath.DeserializeNode(token, prop.PropertyType, options); + prop.SetValue(targetObj, value); } } return targetObj; } + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + throw new NotSupportedException("Serialization is not supported for path-mapped RTE models."); + } + } - public override void WriteJson(JsonWriter writer, object value, - JsonSerializer serializer) + internal static class RTEJsonPath + { + internal static JsonNode SelectPath(JsonObject root, string path) + { + if (root == null || string.IsNullOrEmpty(path)) + return null; + + string[] segments = path.Split('.'); + JsonNode current = root; + foreach (string segment in segments) + { + if (current is JsonObject jo && jo.TryGetPropertyValue(segment, out JsonNode next)) + current = next; + else + return null; + } + + return current; + } + + internal static bool IsJsonNull(JsonNode node) { + if (node == null) + return true; + return node is JsonValue jv && jv.GetValueKind() == JsonValueKind.Null; + } + internal static object DeserializeNode(JsonNode token, Type propertyType, JsonSerializerOptions options) + { + string json = token.ToJsonString(options); + return JsonSerializer.Deserialize(json, propertyType, options); + } + } + + internal sealed class RTEJsonConverterImpl : JsonConverter> where T : IEmbeddedObject + { + public override Models.JsonRTENode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonObject jo = JsonNode.Parse(ref reader) as JsonObject ?? new JsonObject(); + var targetObj = new Models.JsonRTENode(); + + foreach (PropertyInfo prop in typeof(Models.JsonRTENode).GetProperties().Where(p => p.CanRead && p.CanWrite)) + { + JsonPropertyNameAttribute att = prop.GetCustomAttribute(); + string jsonPath = att?.Name ?? prop.Name; + JsonNode token = RTEJsonPath.SelectPath(jo, jsonPath); + + if (token != null && !RTEJsonPath.IsJsonNull(token)) + { + object value = RTEJsonPath.DeserializeNode(token, prop.PropertyType, options); + prop.SetValue(targetObj, value); + } + } + + return targetObj; + } + + public override void Write(Utf8JsonWriter writer, Models.JsonRTENode value, JsonSerializerOptions options) + { + throw new NotSupportedException("Serialization is not supported for JsonRTENode."); + } + } + + internal sealed class RTEJsonNodesConverterImpl : JsonConverter> where T : IEmbeddedObject + { + public override Models.JsonRTENodes Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + JsonObject jo = JsonNode.Parse(ref reader) as JsonObject ?? new JsonObject(); + var targetObj = new Models.JsonRTENodes(); + + foreach (PropertyInfo prop in typeof(Models.JsonRTENodes).GetProperties().Where(p => p.CanRead && p.CanWrite)) + { + JsonPropertyNameAttribute att = prop.GetCustomAttribute(); + string jsonPath = att?.Name ?? prop.Name; + JsonNode token = RTEJsonPath.SelectPath(jo, jsonPath); + + if (token != null && !RTEJsonPath.IsJsonNull(token)) + { + object val = RTEJsonPath.DeserializeNode(token, prop.PropertyType, options); + prop.SetValue(targetObj, val); + } + } + + return targetObj; + } + + public override void Write(Utf8JsonWriter writer, Models.JsonRTENodes value, JsonSerializerOptions options) + { + throw new NotSupportedException("Serialization is not supported for JsonRTENodes."); } } } diff --git a/Contentstack.Utils/Endpoints/Endpoint.cs b/Contentstack.Utils/Endpoints/Endpoint.cs new file mode 100644 index 0000000..3e99f2f --- /dev/null +++ b/Contentstack.Utils/Endpoints/Endpoint.cs @@ -0,0 +1,287 @@ +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Text.RegularExpressions; + +namespace Contentstack.Utils.Endpoints +{ + /// + /// Resolves Contentstack service URLs for any supported region. + /// + /// All public methods are static — no instantiation required. + /// + /// Example: + /// + /// // Full URL + /// string url = Endpoint.GetContentstackEndpoint("na", "contentDelivery"); + /// // → "https://cdn.contentstack.io" + /// + /// // Host only (omit https://) — useful for SDK host configuration + /// string host = Endpoint.GetContentstackEndpoint("eu", "contentDelivery", omitHttps: true); + /// // → "eu-cdn.contentstack.com" + /// + /// // All endpoints for a region + /// Dictionary<string, string> all = Endpoint.GetContentstackEndpoint("azure-na"); + /// // → { "contentDelivery": "...", "contentManagement": "...", ... } + /// + /// + public static class Endpoint + { + private const string RegionsUrl = "https://artifacts.contentstack.com/regions.json"; + + // Module-level cache — loaded once per process, shared across all calls. + private static JsonElement[]? _regionsData; + private static readonly object _cacheLock = new object(); + private static readonly HttpClient _httpClient = new HttpClient + { + Timeout = TimeSpan.FromSeconds(30) + }; + + /// + /// Resolves a single Contentstack service endpoint URL for the given region. + /// + /// + /// Region ID or alias (case-insensitive). Examples: "na", "us", "eu", "AWS-NA", "azure_eu", "gcp-na". + /// + /// + /// Service key. Valid keys include: "contentDelivery", "contentManagement", "auth", + /// "graphqlDelivery", "preview", "images", "assets", "automate", "launch", + /// "developerHub", "brandKit", "genAI", "personalizeManagement", + /// "personalizeEdge", "composableStudio", "assetManagement". + /// + /// + /// When true, strips the https:// scheme from the returned URL. + /// Useful when passing the host to an SDK that constructs its own URLs. + /// + /// The endpoint URL string for the specified service. + /// If region is empty or whitespace. + /// If region or service is not found. + /// If the registry cannot be read or is corrupt. + public static string GetContentstackEndpoint(string region, string service, bool omitHttps = false) + { + if (string.IsNullOrWhiteSpace(region)) + throw new ArgumentException("Empty region provided. Please put valid region.", nameof(region)); + + var regions = LoadRegions(); + string normalized = region.Trim().ToLowerInvariant(); + var regionEl = FindRegion(regions, normalized); + + if (regionEl == null) + throw new KeyNotFoundException($"Invalid region: {region}"); + + var endpoints = regionEl.Value.GetProperty("endpoints"); + if (!endpoints.TryGetProperty(service, out var urlEl)) + { + string regionId = regionEl.Value.GetProperty("id").GetString()!; + throw new KeyNotFoundException($"Service \"{service}\" not found for region \"{regionId}\""); + } + + string url = urlEl.GetString()!; + return omitHttps ? StripHttps(url) : url; + } + + /// + /// Returns all service endpoint URLs for the given region. + /// + /// Region ID or alias (case-insensitive). + /// When true, strips the https:// scheme from all returned URLs. + /// Dictionary mapping service keys to endpoint URLs. + /// If region is empty or whitespace. + /// If region is not found. + /// If the registry cannot be read or is corrupt. + public static Dictionary GetContentstackEndpoint(string region, bool omitHttps = false) + { + if (string.IsNullOrWhiteSpace(region)) + throw new ArgumentException("Empty region provided. Please put valid region.", nameof(region)); + + var regions = LoadRegions(); + string normalized = region.Trim().ToLowerInvariant(); + var regionEl = FindRegion(regions, normalized); + + if (regionEl == null) + throw new KeyNotFoundException($"Invalid region: {region}"); + + var result = new Dictionary(); + var endpoints = regionEl.Value.GetProperty("endpoints"); + foreach (var ep in endpoints.EnumerateObject()) + { + string url = ep.Value.GetString()!; + result[ep.Name] = omitHttps ? StripHttps(url) : url; + } + return result; + } + + /// + /// Clears the in-memory region cache. Intended for testing only — forces the + /// next call to re-read regions.json from disk or re-download from CDN. + /// + public static void ResetCache() + { + lock (_cacheLock) + { + _regionsData = null; + } + } + + // ------------------------------------------------------------------ + // Internal helpers + // ------------------------------------------------------------------ + + /// + /// Load and cache the regions registry. + /// + /// Resolution order: + /// 1. In-memory cache — zero I/O after the first call in a process + /// 2. Local file on disk — Assets/regions.json next to the DLL + /// (written by DownloadAndSave or refresh-region.cs) + /// 3. CDN download — fetches from artifacts.contentstack.com, + /// writes to disk for future calls (silent on failure) + /// + private static JsonElement[] LoadRegions() + { + lock (_cacheLock) + { + if (_regionsData != null) + return _regionsData; + + string localFile = GetLocalFilePath(); + + // Step 2 — local file on disk + string? json = ReadLocalFile(localFile); + + // Step 3 — CDN download, writes to disk so next startup skips this step + if (json == null) + json = DownloadAndSave(localFile); + + if (json == null) + throw new InvalidOperationException( + "contentstack_utils: regions.json not found and could not be downloaded. " + + "Run 'dotnet run Scripts/refresh-region.cs' and ensure network access."); + + JsonDocument doc; + try + { + doc = JsonDocument.Parse(json); + } + catch (JsonException ex) + { + throw new InvalidOperationException( + "contentstack_utils: regions.json is corrupt. " + + "Run 'dotnet run Scripts/refresh-region.cs' to re-download it.", ex); + } + + if (!doc.RootElement.TryGetProperty("regions", out var regionsEl) || + regionsEl.ValueKind != JsonValueKind.Array) + { + throw new InvalidOperationException( + "contentstack_utils: regions.json is corrupt. " + + "Run 'dotnet run Scripts/refresh-region.cs' to re-download it."); + } + + var list = new List(); + foreach (var r in regionsEl.EnumerateArray()) + list.Add(r.Clone()); + + _regionsData = list.ToArray(); + return _regionsData; + } + } + + /// + /// Returns the path to regions.json next to the DLL + /// + internal static string GetLocalFilePath() + { + string assemblyDir = Path.GetDirectoryName(typeof(Endpoint).Assembly.Location) + ?? AppContext.BaseDirectory; + return Path.Combine(assemblyDir, "Assets", "regions.json"); + } + + /// + /// Reads regions.json from disk. Returns null if the file does not exist. + /// + private static string? ReadLocalFile(string path) + { + if (!File.Exists(path)) + return null; + try + { + return File.ReadAllText(path); + } + catch + { + return null; + } + } + + /// + /// Downloads regions.json from the CDN and writes it to disk so that future + /// process startups read from the local file instead of downloading again. + /// Silent on all failures (network error, permission denied). + /// + private static string? DownloadAndSave(string dest) + { + try + { + var task = _httpClient.GetStringAsync(RegionsUrl); + task.Wait(); + string data = task.Result; + + using var doc = JsonDocument.Parse(data); + if (!doc.RootElement.TryGetProperty("regions", out _)) + return null; + + // Write to disk — next startup reads from local file (Step 2). + // Silent on PermissionError or read-only filesystem + try + { + Directory.CreateDirectory(Path.GetDirectoryName(dest)!); + File.WriteAllText(dest, data); + } + catch { } + + return data; + } + catch + { + return null; + } + } + + /// + /// Two-pass region lookup: ID match wins over alias match. + /// Input must already be lowercased and trimmed. + /// + private static JsonElement? FindRegion(JsonElement[] regions, string normalized) + { + // Pass 1 — exact id match + foreach (var r in regions) + { + if (r.TryGetProperty("id", out var id) && + id.GetString()?.ToLowerInvariant() == normalized) + return r; + } + + // Pass 2 — alias match + foreach (var r in regions) + { + if (!r.TryGetProperty("alias", out var aliases)) continue; + foreach (var alias in aliases.EnumerateArray()) + { + if (alias.GetString()?.ToLowerInvariant() == normalized) + return r; + } + } + + return null; + } + + private static string StripHttps(string url) + { + return Regex.Replace(url, @"^https?://", string.Empty); + } + } +} diff --git a/Contentstack.Utils/Interfaces/IEdges.cs b/Contentstack.Utils/Interfaces/IEdges.cs index a127b6b..bd0ccce 100644 --- a/Contentstack.Utils/Interfaces/IEdges.cs +++ b/Contentstack.Utils/Interfaces/IEdges.cs @@ -1,11 +1,11 @@ using System; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Interfaces { public class IEdges where T: IEmbeddedObject { - [JsonProperty("node")] + [JsonPropertyName("node")] public T Node { get; diff --git a/Contentstack.Utils/JsonAttrValue.cs b/Contentstack.Utils/JsonAttrValue.cs new file mode 100644 index 0000000..477239b --- /dev/null +++ b/Contentstack.Utils/JsonAttrValue.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace Contentstack.Utils +{ + /// + /// Newtonsoft.Json deserialized scalar attributes as . + /// System.Text.Json stores dynamic values as — unwrap for the same runtime behavior. + /// + internal static class JsonAttrValue + { + internal static string AsString(object value) + { + if (value == null) + return null; + + if (value is string s) + return s; + + if (value is JsonElement je) + { + switch (je.ValueKind) + { + case JsonValueKind.String: + return je.GetString(); + case JsonValueKind.Number: + return je.GetRawText(); + case JsonValueKind.True: + return "true"; + case JsonValueKind.False: + return "false"; + case JsonValueKind.Null: + return null; + default: + return je.ToString(); + } + } + + return Convert.ToString(value); + } + + internal static bool TryGetStyleObject(object styleVal, out Dictionary styles) + { + styles = null; + if (styleVal == null) + return false; + + if (styleVal is JsonObject jo) + { + styles = JsonSerializer.Deserialize>(jo.ToJsonString()); + return styles != null; + } + + if (styleVal is JsonElement je && je.ValueKind == JsonValueKind.Object) + { + styles = JsonSerializer.Deserialize>(je.GetRawText()); + return styles != null; + } + + return false; + } + } +} diff --git a/Contentstack.Utils/Models/JsonRTENode.cs b/Contentstack.Utils/Models/JsonRTENode.cs index b69a1bc..068f573 100644 --- a/Contentstack.Utils/Models/JsonRTENode.cs +++ b/Contentstack.Utils/Models/JsonRTENode.cs @@ -1,17 +1,16 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Models { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] - public class JsonRTENode where T: IEmbeddedObject + [JsonConverter(typeof(RTEJsonConverterFactory))] + public class JsonRTENode where T : IEmbeddedObject { - [JsonProperty("json")] + [JsonPropertyName("json")] public Node Json { get; set; } - [JsonProperty("embedded_itemsConnection.edges")] + [JsonPropertyName("embedded_itemsConnection.edges")] public List> Edges { get; set; } } } diff --git a/Contentstack.Utils/Models/JsonRTENodes.cs b/Contentstack.Utils/Models/JsonRTENodes.cs index 31dbfcd..538115e 100644 --- a/Contentstack.Utils/Models/JsonRTENodes.cs +++ b/Contentstack.Utils/Models/JsonRTENodes.cs @@ -1,17 +1,16 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Contentstack.Utils.Converters; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Models { - [Newtonsoft.Json.JsonConverter(typeof(RTEJsonConverter))] + [JsonConverter(typeof(RTEJsonConverterFactory))] public class JsonRTENodes where T : IEmbeddedObject { - [JsonProperty("json")] + [JsonPropertyName("json")] public List Json { get; set; } - [JsonProperty("embedded_itemsConnection.edges")] + [JsonPropertyName("embedded_itemsConnection.edges")] public List> Edges { get; set; } } } diff --git a/Contentstack.Utils/Models/Metadata.cs b/Contentstack.Utils/Models/Metadata.cs index e87ee17..d29e385 100644 --- a/Contentstack.Utils/Models/Metadata.cs +++ b/Contentstack.Utils/Models/Metadata.cs @@ -1,5 +1,6 @@ -using System; +using System; using System.Runtime.CompilerServices; +using Contentstack.Utils; using Contentstack.Utils.Enums; using HtmlAgilityPack; @@ -71,13 +72,13 @@ public static implicit operator Metadata(HtmlNode node) public static implicit operator Metadata(Node node) { StyleType styleType; - if (!node.attrs.ContainsKey("display-type") || !(Enum.TryParse((string)node.attrs["display-type"], true, out styleType))) + if (!node.attrs.ContainsKey("display-type") || !(Enum.TryParse(JsonAttrValue.AsString(node.attrs["display-type"]), true, out styleType))) { styleType = StyleType.Block; } EmbedItemType embedItemType; - if (!node.attrs.ContainsKey("type") || !(Enum.TryParse((string)node.attrs["type"], true, out embedItemType))) + if (!node.attrs.ContainsKey("type") || !(Enum.TryParse(JsonAttrValue.AsString(node.attrs["type"]), true, out embedItemType))) { embedItemType = EmbedItemType.Entry; } @@ -89,10 +90,10 @@ public static implicit operator Metadata(Node node) string itemUID = ""; if (node.attrs.ContainsKey("entry-uid")) { - itemUID = (string)node.attrs["entry-uid"]; + itemUID = JsonAttrValue.AsString(node.attrs["entry-uid"]); }else if (node.attrs.ContainsKey("asset-uid")) { - itemUID = (string)node.attrs["asset-uid"]; + itemUID = JsonAttrValue.AsString(node.attrs["asset-uid"]); } return new Metadata() @@ -102,7 +103,7 @@ public static implicit operator Metadata(Node node) StyleType = styleType, ItemType = embedItemType, ItemUid = itemUID, - ContentTypeUid = node.attrs.ContainsKey("content-type-uid") ? (string)node.attrs["content-type-uid"] : "", + ContentTypeUid = node.attrs.ContainsKey("content-type-uid") ? JsonAttrValue.AsString(node.attrs["content-type-uid"]) : "", attributes = node.attrs }; diff --git a/Contentstack.Utils/Models/Node.cs b/Contentstack.Utils/Models/Node.cs index b64a70e..ba7e659 100644 --- a/Contentstack.Utils/Models/Node.cs +++ b/Contentstack.Utils/Models/Node.cs @@ -1,9 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using Contentstack.Utils.Converters; -using Contentstack.Utils.Enums; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json.Serialization; namespace Contentstack.Utils.Models { diff --git a/Contentstack.Utils/Models/Options.cs b/Contentstack.Utils/Models/Options.cs index 724322a..d8433a6 100644 --- a/Contentstack.Utils/Models/Options.cs +++ b/Contentstack.Utils/Models/Options.cs @@ -2,7 +2,8 @@ using System.Collections.Generic; using Contentstack.Utils.Enums; using Contentstack.Utils.Interfaces; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using Contentstack.Utils; namespace Contentstack.Utils.Models { @@ -105,14 +106,12 @@ public virtual string RenderNode(string nodeType, Node node, NodeChildrenCallBac var styleVal = node.attrs["style"]; if (styleVal != null) { - if (styleVal is string) + if (styleVal is string styleStr) { - styleAttrs = $" style=\"{styleVal}\""; + styleAttrs = $" style=\"{styleStr}\""; } - else if (styleVal is JObject) + else if (JsonAttrValue.TryGetStyleObject(styleVal, out Dictionary styleDictionary) && styleDictionary != null) { - var styleObject = (JObject)styleVal; - var styleDictionary = styleObject.ToObject>(); styleAttrs = " style=\""; foreach (var pair in styleDictionary) { @@ -129,28 +128,28 @@ public virtual string RenderNode(string nodeType, Node node, NodeChildrenCallBac case "a": if (node.attrs?.ContainsKey("url")==true) { - href = (string)node.attrs["url"]; + href = JsonAttrValue.AsString(node.attrs["url"]); } if (node.attrs?.ContainsKey("target") == true) { - target = (string)node.attrs["target"]; + target = JsonAttrValue.AsString(node.attrs["target"]); } if (node.attrs?.ContainsKey("title") == true) { - title = (string)node.attrs["title"]; + title = JsonAttrValue.AsString(node.attrs["title"]); } return $"{callBack(node.children)}"; case "img": if (node.attrs.ContainsKey("url")) { - href = (string)node.attrs["url"]; + href = JsonAttrValue.AsString(node.attrs["url"]); } return $"{callBack(node.children)}"; case "embed": if (node.attrs.ContainsKey("url")) { - href = (string)node.attrs["url"]; + href = JsonAttrValue.AsString(node.attrs["url"]); } return $"{callBack(node.children)}"; case "fragment": diff --git a/Contentstack.Utils/Utils.cs b/Contentstack.Utils/Utils.cs index 4e51c19..421896f 100644 --- a/Contentstack.Utils/Utils.cs +++ b/Contentstack.Utils/Utils.cs @@ -5,8 +5,8 @@ using Contentstack.Utils.Interfaces; using System; using Contentstack.Utils.Enums; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Contentstack.Utils { @@ -289,7 +289,16 @@ private static object GetParentTagsValue(string dataValue, bool tagsAsObject) } } - public static JObject GetVariantAliases(JObject entry, string contentTypeUid) + private static readonly JsonSerializerOptions CompactJsonOptions = new JsonSerializerOptions { WriteIndented = false }; + + private static bool IsJsonNull(JsonNode node) + { + if (node == null) + return true; + return node is JsonValue jv && jv.GetValueKind() == JsonValueKind.Null; + } + + public static JsonObject GetVariantAliases(JsonObject entry, string contentTypeUid) { if (string.IsNullOrEmpty(contentTypeUid)) { @@ -299,14 +308,14 @@ public static JObject GetVariantAliases(JObject entry, string contentTypeUid) { throw new ArgumentException("Entry must not be null."); } - if (!entry.ContainsKey("uid") || entry["uid"] == null || entry["uid"].Type == JTokenType.Null) + if (!entry.TryGetPropertyValue("uid", out JsonNode uidNode) || IsJsonNull(uidNode)) { throw new ArgumentException("Entry must contain uid."); } - string entryUid = entry["uid"]?.ToString() ?? ""; - JArray variantsArray = ExtractVariantAliasesFromEntry(entry); - JObject result = new JObject + string entryUid = uidNode?.ToString() ?? ""; + JsonArray variantsArray = ExtractVariantAliasesFromEntry(entry); + var result = new JsonObject { ["entry_uid"] = entryUid, ["contenttype_uid"] = contentTypeUid, @@ -315,7 +324,7 @@ public static JObject GetVariantAliases(JObject entry, string contentTypeUid) return result; } - public static JArray GetVariantAliases(JArray entries, string contentTypeUid) + public static JsonArray GetVariantAliases(JsonArray entries, string contentTypeUid) { if (string.IsNullOrEmpty(contentTypeUid)) { @@ -323,13 +332,15 @@ public static JArray GetVariantAliases(JArray entries, string contentTypeUid) } if (entries == null) { - return new JArray(); + return new JsonArray(); } - JArray variantResults = new JArray(); - foreach (JToken token in entries) + var variantResults = new JsonArray(); + foreach (JsonNode token in entries) { - JObject entry = token as JObject; - if (entry != null && entry.ContainsKey("uid") && entry["uid"] != null && entry["uid"].Type != JTokenType.Null) + if (token is JsonObject entry && + entry.TryGetPropertyValue("uid", out JsonNode uidNode) && + uidNode != null && + !IsJsonNull(uidNode)) { variantResults.Add(GetVariantAliases(entry, contentTypeUid)); } @@ -342,17 +353,17 @@ public static JArray GetVariantAliases(JArray entries, string contentTypeUid) /// /// Entry JSON (e.g. from the Delivery API), or null to produce an empty payload. /// Content type UID for the entry. - /// A with a data-csvariants key whose value is a compact JSON array string. - public static JObject GetVariantMetadataTags(JObject entry, string contentTypeUid) + /// A with a data-csvariants key whose value is a compact JSON array string. + public static JsonObject GetVariantMetadataTags(JsonObject entry, string contentTypeUid) { if (entry == null) { - JObject result = new JObject(); - result["data-csvariants"] = "[]"; - return result; + var empty = new JsonObject(); + empty["data-csvariants"] = "[]"; + return empty; } - JArray entries = new JArray(); - entries.Add(entry); + var entries = new JsonArray(); + entries.Add(entry.DeepClone()); return GetVariantMetadataTags(entries, contentTypeUid); } @@ -361,10 +372,10 @@ public static JObject GetVariantMetadataTags(JObject entry, string contentTypeUi /// /// Array of entry JSON objects, or null to produce an empty payload. /// Content type UID shared by these entries. - /// A with a data-csvariants key whose value is a compact JSON array string. - public static JObject GetVariantMetadataTags(JArray entries, string contentTypeUid) + /// A with a data-csvariants key whose value is a compact JSON array string. + public static JsonObject GetVariantMetadataTags(JsonArray entries, string contentTypeUid) { - JObject result = new JObject(); + var result = new JsonObject(); if (entries == null) { result["data-csvariants"] = "[]"; @@ -375,46 +386,44 @@ public static JObject GetVariantMetadataTags(JArray entries, string contentTypeU throw new ArgumentException("ContentType is required."); } - JArray variantResults = GetVariantAliases(entries, contentTypeUid); - result["data-csvariants"] = variantResults.ToString(Formatting.None); + JsonArray variantResults = GetVariantAliases(entries, contentTypeUid); + result["data-csvariants"] = variantResults.ToJsonString(CompactJsonOptions); return result; } /// - /// Prefer . This alias exists for backward compatibility and will be removed in a future major release. + /// Prefer . This alias exists for backward compatibility and will be removed in a future major release. /// [Obsolete("Use GetVariantMetadataTags instead. This method will be removed in a future major release.")] - public static JObject GetDataCsvariantsAttribute(JObject entry, string contentTypeUid) + public static JsonObject GetDataCsvariantsAttribute(JsonObject entry, string contentTypeUid) { return GetVariantMetadataTags(entry, contentTypeUid); } /// - /// Prefer . This alias exists for backward compatibility and will be removed in a future major release. + /// Prefer . This alias exists for backward compatibility and will be removed in a future major release. /// [Obsolete("Use GetVariantMetadataTags instead. This method will be removed in a future major release.")] - public static JObject GetDataCsvariantsAttribute(JArray entries, string contentTypeUid) + public static JsonObject GetDataCsvariantsAttribute(JsonArray entries, string contentTypeUid) { return GetVariantMetadataTags(entries, contentTypeUid); } - private static JArray ExtractVariantAliasesFromEntry(JObject entry) + private static JsonArray ExtractVariantAliasesFromEntry(JsonObject entry) { - JArray variantArray = new JArray(); - JObject publishDetails = entry["publish_details"] as JObject; - if (publishDetails == null) + var variantArray = new JsonArray(); + if (!entry.TryGetPropertyValue("publish_details", out JsonNode pdNode) || pdNode is not JsonObject publishDetails) { return variantArray; } - JObject variants = publishDetails["variants"] as JObject; - if (variants == null) + if (!publishDetails.TryGetPropertyValue("variants", out JsonNode vNode) || vNode is not JsonObject variants) { return variantArray; } - foreach (JProperty prop in variants.Properties()) + foreach (KeyValuePair kvp in variants) { - if (prop.Value is JObject valueObj) + if (kvp.Value is JsonObject valueObj) { string alias = valueObj["alias"]?.ToString(); if (!string.IsNullOrEmpty(alias)) @@ -425,5 +434,19 @@ private static JArray ExtractVariantAliasesFromEntry(JObject entry) } return variantArray; } + + /// + /// Resolves a single Contentstack service endpoint URL for the given region. + /// Proxy for . + /// + public static string GetContentstackEndpoint(string region, string service, bool omitHttps = false) + => Endpoints.Endpoint.GetContentstackEndpoint(region, service, omitHttps); + + /// + /// Returns all service endpoint URLs for the given region. + /// Proxy for . + /// + public static Dictionary GetContentstackEndpoint(string region, bool omitHttps = false) + => Endpoints.Endpoint.GetContentstackEndpoint(region, omitHttps); } } diff --git a/Directory.Build.props b/Directory.Build.props index 8bdd5df..072c9eb 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,5 @@ - 1.2.0 + 2.0.0-beta.2 diff --git a/Scripts/refresh-region.py b/Scripts/refresh-region.py new file mode 100644 index 0000000..d7ed101 --- /dev/null +++ b/Scripts/refresh-region.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# Refresh regions.json from the Contentstack CDN. +# Usage (run from your project root): +# python3 Scripts/refresh-region.py +# python Scripts/refresh-region.py + +import glob +import json +import os +import ssl +import sys +import urllib.request + +REGIONS_URL = "https://artifacts.contentstack.com/regions.json" + +print(f"Fetching {REGIONS_URL} ...") + + +def _fetch(url): + # First attempt: normal SSL verification + try: + with urllib.request.urlopen(url, timeout=30) as r: + return r.read().decode("utf-8") + except urllib.error.URLError as e: + if "CERTIFICATE_VERIFY_FAILED" not in str(e): + raise + # macOS python.org builds often lack system certs — retry without verification + print("WARNING: SSL certificate verification failed. Retrying without verification.", file=sys.stderr) + print(" To fix permanently, run: /Applications/Python*/Install\\ Certificates.command", file=sys.stderr) + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + with urllib.request.urlopen(url, timeout=30, context=ctx) as r: + return r.read().decode("utf-8") + + +try: + raw = _fetch(REGIONS_URL) +except Exception as e: + print(f"ERROR: Could not download regions.json: {e}", file=sys.stderr) + sys.exit(1) + +try: + data = json.loads(raw) +except json.JSONDecodeError as e: + print(f"ERROR: Downloaded content is not valid JSON: {e}", file=sys.stderr) + sys.exit(1) + +if "regions" not in data: + print("ERROR: Downloaded JSON does not contain a 'regions' key.", file=sys.stderr) + sys.exit(1) + +region_count = len(data["regions"]) + +# Scan bin/ for every copy of the DLL (covers Debug/Release, any TFM, any nesting). +dll_pattern = os.path.join(os.getcwd(), "**", "Contentstack.Utils.dll") +found = [ + os.path.dirname(dll) + for dll in glob.glob(dll_pattern, recursive=True) + if os.sep + "bin" + os.sep in dll +] + +if not found: + print("[bin] No build output found — run 'dotnet build' first, then re-run this script.") + sys.exit(1) + +for bin_dir in found: + assets_dir = os.path.join(bin_dir, "Assets") + os.makedirs(assets_dir, exist_ok=True) + dest = os.path.join(assets_dir, "regions.json") + with open(dest, "w", encoding="utf-8") as f: + f.write(raw) + print(f"[bin] Wrote {region_count} regions → {dest}") diff --git a/build/contentstack.utils.targets b/build/contentstack.utils.targets new file mode 100644 index 0000000..5dca070 --- /dev/null +++ b/build/contentstack.utils.targets @@ -0,0 +1,14 @@ + + + <_RefreshScriptDest>$(MSBuildProjectDirectory)/Scripts/refresh-region.py + <_RefreshScriptSrc>$(MSBuildThisFileDirectory)../contentFiles/cs/any/Scripts/refresh-region.py + + + + + + + +