Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to this project will be documented in this file.

## [1.5.0] - 2026-03-31

- **`getVariantMetadataTags`** is the canonical API for `data-csvariants`; **`getDataCsvariantsAttribute`** is deprecated (delegates to it until removed in a major release).

## [1.4.0] - 2026-03-30

### Enhancement
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Add the following to your Podfile:
let package = Package(
name: "YourProject",
dependencies: [
.package(url: "https://github.com/tid-kijyun/ContentstackUtils.git", from: "1.3.1"),
.package(url: "https://github.com/tid-kijyun/ContentstackUtils.git", from: "1.5.0"),
],
targets: [
.target(
Expand Down Expand Up @@ -257,3 +257,7 @@ graphQLClient.fetch (query: ProductsQuery(), cachePolicy: CachePolicy.fetchIgnor
}
```

### Variant metadata (`data-csvariants`)

To build the JSON string for the `data-csvariants` HTML attribute from Delivery API entry JSON, use **`ContentstackUtils.getVariantMetadataTags`**. See [Docs/variant-metadata-api.md](Docs/variant-metadata-api.md) for parameters, return values, and migration from the deprecated `getDataCsvariantsAttribute` APIs.

18 changes: 14 additions & 4 deletions Sources/ContentstackUtils/ContentstackUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,24 +115,34 @@ public struct ContentstackUtils {
}
}

public static func getDataCsvariantsAttribute(entry: [String: Any]?, contentTypeUid: String) throws -> [String: Any]{
/// Builds the `data-csvariants` HTML attribute payload from one entry (or `nil` → empty JSON array string).
public static func getVariantMetadataTags(entry: [String: Any]?, contentTypeUid: String) throws -> [String: Any] {
guard let e = entry else {
return ["data-csvariants": "[]"]
}

let payload = try getVariantAliases(entry: e, contentTypeUid: contentTypeUid)
let s = try jsonString(for: [payload])
return ["data-csvariants": s]

}

public static func getDataCsvariantsAttribute(entries: [[String: Any]], contentTypeUid: String) throws -> [String: Any]{
/// Builds the `data-csvariants` HTML attribute payload for multiple entries (empty input → `"[]"`).
public static func getVariantMetadataTags(entries: [[String: Any]], contentTypeUid: String) throws -> [String: Any] {
try validateContentTypeUid(contentTypeUid)
let payloads = try getVariantAliases(entries: entries, contentTypeUid: contentTypeUid)
let s = try jsonString(for: payloads)
return ["data-csvariants": s]
}

@available(*, deprecated, message: "Use getVariantMetadataTags(entry:contentTypeUid:). Will be removed in a future major release.")
public static func getDataCsvariantsAttribute(entry: [String: Any]?, contentTypeUid: String) throws -> [String: Any] {
try getVariantMetadataTags(entry: entry, contentTypeUid: contentTypeUid)
}

@available(*, deprecated, message: "Use getVariantMetadataTags(entries:contentTypeUid:). Will be removed in a future major release.")
public static func getDataCsvariantsAttribute(entries: [[String: Any]], contentTypeUid: String) throws -> [String: Any] {
try getVariantMetadataTags(entries: entries, contentTypeUid: contentTypeUid)
}


private static func validateContentTypeUid(_ contentTypeUid: String) throws {
if contentTypeUid.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Expand Down
77 changes: 59 additions & 18 deletions Tests/ContentstackUtilsTests/VariantUtilityTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ final class VariantUtilityTests: XCTestCase {
return arr.compactMap { $0 as? [String: Any] }
}

/// JSON string equality is unstable (key order); compare payload semantics.
private func assertSemanticEqualDataCsvariantsPayload(_ a: [String: Any], _ b: [String: Any], file: StaticString = #file, line: UInt = #line) throws {
let sa = try XCTUnwrap(a["data-csvariants"] as? String, file: file, line: line)
let sb = try XCTUnwrap(b["data-csvariants"] as? String, file: file, line: line)
let aArr = try JSONSerialization.jsonObject(with: Data(sa.utf8)) as! [Any]
let bArr = try JSONSerialization.jsonObject(with: Data(sb.utf8)) as! [Any]
XCTAssertEqual(aArr.count, bArr.count, file: file, line: line)
for i in 0..<aArr.count {
guard let da = aArr[i] as? [String: Any], let db = bArr[i] as? [String: Any] else {
XCTFail("Expected object at index \(i)", file: file, line: line)
return
}
XCTAssertEqual(da["entry_uid"] as? String, db["entry_uid"] as? String, file: file, line: line)
XCTAssertEqual(da["contenttype_uid"] as? String, db["contenttype_uid"] as? String, file: file, line: line)
let va = Set(da["variants"] as? [String] ?? [])
let vb = Set(db["variants"] as? [String] ?? [])
XCTAssertEqual(va, vb, file: file, line: line)
}
}

func testGetVariantAliasesSingleEntry() throws {
let root = try TestDecodable.loadJSONObject(named: "variantsSingleEntry")
let entry = try XCTUnwrap(root["entry"] as? [String: Any])
Expand All @@ -31,10 +51,10 @@ final class VariantUtilityTests: XCTestCase {
XCTAssertEqual(stringSet(from: result["variants"]), ["cs_personalize_0_0", "cs_personalize_0_3"])
}

func testGetDataCsvariantsAttributeSingleEntry() throws {
func testGetVariantMetadataTagsSingleEntry() throws {
let root = try TestDecodable.loadJSONObject(named: "variantsSingleEntry")
let entry = try XCTUnwrap(root["entry"] as? [String: Any])
let wrapper = try ContentstackUtils.getDataCsvariantsAttribute(entry: entry, contentTypeUid: contentTypeUid)
let wrapper = try ContentstackUtils.getVariantMetadataTags(entry: entry, contentTypeUid: contentTypeUid)

let parsed = try parseDataCsvariantsArray(wrapper)
XCTAssertEqual(parsed.count, 1)
Expand Down Expand Up @@ -66,10 +86,10 @@ final class VariantUtilityTests: XCTestCase {
XCTAssertEqual((third["variants"] as? [String])?.count, 0)
}

func testGetDataCsvariantsAttributeMultipleEntries() throws {
func testGetVariantMetadataTagsMultipleEntries() throws {
let root = try TestDecodable.loadJSONObject(named: "variantsEntries")
let entries = try XCTUnwrap(root["entries"] as? [[String: Any]])
let wrapper = try ContentstackUtils.getDataCsvariantsAttribute(entries: entries, contentTypeUid: contentTypeUid)
let wrapper = try ContentstackUtils.getVariantMetadataTags(entries: entries, contentTypeUid: contentTypeUid)

let parsed = try parseDataCsvariantsArray(wrapper)
XCTAssertEqual(parsed.count, 3)
Expand Down Expand Up @@ -102,11 +122,31 @@ final class VariantUtilityTests: XCTestCase {
}
}

func testGetDataCsvariantsAttributeWhenEntryNil() throws {
let result = try ContentstackUtils.getDataCsvariantsAttribute(entry: nil, contentTypeUid: "landing_page")
func testGetVariantMetadataTagsWhenEntryNil() throws {
let result = try ContentstackUtils.getVariantMetadataTags(entry: nil, contentTypeUid: "landing_page")
XCTAssertEqual(result["data-csvariants"] as? String, "[]")
}

/// Deprecated `getDataCsvariantsAttribute` must match canonical `getVariantMetadataTags` until removal.
func testDeprecatedGetDataCsvariantsAttributeDelegatesToGetVariantMetadataTags() throws {
let root = try TestDecodable.loadJSONObject(named: "variantsSingleEntry")
let entry = try XCTUnwrap(root["entry"] as? [String: Any])

let canonical = try ContentstackUtils.getVariantMetadataTags(entry: entry, contentTypeUid: contentTypeUid)
let deprecated = try ContentstackUtils.getDataCsvariantsAttribute(entry: entry, contentTypeUid: contentTypeUid)
try assertSemanticEqualDataCsvariantsPayload(canonical, deprecated)

let multiRoot = try TestDecodable.loadJSONObject(named: "variantsEntries")
let entries = try XCTUnwrap(multiRoot["entries"] as? [[String: Any]])
let canonicalMany = try ContentstackUtils.getVariantMetadataTags(entries: entries, contentTypeUid: contentTypeUid)
let deprecatedMany = try ContentstackUtils.getDataCsvariantsAttribute(entries: entries, contentTypeUid: contentTypeUid)
try assertSemanticEqualDataCsvariantsPayload(canonicalMany, deprecatedMany)

let nilCanonical = try ContentstackUtils.getVariantMetadataTags(entry: nil, contentTypeUid: "x")
let nilDeprecated = try ContentstackUtils.getDataCsvariantsAttribute(entry: nil, contentTypeUid: "x")
XCTAssertEqual(nilCanonical["data-csvariants"] as? String, nilDeprecated["data-csvariants"] as? String)
}

// MARK: - Edge cases

func testGetVariantAliasesEmptyEntriesArray() throws {
Expand All @@ -120,8 +160,8 @@ final class VariantUtilityTests: XCTestCase {
}
}

func testGetDataCsvariantsAttributeEmptyEntriesArray() throws {
let wrapper = try ContentstackUtils.getDataCsvariantsAttribute(entries: [], contentTypeUid: contentTypeUid)
func testGetVariantMetadataTagsEmptyEntriesArray() throws {
let wrapper = try ContentstackUtils.getVariantMetadataTags(entries: [], contentTypeUid: contentTypeUid)
XCTAssertEqual(wrapper["data-csvariants"] as? String, "[]")
}

Expand Down Expand Up @@ -175,38 +215,39 @@ final class VariantUtilityTests: XCTestCase {
}
}

func testGetDataCsvariantsAttributeThrowsWhenContentTypeUidEmptyWithEntry() throws {
func testGetVariantMetadataTagsThrowsWhenContentTypeUidEmptyWithEntry() throws {
let root = try TestDecodable.loadJSONObject(named: "variantsSingleEntry")
let entry = try XCTUnwrap(root["entry"] as? [String: Any])
XCTAssertThrowsError(try ContentstackUtils.getDataCsvariantsAttribute(entry: entry, contentTypeUid: "")) { error in
XCTAssertThrowsError(try ContentstackUtils.getVariantMetadataTags(entry: entry, contentTypeUid: "")) { error in
XCTAssertTrue(error is ContentstackUtils.VariantUtilityError)
}
}

func testGetDataCsvariantsAttributeEntriesThrowsWhenContentTypeUidWhitespaceOnly() {
XCTAssertThrowsError(try ContentstackUtils.getDataCsvariantsAttribute(entries: [], contentTypeUid: "\t\n")) { error in
func testGetVariantMetadataTagsEntriesThrowsWhenContentTypeUidWhitespaceOnly() {
XCTAssertThrowsError(try ContentstackUtils.getVariantMetadataTags(entries: [], contentTypeUid: "\t\n")) { error in
XCTAssertTrue(error is ContentstackUtils.VariantUtilityError)
}
}

#if !canImport(ObjectiveC)
static var allTests = [
("testGetVariantAliasesSingleEntry", testGetVariantAliasesSingleEntry),
("testGetDataCsvariantsAttributeSingleEntry", testGetDataCsvariantsAttributeSingleEntry),
("testGetVariantMetadataTagsSingleEntry", testGetVariantMetadataTagsSingleEntry),
("testGetVariantAliasesMultipleEntries", testGetVariantAliasesMultipleEntries),
("testGetDataCsvariantsAttributeMultipleEntries", testGetDataCsvariantsAttributeMultipleEntries),
("testGetVariantMetadataTagsMultipleEntries", testGetVariantMetadataTagsMultipleEntries),
("testGetVariantAliasesThrowsWhenEntryMissingUid", testGetVariantAliasesThrowsWhenEntryMissingUid),
("testGetVariantAliasesThrowsWhenContentTypeUidEmpty", testGetVariantAliasesThrowsWhenContentTypeUidEmpty),
("testGetDataCsvariantsAttributeWhenEntryNil", testGetDataCsvariantsAttributeWhenEntryNil),
("testGetVariantMetadataTagsWhenEntryNil", testGetVariantMetadataTagsWhenEntryNil),
("testDeprecatedGetDataCsvariantsAttributeDelegatesToGetVariantMetadataTags", testDeprecatedGetDataCsvariantsAttributeDelegatesToGetVariantMetadataTags),
("testGetVariantAliasesEmptyEntriesArray", testGetVariantAliasesEmptyEntriesArray),
("testGetVariantAliasesEmptyEntriesThrowsWhenContentTypeUidEmpty", testGetVariantAliasesEmptyEntriesThrowsWhenContentTypeUidEmpty),
("testGetDataCsvariantsAttributeEmptyEntriesArray", testGetDataCsvariantsAttributeEmptyEntriesArray),
("testGetVariantMetadataTagsEmptyEntriesArray", testGetVariantMetadataTagsEmptyEntriesArray),
("testGetVariantAliasesEntryWithoutPublishDetails", testGetVariantAliasesEntryWithoutPublishDetails),
("testGetVariantAliasesEntryWithEmptyVariantsMap", testGetVariantAliasesEntryWithEmptyVariantsMap),
("testGetVariantAliasesSkipsInvalidVariantValues", testGetVariantAliasesSkipsInvalidVariantValues),
("testGetVariantAliasesThrowsWhenUidIsEmptyString", testGetVariantAliasesThrowsWhenUidIsEmptyString),
("testGetDataCsvariantsAttributeThrowsWhenContentTypeUidEmptyWithEntry", testGetDataCsvariantsAttributeThrowsWhenContentTypeUidEmptyWithEntry),
("testGetDataCsvariantsAttributeEntriesThrowsWhenContentTypeUidWhitespaceOnly", testGetDataCsvariantsAttributeEntriesThrowsWhenContentTypeUidWhitespaceOnly),
("testGetVariantMetadataTagsThrowsWhenContentTypeUidEmptyWithEntry", testGetVariantMetadataTagsThrowsWhenContentTypeUidEmptyWithEntry),
("testGetVariantMetadataTagsEntriesThrowsWhenContentTypeUidWhitespaceOnly", testGetVariantMetadataTagsEntriesThrowsWhenContentTypeUidWhitespaceOnly),
]
#endif
}