Skip to content

Development #83

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 6, 2025
Merged
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,7 @@ docs
fastlane/
Gemfile
#config file
Tests/config.json
Tests/config.json

snyk_output.json
talisman_output.json
4 changes: 4 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ fileignoreconfig:
checksum: 10ca4b0b986ae6166f69d7f985ca5b06238ac70dac3dd378fbf3dbdfd966afff
- filename: Contentstack.xcodeproj/xcshareddata/xcschemes/Contentstack iOS Tests.xcscheme
checksum: c439f6d268ae2ea0af023daeffdff2af5928d0610f90fa14c9e6e6ce7e4b3fad
- filename: ContentstackSwift.xcodeproj/project.pbxproj
checksum: dfabf06aeff3576c9347e52b3c494635477d81c7d121d8f1435d79f28829f4d1
- filename: ContentstackSwift.xcodeproj/project.pbxproj
checksum: 8937f832171f26061a209adcd808683f7bdfb739e7fc49aecd853d5055466251
version: ""


Expand Down
54 changes: 54 additions & 0 deletions ContentstackSwift.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions Sources/CSURLSessionDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// CSURLSessionDelegate.swift
// ContentstackSwift
//
// Created by Reeshika Hosmani on 19/05/25.
//

import Foundation

/// Protocol for SSL pinning customization in Contentstack SDK
@objc public protocol CSURLSessionDelegate: NSObjectProtocol, URLSessionDelegate {

/// Tells the delegate that the session received an authentication challenge.
/// - Parameters:
/// - session: The session that received the authentication challenge.
/// - challenge: An object that contains the request for authentication.
/// - completionHandler: A handler that your delegate method must call.
@objc func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

/// Tells the delegate that the session task received an authentication challenge.
/// - Parameters:
/// - session: The session containing the task that received the authentication challenge.
/// - task: The task that received the authentication challenge.
/// - challenge: An object that contains the request for authentication.
/// - completionHandler: A handler that your delegate method must call.
@objc optional func urlSession(_ session: URLSession,
task: URLSessionTask,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
}
3 changes: 3 additions & 0 deletions Sources/ContentstackConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public struct ContentstackConfig {
/// The configuration for the URLSession.
/// Note that HTTP headers will be overwritten internally by the SDK so that requests can be authorized correctly.
public var sessionConfiguration: URLSessionConfiguration = .default

/// Delegate for handling SSL pinning and URL session customization
public var urlSessionDelegate: CSURLSessionDelegate?

/// Computed version of the user agent, including OS name and version
internal func userAgentString() -> String {
Expand Down
19 changes: 18 additions & 1 deletion Sources/ContentstackResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private protocol HomogeneousResponse: ResponseParams {
}

internal enum ResponseCodingKeys: String, CodingKey {
case entries, entry, assets, asset, skip, limit, errors, count
case entries, entry, assets, asset, skip, limit, errors, count, globalFields, globalField
case contentTypes = "content_types", contentType = "content_type"
}

Expand Down Expand Up @@ -90,6 +90,23 @@ where ItemType: EndpointAccessible & Decodable {
}
self.items = taxonomies
}
case .globalfields:
// Decode entire response as [String: AnyDecodable] using singleValueContainer
let fullResponseContainer = try decoder.singleValueContainer()
let fullResponse = try fullResponseContainer.decode([String: AnyDecodable].self)

if let globalFieldsArray = fullResponse["global_fields"]?.value as? [[String: Any]] {
for item in globalFieldsArray {
let data = try JSONSerialization.data(withJSONObject: item, options: [])
let model = try JSONDecoder().decode(ItemType.self, from: data)
self.items.append(model)
}
} else if let globalField = fullResponse["global_field"]?.value as? [String: Any] {
let data = try JSONSerialization.data(withJSONObject: globalField, options: [])
let model = try JSONDecoder().decode(ItemType.self, from: data)
self.items = [model]
}

default:
print("sync")
}
Expand Down
2 changes: 2 additions & 0 deletions Sources/EndPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public enum Endpoint: String {
case sync = "stacks/sync"

case taxnomies = "taxonomies"

case globalfields = "global_fields"
/// The path component string for the current endpoint.
public var pathComponent: String {
return rawValue
Expand Down
95 changes: 95 additions & 0 deletions Sources/GlobalField.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//
// GlobalField.swift
// ContentstackSwift
//
// Created by Reeshika Hosmani on 26/05/25.
//

import Foundation

public class GlobalField: CachePolicyAccessible{

public var cachePolicy: CachePolicy = .networkOnly
/// URI Parameters
internal var parameters: Parameters = [:]
internal var headers: [String: String] = [:]
internal var stack: Stack
/// Unique ID of the global_field of which you wish to retrieve the details.
internal var uid: String?
/// Query Parameters
public var queryParameter: [String: Any] = [:]

internal required init(stack: Stack) {
self.stack = stack
}
internal required init(_ uid: String?, stack: Stack) {
self.uid = uid
self.stack = stack
}

public func includeBranch() -> GlobalField {
self.parameters[QueryParameter.includeBranch] = true
return self
}

public func includeGlobalFieldSchema() -> GlobalField {
self.parameters[QueryParameter.includeGlobalFieldSchema] = true
return self
}

}

extension GlobalField: ResourceQueryable {
/// This call fetches the latest version of a specific `Global Field` of a particular stack.
/// - Parameters:
/// - completion: A handler which will be called on completion of the operation.
///
/// Example usage:
/// ```
/// let stack = Contentstack.stack(apiKey: apiKey,
/// deliveryToken: deliveryToken,
/// environment: environment)
///
/// stack.globalField
/// .fetch { (result: Result<GlobalFieldModel, Error>, response: ResponseType) in
/// switch result {
/// case .success(let model):
/// //Model retrive from API
/// case .failure(let error):
/// //Error Message
/// }
/// }
/// ```
public func fetch<ResourceType>(_ completion: @escaping (Result<ResourceType, Error>, ResponseType) -> Void)
where ResourceType: EndpointAccessible & Decodable {
guard let uid = self.uid else { fatalError("Please provide Global Field uid") }
self.stack.fetch(endpoint: ResourceType.endpoint,
cachePolicy: self.cachePolicy,
parameters: parameters + [QueryParameter.uid: uid],
headers: headers,
then: { (result: Result<ContentstackResponse<ResourceType>, Error>, response: ResponseType) in
switch result {
case .success(let contentStackResponse):
if let resource = contentStackResponse.items.first {
completion(.success(resource), response)
} else {
completion(.failure(SDKError.invalidUID(string: uid)), response)
}
case .failure(let error):
completion(.failure(error), response)
}
})
}
}

extension GlobalField : Queryable{
public func find<ResourceType>(_ completion: @escaping ResultsHandler<ContentstackResponse<ResourceType>>) where ResourceType :Decodable & EndpointAccessible {
if self.queryParameter.count > 0,
let query = self.queryParameter.jsonString {
self.parameters[QueryParameter.query] = query
}
self.stack.fetch(endpoint: ResourceType.endpoint,
cachePolicy: self.cachePolicy, parameters: parameters, headers: headers, then: completion)
}

}
131 changes: 131 additions & 0 deletions Sources/GlobalFieldModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//
// GlobalFieldModel.swift
// ContentstackSwift
//
// Created by Reeshika Hosmani on 27/05/25.
//

import Foundation

// Helper for decoding [String: Any] and other nested unknown types
public struct AnyDecodable: Decodable {
public let value: Any

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

if let int = try? container.decode(Int.self) {
value = int
} else if let double = try? container.decode(Double.self) {
value = double
} else if let bool = try? container.decode(Bool.self) {
value = bool
} else if let string = try? container.decode(String.self) {
value = string
} else if let array = try? container.decode([AnyDecodable].self) {
value = array.map { $0.value }
} else if let dict = try? container.decode([String: AnyDecodable].self) {
value = dict.mapValues { $0.value }
} else {
value = NSNull()
}
}
}

public protocol GlobalFieldDecodable: GlobalFields, FieldKeysQueryable, EndpointAccessible, Decodable {}

public final class GlobalFieldModel : GlobalFieldDecodable {
public var title: String

public var uid: String

public var createdAt: Date?

public var updatedAt: Date?

public var schema: [[String: Any]] = []

public var description: String?

public var maintainRevisions: Bool?

public var inbuiltClass: Bool?

public var version: Int?

public var branch: String?

public var lastActivity: [String: Any] = [:]

public enum FieldKeys: String, CodingKey {
case title, uid, description
case createdAt = "created_at"
case updatedAt = "updated_at"
case maintainRevisions = "maintain_revisions"
case version = "_version"
case branch = "_branch"
case lastActivity = "last_activity"
case inbuiltClass = "inbuilt_class"
case schema
}

public enum QueryableCodingKey: String, CodingKey {
case uid, title, description
case createdAt = "created_at"
case updatedAt = "updated_at"
}

// public required init(from decoder: Decoder) throws {
// let container = try decoder.container(keyedBy: FieldKeys.self)
// uid = try container.decode(String.self, forKey: .uid)
// title = try container.decode(String.self, forKey: .title)
// description = try? container.decodeIfPresent(String.self, forKey: .description)
// createdAt = try? container.decode(Date.self, forKey: .createdAt)
// updatedAt = try? container.decode(Date.self, forKey: .updatedAt)
// maintainRevisions = try? container.decode(Bool.self, forKey: .maintainRevisions)
// version = try? container.decode(Int.self, forKey: .version)
// branch = try? container.decode(String.self, forKey: .branch)
// inbuiltClass = try? container.decode(Bool.self, forKey: .inbuiltClass)
// let containerFields = try? decoder.container(keyedBy: JSONCodingKeys.self)
// let globalFieldSchema = try containerFields?.decode(Dictionary<String, Any>.self)
// if let schema = globalFieldSchema?["schema"] as? [[String: Any]] {
// self.schema = schema
// }
//// if let decodedSchema = try? container.decodeIfPresent([ [String: AnyDecodable] ].self, forKey: .schema) {
//// self.schema = decodedSchema.map { $0.mapValues { $0.value } }
//// }
// if let decodedLastActivity = try? container.decodeIfPresent([String: AnyDecodable].self, forKey: .lastActivity) {
// lastActivity = decodedLastActivity.mapValues { $0.value }
// }
// }


public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: FieldKeys.self)

uid = try container.decode(String.self, forKey: .uid)
title = try container.decode(String.self, forKey: .title)
description = try? container.decodeIfPresent(String.self, forKey: .description)
createdAt = try? container.decode(Date.self, forKey: .createdAt)
updatedAt = try? container.decode(Date.self, forKey: .updatedAt)
maintainRevisions = try? container.decode(Bool.self, forKey: .maintainRevisions)
version = try? container.decode(Int.self, forKey: .version)
branch = try? container.decode(String.self, forKey: .branch)
inbuiltClass = try? container.decode(Bool.self, forKey: .inbuiltClass)

// ✅ Decode schema directly and safely
if let decodedSchema = try? container.decodeIfPresent([[String: AnyDecodable]].self, forKey: .schema) {
self.schema = decodedSchema.map { $0.mapValues { $0.value } }
}

if let decodedLastActivity = try? container.decodeIfPresent([String: AnyDecodable].self, forKey: .lastActivity) {
lastActivity = decodedLastActivity.mapValues { $0.value }
}
}
}

