diff --git a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Lib/Logger.swift b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Lib/Logger.swift index baf31052..6cb895e1 100644 --- a/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Lib/Logger.swift +++ b/platforms/swift/Samples/CheckoutKitSwiftDemo/CheckoutKitSwiftDemo/Sources/Lib/Logger.swift @@ -1,10 +1,10 @@ import Foundation import ShopifyCheckoutKit -class FileLogger: Logger { - private var fileHandle: FileHandle? +final class FileLogger: Logger { + private let fileHandle: FileHandle? - var logFileUrl: URL + let logFileUrl: URL public init(_ filename: String) { let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) @@ -18,6 +18,7 @@ class FileLogger: Logger { do { fileHandle = try FileHandle(forWritingTo: logFileUrl) } catch let error as NSError { + fileHandle = nil print("Couldn't open the log file. Error: \(error)") } } diff --git a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift index 15ccd698..da76fea0 100644 --- a/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift +++ b/platforms/swift/Sources/ShopifyAcceleratedCheckouts/ShopifyAcceleratedCheckouts.swift @@ -8,13 +8,16 @@ public enum ShopifyAcceleratedCheckouts { /// The logging level for Accelerated Checkouts operations /// Default: .error - which will emit "error" and "fault" logs - public static var logLevel: LogLevel = .error { - didSet { - logger.logLevel = logLevel + public static var logLevel: LogLevel { + get { + logger.logLevel + } + set { + logger.logLevel = newValue } } /// Shared logger for ShopifyAcceleratedCheckouts /// To modify the logLevel - internal static var logger = OSLogger(prefix: name, logLevel: logLevel) + internal static let logger = OSLogger(prefix: name, logLevel: .error) } diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/Configuration.swift b/platforms/swift/Sources/ShopifyCheckoutKit/Configuration.swift index ab0da97b..be8bcc53 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/Configuration.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/Configuration.swift @@ -1,6 +1,6 @@ import UIKit -public struct Platform: Equatable { +public struct Platform: Equatable, Sendable { public let identifier: String public let version: String? @@ -11,7 +11,7 @@ public struct Platform: Equatable { } } -public struct Configuration { +public struct Configuration: Sendable { /// Determines the color scheme used when checkout is presented. /// /// By default, the color scheme is determined based on the current @@ -48,7 +48,7 @@ public struct Configuration { } extension Configuration { - public enum ColorScheme: String, CaseIterable { + public enum ColorScheme: String, CaseIterable, Sendable { /// Uses a light, idiomatic color scheme. case light /// Uses a dark, idiomatic color scheme. @@ -61,7 +61,7 @@ extension Configuration { } extension Configuration { - public struct Confetti { + public struct Confetti: Sendable { public var enabled: Bool = false public var particles = [UIImage]() diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/LockedValue.swift b/platforms/swift/Sources/ShopifyCheckoutKit/LockedValue.swift new file mode 100644 index 00000000..3a36e2c4 --- /dev/null +++ b/platforms/swift/Sources/ShopifyCheckoutKit/LockedValue.swift @@ -0,0 +1,100 @@ +import Foundation +import os.lock + +private protocol LockedValueStorage: Sendable { + associatedtype Value: Sendable + + func get() -> Value + func set(_ newValue: Value) + func update(_ block: (inout Value) -> Void) +} + +@available(iOS 16.0, *) +private final class OSAllocatedUnfairLockedValueStorage: LockedValueStorage { + /// When the package minimum deployment target is iOS 18, consider + /// replacing this storage with Synchronization.Mutex. + private let lock: OSAllocatedUnfairLock + + init(_ value: Value) { + lock = OSAllocatedUnfairLock(initialState: value) + } + + func get() -> Value { + lock.withLock { $0 } + } + + func set(_ newValue: Value) { + lock.withLock { + $0 = newValue + } + } + + func update(_ block: (inout Value) -> Void) { + lock.withLockUnchecked { + block(&$0) + } + } +} + +/// iOS 15 fallback for lock-backed Sendable storage. +/// +/// SAFETY: +/// - `value` is only read and written while holding `lock`. +/// - `Value` is constrained to `Sendable`, so returned values can cross concurrency domains. +/// - This exists because `OSAllocatedUnfairLock` is only available on iOS 16+. +/// +/// Delete this fallback when the package minimum deployment target is iOS 16. +@available(iOS, deprecated: 16.0, message: "Use OSAllocatedUnfairLockedValueStorage on iOS 16+.") +private final class NSLockedValueStorage: LockedValueStorage, @unchecked Sendable { + private let lock = NSLock() + private var value: Value + + init(_ value: Value) { + self.value = value + } + + func get() -> Value { + lock.withLock { value } + } + + func set(_ newValue: Value) { + lock.withLock { + value = newValue + } + } + + func update(_ block: (inout Value) -> Void) { + lock.withLock { + block(&value) + } + } +} + +/// Lock-backed storage for mutable values that must remain synchronously readable and writable. +/// +/// Use this for module-internal backing storage when a public or static mutable API needs to +/// preserve synchronous mutation, but the stored value would otherwise be non-isolated shared +/// mutable state under Swift 6 concurrency checking. +final class LockedValue: Sendable { + private let storage: any LockedValueStorage + + init(_ value: Value) { + if #available(iOS 16.0, *) { + storage = OSAllocatedUnfairLockedValueStorage(value) + } else { + storage = NSLockedValueStorage(value) + } + } + + func get() -> Value { + storage.get() + } + + func set(_ newValue: Value) { + storage.set(newValue) + } + + func update(_ block: (inout Value) -> Void) { + storage.update(block) + } +} diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/Logger.swift b/platforms/swift/Sources/ShopifyCheckoutKit/Logger.swift index 0ce01e2d..6081d9dd 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/Logger.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/Logger.swift @@ -3,28 +3,55 @@ import os.log private let subsystem = "com.shopify.checkoutkit" -public enum LogLevel: String, CaseIterable { +public enum LogLevel: String, CaseIterable, Sendable { case all case debug case error case none } -public class OSLogger { +public final class OSLogger: Sendable { private let logger = OSLog(subsystem: subsystem, category: OSLog.Category.pointsOfInterest) - private var prefix: String - package var logLevel: LogLevel + private let prefix: String + private let lockedLogLevel: LockedValue + private let sendToOSLogHandler: (@Sendable (String, OSLogType) -> Void)? - public static var shared = OSLogger() + package var logLevel: LogLevel { + get { + lockedLogLevel.get() + } + set { + lockedLogLevel.set(newValue) + } + } + + private static let lockedSharedLogger = LockedValue(OSLogger()) + + public static var shared: OSLogger { + get { + lockedSharedLogger.get() + } + set { + lockedSharedLogger.set(newValue) + } + } + + public convenience init() { + self.init(prefix: "ShopifyCheckoutKit", logLevel: ShopifyCheckoutKit.configuration.logLevel) + } - public init() { - prefix = "ShopifyCheckoutKit" - logLevel = ShopifyCheckoutKit.configuration.logLevel + public convenience init(prefix: String, logLevel: LogLevel) { + self.init(prefix: prefix, logLevel: logLevel, sendToOSLogHandler: nil) } - public init(prefix: String, logLevel: LogLevel) { + init( + prefix: String, + logLevel: LogLevel, + sendToOSLogHandler: (@Sendable (String, OSLogType) -> Void)? + ) { self.prefix = prefix - self.logLevel = logLevel + lockedLogLevel = LockedValue(logLevel) + self.sendToOSLogHandler = sendToOSLogHandler } public func debug(_ message: String) { @@ -54,24 +81,30 @@ public class OSLogger { /// Capturing `os_log` output is not possible /// This indirection lets us capture messages in `LoggerTests.swift` internal func sendToOSLog(_ message: String, type: OSLogType) { - os_log("%@", log: logger, type: type, message) + if let sendToOSLogHandler { + sendToOSLogHandler(message, type) + } else { + os_log("%@", log: logger, type: type, message) + } } private func shouldEmit(_ choice: LogLevel) -> Bool { - if logLevel == .none { + let currentLogLevel = logLevel + + if currentLogLevel == .none { return false } - return logLevel == .all || logLevel == choice + return currentLogLevel == .all || currentLogLevel == choice } } -public protocol Logger { +public protocol Logger: Sendable { func log(_ message: String) func clearLogs() } -public class NoOpLogger: Logger { +public final class NoOpLogger: Logger { public func log(_: String) {} public func clearLogs() {} diff --git a/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift b/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift index 5ee84e63..caae2187 100644 --- a/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift +++ b/platforms/swift/Sources/ShopifyCheckoutKit/ShopifyCheckoutKit.swift @@ -6,16 +6,26 @@ import UIKit /// The version of the `ShopifyCheckoutKit` library. public let version = "4.0.0-alpha.1" +private let lockedCheckoutKitConfiguration = LockedValue(Configuration()) + /// The configuration options for the `ShopifyCheckoutKit` library. -public var configuration = Configuration() { - didSet { - OSLogger.shared.logLevel = configuration.logLevel +public var configuration: Configuration { + get { lockedCheckoutKitConfiguration.get() } + set { + lockedCheckoutKitConfiguration.set(newValue) + applyConfigurationChange() } } /// A convienence function for configuring the `ShopifyCheckoutKit` library. public func configure(_ block: (inout Configuration) -> Void) { - block(&configuration) + lockedCheckoutKitConfiguration.update(block) + applyConfigurationChange() +} + +private func applyConfigurationChange() { + let configuration = lockedCheckoutKitConfiguration.get() + OSLogger.shared.logLevel = configuration.logLevel } @MainActor diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/ConfigurationTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/ConfigurationTests.swift index 1792170d..d0a6ec8f 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/ConfigurationTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/ConfigurationTests.swift @@ -5,7 +5,15 @@ import XCTest class ConfigurationTests: XCTestCase { override func setUp() { super.setUp() - // Reset configuration to defaults + resetConfigurationState() + } + + override func tearDown() { + resetConfigurationState() + super.tearDown() + } + + private func resetConfigurationState() { ShopifyCheckoutKit.configuration = Configuration() } @@ -27,4 +35,27 @@ class ConfigurationTests: XCTestCase { ShopifyCheckoutKit.configuration.closeButtonTintColor = nil XCTAssertNil(ShopifyCheckoutKit.configuration.closeButtonTintColor) } + + func testColorSchemeCanBeSetDirectly() { + ShopifyCheckoutKit.configuration.colorScheme = .light + + XCTAssertEqual(ShopifyCheckoutKit.configuration.colorScheme, .light) + } + + func testConfigureCanBatchConfigurationChanges() { + ShopifyCheckoutKit.configure { + $0.colorScheme = .dark + $0.closeButtonTintColor = .blue + } + + XCTAssertEqual(ShopifyCheckoutKit.configuration.colorScheme, .dark) + XCTAssertEqual(ShopifyCheckoutKit.configuration.closeButtonTintColor, .blue) + } + + func testDirectConfigurationMutationUpdatesLogger() { + ShopifyCheckoutKit.configuration.logLevel = .all + + XCTAssertEqual(ShopifyCheckoutKit.configuration.logLevel, .all) + XCTAssertEqual(OSLogger.shared.logLevel, .all) + } } diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/LockedValueTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/LockedValueTests.swift new file mode 100644 index 00000000..2e4e1577 --- /dev/null +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/LockedValueTests.swift @@ -0,0 +1,41 @@ +@testable import ShopifyCheckoutKit +import XCTest + +final class LockedValueTests: XCTestCase { + func testGetReturnsInitialValue() { + let value = LockedValue("initial") + + XCTAssertEqual(value.get(), "initial") + } + + func testSetReplacesStoredValue() { + let value = LockedValue("initial") + + value.set("updated") + + XCTAssertEqual(value.get(), "updated") + } + + func testUpdateMutatesStoredValue() { + let value = LockedValue(["initial"]) + + value.update { storedValue in + storedValue.append("updated") + } + + XCTAssertEqual(value.get(), ["initial", "updated"]) + } + + func testUpdateSerializesConcurrentMutations() { + let value = LockedValue(0) + let iterations = 1000 + + DispatchQueue.concurrentPerform(iterations: iterations) { _ in + value.update { storedValue in + storedValue += 1 + } + } + + XCTAssertEqual(value.get(), iterations) + } +} diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/LoggerTests.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/LoggerTests.swift index fc0590cd..b4c22522 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/LoggerTests.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/LoggerTests.swift @@ -2,21 +2,44 @@ import os.log @testable import ShopifyCheckoutKit import XCTest -class TestableOSLogger: OSLogger { - private(set) var capturedMessages: [(message: String, type: OSLogType)] = [] - private let testPrefix: String - override init() { - testPrefix = "ShopifyCheckoutKit" - super.init() +final class TestableOSLogger: Sendable { + private let capturedMessagesStorage = LockedValue([(message: String, type: OSLogType)]()) + private let logger: OSLogger + + var capturedMessages: [(message: String, type: OSLogType)] { + capturedMessagesStorage.get() } - override init(prefix: String, logLevel: LogLevel) { - testPrefix = prefix - super.init(prefix: prefix, logLevel: logLevel) + convenience init() { + self.init(prefix: "ShopifyCheckoutKit", logLevel: ShopifyCheckoutKit.configuration.logLevel) } - override func sendToOSLog(_ message: String, type: OSLogType) { - capturedMessages.append((message: message, type: type)) + init(prefix: String, logLevel: LogLevel) { + logger = OSLogger( + prefix: prefix, + logLevel: logLevel, + sendToOSLogHandler: { [capturedMessagesStorage] message, type in + capturedMessagesStorage.update { + $0.append((message: message, type: type)) + } + } + ) + } + + func info(_ message: String) { + logger.info(message) + } + + func debug(_ message: String) { + logger.debug(message) + } + + func error(_ message: String) { + logger.error(message) + } + + func fault(_ message: String) { + logger.fault(message) } } @@ -37,9 +60,23 @@ final class OSLoggerTests: XCTestCase { XCTAssertNotNil(OSLogger.shared) } - func test_defaultInitializer_withNoParameters_shouldMaintainBackwardsCompatibility() { + func test_defaultInitializer_withNoParameters_shouldUseConfigurationLogLevel() { + ShopifyCheckoutKit.configure { $0.logLevel = .debug } + let logger = OSLogger() - XCTAssertNotNil(logger) + + XCTAssertEqual(logger.logLevel, .debug) + } + + func test_sharedLogger_canBeReplaced() { + let originalLogger = OSLogger.shared + defer { OSLogger.shared = originalLogger } + + let replacementLogger = OSLogger(prefix: "Replacement", logLevel: .debug) + + OSLogger.shared = replacementLogger + + XCTAssertTrue(OSLogger.shared === replacementLogger) } func test_logLevelNone_withAllLogCalls_shouldBlockAllLogging() { diff --git a/platforms/swift/Tests/ShopifyCheckoutKitTests/Mocks/MockLogger.swift b/platforms/swift/Tests/ShopifyCheckoutKitTests/Mocks/MockLogger.swift index d5a8ccf0..06ffaf45 100644 --- a/platforms/swift/Tests/ShopifyCheckoutKitTests/Mocks/MockLogger.swift +++ b/platforms/swift/Tests/ShopifyCheckoutKitTests/Mocks/MockLogger.swift @@ -1,11 +1,7 @@ @testable import ShopifyCheckoutKit -class MockLogger: NoOpLogger { - var loggedError: Error? - var loggedMessage: String? +struct MockLogger: Logger { + func log(_: String) {} - func logError(_ error: Error, _ message: String) { - loggedError = error - loggedMessage = message - } + func clearLogs() {} } diff --git a/platforms/swift/api/ShopifyAcceleratedCheckouts.json b/platforms/swift/api/ShopifyAcceleratedCheckouts.json index 9bee51c1..d5daeb64 100644 --- a/platforms/swift/api/ShopifyAcceleratedCheckouts.json +++ b/platforms/swift/api/ShopifyAcceleratedCheckouts.json @@ -131,11 +131,6 @@ "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO8logLevel0A11CheckoutKit03LogE0OvpZ", "moduleName": "ShopifyAcceleratedCheckouts", "static": true, - "declAttributes": [ - "HasInitialValue", - "HasStorage" - ], - "hasStorage": true, "accessors": [ { "kind": "Accessor", @@ -154,7 +149,6 @@ "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO8logLevel0A11CheckoutKit03LogE0OvgZ", "moduleName": "ShopifyAcceleratedCheckouts", "static": true, - "implicit": true, "accessorKind": "get" }, { @@ -179,7 +173,6 @@ "mangledName": "$s27ShopifyAcceleratedCheckoutsAAO8logLevel0A11CheckoutKit03LogE0OvsZ", "moduleName": "ShopifyAcceleratedCheckouts", "static": true, - "implicit": true, "accessorKind": "set" } ] diff --git a/platforms/swift/api/ShopifyCheckoutKit.json b/platforms/swift/api/ShopifyCheckoutKit.json index 49d87b3c..8ca66d23 100644 --- a/platforms/swift/api/ShopifyCheckoutKit.json +++ b/platforms/swift/api/ShopifyCheckoutKit.json @@ -95,6 +95,13 @@ "declKind": "Import", "moduleName": "ShopifyCheckoutKit" }, + { + "kind": "Import", + "name": "os.lock", + "printedName": "os.lock", + "declKind": "Import", + "moduleName": "ShopifyCheckoutKit" + }, { "kind": "Import", "name": "os.log", @@ -2631,6 +2638,20 @@ "usr": "s:SQ", "mangledName": "$sSQ" }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", @@ -3841,6 +3862,20 @@ "usr": "s:s12CaseIterableP", "mangledName": "$ss12CaseIterableP" }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", @@ -4039,6 +4074,20 @@ "moduleName": "ShopifyCheckoutKit", "isFromExtension": true, "conformances": [ + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", @@ -4061,6 +4110,20 @@ "mangledName": "$s18ShopifyCheckoutKit13ConfigurationV", "moduleName": "ShopifyCheckoutKit", "conformances": [ + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", @@ -4486,6 +4549,20 @@ "usr": "s:s12CaseIterableP", "mangledName": "$ss12CaseIterableP" }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", @@ -4525,9 +4602,8 @@ "moduleName": "ShopifyCheckoutKit", "isInternal": true, "declAttributes": [ - "HasStorage" + "Final" ], - "hasStorage": true, "accessors": [ { "kind": "Accessor", @@ -4545,10 +4621,9 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerC8logLevelAA03LogF0Ovg", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC8logLevelAA03LogF0Ovg", "moduleName": "ShopifyCheckoutKit", - "implicit": true, "isInternal": true, "declAttributes": [ - "Transparent" + "Final" ], "accessorKind": "get" }, @@ -4573,10 +4648,9 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerC8logLevelAA03LogF0Ovs", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC8logLevelAA03LogF0Ovs", "moduleName": "ShopifyCheckoutKit", - "implicit": true, "isInternal": true, "declAttributes": [ - "Transparent" + "Final" ], "accessorKind": "set" } @@ -4600,11 +4674,8 @@ "moduleName": "ShopifyCheckoutKit", "static": true, "declAttributes": [ - "HasInitialValue", - "Final", - "HasStorage" + "Final" ], - "hasStorage": true, "accessors": [ { "kind": "Accessor", @@ -4623,10 +4694,8 @@ "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC6sharedACvgZ", "moduleName": "ShopifyCheckoutKit", "static": true, - "implicit": true, "declAttributes": [ - "Final", - "Transparent" + "Final" ], "accessorKind": "get" }, @@ -4652,10 +4721,8 @@ "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC6sharedACvsZ", "moduleName": "ShopifyCheckoutKit", "static": true, - "implicit": true, "declAttributes": [ - "Final", - "Transparent" + "Final" ], "accessorKind": "set" } @@ -4677,7 +4744,7 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerCACycfc", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerCACycfc", "moduleName": "ShopifyCheckoutKit", - "init_kind": "Designated" + "init_kind": "Convenience" }, { "kind": "Constructor", @@ -4707,7 +4774,7 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerC6prefix8logLevelACSS_AA03LogG0Otcfc", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC6prefix8logLevelACSS_AA03LogG0Otcfc", "moduleName": "ShopifyCheckoutKit", - "init_kind": "Designated" + "init_kind": "Convenience" }, { "kind": "Function", @@ -4730,6 +4797,9 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerC5debugyySSF", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC5debugyySSF", "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Final" + ], "funcSelfKind": "NonMutating" }, { @@ -4753,6 +4823,9 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerC4infoyySSF", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC4infoyySSF", "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Final" + ], "funcSelfKind": "NonMutating" }, { @@ -4776,6 +4849,9 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerC5erroryySSF", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC5erroryySSF", "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Final" + ], "funcSelfKind": "NonMutating" }, { @@ -4799,6 +4875,9 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerC5faultyySSF", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC5faultyySSF", "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Final" + ], "funcSelfKind": "NonMutating" } ], @@ -4806,7 +4885,25 @@ "usr": "s:18ShopifyCheckoutKit8OSLoggerC", "mangledName": "$s18ShopifyCheckoutKit8OSLoggerC", "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Final" + ], + "hasMissingDesignatedInitializers": true, "conformances": [ + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", @@ -4879,6 +4976,7 @@ "usr": "s:18ShopifyCheckoutKit6LoggerP", "mangledName": "$s18ShopifyCheckoutKit6LoggerP", "moduleName": "ShopifyCheckoutKit", + "genericSig": "", "conformances": [ { "kind": "Conformance", @@ -4893,6 +4991,20 @@ "printedName": "Copyable", "usr": "s:s8CopyableP", "mangledName": "$ss8CopyableP" + }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" } ] }, @@ -4922,6 +5034,9 @@ "usr": "s:18ShopifyCheckoutKit10NoOpLoggerC3logyySSF", "mangledName": "$s18ShopifyCheckoutKit10NoOpLoggerC3logyySSF", "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Final" + ], "funcSelfKind": "NonMutating" }, { @@ -4939,6 +5054,9 @@ "usr": "s:18ShopifyCheckoutKit10NoOpLoggerC9clearLogsyyF", "mangledName": "$s18ShopifyCheckoutKit10NoOpLoggerC9clearLogsyyF", "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Final" + ], "funcSelfKind": "NonMutating" } ], @@ -4946,6 +5064,9 @@ "usr": "s:18ShopifyCheckoutKit10NoOpLoggerC", "mangledName": "$s18ShopifyCheckoutKit10NoOpLoggerC", "moduleName": "ShopifyCheckoutKit", + "declAttributes": [ + "Final" + ], "hasMissingDesignatedInitializers": true, "conformances": [ { @@ -4955,6 +5076,20 @@ "usr": "s:18ShopifyCheckoutKit6LoggerP", "mangledName": "$s18ShopifyCheckoutKit6LoggerP" }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", @@ -5361,11 +5496,6 @@ "usr": "s:18ShopifyCheckoutKit13configurationAA13ConfigurationVvp", "mangledName": "$s18ShopifyCheckoutKit13configurationAA13ConfigurationVvp", "moduleName": "ShopifyCheckoutKit", - "declAttributes": [ - "HasInitialValue", - "HasStorage" - ], - "hasStorage": true, "accessors": [ { "kind": "Accessor", @@ -5383,7 +5513,6 @@ "usr": "s:18ShopifyCheckoutKit13configurationAA13ConfigurationVvg", "mangledName": "$s18ShopifyCheckoutKit13configurationAA13ConfigurationVvg", "moduleName": "ShopifyCheckoutKit", - "implicit": true, "accessorKind": "get" }, { @@ -5407,7 +5536,6 @@ "usr": "s:18ShopifyCheckoutKit13configurationAA13ConfigurationVvs", "mangledName": "$s18ShopifyCheckoutKit13configurationAA13ConfigurationVvs", "moduleName": "ShopifyCheckoutKit", - "implicit": true, "accessorKind": "set" } ] diff --git a/platforms/swift/api/ShopifyCheckoutProtocol.json b/platforms/swift/api/ShopifyCheckoutProtocol.json index 786e479b..f40a1f54 100644 --- a/platforms/swift/api/ShopifyCheckoutProtocol.json +++ b/platforms/swift/api/ShopifyCheckoutProtocol.json @@ -1341,6 +1341,96 @@ "name": "Checkout", "printedName": "Checkout", "children": [ + { + "kind": "Var", + "name": "attribution", + "printedName": "attribution", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]?", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Swift.String]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:SD" + } + ], + "usr": "s:Sq" + } + ], + "declKind": "Var", + "usr": "s:23ShopifyCheckoutProtocol0B0V11attributionSDyS2SGSgvp", + "mangledName": "$s23ShopifyCheckoutProtocol0B0V11attributionSDyS2SGSgvp", + "moduleName": "ShopifyCheckoutProtocol", + "declAttributes": [ + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]?", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Swift.String]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:SD" + } + ], + "usr": "s:Sq" + } + ], + "declKind": "Accessor", + "usr": "s:23ShopifyCheckoutProtocol0B0V11attributionSDyS2SGSgvg", + "mangledName": "$s23ShopifyCheckoutProtocol0B0V11attributionSDyS2SGSgvg", + "moduleName": "ShopifyCheckoutProtocol", + "implicit": true, + "declAttributes": [ + "Transparent" + ], + "accessorKind": "get" + } + ] + }, { "kind": "Var", "name": "buyer", @@ -2352,6 +2442,43 @@ "name": "CodingKeys", "printedName": "CodingKeys", "children": [ + { + "kind": "Var", + "name": "attribution", + "printedName": "attribution", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(ShopifyCheckoutProtocol.Checkout.CodingKeys.Type) -> ShopifyCheckoutProtocol.Checkout.CodingKeys", + "children": [ + { + "kind": "TypeNominal", + "name": "CodingKeys", + "printedName": "ShopifyCheckoutProtocol.Checkout.CodingKeys", + "usr": "s:23ShopifyCheckoutProtocol0B0V10CodingKeysO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "ShopifyCheckoutProtocol.Checkout.CodingKeys.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "CodingKeys", + "printedName": "ShopifyCheckoutProtocol.Checkout.CodingKeys", + "usr": "s:23ShopifyCheckoutProtocol0B0V10CodingKeysO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:23ShopifyCheckoutProtocol0B0V10CodingKeysO11attributionyA2EmF", + "mangledName": "$s23ShopifyCheckoutProtocol0B0V10CodingKeysO11attributionyA2EmF", + "moduleName": "ShopifyCheckoutProtocol" + }, { "kind": "Var", "name": "buyer", @@ -3335,7 +3462,7 @@ { "kind": "Constructor", "name": "init", - "printedName": "init(buyer:context:continueURL:currency:discounts:expiresAt:fulfillment:id:lineItems:links:messages:order:payment:signals:status:totals:ucp:)", + "printedName": "init(attribution:buyer:context:continueURL:currency:discounts:expiresAt:fulfillment:id:lineItems:links:messages:order:payment:signals:status:totals:ucp:)", "children": [ { "kind": "TypeNominal", @@ -3343,6 +3470,34 @@ "printedName": "ShopifyCheckoutProtocol.Checkout", "usr": "s:23ShopifyCheckoutProtocol0B0V" }, + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]?", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Swift.String]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:SD" + } + ], + "usr": "s:Sq" + }, { "kind": "TypeNominal", "name": "Optional", @@ -3559,8 +3714,8 @@ } ], "declKind": "Constructor", - "usr": "s:23ShopifyCheckoutProtocol0B0V5buyer7context11continueURL8currency9discounts9expiresAt11fulfillment2id9lineItems5links8messages5order7payment7signals6status6totals3ucpAcA5BuyerVSg_AA7ContextVSgSSSgSSAA0B9DiscountsVSg10Foundation4DateVSgAA0B11FulfillmentVSgSSSayAA8LineItemVGSayAA4LinkVGSayAA7MessageVGSgAA17OrderConfirmationVSgAA7PaymentVSgAA7SignalsVSgAA0B6StatusOSayAA0B5TotalVGAA25UCPCheckoutResponseSchemaVtcfc", - "mangledName": "$s23ShopifyCheckoutProtocol0B0V5buyer7context11continueURL8currency9discounts9expiresAt11fulfillment2id9lineItems5links8messages5order7payment7signals6status6totals3ucpAcA5BuyerVSg_AA7ContextVSgSSSgSSAA0B9DiscountsVSg10Foundation4DateVSgAA0B11FulfillmentVSgSSSayAA8LineItemVGSayAA4LinkVGSayAA7MessageVGSgAA17OrderConfirmationVSgAA7PaymentVSgAA7SignalsVSgAA0B6StatusOSayAA0B5TotalVGAA25UCPCheckoutResponseSchemaVtcfc", + "usr": "s:23ShopifyCheckoutProtocol0B0V11attribution5buyer7context11continueURL8currency9discounts9expiresAt11fulfillment2id9lineItems5links8messages5order7payment7signals6status6totals3ucpACSDyS2SGSg_AA5BuyerVSgAA7ContextVSgSSSgSSAA0B9DiscountsVSg10Foundation4DateVSgAA0B11FulfillmentVSgSSSayAA8LineItemVGSayAA4LinkVGSayAA7MessageVGSgAA17OrderConfirmationVSgAA7PaymentVSgAA7SignalsVSgAA0B6StatusOSayAA0B5TotalVGAA25UCPCheckoutResponseSchemaVtcfc", + "mangledName": "$s23ShopifyCheckoutProtocol0B0V11attribution5buyer7context11continueURL8currency9discounts9expiresAt11fulfillment2id9lineItems5links8messages5order7payment7signals6status6totals3ucpACSDyS2SGSg_AA5BuyerVSgAA7ContextVSgSSSgSSAA0B9DiscountsVSg10Foundation4DateVSgAA0B11FulfillmentVSgSSSayAA8LineItemVGSayAA4LinkVGSayAA7MessageVGSgAA17OrderConfirmationVSgAA7PaymentVSgAA7SignalsVSgAA0B6StatusOSayAA0B5TotalVGAA25UCPCheckoutResponseSchemaVtcfc", "moduleName": "ShopifyCheckoutProtocol", "init_kind": "Designated" }, @@ -3703,7 +3858,7 @@ { "kind": "Function", "name": "with", - "printedName": "with(buyer:context:continueURL:currency:discounts:expiresAt:fulfillment:id:lineItems:links:messages:order:payment:signals:status:totals:ucp:)", + "printedName": "with(attribution:buyer:context:continueURL:currency:discounts:expiresAt:fulfillment:id:lineItems:links:messages:order:payment:signals:status:totals:ucp:)", "children": [ { "kind": "TypeNominal", @@ -3711,6 +3866,43 @@ "printedName": "ShopifyCheckoutProtocol.Checkout", "usr": "s:23ShopifyCheckoutProtocol0B0V" }, + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]??", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]?", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Swift.String]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:SD" + } + ], + "usr": "s:Sq" + } + ], + "hasDefaultArg": true, + "usr": "s:Sq" + }, { "kind": "TypeNominal", "name": "Optional", @@ -4080,8 +4272,8 @@ } ], "declKind": "Func", - "usr": "s:23ShopifyCheckoutProtocol0B0V4with5buyer7context11continueURL8currency9discounts9expiresAt11fulfillment2id9lineItems5links8messages5order7payment7signals6status6totals3ucpAcA5BuyerVSgSg_AA7ContextVSgSgSSSgSgA2_AA0B9DiscountsVSgSg10Foundation4DateVSgSgAA0B11FulfillmentVSgSgA2_SayAA8LineItemVGSgSayAA4LinkVGSgSayAA7MessageVGSgSgAA17OrderConfirmationVSgSgAA7PaymentVSgSgAA7SignalsVSgSgAA0B6StatusOSgSayAA0B5TotalVGSgAA25UCPCheckoutResponseSchemaVSgtF", - "mangledName": "$s23ShopifyCheckoutProtocol0B0V4with5buyer7context11continueURL8currency9discounts9expiresAt11fulfillment2id9lineItems5links8messages5order7payment7signals6status6totals3ucpAcA5BuyerVSgSg_AA7ContextVSgSgSSSgSgA2_AA0B9DiscountsVSgSg10Foundation4DateVSgSgAA0B11FulfillmentVSgSgA2_SayAA8LineItemVGSgSayAA4LinkVGSgSayAA7MessageVGSgSgAA17OrderConfirmationVSgSgAA7PaymentVSgSgAA7SignalsVSgSgAA0B6StatusOSgSayAA0B5TotalVGSgAA25UCPCheckoutResponseSchemaVSgtF", + "usr": "s:23ShopifyCheckoutProtocol0B0V4with11attribution5buyer7context11continueURL8currency9discounts9expiresAt11fulfillment2id9lineItems5links8messages5order7payment7signals6status6totals3ucpACSDyS2SGSgSg_AA5BuyerVSgSgAA7ContextVSgSgSSSgSgA6_AA0B9DiscountsVSgSg10Foundation4DateVSgSgAA0B11FulfillmentVSgSgA6_SayAA8LineItemVGSgSayAA4LinkVGSgSayAA7MessageVGSgSgAA17OrderConfirmationVSgSgAA7PaymentVSgSgAA7SignalsVSgSgAA0B6StatusOSgSayAA0B5TotalVGSgAA25UCPCheckoutResponseSchemaVSgtF", + "mangledName": "$s23ShopifyCheckoutProtocol0B0V4with11attribution5buyer7context11continueURL8currency9discounts9expiresAt11fulfillment2id9lineItems5links8messages5order7payment7signals6status6totals3ucpACSDyS2SGSgSg_AA5BuyerVSgSgAA7ContextVSgSgSSSgSgA6_AA0B9DiscountsVSgSg10Foundation4DateVSgSgAA0B11FulfillmentVSgSgA6_SayAA8LineItemVGSgSayAA4LinkVGSgSayAA7MessageVGSgSgAA17OrderConfirmationVSgSgAA7PaymentVSgSgAA7SignalsVSgSgAA0B6StatusOSgSayAA0B5TotalVGSgAA25UCPCheckoutResponseSchemaVSgtF", "moduleName": "ShopifyCheckoutProtocol", "isFromExtension": true, "funcSelfKind": "NonMutating" @@ -40603,6 +40795,96 @@ } ] }, + { + "kind": "Var", + "name": "attribution", + "printedName": "attribution", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]?", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Swift.String]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:SD" + } + ], + "usr": "s:Sq" + } + ], + "declKind": "Var", + "usr": "s:23ShopifyCheckoutProtocol5OrderV11attributionSDyS2SGSgvp", + "mangledName": "$s23ShopifyCheckoutProtocol5OrderV11attributionSDyS2SGSgvp", + "moduleName": "ShopifyCheckoutProtocol", + "declAttributes": [ + "HasStorage" + ], + "isLet": true, + "hasStorage": true, + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]?", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Swift.String]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:SD" + } + ], + "usr": "s:Sq" + } + ], + "declKind": "Accessor", + "usr": "s:23ShopifyCheckoutProtocol5OrderV11attributionSDyS2SGSgvg", + "mangledName": "$s23ShopifyCheckoutProtocol5OrderV11attributionSDyS2SGSgvg", + "moduleName": "ShopifyCheckoutProtocol", + "implicit": true, + "declAttributes": [ + "Transparent" + ], + "accessorKind": "get" + } + ] + }, { "kind": "Var", "name": "checkoutID", @@ -41185,6 +41467,43 @@ "mangledName": "$s23ShopifyCheckoutProtocol5OrderV10CodingKeysO11adjustmentsyA2EmF", "moduleName": "ShopifyCheckoutProtocol" }, + { + "kind": "Var", + "name": "attribution", + "printedName": "attribution", + "children": [ + { + "kind": "TypeFunc", + "name": "Function", + "printedName": "(ShopifyCheckoutProtocol.Order.CodingKeys.Type) -> ShopifyCheckoutProtocol.Order.CodingKeys", + "children": [ + { + "kind": "TypeNominal", + "name": "CodingKeys", + "printedName": "ShopifyCheckoutProtocol.Order.CodingKeys", + "usr": "s:23ShopifyCheckoutProtocol5OrderV10CodingKeysO" + }, + { + "kind": "TypeNominal", + "name": "Metatype", + "printedName": "ShopifyCheckoutProtocol.Order.CodingKeys.Type", + "children": [ + { + "kind": "TypeNominal", + "name": "CodingKeys", + "printedName": "ShopifyCheckoutProtocol.Order.CodingKeys", + "usr": "s:23ShopifyCheckoutProtocol5OrderV10CodingKeysO" + } + ] + } + ] + } + ], + "declKind": "EnumElement", + "usr": "s:23ShopifyCheckoutProtocol5OrderV10CodingKeysO11attributionyA2EmF", + "mangledName": "$s23ShopifyCheckoutProtocol5OrderV10CodingKeysO11attributionyA2EmF", + "moduleName": "ShopifyCheckoutProtocol" + }, { "kind": "Var", "name": "checkoutID", @@ -41909,7 +42228,7 @@ { "kind": "Constructor", "name": "init", - "printedName": "init(adjustments:checkoutID:currency:fulfillment:id:label:lineItems:messages:permalinkURL:totals:ucp:)", + "printedName": "init(adjustments:attribution:checkoutID:currency:fulfillment:id:label:lineItems:messages:permalinkURL:totals:ucp:)", "children": [ { "kind": "TypeNominal", @@ -41939,6 +42258,34 @@ ], "usr": "s:Sq" }, + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]?", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Swift.String]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:SD" + } + ], + "usr": "s:Sq" + }, { "kind": "TypeNominal", "name": "String", @@ -42041,8 +42388,8 @@ } ], "declKind": "Constructor", - "usr": "s:23ShopifyCheckoutProtocol5OrderV11adjustments10checkoutID8currency11fulfillment2id5label9lineItems8messages12permalinkURL6totals3ucpACSayAA10AdjustmentVGSg_S2SAA11FulfillmentVS2SSgSayAA0D8LineItemVGSayAA7MessageVGSgSSSayAA0B5TotalVGAA22UCPOrderResponseSchemaVtcfc", - "mangledName": "$s23ShopifyCheckoutProtocol5OrderV11adjustments10checkoutID8currency11fulfillment2id5label9lineItems8messages12permalinkURL6totals3ucpACSayAA10AdjustmentVGSg_S2SAA11FulfillmentVS2SSgSayAA0D8LineItemVGSayAA7MessageVGSgSSSayAA0B5TotalVGAA22UCPOrderResponseSchemaVtcfc", + "usr": "s:23ShopifyCheckoutProtocol5OrderV11adjustments11attribution10checkoutID8currency11fulfillment2id5label9lineItems8messages12permalinkURL6totals3ucpACSayAA10AdjustmentVGSg_SDyS2SGSgS2SAA11FulfillmentVS2SSgSayAA0D8LineItemVGSayAA7MessageVGSgSSSayAA0B5TotalVGAA22UCPOrderResponseSchemaVtcfc", + "mangledName": "$s23ShopifyCheckoutProtocol5OrderV11adjustments11attribution10checkoutID8currency11fulfillment2id5label9lineItems8messages12permalinkURL6totals3ucpACSayAA10AdjustmentVGSg_SDyS2SGSgS2SAA11FulfillmentVS2SSgSayAA0D8LineItemVGSayAA7MessageVGSgSSSayAA0B5TotalVGAA22UCPOrderResponseSchemaVtcfc", "moduleName": "ShopifyCheckoutProtocol", "init_kind": "Designated" }, @@ -42185,7 +42532,7 @@ { "kind": "Function", "name": "with", - "printedName": "with(adjustments:checkoutID:currency:fulfillment:id:label:lineItems:messages:permalinkURL:totals:ucp:)", + "printedName": "with(adjustments:attribution:checkoutID:currency:fulfillment:id:label:lineItems:messages:permalinkURL:totals:ucp:)", "children": [ { "kind": "TypeNominal", @@ -42224,6 +42571,43 @@ "hasDefaultArg": true, "usr": "s:Sq" }, + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]??", + "children": [ + { + "kind": "TypeNominal", + "name": "Optional", + "printedName": "[Swift.String : Swift.String]?", + "children": [ + { + "kind": "TypeNominal", + "name": "Dictionary", + "printedName": "[Swift.String : Swift.String]", + "children": [ + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + }, + { + "kind": "TypeNominal", + "name": "String", + "printedName": "Swift.String", + "usr": "s:SS" + } + ], + "usr": "s:SD" + } + ], + "usr": "s:Sq" + } + ], + "hasDefaultArg": true, + "usr": "s:Sq" + }, { "kind": "TypeNominal", "name": "Optional", @@ -42416,8 +42800,8 @@ } ], "declKind": "Func", - "usr": "s:23ShopifyCheckoutProtocol5OrderV4with11adjustments10checkoutID8currency11fulfillment2id5label9lineItems8messages12permalinkURL6totals3ucpACSayAA10AdjustmentVGSgSg_SSSgAuA11FulfillmentVSgA2USgSayAA0D8LineItemVGSgSayAA7MessageVGSgSgAUSayAA0B5TotalVGSgAA22UCPOrderResponseSchemaVSgtF", - "mangledName": "$s23ShopifyCheckoutProtocol5OrderV4with11adjustments10checkoutID8currency11fulfillment2id5label9lineItems8messages12permalinkURL6totals3ucpACSayAA10AdjustmentVGSgSg_SSSgAuA11FulfillmentVSgA2USgSayAA0D8LineItemVGSgSayAA7MessageVGSgSgAUSayAA0B5TotalVGSgAA22UCPOrderResponseSchemaVSgtF", + "usr": "s:23ShopifyCheckoutProtocol5OrderV4with11adjustments11attribution10checkoutID8currency11fulfillment2id5label9lineItems8messages12permalinkURL6totals3ucpACSayAA10AdjustmentVGSgSg_SDyS2SGSgSgSSSgAyA11FulfillmentVSgA2YSgSayAA0D8LineItemVGSgSayAA7MessageVGSgSgAYSayAA0B5TotalVGSgAA22UCPOrderResponseSchemaVSgtF", + "mangledName": "$s23ShopifyCheckoutProtocol5OrderV4with11adjustments11attribution10checkoutID8currency11fulfillment2id5label9lineItems8messages12permalinkURL6totals3ucpACSayAA10AdjustmentVGSgSg_SDyS2SGSgSgSSSgAyA11FulfillmentVSgA2YSgSayAA0D8LineItemVGSgSayAA7MessageVGSgSgAYSayAA0B5TotalVGSgAA22UCPOrderResponseSchemaVSgtF", "moduleName": "ShopifyCheckoutProtocol", "isFromExtension": true, "funcSelfKind": "NonMutating" @@ -66404,43 +66788,6 @@ ], "funcSelfKind": "NonMutating" }, - { - "kind": "Var", - "name": "hashValue", - "printedName": "hashValue", - "children": [ - { - "kind": "TypeNominal", - "name": "Int", - "printedName": "Swift.Int", - "usr": "s:Si" - } - ], - "declKind": "Var", - "usr": "s:23ShopifyCheckoutProtocol8JSONNullC9hashValueSivp", - "mangledName": "$s23ShopifyCheckoutProtocol8JSONNullC9hashValueSivp", - "moduleName": "ShopifyCheckoutProtocol", - "accessors": [ - { - "kind": "Accessor", - "name": "Get", - "printedName": "Get()", - "children": [ - { - "kind": "TypeNominal", - "name": "Int", - "printedName": "Swift.Int", - "usr": "s:Si" - } - ], - "declKind": "Accessor", - "usr": "s:23ShopifyCheckoutProtocol8JSONNullC9hashValueSivg", - "mangledName": "$s23ShopifyCheckoutProtocol8JSONNullC9hashValueSivg", - "moduleName": "ShopifyCheckoutProtocol", - "accessorKind": "get" - } - ] - }, { "kind": "Function", "name": "hash", @@ -66463,6 +66810,9 @@ "usr": "s:23ShopifyCheckoutProtocol8JSONNullC4hash4intoys6HasherVz_tF", "mangledName": "$s23ShopifyCheckoutProtocol8JSONNullC4hash4intoys6HasherVz_tF", "moduleName": "ShopifyCheckoutProtocol", + "declAttributes": [ + "Final" + ], "funcSelfKind": "NonMutating" }, { @@ -66532,14 +66882,65 @@ "usr": "s:23ShopifyCheckoutProtocol8JSONNullC6encode2toys7Encoder_p_tKF", "mangledName": "$s23ShopifyCheckoutProtocol8JSONNullC6encode2toys7Encoder_p_tKF", "moduleName": "ShopifyCheckoutProtocol", + "declAttributes": [ + "Final" + ], "throwing": true, "funcSelfKind": "NonMutating" + }, + { + "kind": "Var", + "name": "hashValue", + "printedName": "hashValue", + "children": [ + { + "kind": "TypeNominal", + "name": "Int", + "printedName": "Swift.Int", + "usr": "s:Si" + } + ], + "declKind": "Var", + "usr": "s:23ShopifyCheckoutProtocol8JSONNullC9hashValueSivp", + "mangledName": "$s23ShopifyCheckoutProtocol8JSONNullC9hashValueSivp", + "moduleName": "ShopifyCheckoutProtocol", + "implicit": true, + "declAttributes": [ + "Final" + ], + "accessors": [ + { + "kind": "Accessor", + "name": "Get", + "printedName": "Get()", + "children": [ + { + "kind": "TypeNominal", + "name": "Int", + "printedName": "Swift.Int", + "usr": "s:Si" + } + ], + "declKind": "Accessor", + "usr": "s:23ShopifyCheckoutProtocol8JSONNullC9hashValueSivg", + "mangledName": "$s23ShopifyCheckoutProtocol8JSONNullC9hashValueSivg", + "moduleName": "ShopifyCheckoutProtocol", + "implicit": true, + "declAttributes": [ + "Final" + ], + "accessorKind": "get" + } + ] } ], "declKind": "Class", "usr": "s:23ShopifyCheckoutProtocol8JSONNullC", "mangledName": "$s23ShopifyCheckoutProtocol8JSONNullC", "moduleName": "ShopifyCheckoutProtocol", + "declAttributes": [ + "Final" + ], "conformances": [ { "kind": "Conformance", @@ -66562,6 +66963,13 @@ "usr": "s:SH", "mangledName": "$sSH" }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, { "kind": "Conformance", "name": "Equatable", @@ -66569,6 +66977,13 @@ "usr": "s:SQ", "mangledName": "$sSQ" }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", @@ -66606,11 +67021,8 @@ "mangledName": "$s23ShopifyCheckoutProtocol7JSONAnyC5valueypvp", "moduleName": "ShopifyCheckoutProtocol", "declAttributes": [ - "Final", - "HasStorage" + "Final" ], - "isLet": true, - "hasStorage": true, "accessors": [ { "kind": "Accessor", @@ -66627,10 +67039,8 @@ "usr": "s:23ShopifyCheckoutProtocol7JSONAnyC5valueypvg", "mangledName": "$s23ShopifyCheckoutProtocol7JSONAnyC5valueypvg", "moduleName": "ShopifyCheckoutProtocol", - "implicit": true, "declAttributes": [ - "Final", - "Transparent" + "Final" ], "accessorKind": "get" } @@ -66685,6 +67095,9 @@ "usr": "s:23ShopifyCheckoutProtocol7JSONAnyC6encode2toys7Encoder_p_tKF", "mangledName": "$s23ShopifyCheckoutProtocol7JSONAnyC6encode2toys7Encoder_p_tKF", "moduleName": "ShopifyCheckoutProtocol", + "declAttributes": [ + "Final" + ], "throwing": true, "funcSelfKind": "NonMutating" } @@ -66693,6 +67106,9 @@ "usr": "s:23ShopifyCheckoutProtocol7JSONAnyC", "mangledName": "$s23ShopifyCheckoutProtocol7JSONAnyC", "moduleName": "ShopifyCheckoutProtocol", + "declAttributes": [ + "Final" + ], "conformances": [ { "kind": "Conformance", @@ -66708,6 +67124,20 @@ "usr": "s:SE", "mangledName": "$sSE" }, + { + "kind": "Conformance", + "name": "Sendable", + "printedName": "Sendable", + "usr": "s:s8SendableP", + "mangledName": "$ss8SendableP" + }, + { + "kind": "Conformance", + "name": "SendableMetatype", + "printedName": "SendableMetatype", + "usr": "s:s16SendableMetatypeP", + "mangledName": "$ss16SendableMetatypeP" + }, { "kind": "Conformance", "name": "Copyable", diff --git a/protocol/languages/swift/Sources/ShopifyCheckoutProtocol/Generated/Models.swift b/protocol/languages/swift/Sources/ShopifyCheckoutProtocol/Generated/Models.swift index e9b0269c..725e85a3 100644 --- a/protocol/languages/swift/Sources/ShopifyCheckoutProtocol/Generated/Models.swift +++ b/protocol/languages/swift/Sources/ShopifyCheckoutProtocol/Generated/Models.swift @@ -12,6 +12,7 @@ import Foundation /// Base checkout schema. Extensions compose onto this using allOf. // MARK: - Checkout public struct Checkout: Codable, Sendable { + public let attribution: [String: String]? /// Representation of the buyer. public let buyer: Buyer? public let context: Context? @@ -47,7 +48,7 @@ public struct Checkout: Codable, Sendable { public let ucp: UCPCheckoutResponseSchema public enum CodingKeys: String, CodingKey { - case buyer, context + case attribution, buyer, context case continueURL = "continue_url" case currency, discounts case expiresAt = "expires_at" @@ -56,7 +57,8 @@ public struct Checkout: Codable, Sendable { case links, messages, order, payment, signals, status, totals, ucp } - public init(buyer: Buyer?, context: Context?, continueURL: String?, currency: String, discounts: CheckoutDiscounts?, expiresAt: Date?, fulfillment: CheckoutFulfillment?, id: String, lineItems: [LineItem], links: [Link], messages: [Message]?, order: OrderConfirmation?, payment: Payment?, signals: Signals?, status: CheckoutStatus, totals: [CheckoutTotal], ucp: UCPCheckoutResponseSchema) { + public init(attribution: [String: String]?, buyer: Buyer?, context: Context?, continueURL: String?, currency: String, discounts: CheckoutDiscounts?, expiresAt: Date?, fulfillment: CheckoutFulfillment?, id: String, lineItems: [LineItem], links: [Link], messages: [Message]?, order: OrderConfirmation?, payment: Payment?, signals: Signals?, status: CheckoutStatus, totals: [CheckoutTotal], ucp: UCPCheckoutResponseSchema) { + self.attribution = attribution self.buyer = buyer self.context = context self.continueURL = continueURL @@ -96,6 +98,7 @@ public extension Checkout { } func with( + attribution: [String: String]?? = nil, buyer: Buyer?? = nil, context: Context?? = nil, continueURL: String?? = nil, @@ -115,6 +118,7 @@ public extension Checkout { ucp: UCPCheckoutResponseSchema? = nil ) -> Checkout { return Checkout( + attribution: attribution ?? self.attribution, buyer: buyer ?? self.buyer, context: context ?? self.context, continueURL: continueURL ?? self.continueURL, @@ -1387,10 +1391,6 @@ public extension Link { /// Container for error, warning, or info messages. // MARK: - Message public struct Message: Codable, Sendable { - /// Warning code. Machine-readable identifier for the warning type (e.g., final_sale, prop65, - /// fulfillment_changed, age_restricted, etc.). - /// - /// Info code for programmatic handling. public let code: String? /// Human-readable message. /// @@ -2447,6 +2447,9 @@ public struct Order: Codable, Sendable { /// Post-order events (refunds, returns, credits, disputes, cancellations, etc.) that exist /// independently of fulfillment. public let adjustments: [Adjustment]? + /// Snapshot of the attribution associated with the originating checkout. Read-only on the + /// order. + public let attribution: [String: String]? /// Associated checkout ID for reconciliation. public let checkoutID: String /// ISO 4217 currency code. MUST match the currency from the originating checkout session. @@ -2469,7 +2472,7 @@ public struct Order: Codable, Sendable { public let ucp: UCPOrderResponseSchema public enum CodingKeys: String, CodingKey { - case adjustments + case adjustments, attribution case checkoutID = "checkout_id" case currency, fulfillment, id, label case lineItems = "line_items" @@ -2478,8 +2481,9 @@ public struct Order: Codable, Sendable { case totals, ucp } - public init(adjustments: [Adjustment]?, checkoutID: String, currency: String, fulfillment: Fulfillment, id: String, label: String?, lineItems: [OrderLineItem], messages: [Message]?, permalinkURL: String, totals: [CheckoutTotal], ucp: UCPOrderResponseSchema) { + public init(adjustments: [Adjustment]?, attribution: [String: String]?, checkoutID: String, currency: String, fulfillment: Fulfillment, id: String, label: String?, lineItems: [OrderLineItem], messages: [Message]?, permalinkURL: String, totals: [CheckoutTotal], ucp: UCPOrderResponseSchema) { self.adjustments = adjustments + self.attribution = attribution self.checkoutID = checkoutID self.currency = currency self.fulfillment = fulfillment @@ -2513,6 +2517,7 @@ public extension Order { func with( adjustments: [Adjustment]?? = nil, + attribution: [String: String]?? = nil, checkoutID: String? = nil, currency: String? = nil, fulfillment: Fulfillment? = nil, @@ -2526,6 +2531,7 @@ public extension Order { ) -> Order { return Order( adjustments: adjustments ?? self.adjustments, + attribution: attribution ?? self.attribution, checkoutID: checkoutID ?? self.checkoutID, currency: currency ?? self.currency, fulfillment: fulfillment ?? self.fulfillment, @@ -4143,247 +4149,5 @@ func newJSONEncoder() -> JSONEncoder { } // MARK: - Encode/decode helpers - -public class JSONNull: Codable, Hashable { - - public static func == (lhs: JSONNull, rhs: JSONNull) -> Bool { - return true - } - - public var hashValue: Int { - return 0 - } - - public func hash(into hasher: inout Hasher) { - // No-op - } - - public init() {} - - public required init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - if !container.decodeNil() { - throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull")) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encodeNil() - } -} - -class JSONCodingKey: CodingKey { - let key: String - - required init?(intValue: Int) { - return nil - } - - required init?(stringValue: String) { - key = stringValue - } - - var intValue: Int? { - return nil - } - - var stringValue: String { - return key - } -} - -public class JSONAny: Codable { - - public let value: Any - - static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { - let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") - return DecodingError.typeMismatch(JSONAny.self, context) - } - - static func encodingError(forValue value: Any, codingPath: [CodingKey]) -> EncodingError { - let context = EncodingError.Context(codingPath: codingPath, debugDescription: "Cannot encode JSONAny") - return EncodingError.invalidValue(value, context) - } - - static func decode(from container: SingleValueDecodingContainer) throws -> Any { - if let value = try? container.decode(Bool.self) { - return value - } - if let value = try? container.decode(Int64.self) { - return value - } - if let value = try? container.decode(Double.self) { - return value - } - if let value = try? container.decode(String.self) { - return value - } - if container.decodeNil() { - return JSONNull() - } - throw decodingError(forCodingPath: container.codingPath) - } - - static func decode(from container: inout UnkeyedDecodingContainer) throws -> Any { - if let value = try? container.decode(Bool.self) { - return value - } - if let value = try? container.decode(Int64.self) { - return value - } - if let value = try? container.decode(Double.self) { - return value - } - if let value = try? container.decode(String.self) { - return value - } - if let value = try? container.decodeNil() { - if value { - return JSONNull() - } - } - if var container = try? container.nestedUnkeyedContainer() { - return try decodeArray(from: &container) - } - if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { - return try decodeDictionary(from: &container) - } - throw decodingError(forCodingPath: container.codingPath) - } - - static func decode(from container: inout KeyedDecodingContainer, forKey key: JSONCodingKey) throws -> Any { - if let value = try? container.decode(Bool.self, forKey: key) { - return value - } - if let value = try? container.decode(Int64.self, forKey: key) { - return value - } - if let value = try? container.decode(Double.self, forKey: key) { - return value - } - if let value = try? container.decode(String.self, forKey: key) { - return value - } - if let value = try? container.decodeNil(forKey: key) { - if value { - return JSONNull() - } - } - if var container = try? container.nestedUnkeyedContainer(forKey: key) { - return try decodeArray(from: &container) - } - if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { - return try decodeDictionary(from: &container) - } - throw decodingError(forCodingPath: container.codingPath) - } - - static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> [Any] { - var arr: [Any] = [] - while !container.isAtEnd { - let value = try decode(from: &container) - arr.append(value) - } - return arr - } - - static func decodeDictionary(from container: inout KeyedDecodingContainer) throws -> [String: Any] { - var dict = [String: Any]() - for key in container.allKeys { - let value = try decode(from: &container, forKey: key) - dict[key.stringValue] = value - } - return dict - } - - static func encode(to container: inout UnkeyedEncodingContainer, array: [Any]) throws { - for value in array { - if let value = value as? Bool { - try container.encode(value) - } else if let value = value as? Int64 { - try container.encode(value) - } else if let value = value as? Double { - try container.encode(value) - } else if let value = value as? String { - try container.encode(value) - } else if value is JSONNull { - try container.encodeNil() - } else if let value = value as? [Any] { - var container = container.nestedUnkeyedContainer() - try encode(to: &container, array: value) - } else if let value = value as? [String: Any] { - var container = container.nestedContainer(keyedBy: JSONCodingKey.self) - try encode(to: &container, dictionary: value) - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } - } - } - - static func encode(to container: inout KeyedEncodingContainer, dictionary: [String: Any]) throws { - for (key, value) in dictionary { - let key = JSONCodingKey(stringValue: key)! - if let value = value as? Bool { - try container.encode(value, forKey: key) - } else if let value = value as? Int64 { - try container.encode(value, forKey: key) - } else if let value = value as? Double { - try container.encode(value, forKey: key) - } else if let value = value as? String { - try container.encode(value, forKey: key) - } else if value is JSONNull { - try container.encodeNil(forKey: key) - } else if let value = value as? [Any] { - var container = container.nestedUnkeyedContainer(forKey: key) - try encode(to: &container, array: value) - } else if let value = value as? [String: Any] { - var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) - try encode(to: &container, dictionary: value) - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } - } - } - - static func encode(to container: inout SingleValueEncodingContainer, value: Any) throws { - if let value = value as? Bool { - try container.encode(value) - } else if let value = value as? Int64 { - try container.encode(value) - } else if let value = value as? Double { - try container.encode(value) - } else if let value = value as? String { - try container.encode(value) - } else if value is JSONNull { - try container.encodeNil() - } else { - throw encodingError(forValue: value, codingPath: container.codingPath) - } - } - - public required init(from decoder: Decoder) throws { - if var arrayContainer = try? decoder.unkeyedContainer() { - self.value = try JSONAny.decodeArray(from: &arrayContainer) - } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { - self.value = try JSONAny.decodeDictionary(from: &container) - } else { - let container = try decoder.singleValueContainer() - self.value = try JSONAny.decode(from: container) - } - } - - public func encode(to encoder: Encoder) throws { - if let arr = self.value as? [Any] { - var container = encoder.unkeyedContainer() - try JSONAny.encode(to: &container, array: arr) - } else if let dict = self.value as? [String: Any] { - var container = encoder.container(keyedBy: JSONCodingKey.self) - try JSONAny.encode(to: &container, dictionary: dict) - } else { - var container = encoder.singleValueContainer() - try JSONAny.encode(to: &container, value: self.value) - } - } -} +// quicktype's JSONAny/JSONNull helper suffix is intentionally replaced here. +// See ../JSONAny.swift for the maintained Swift implementation. diff --git a/protocol/languages/swift/Sources/ShopifyCheckoutProtocol/JSONAny.swift b/protocol/languages/swift/Sources/ShopifyCheckoutProtocol/JSONAny.swift new file mode 100644 index 00000000..e7eb4e8a --- /dev/null +++ b/protocol/languages/swift/Sources/ShopifyCheckoutProtocol/JSONAny.swift @@ -0,0 +1,302 @@ +// quicktype emits JSONAny/JSONNull helpers directly into Generated/Models.swift. +// protocol/scripts/generate_models.mjs verifies that helper suffix by SHA, replaces +// it with a marker comment, and relies on this file for the maintained implementation. +// Keeping these types here lets Swift tooling lint, format, and type-check them. + +// MARK: - Encode/decode helpers + +public final class JSONNull: Codable, Hashable, Sendable { + public static func == (_: JSONNull, _: JSONNull) -> Bool { + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(0) + } + + public init() {} + + public required init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if !container.decodeNil() { + throw DecodingError.typeMismatch( + JSONNull.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Wrong type for JSONNull" + ) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encodeNil() + } +} + +final class JSONCodingKey: CodingKey, Sendable { + let key: String + + required init?(intValue _: Int) { + return nil + } + + required init?(stringValue: String) { + key = stringValue + } + + var intValue: Int? { + return nil + } + + var stringValue: String { + return key + } +} + +private enum JSONValue: Sendable { + case bool(Bool) + case int(Int64) + case double(Double) + case string(String) + case null + case array([JSONValue]) + case object([String: JSONValue]) + + var value: Any { + switch self { + case let .bool(value): + return value + case let .int(value): + return value + case let .double(value): + return value + case let .string(value): + return value + case .null: + return JSONNull() + case let .array(values): + return values.map(\.value) + case let .object(values): + return values.mapValues { $0.value } + } + } +} + +public final class JSONAny: Codable, Sendable { + private let storage: JSONValue + + public var value: Any { + storage.value + } + + private static func decodingError(forCodingPath codingPath: [CodingKey]) -> DecodingError { + let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Cannot decode JSONAny") + return DecodingError.typeMismatch(JSONAny.self, context) + } + + private static func decode(from container: SingleValueDecodingContainer) throws -> JSONValue { + if let value = try? container.decode(Bool.self) { + return .bool(value) + } + if let value = try? container.decode(Int64.self) { + return .int(value) + } + if let value = try? container.decode(Double.self) { + return .double(value) + } + if let value = try? container.decode(String.self) { + return .string(value) + } + if container.decodeNil() { + return .null + } + throw decodingError(forCodingPath: container.codingPath) + } + + private static func decode(from container: inout UnkeyedDecodingContainer) throws -> JSONValue { + if let value = try? container.decode(Bool.self) { + return .bool(value) + } + if let value = try? container.decode(Int64.self) { + return .int(value) + } + if let value = try? container.decode(Double.self) { + return .double(value) + } + if let value = try? container.decode(String.self) { + return .string(value) + } + if let value = try? container.decodeNil() { + if value { + return .null + } + } + if var container = try? container.nestedUnkeyedContainer() { + return try decodeArray(from: &container) + } + if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self) { + return try decodeDictionary(from: &container) + } + throw decodingError(forCodingPath: container.codingPath) + } + + private static func decode( + from container: inout KeyedDecodingContainer, + forKey key: JSONCodingKey + ) throws -> JSONValue { + if let value = try? container.decode(Bool.self, forKey: key) { + return .bool(value) + } + if let value = try? container.decode(Int64.self, forKey: key) { + return .int(value) + } + if let value = try? container.decode(Double.self, forKey: key) { + return .double(value) + } + if let value = try? container.decode(String.self, forKey: key) { + return .string(value) + } + if let value = try? container.decodeNil(forKey: key) { + if value { + return .null + } + } + if var container = try? container.nestedUnkeyedContainer(forKey: key) { + return try decodeArray(from: &container) + } + if var container = try? container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) { + return try decodeDictionary(from: &container) + } + throw decodingError(forCodingPath: container.codingPath) + } + + private static func decodeArray(from container: inout UnkeyedDecodingContainer) throws -> JSONValue { + var values: [JSONValue] = [] + while !container.isAtEnd { + let value = try decode(from: &container) + values.append(value) + } + return .array(values) + } + + private static func decodeDictionary(from container: inout KeyedDecodingContainer) throws -> JSONValue { + var values = [String: JSONValue]() + for key in container.allKeys { + let value = try decode(from: &container, forKey: key) + values[key.stringValue] = value + } + return .object(values) + } + + private static func encode(to container: inout UnkeyedEncodingContainer, array: [JSONValue]) throws { + for value in array { + try encode(to: &container, value: value) + } + } + + private static func encode( + to container: inout KeyedEncodingContainer, + dictionary: [String: JSONValue] + ) throws { + for (key, value) in dictionary { + let key = JSONCodingKey(stringValue: key)! + try encode(to: &container, value: value, forKey: key) + } + } + + private static func encode(to container: inout SingleValueEncodingContainer, value: JSONValue) throws { + switch value { + case let .bool(value): + try container.encode(value) + case let .int(value): + try container.encode(value) + case let .double(value): + try container.encode(value) + case let .string(value): + try container.encode(value) + case .null: + try container.encodeNil() + case .array, .object: + throw EncodingError.invalidValue( + value.value, + EncodingError.Context( + codingPath: container.codingPath, + debugDescription: "Cannot encode nested JSON value in a single-value container" + ) + ) + } + } + + private static func encode(to container: inout UnkeyedEncodingContainer, value: JSONValue) throws { + switch value { + case let .bool(value): + try container.encode(value) + case let .int(value): + try container.encode(value) + case let .double(value): + try container.encode(value) + case let .string(value): + try container.encode(value) + case .null: + try container.encodeNil() + case let .array(values): + var container = container.nestedUnkeyedContainer() + try encode(to: &container, array: values) + case let .object(values): + var container = container.nestedContainer(keyedBy: JSONCodingKey.self) + try encode(to: &container, dictionary: values) + } + } + + private static func encode( + to container: inout KeyedEncodingContainer, + value: JSONValue, + forKey key: JSONCodingKey + ) throws { + switch value { + case let .bool(value): + try container.encode(value, forKey: key) + case let .int(value): + try container.encode(value, forKey: key) + case let .double(value): + try container.encode(value, forKey: key) + case let .string(value): + try container.encode(value, forKey: key) + case .null: + try container.encodeNil(forKey: key) + case let .array(values): + var container = container.nestedUnkeyedContainer(forKey: key) + try encode(to: &container, array: values) + case let .object(values): + var container = container.nestedContainer(keyedBy: JSONCodingKey.self, forKey: key) + try encode(to: &container, dictionary: values) + } + } + + public required init(from decoder: Decoder) throws { + if var arrayContainer = try? decoder.unkeyedContainer() { + storage = try JSONAny.decodeArray(from: &arrayContainer) + } else if var container = try? decoder.container(keyedBy: JSONCodingKey.self) { + storage = try JSONAny.decodeDictionary(from: &container) + } else { + let container = try decoder.singleValueContainer() + storage = try JSONAny.decode(from: container) + } + } + + public func encode(to encoder: Encoder) throws { + switch storage { + case let .array(values): + var container = encoder.unkeyedContainer() + try JSONAny.encode(to: &container, array: values) + case let .object(values): + var container = encoder.container(keyedBy: JSONCodingKey.self) + try JSONAny.encode(to: &container, dictionary: values) + default: + var container = encoder.singleValueContainer() + try JSONAny.encode(to: &container, value: storage) + } + } +} diff --git a/protocol/scripts/generate_models.mjs b/protocol/scripts/generate_models.mjs index cf90f861..6ea0583f 100755 --- a/protocol/scripts/generate_models.mjs +++ b/protocol/scripts/generate_models.mjs @@ -22,6 +22,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import crypto from "node:crypto"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -37,6 +38,16 @@ import { const SCHEMA_SOURCE_DIR = path.join(PROTOCOL_DIR, "schemas"); const SERVICES_DIR = path.join(PROTOCOL_DIR, "services", "shopping"); +const SWIFT_JSON_HELPER_MARKER = "// MARK: - Encode/decode helpers"; +const SWIFT_JSON_HELPER_REPLACEMENT = `// MARK: - Encode/decode helpers +// quicktype's JSONAny/JSONNull helper suffix is intentionally replaced here. +// See ../JSONAny.swift for the maintained Swift implementation. +`; +// quicktype 23.2.6's Swift helper suffix for: +// --lang swift --swift-5-support --access-level public --sendable +// Guarding the whole suffix keeps this normalization fail-fast if quicktype fixes +// or changes the helper block instead of silently clobbering future output. +const QUICKTYPE_23_2_6_SWIFT_JSON_HELPER_SHA256 = "02b7721a424fdb5a586a773116130f0b273551f9bfd5d9111a1c700581ec5e7e"; function usage() { console.error("Usage: generate_models.sh --lang [--output ]"); @@ -176,9 +187,10 @@ async function prepareCodegenSchemas(tempDir) { // Message discriminators are defined across the message variant schemas. Give // each variant the same local title so quicktype emits a single MessageType symbol. for (const messageSchema of ["message_error", "message_warning", "message_info"]) { - const schema = await readJson(path.join(specDir, "types", `${messageSchema}.json`)); + const schemaPath = path.join(schemaDir, "common", "types", `${messageSchema}.json`); + const schema = await readJson(schemaPath); schema.properties.type.title = "MessageType"; - await writeJson(path.join(specDir, "types", `${messageSchema}.json`), schema); + await writeJson(schemaPath, schema); } // Extension schemas bring in repeated generic property names like `type` and @@ -268,7 +280,7 @@ function commonSchemaSources(specDir) { "--src", path.join(specDir, "order.json"), "--src", - path.join(specDir, "types", "error_response.json"), + path.join(specDir, "..", "common", "types", "error_response.json"), "--src", path.join(specDir, "instruments_change_result.json"), "--src", @@ -332,7 +344,25 @@ async function generateSwift(specDir, output) { output, ]); - await normalizeGeneratedFile(output); + await normalizeGeneratedFile(output, (source) => { + // quicktype's --sendable option marks generated models as Sendable, but quicktype 23.2.6 + // still emits dynamic JSON helper types that are not fully Swift 6 concurrency-safe. + // Drop only the exact helper suffix quicktype 23.2.6 emits. Maintained helper + // implementations live in ShopifyCheckoutProtocol/JSONAny.swift so Swift tooling can + // lint, format, and type-check them normally. + const helperStart = source.indexOf(SWIFT_JSON_HELPER_MARKER); + if (helperStart === -1) { + throw new Error("Swift JSON helper normalization failed; quicktype output may have changed"); + } + + const generatedHelper = source.slice(helperStart); + const generatedHelperHash = crypto.createHash("sha256").update(generatedHelper).digest("hex"); + if (generatedHelperHash !== QUICKTYPE_23_2_6_SWIFT_JSON_HELPER_SHA256) { + throw new Error(`Swift JSON helper normalization failed; quicktype helper output changed (sha256: ${generatedHelperHash})`); + } + + return `${source.slice(0, helperStart)}${SWIFT_JSON_HELPER_REPLACEMENT}`; + }); } async function generateTypescript(specDir, output) {