extension GlobalFieldModel : EndpointAccessible {
public static var endpoint: Endpoint {
return .globalfields
}
}
4 changes: 4 additions & 0 deletions Sources/QueryParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ internal enum QueryParameter {
internal static let includeFallback = "include_fallback"

internal static let includeEmbeddedItems = "include_embedded_items"

internal static let includeBranch = "include_branch"

internal static let includeGlobalFieldSchema = "include_global_field_schema"
}

extension Query {
Expand Down
4 changes: 2 additions & 2 deletions Sources/QueryProtocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ extension BaseQuery {
}
/// The base Queryable protocol to fetch instance for `ContentType`, `Asset`, and `Entry`.
public protocol ResourceQueryable {
/// This call fetches the latest version of a specific `ContentType`, `Asset`, and `Entry` of a particular stack.
/// This call fetches the latest version of a specific `ContentType`, `Asset`, `Entry`and `Global Field` of a particular stack.
/// - Parameters:
/// - completion: A handler which will be called on completion of the operation.
func fetch<ResourceType>(_ completion: @escaping ResultsHandler<ResourceType>)
Expand All @@ -562,7 +562,7 @@ public protocol ResourceQueryable {
/// The base Queryable protocol to find collections for content types, assets, and entries.
public protocol Queryable {
/// This is a generic find method which can be used to fetch collections of `ContentType`,
/// `Entry`, and `Asset` instances.
/// `Entry`, `Asset` ,`Global fields`instances.
/// - Parameters:
/// - completion: A handler which will be called on completion of the operation.
func find<ResourceType>(_ completion: @escaping ResultsHandler<ContentstackResponse<ResourceType>>)
Expand Down
Loading
Loading