Last active
January 17, 2026 21:39
-
-
Save couchdeveloper/8a95dea4db78d53a8d3abc8968641dcb to your computer and use it in GitHub Desktop.
Static JSON Encoder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import struct Foundation.Date | |
| import class Foundation.DateFormatter | |
| import class Foundation.ISO8601DateFormatter | |
| // MARK: - Root Tag Protocol | |
| /// Root protocol for all encoding strategies. | |
| /// | |
| /// This is a tag protocol that enables generic constraints and extensions. | |
| public protocol EncodingStrategies: Sendable {} | |
| // MARK: - Date Encoding Precision | |
| /// The precision of date encoding (for timestamps). | |
| /// | |
| /// - `seconds`: Encodes with second precision | |
| /// - `milliseconds`: Encodes with millisecond precision | |
| /// - `formatted`: Uses a formatter (custom precision) | |
| public enum DateEncodingStrategyPrecision: Sendable { | |
| case seconds | |
| case milliseconds | |
| case formatted | |
| } | |
| // MARK: - Individual Strategy Protocols (Lego Bricks) | |
| /// Protocol for date encoding strategies. | |
| /// | |
| /// Conforming types provide a static method for encoding dates to strings, | |
| /// along with metadata and capabilities that enable compile-time optimization | |
| /// and specialization. | |
| public protocol DateEncodingStrategy: Sendable { | |
| /// The precision of the date encoding (for timestamps). | |
| typealias Precision = DateEncodingStrategyPrecision | |
| /// Indicates whether this strategy uses a formatter (vs raw numeric conversion). | |
| /// | |
| /// - `true`: Uses DateFormatter or ISO8601DateFormatter | |
| /// - `false`: Direct numeric conversion (timestamps) | |
| static var usesFormatter: Bool { get } | |
| /// The precision of the encoded date. | |
| static var precision: Precision { get } | |
| /// A human-readable description of the strategy. | |
| static var description: String { get } | |
| /// Indicates whether the formatter is cached (if applicable). | |
| /// | |
| /// Only relevant when `usesFormatter == true`. | |
| static var cacheFormatter: Bool { get } | |
| /// Encodes a date to a string representation. | |
| /// | |
| /// - Parameter date: The date to encode | |
| /// - Returns: The string representation of the date | |
| /// - Throws: An error if encoding fails | |
| static func encode(_ date: Date) throws -> String | |
| } | |
| /// Protocol for array key encoding strategies. | |
| /// | |
| /// Determines how array elements are represented in parameter names. | |
| public protocol ArrayKeyEncodingStrategy: Sendable { | |
| /// Encodes an array element key. | |
| /// | |
| /// - Parameters: | |
| /// - base: The base key (parent) or empty string for top-level | |
| /// - isEmpty: True if using bracket-style notation (empty string key) | |
| /// - index: The array index (for indices style), or nil | |
| /// - Returns: The formatted key for the array element | |
| static func encodeKey(base: String, isEmpty: Bool, index: Int?) -> String | |
| } | |
| /// Protocol for object (nested) key encoding strategies. | |
| /// | |
| /// Determines how nested object properties are represented. | |
| public protocol ObjectKeyEncodingStrategy: Sendable { | |
| /// Encodes a nested object key. | |
| /// | |
| /// - Parameters: | |
| /// - base: The base key (parent) | |
| /// - segment: The nested property name | |
| /// - Returns: The formatted key combining base and segment | |
| static func encodeKey(base: String, segment: String) -> String | |
| } | |
| /// Protocol for key transformation strategies. | |
| /// | |
| /// Transforms coding key names (e.g., camelCase to snake_case). | |
| public protocol KeyTransformStrategy: Sendable { | |
| /// Transforms a coding key according to the naming strategy. | |
| /// | |
| /// - Parameter key: The original key string | |
| /// - Returns: The transformed key string | |
| static func transform(_ key: String) -> String | |
| } | |
| /// Protocol for top-level key strategies. | |
| /// | |
| /// Handles how top-level values without explicit keys are named. | |
| public protocol TopLevelKeyStrategy: Sendable { | |
| /// Handles the top-level key. | |
| /// | |
| /// - Parameter suggestedKey: A suggested key (may be empty) | |
| /// - Returns: The actual key to use, or empty string to allow empty keys | |
| static func topLevelKey(_ suggestedKey: String) -> String | |
| } | |
| // MARK: - Concrete Date Strategies | |
| /// ISO8601 date encoding strategy with cached formatter. | |
| /// | |
| /// The formatter is created once and reused for all encoding operations, | |
| /// avoiding the expensive cost of creating a new formatter each time. | |
| public struct ISO8601DateFormatterStrategy: DateEncodingStrategy { | |
| @available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) | |
| @usableFromInline | |
| nonisolated(unsafe) static let formatter = ISO8601DateFormatter() | |
| public static let usesFormatter: Bool = true | |
| public static let precision: Precision = .formatted | |
| public static let description: String = "ISO8601 formatted dates" | |
| public static let cacheFormatter: Bool = true | |
| @inlinable | |
| public static func encode(_ date: Date) throws -> String { | |
| if #available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) { | |
| return formatter.string(from: date) | |
| } else { | |
| return String(date.timeIntervalSince1970) | |
| } | |
| } | |
| } | |
| /// Unix timestamp date encoding strategy. | |
| /// | |
| /// Encodes dates as seconds since January 1, 1970 (Unix epoch). | |
| /// No formatter needed - zero allocation overhead. | |
| public struct UnixTimestampStrategy: DateEncodingStrategy { | |
| public static let usesFormatter: Bool = false | |
| public static let precision: Precision = .seconds | |
| public static let description: String = "Unix timestamp (seconds since 1970)" | |
| public static let cacheFormatter: Bool = false | |
| @inlinable | |
| public static func encode(_ date: Date) throws -> String { | |
| String(date.timeIntervalSince1970) | |
| } | |
| } | |
| /// Reference date timestamp encoding strategy. | |
| /// | |
| /// Encodes dates as seconds since the reference date (January 1, 2001). | |
| /// No formatter needed - zero allocation overhead. | |
| public struct ReferenceDateTimestampStrategy: DateEncodingStrategy { | |
| public static let usesFormatter: Bool = false | |
| public static let precision: Precision = .seconds | |
| public static let description: String = "Reference date timestamp (seconds since 2001)" | |
| public static let cacheFormatter: Bool = false | |
| @inlinable | |
| public static func encode(_ date: Date) throws -> String { | |
| String(date.timeIntervalSinceReferenceDate) | |
| } | |
| } | |
| // MARK: - Concrete Array Key Strategies | |
| /// Brackets array key encoding strategy. | |
| /// | |
| /// Produces keys like: `items[]=1&items[]=2` | |
| public struct BracketsArrayKeyStrategy: ArrayKeyEncodingStrategy { | |
| @inlinable | |
| public static func encodeKey(base: String, isEmpty: Bool, index: Int?) -> String { | |
| if isEmpty { | |
| return base.isEmpty ? "" : base + "[]" | |
| } else if let index = index { | |
| return base.isEmpty ? String(index) : "\(base)[\(index)]" | |
| } | |
| return base | |
| } | |
| } | |
| /// Indices array key encoding strategy. | |
| /// | |
| /// Produces keys like: `items[0]=1&items[1]=2` | |
| public struct IndicesArrayKeyStrategy: ArrayKeyEncodingStrategy { | |
| @inlinable | |
| public static func encodeKey(base: String, isEmpty: Bool, index: Int?) -> String { | |
| if let index = index { | |
| return base.isEmpty ? String(index) : "\(base)[\(index)]" | |
| } | |
| return base | |
| } | |
| } | |
| /// No brackets array key encoding strategy. | |
| /// | |
| /// Produces keys like: `items=1&items=2` | |
| public struct NoBracketsArrayKeyStrategy: ArrayKeyEncodingStrategy { | |
| @inlinable | |
| public static func encodeKey(base: String, isEmpty: Bool, index: Int?) -> String { | |
| base | |
| } | |
| } | |
| // MARK: - Concrete Object Key Strategies | |
| /// Brackets object key encoding strategy. | |
| /// | |
| /// Produces keys like: `user[name]=Ana&user[address][city]=NYC` | |
| public struct BracketsObjectKeyStrategy: ObjectKeyEncodingStrategy { | |
| @inlinable | |
| public static func encodeKey(base: String, segment: String) -> String { | |
| if base.isEmpty { | |
| return segment | |
| } | |
| return "\(base)[\(segment)]" | |
| } | |
| } | |
| /// Dot notation object key encoding strategy. | |
| /// | |
| /// Produces keys like: `user.name=Ana&user.address.city=NYC` | |
| public struct DotNotationObjectKeyStrategy: ObjectKeyEncodingStrategy { | |
| @inlinable | |
| public static func encodeKey(base: String, segment: String) -> String { | |
| if base.isEmpty { | |
| return segment | |
| } | |
| return "\(base).\(segment)" | |
| } | |
| } | |
| // MARK: - Concrete Key Transform Strategies | |
| /// Identity key transformation (no transformation). | |
| public struct IdentityKeyTransform: KeyTransformStrategy { | |
| @inlinable | |
| public static func transform(_ key: String) -> String { | |
| key | |
| } | |
| } | |
| /// Snake case key transformation. | |
| /// | |
| /// Converts `camelCase` to `snake_case`. | |
| public struct SnakeCaseKeyTransform: KeyTransformStrategy { | |
| @inlinable | |
| public static func transform(_ key: String) -> String { | |
| guard !key.isEmpty else { return key } | |
| var result = "" | |
| var previousWasUppercase = false | |
| var index = key.startIndex | |
| while index < key.endIndex { | |
| let char = key[index] | |
| if char.isUppercase { | |
| if index != key.startIndex && !previousWasUppercase { | |
| result.append("_") | |
| } | |
| result.append(char.lowercased()) | |
| previousWasUppercase = true | |
| } else { | |
| result.append(char) | |
| previousWasUppercase = false | |
| } | |
| index = key.index(after: index) | |
| } | |
| return result | |
| } | |
| } | |
| // MARK: - Concrete Top-Level Key Strategies | |
| /// Allow empty top-level keys. | |
| /// | |
| /// Top-level primitives/arrays can have empty keys: `=value` or `[]=item` | |
| public struct AllowEmptyTopLevelKeyStrategy: TopLevelKeyStrategy { | |
| @inlinable | |
| public static func topLevelKey(_ suggestedKey: String) -> String { | |
| suggestedKey | |
| } | |
| } | |
| /// Require a specific root key for top-level values. | |
| public struct RequireRootKeyStrategy: TopLevelKeyStrategy { | |
| public static let rootKey: String = "root" | |
| @inlinable | |
| public static func topLevelKey(_ suggestedKey: String) -> String { | |
| suggestedKey.isEmpty ? rootKey : suggestedKey | |
| } | |
| } | |
| // MARK: - Concrete Composite Strategy Types | |
| /// Standard query string encoding strategies with brackets notation. | |
| /// | |
| /// - Arrays: `items[]=1&items[]=2` | |
| /// - Objects: `user[name]=Ana` | |
| /// - Dates: ISO8601 | |
| /// - Keys: As-is | |
| public struct BracketsEncodingStrategies: URLEncodingStrategies { | |
| public typealias DateStrategy = ISO8601DateFormatterStrategy | |
| public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy | |
| public typealias ObjectKeyStrategy = BracketsObjectKeyStrategy | |
| public typealias KeyTransform = IdentityKeyTransform | |
| public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy | |
| public init() {} | |
| } | |
| /// URL template encoding strategies with dot notation. | |
| /// | |
| /// - Arrays: `items[]=1&items[]=2` | |
| /// - Objects: `user.name=Ana` (dot notation) | |
| /// - Dates: ISO8601 | |
| /// - Keys: As-is | |
| public struct DotNotationEncodingStrategies: URLEncodingStrategies { | |
| public typealias DateStrategy = ISO8601DateFormatterStrategy | |
| public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy | |
| public typealias ObjectKeyStrategy = DotNotationObjectKeyStrategy | |
| public typealias KeyTransform = IdentityKeyTransform | |
| public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy | |
| public init() {} | |
| } | |
| /// Array encoding with indices instead of brackets. | |
| /// | |
| /// - Arrays: `items[0]=1&items[1]=2` | |
| /// - Objects: `user[name]=Ana` | |
| /// - Dates: ISO8601 | |
| /// - Keys: As-is | |
| public struct IndicesEncodingStrategies: URLEncodingStrategies { | |
| public typealias DateStrategy = ISO8601DateFormatterStrategy | |
| public typealias ArrayKeyStrategy = IndicesArrayKeyStrategy | |
| public typealias ObjectKeyStrategy = BracketsObjectKeyStrategy | |
| public typealias KeyTransform = IdentityKeyTransform | |
| public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy | |
| public init() {} | |
| } | |
| /// Snake case key transformation strategy. | |
| /// | |
| /// - Arrays: `items[]=1&items[]=2` | |
| /// - Objects: `user[home_address]=value` | |
| /// - Dates: ISO8601 | |
| /// - Keys: Converted to snake_case | |
| public struct SnakeCaseEncodingStrategies: URLEncodingStrategies { | |
| public typealias DateStrategy = ISO8601DateFormatterStrategy | |
| public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy | |
| public typealias ObjectKeyStrategy = BracketsObjectKeyStrategy | |
| public typealias KeyTransform = SnakeCaseKeyTransform | |
| public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy | |
| public init() {} | |
| } | |
| /// Unix timestamp dates strategy. | |
| /// | |
| /// - Arrays: `items[]=1&items[]=2` | |
| /// - Objects: `user[name]=Ana` | |
| /// - Dates: Unix timestamp | |
| /// - Keys: As-is | |
| public struct UnixTimestampEncodingStrategies: URLEncodingStrategies { | |
| public typealias DateStrategy = UnixTimestampStrategy | |
| public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy | |
| public typealias ObjectKeyStrategy = BracketsObjectKeyStrategy | |
| public typealias KeyTransform = IdentityKeyTransform | |
| public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy | |
| public init() {} | |
| } | |
| /// Combined dot notation + snake_case strategy. | |
| /// | |
| /// - Arrays: `items[]=1&items[]=2` | |
| /// - Objects: `user.home_address=value` | |
| /// - Dates: ISO8601 | |
| /// - Keys: snake_case | |
| public struct DotNotationSnakeCaseStrategies: URLEncodingStrategies { | |
| public typealias DateStrategy = ISO8601DateFormatterStrategy | |
| public typealias ArrayKeyStrategy = BracketsArrayKeyStrategy | |
| public typealias ObjectKeyStrategy = DotNotationObjectKeyStrategy | |
| public typealias KeyTransform = SnakeCaseKeyTransform | |
| public typealias TopLevelKey = AllowEmptyTopLevelKeyStrategy | |
| public init() {} | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import struct Foundation.Data | |
| import struct Foundation.Date | |
| // MARK: - JSON-Specific Strategy Protocols | |
| /// Protocol for data encoding strategies in JSON. | |
| /// | |
| /// Determines how `Data` values are encoded (base64, hex, etc.). | |
| public protocol DataEncodingStrategy: Sendable { | |
| /// Encodes a Data value to a string representation. | |
| static func encode(_ data: Data) throws -> String | |
| } | |
| /// Protocol for floating point encoding strategies in JSON. | |
| /// | |
| /// Determines how special floating point values (NaN, Infinity) are handled. | |
| public protocol FloatingPointEncodingStrategy: Sendable { | |
| /// Whether to throw an error on non-conforming floats (NaN, Infinity). | |
| static var throwOnNonConformingFloat: Bool { get } | |
| /// Encodes a non-conforming float (only called if throwOnNonConformingFloat is false). | |
| static func encodeNonConformingFloat(_ value: Double) -> String | |
| } | |
| // MARK: - JSON Encoding Strategies Protocol | |
| /// Composite protocol for JSON encoding strategies. | |
| /// | |
| /// Combines the necessary strategy components for JSON encoding. | |
| public protocol JSONEncodingStrategies: EncodingStrategies { | |
| /// The date encoding strategy to use. | |
| associatedtype DateStrategy: DateEncodingStrategy | |
| /// The key transformation strategy to use. | |
| associatedtype KeyTransform: KeyTransformStrategy | |
| /// The data encoding strategy to use. | |
| associatedtype DataStrategy: DataEncodingStrategy | |
| /// The floating point encoding strategy to use. | |
| associatedtype FloatingPointStrategy: FloatingPointEncodingStrategy | |
| } | |
| // MARK: - Concrete Data Strategies | |
| /// Base64 data encoding strategy (JSON standard). | |
| public struct Base64DataStrategy: DataEncodingStrategy { | |
| @inlinable | |
| public static func encode(_ data: Data) throws -> String { | |
| data.base64EncodedString() | |
| } | |
| } | |
| /// Hex string data encoding strategy. | |
| public struct HexDataStrategy: DataEncodingStrategy { | |
| @inlinable | |
| public static func encode(_ data: Data) throws -> String { | |
| data.map { String(format: "%02x", $0) }.joined() | |
| } | |
| } | |
| // MARK: - Concrete Floating Point Strategies | |
| /// Strict floating point strategy (throws on NaN/Infinity). | |
| public struct StrictFloatingPointStrategy: FloatingPointEncodingStrategy { | |
| public static let throwOnNonConformingFloat: Bool = true | |
| @inlinable | |
| public static func encodeNonConformingFloat(_ value: Double) -> String { | |
| fatalError("Non-conforming float encountered") | |
| } | |
| } | |
| /// Permissive floating point strategy (encodes NaN/Infinity as strings). | |
| public struct PermissiveFloatingPointStrategy: FloatingPointEncodingStrategy { | |
| public static let throwOnNonConformingFloat: Bool = false | |
| @inlinable | |
| public static func encodeNonConformingFloat(_ value: Double) -> String { | |
| if value.isNaN { | |
| return "\"NaN\"" | |
| } else if value == .infinity { | |
| return "\"Infinity\"" | |
| } else if value == -.infinity { | |
| return "\"-Infinity\"" | |
| } | |
| return "\(value)" | |
| } | |
| } | |
| // MARK: - Concrete JSON Strategy Combinations | |
| /// Standard JSON encoding strategies. | |
| /// | |
| /// - Dates: ISO8601 | |
| /// - Keys: As-is | |
| /// - Data: Base64 | |
| /// - Floats: Strict (throw on NaN/Infinity) | |
| public struct StandardJSONEncodingStrategies: JSONEncodingStrategies { | |
| public typealias DateStrategy = ISO8601DateFormatterStrategy | |
| public typealias KeyTransform = IdentityKeyTransform | |
| public typealias DataStrategy = Base64DataStrategy | |
| public typealias FloatingPointStrategy = StrictFloatingPointStrategy | |
| } | |
| /// Snake case JSON encoding strategies. | |
| public struct SnakeCaseJSONEncodingStrategies: JSONEncodingStrategies { | |
| public typealias DateStrategy = ISO8601DateFormatterStrategy | |
| public typealias KeyTransform = SnakeCaseKeyTransform | |
| public typealias DataStrategy = Base64DataStrategy | |
| public typealias FloatingPointStrategy = StrictFloatingPointStrategy | |
| } | |
| /// Unix timestamp JSON encoding strategies. | |
| public struct UnixTimestampJSONEncodingStrategies: JSONEncodingStrategies { | |
| public typealias DateStrategy = UnixTimestampStrategy | |
| public typealias KeyTransform = IdentityKeyTransform | |
| public typealias DataStrategy = Base64DataStrategy | |
| public typealias FloatingPointStrategy = StrictFloatingPointStrategy | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import struct Foundation.Data | |
| /// Protocol for JSON output sinks. | |
| /// | |
| /// A JSON sink receives hierarchical JSON encoding events and produces output. | |
| public protocol JSONSink: DefaultConstructible, Sendable { | |
| associatedtype Output: Sendable | |
| /// Writes a null value. | |
| mutating func writeNull() | |
| /// Writes a boolean value. | |
| mutating func writeBool(_ value: Bool) | |
| /// Writes a string value (will be escaped). | |
| mutating func writeString(_ value: String) | |
| /// Writes a number value as a string. | |
| mutating func writeNumber(_ value: String) | |
| /// Begins a JSON object. | |
| mutating func beginObject() | |
| /// Ends a JSON object. | |
| mutating func endObject() | |
| /// Begins a JSON array. | |
| mutating func beginArray() | |
| /// Ends a JSON array. | |
| mutating func endArray() | |
| /// Writes a key in an object (will be escaped). | |
| mutating func writeKey(_ key: String) | |
| /// Returns the final output. | |
| func output() -> Output | |
| } | |
| // MARK: - High-Performance Data-based JSON Sink | |
| /// A high-performance JSON sink that builds JSON as Data. | |
| /// | |
| /// **Performance optimizations:** | |
| /// - Pre-allocated buffer with capacity estimation | |
| /// - Direct UTF-8 byte writing with unsafe buffers | |
| /// - Zero-copy string operations where possible | |
| /// - Minimal allocations | |
| public struct JSONDataSink: JSONSink { | |
| @usableFromInline | |
| static let nullBytes: [UInt8] = [110, 117, 108, 108] // "null" | |
| @usableFromInline | |
| static let trueBytes: [UInt8] = [116, 114, 117, 101] // "true" | |
| @usableFromInline | |
| static let falseBytes: [UInt8] = [102, 97, 108, 115, 101] // "false" | |
| @usableFromInline | |
| static let unicodeEscapePrefix: [UInt8] = [92, 117, 48, 48] // "\u00" | |
| // Common escape sequences as static constants | |
| @usableFromInline | |
| static let escapeQuote: [UInt8] = [92, 34] // \" | |
| @usableFromInline | |
| static let escapeBackslash: [UInt8] = [92, 92] // \\ | |
| @usableFromInline | |
| static let escapeNewline: [UInt8] = [92, 110] // \n | |
| @usableFromInline | |
| static let escapeReturn: [UInt8] = [92, 114] // \r | |
| @usableFromInline | |
| static let escapeTab: [UInt8] = [92, 116] // \t | |
| @usableFromInline | |
| var buffer: [UInt8] | |
| public init() { | |
| // Pre-allocate reasonable capacity to reduce reallocations | |
| self.buffer = [] | |
| self.buffer.reserveCapacity(4096) | |
| } | |
| @inlinable | |
| mutating func writeComma() { | |
| buffer.append(44) // ASCII comma | |
| } | |
| @inlinable | |
| mutating func removeTrailingCommaIfNeeded() { | |
| // Remove trailing comma from empty or last element | |
| if buffer.last == 44 { | |
| buffer.removeLast() | |
| } | |
| } | |
| @inlinable | |
| public mutating func writeNull() { | |
| buffer.append(contentsOf: Self.nullBytes) | |
| writeComma() | |
| } | |
| @inlinable | |
| public mutating func writeBool(_ value: Bool) { | |
| buffer.append(contentsOf: value ? Self.trueBytes : Self.falseBytes) | |
| writeComma() | |
| } | |
| @inlinable | |
| public mutating func writeString(_ value: String) { | |
| buffer.append(34) // Opening quote | |
| writeEscapedStringFast(value) | |
| buffer.append(34) // Closing quote | |
| writeComma() | |
| } | |
| @inlinable | |
| public mutating func writeNumber(_ value: String) { | |
| // Fast path: write UTF-8 bytes directly | |
| value.utf8.withContiguousStorageIfAvailable { utf8 in | |
| buffer.append(contentsOf: utf8) | |
| } ?? value.utf8.forEach { buffer.append($0) } | |
| writeComma() | |
| } | |
| @inlinable | |
| public mutating func beginObject() { | |
| buffer.append(123) // { | |
| } | |
| @inlinable | |
| public mutating func endObject() { | |
| removeTrailingCommaIfNeeded() | |
| buffer.append(125) // } | |
| writeComma() | |
| } | |
| @inlinable | |
| public mutating func beginArray() { | |
| buffer.append(91) // [ | |
| } | |
| @inlinable | |
| public mutating func endArray() { | |
| removeTrailingCommaIfNeeded() | |
| buffer.append(93) // ] | |
| writeComma() | |
| } | |
| @inlinable | |
| public mutating func writeKey(_ key: String) { | |
| buffer.append(34) // Opening quote | |
| writeEscapedStringFast(key) | |
| buffer.append(34) // Closing quote | |
| buffer.append(58) // : | |
| } | |
| @inlinable | |
| public func output() -> Data { | |
| var finalBuffer = buffer | |
| // Remove trailing comma from root value | |
| if finalBuffer.last == 44 { | |
| finalBuffer.removeLast() | |
| } | |
| return Data(finalBuffer) | |
| } | |
| // MARK: - Fast String Escaping | |
| @usableFromInline | |
| mutating func writeEscapedStringFast(_ string: String) { | |
| // Use UTF8 view for zero-copy access | |
| let utf8Bytes = string.utf8 | |
| // Fast scan: check if escaping is needed | |
| var needsEscaping = false | |
| for byte in utf8Bytes { | |
| if byte == 34 || byte == 92 || byte < 32 { | |
| needsEscaping = true | |
| break | |
| } | |
| } | |
| if !needsEscaping { | |
| // Fast path: no escaping needed, bulk copy if contiguous | |
| utf8Bytes.withContiguousStorageIfAvailable { bytes in | |
| buffer.append(contentsOf: bytes) | |
| } ?? utf8Bytes.forEach { buffer.append($0) } | |
| } else { | |
| // Slow path: escape as needed | |
| let oldCount = buffer.count | |
| let estimatedSize = utf8Bytes.count + (utf8Bytes.count >> 3) // Estimate 12.5% escaping overhead | |
| buffer.reserveCapacity(oldCount + estimatedSize) | |
| for byte in utf8Bytes { | |
| switch byte { | |
| case 34: // " | |
| buffer.append(contentsOf: Self.escapeQuote) | |
| case 92: // \ | |
| buffer.append(contentsOf: Self.escapeBackslash) | |
| case 10: // \n | |
| buffer.append(contentsOf: Self.escapeNewline) | |
| case 13: // \r | |
| buffer.append(contentsOf: Self.escapeReturn) | |
| case 9: // \t | |
| buffer.append(contentsOf: Self.escapeTab) | |
| case 0..<32: // Other control characters | |
| // \u00XX | |
| writeControlCharacterEscape(byte) | |
| default: | |
| buffer.append(byte) | |
| } | |
| } | |
| } | |
| } | |
| @usableFromInline | |
| mutating func writeControlCharacterEscape(_ byte: UInt8) { | |
| // \u00XX format - write directly as bytes | |
| buffer.append(contentsOf: Self.unicodeEscapePrefix) | |
| // Hex digits | |
| let high = byte >> 4 | |
| let low = byte & 0x0F | |
| buffer.append(high < 10 ? (48 + high) : (87 + high)) // 0-9 or a-f | |
| buffer.append(low < 10 ? (48 + low) : (87 + low)) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Apache License | |
| Version 2.0, January 2004 | |
| http://www.apache.org/licenses/ | |
| TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | |
| 1. Definitions. | |
| "License" shall mean the terms and conditions for use, reproduction, | |
| and distribution as defined by Sections 1 through 9 of this document. | |
| "Licensor" shall mean the copyright owner or entity authorized by | |
| the copyright owner that is granting the License. | |
| "Legal Entity" shall mean the union of the acting entity and all | |
| other entities that control, are controlled by, or are under common | |
| control with that entity. For the purposes of this definition, | |
| "control" means (i) the power, direct or indirect, to cause the | |
| direction or management of such entity, whether by contract or | |
| otherwise, or (ii) ownership of fifty percent (50%) or more of the | |
| outstanding shares, or (iii) beneficial ownership of such entity. | |
| "You" (or "Your") shall mean an individual or Legal Entity | |
| exercising permissions granted by this License. | |
| "Source" form shall mean the preferred form for making modifications, | |
| including but not limited to software source code, documentation | |
| source, and configuration files. | |
| "Object" form shall mean any form resulting from mechanical | |
| transformation or translation of a Source form, including but | |
| not limited to compiled object code, generated documentation, | |
| and conversions to other media types. | |
| "Work" shall mean the work of authorship, whether in Source or | |
| Object form, made available under the License, as indicated by a | |
| copyright notice that is included in or attached to the work | |
| (an example is provided in the Appendix below). | |
| "Derivative Works" shall mean any work, whether in Source or Object | |
| form, that is based on (or derived from) the Work and for which the | |
| editorial revisions, annotations, elaborations, or other modifications | |
| represent, as a whole, an original work of authorship. For the purposes | |
| of this License, Derivative Works shall not include works that remain | |
| separable from, or merely link (or bind by name) to the interfaces of, | |
| the Work and Derivative Works thereof. | |
| "Contribution" shall mean any work of authorship, including | |
| the original version of the Work and any modifications or additions | |
| to that Work or Derivative Works thereof, that is intentionally | |
| submitted to Licensor for inclusion in the Work by the copyright owner | |
| or by an individual or Legal Entity authorized to submit on behalf of | |
| the copyright owner. For the purposes of this definition, "submitted" | |
| means any form of electronic, verbal, or written communication sent | |
| to the Licensor or its representatives, including but not limited to | |
| communication on electronic mailing lists, source code control systems, | |
| and issue tracking systems that are managed by, or on behalf of, the | |
| Licensor for the purpose of discussing and improving the Work, but | |
| excluding communication that is conspicuously marked or otherwise | |
| designated in writing by the copyright owner as "Not a Contribution." | |
| "Contributor" shall mean Licensor and any individual or Legal Entity | |
| on behalf of whom a Contribution has been received by Licensor and | |
| subsequently incorporated within the Work. | |
| 2. Grant of Copyright License. Subject to the terms and conditions of | |
| this License, each Contributor hereby grants to You a perpetual, | |
| worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| copyright license to reproduce, prepare Derivative Works of, | |
| publicly display, publicly perform, sublicense, and distribute the | |
| Work and such Derivative Works in Source or Object form. | |
| 3. Grant of Patent License. Subject to the terms and conditions of | |
| this License, each Contributor hereby grants to You a perpetual, | |
| worldwide, non-exclusive, no-charge, royalty-free, irrevocable | |
| (except as stated in this section) patent license to make, have made, | |
| use, offer to sell, sell, import, and otherwise transfer the Work, | |
| where such license applies only to those patent claims licensable | |
| by such Contributor that are necessarily infringed by their | |
| Contribution(s) alone or by combination of their Contribution(s) | |
| with the Work to which such Contribution(s) was submitted. If You | |
| institute patent litigation against any entity (including a | |
| cross-claim or counterclaim in a lawsuit) alleging that the Work | |
| or a Contribution incorporated within the Work constitutes direct | |
| or contributory patent infringement, then any patent licenses | |
| granted to You under this License for that Work shall terminate | |
| as of the date such litigation is filed. | |
| 4. Redistribution. You may reproduce and distribute copies of the | |
| Work or Derivative Works thereof in any medium, with or without | |
| modifications, and in Source or Object form, provided that You | |
| meet the following conditions: | |
| (a) You must give any other recipients of the Work or | |
| Derivative Works a copy of this License; and | |
| (b) You must cause any modified files to carry prominent notices | |
| stating that You changed the files; and | |
| (c) You must retain, in the Source form of any Derivative Works | |
| that You distribute, all copyright, patent, trademark, and | |
| attribution notices from the Source form of the Work, | |
| excluding those notices that do not pertain to any part of | |
| the Derivative Works; and | |
| (d) If the Work includes a "NOTICE" text file as part of its | |
| distribution, then any Derivative Works that You distribute must | |
| include a readable copy of the attribution notices contained | |
| within such NOTICE file, excluding those notices that do not | |
| pertain to any part of the Derivative Works, in at least one | |
| of the following places: within a NOTICE text file distributed | |
| as part of the Derivative Works; within the Source form or | |
| documentation, if provided along with the Derivative Works; or, | |
| within a display generated by the Derivative Works, if and | |
| wherever such third-party notices normally appear. The contents | |
| of the NOTICE file are for informational purposes only and | |
| do not modify the License. You may add Your own attribution | |
| notices within Derivative Works that You distribute, alongside | |
| or as an addendum to the NOTICE text from the Work, provided | |
| that such additional attribution notices cannot be construed | |
| as modifying the License. | |
| You may add Your own copyright statement to Your modifications and | |
| may provide additional or different license terms and conditions | |
| for use, reproduction, or distribution of Your modifications, or | |
| for any such Derivative Works as a whole, provided Your use, | |
| reproduction, and distribution of the Work otherwise complies with | |
| the conditions stated in this License. | |
| 5. Submission of Contributions. Unless You explicitly state otherwise, | |
| any Contribution intentionally submitted for inclusion in the Work | |
| by You to the Licensor shall be under the terms and conditions of | |
| this License, without any additional terms or conditions. | |
| Notwithstanding the above, nothing herein shall supersede or modify | |
| the terms of any separate license agreement you may have executed | |
| with Licensor regarding such Contributions. | |
| 6. Trademarks. This License does not grant permission to use the trade | |
| names, trademarks, service marks, or product names of the Licensor, | |
| except as required for reasonable and customary use in describing the | |
| origin of the Work and reproducing the content of the NOTICE file. | |
| 7. Disclaimer of Warranty. Unless required by applicable law or | |
| agreed to in writing, Licensor provides the Work (and each | |
| Contributor provides its Contributions) on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |
| implied, including, without limitation, any warranties or conditions | |
| of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | |
| PARTICULAR PURPOSE. You are solely responsible for determining the | |
| appropriateness of using or redistributing the Work and assume any | |
| risks associated with Your exercise of permissions under this License. | |
| 8. Limitation of Liability. In no event and under no legal theory, | |
| whether in tort (including negligence), contract, or otherwise, | |
| unless required by applicable law (such as deliberate and grossly | |
| negligent acts) or agreed to in writing, shall any Contributor be | |
| liable to You for damages, including any direct, indirect, special, | |
| incidental, or consequential damages of any character arising as a | |
| result of this License or out of the use or inability to use the | |
| Work (including but not limited to damages for loss of goodwill, | |
| work stoppage, computer failure or malfunction, or any and all | |
| other commercial damages or losses), even if such Contributor | |
| has been advised of the possibility of such damages. | |
| 9. Accepting Warranty or Additional Liability. While redistributing | |
| the Work or Derivative Works thereof, You may choose to offer, | |
| and charge a fee for, acceptance of support, warranty, indemnity, | |
| or other liability obligations and/or rights consistent with this | |
| License. However, in accepting such obligations, You may act only | |
| on Your own behalf and on Your sole responsibility, not on behalf | |
| of any other Contributor, and only if You agree to indemnify, | |
| defend, and hold each Contributor harmless for any liability | |
| incurred by, or claims asserted against, such Contributor by reason | |
| of your accepting any such warranty or additional liability. | |
| END OF TERMS AND CONDITIONS | |
| APPENDIX: How to apply the Apache License to your work. | |
| To apply the Apache License to your work, attach the following | |
| boilerplate notice, with the fields enclosed by brackets "[]" | |
| replaced with your own identifying information. (Don't include | |
| the brackets!) The text should be enclosed in the appropriate | |
| comment syntax for the file format. We also recommend that a | |
| file or class name and description of purpose be included on the | |
| same "printed page" as the copyright notice for easier | |
| identification within third-party archives. | |
| Copyright [2026] [Andreas Grosam] | |
| Licensed under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the License. | |
| You may obtain a copy of the License at | |
| http://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import struct Foundation.Date | |
| import struct Foundation.Data | |
| import struct Foundation.URL | |
| /// A compile-time optimized JSON encoder using static strategy dispatch. | |
| /// | |
| /// This encoder is a pure function with no state. Its configuration is entirely | |
| /// in the type parameters (Sink and Strategies), enabling aggressive compiler | |
| /// optimizations through static dispatch. | |
| /// | |
| /// **Conceptual Model:** | |
| /// The encoder is fundamentally a pure function: `(Encodable) throws -> Output` | |
| /// - The TYPE is the configuration | |
| /// - No stored state | |
| /// - Given same input, always produces same output | |
| /// | |
| /// **Performance Benefits:** | |
| /// - Zero runtime overhead (no enum switches) | |
| /// - Direct static method dispatch | |
| /// - Aggressive inlining by compiler | |
| /// - Dead code elimination | |
| /// - No stored state (zero-size type) | |
| /// | |
| /// **Usage:** | |
| /// ```swift | |
| /// // The encoder is a pure function | |
| /// let encoder = StaticJSONEncoder<JSONStringSink, StandardJSONEncodingStrategies>() | |
| /// let json = try encoder.encode(myValue) | |
| /// | |
| /// // Or use static method directly | |
| /// let json = try StaticJSONEncoder<JSONStringSink, StandardJSONEncodingStrategies>.encode(myValue) | |
| /// ``` | |
| public struct StaticJSONEncoder<Sink: JSONSink, Strategies: JSONEncodingStrategies>: Sendable { | |
| public init() {} | |
| /// Encodes the given value and returns the output directly. | |
| /// | |
| /// This is a pure function - given the same input, it always produces the same output. | |
| /// The encoder has no state; its configuration is entirely in the type parameters. | |
| /// | |
| /// - Parameter value: The value to encode | |
| /// - Returns: The encoded output | |
| /// - Throws: An error if encoding fails | |
| @inlinable | |
| public func encode<T: Encodable>(_ value: T) throws -> Sink.Output { | |
| try Self.encode(value) | |
| } | |
| /// Static encoding function. | |
| /// | |
| /// Since the encoder is a pure function with no state, this static method | |
| /// provides the core functionality. The instance method delegates to this. | |
| /// | |
| /// - Parameter value: The value to encode | |
| /// - Returns: The encoded output | |
| /// - Throws: An error if encoding fails | |
| @inlinable | |
| public static func encode<T: Encodable>(_ value: T) throws -> Sink.Output { | |
| let state = _StaticJSONEncoderState(sink: Sink()) | |
| let encoder = _StaticJSONEncoder<Sink, Strategies>(state: state) | |
| try value.encode(to: encoder) | |
| return state.sink.output() | |
| } | |
| /// Creates a type-erased encoder that hides the Sink and Strategies types. | |
| /// | |
| /// - Returns: A type-erased encoder that only exposes the output type | |
| @inlinable | |
| public func eraseToAnyEncoder() -> AnyJSONEncoder<Sink.Output> { | |
| AnyJSONEncoder(self) | |
| } | |
| } | |
| // MARK: - Type-Erased Encoder | |
| /// A type-erased JSON encoder. | |
| /// | |
| /// This type hides the concrete Sink and Strategies types, exposing only | |
| /// the output type. | |
| public struct AnyJSONEncoder<Output>: Sendable where Output: Sendable { | |
| public let _encode: @Sendable (any Encodable) throws -> Output | |
| /// Creates a type-erased encoder from a concrete encoder. | |
| @inlinable | |
| public init<Sink, Strategies>( | |
| _ encoder: StaticJSONEncoder<Sink, Strategies> | |
| ) where Sink.Output == Output { | |
| self._encode = { value in | |
| try encoder.encode(value) | |
| } | |
| } | |
| /// Encodes a value and returns the output. | |
| @inlinable | |
| public func encode<T: Encodable>(_ value: T) throws -> Output { | |
| try _encode(value) | |
| } | |
| } | |
| // MARK: - Internal Encoder Implementation | |
| /// Mutable state for the encoder. | |
| /// This is a class because it needs to be shared across all containers. | |
| @usableFromInline | |
| internal final class _StaticJSONEncoderState<Sink: JSONSink> { | |
| @usableFromInline var codingPath: [CodingKey] = [] | |
| @usableFromInline var userInfo: [CodingUserInfoKey: Any] = [:] | |
| @usableFromInline var sink: Sink | |
| @usableFromInline | |
| init(sink: Sink) { | |
| self.sink = sink | |
| } | |
| } | |
| /// The encoder struct conforming to the Encoder protocol. | |
| /// This is a struct with no stored state - it just holds a reference to the State class. | |
| @usableFromInline | |
| internal struct _StaticJSONEncoder<Sink: JSONSink, Strategies: JSONEncodingStrategies>: Encoder { | |
| @usableFromInline let state: _StaticJSONEncoderState<Sink> | |
| @usableFromInline var codingPath: [CodingKey] { state.codingPath } | |
| @usableFromInline var userInfo: [CodingUserInfoKey: Any] { state.userInfo } | |
| @usableFromInline | |
| init(state: _StaticJSONEncoderState<Sink>) { | |
| self.state = state | |
| } | |
| @inlinable | |
| func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key: CodingKey { | |
| let container = _StaticJSONKeyedContainer<Key, Sink, Strategies>(encoder: self) | |
| return KeyedEncodingContainer(container) | |
| } | |
| @inlinable | |
| func unkeyedContainer() -> UnkeyedEncodingContainer { | |
| _StaticJSONUnkeyedContainer<Sink, Strategies>(encoder: self) | |
| } | |
| @inlinable | |
| func singleValueContainer() -> SingleValueEncodingContainer { | |
| _StaticJSONSingleValueContainer<Sink, Strategies>(encoder: self) | |
| } | |
| } | |
| // MARK: - Keyed Container | |
| @usableFromInline | |
| internal struct _StaticJSONKeyedContainer<Key: CodingKey, Sink: JSONSink, Strategies: JSONEncodingStrategies>: KeyedEncodingContainerProtocol { | |
| @usableFromInline let encoder: _StaticJSONEncoder<Sink, Strategies> | |
| @usableFromInline var codingPath: [CodingKey] { encoder.state.codingPath } | |
| @usableFromInline | |
| init(encoder: _StaticJSONEncoder<Sink, Strategies>) { | |
| self.encoder = encoder | |
| encoder.state.sink.beginObject() | |
| } | |
| @inlinable | |
| func encodeNil(forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNull() | |
| } | |
| @inlinable | |
| func encode<T>(_ value: T, forKey key: Key) throws where T: Encodable { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.codingPath.append(key) | |
| defer { encoder.state.codingPath.removeLast() } | |
| // Check primitives first (most common case) | |
| if let primitive = value as? _StaticJSONPrimitiveEncodable { | |
| try primitive.encodeToJSON(sink: &encoder.state.sink, strategies: Strategies.self) | |
| } else if let date = value as? Date { | |
| let encoded = try Strategies.DateStrategy.encode(date) | |
| encoder.state.sink.writeString(encoded) | |
| } else if let data = value as? Data { | |
| let encoded = try Strategies.DataStrategy.encode(data) | |
| encoder.state.sink.writeString(encoded) | |
| } else if let url = value as? URL { | |
| encoder.state.sink.writeString(url.absoluteString) | |
| } else { | |
| // Fallback to container encoding | |
| try value.encode(to: encoder) | |
| } | |
| } | |
| // Specialized overloads for common types (no dynamic casts) | |
| @inlinable | |
| func encode(_ value: String, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeString(value) | |
| } | |
| @inlinable | |
| func encode(_ value: Int, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: Int8, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: Int16, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: Int32, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: Bool, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeBool(value) | |
| } | |
| @inlinable | |
| func encode(_ value: Double, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| if !value.isFinite { | |
| if Strategies.FloatingPointStrategy.throwOnNonConformingFloat { | |
| throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value")) | |
| } else { | |
| encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(value)) | |
| } | |
| } else { | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| } | |
| @inlinable | |
| func encode(_ value: Int64, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: UInt, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: UInt8, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: UInt16, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: UInt32, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: UInt64, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| func encode(_ value: Float, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| let double = Double(value) | |
| if !double.isFinite { | |
| if Strategies.FloatingPointStrategy.throwOnNonConformingFloat { | |
| throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value")) | |
| } else { | |
| encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(double)) | |
| } | |
| } else { | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| } | |
| @inlinable | |
| func encode(_ value: Date, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| let encoded = try Strategies.DateStrategy.encode(value) | |
| encoder.state.sink.writeString(encoded) | |
| } | |
| @inlinable | |
| func encode(_ value: Data, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| let encoded = try Strategies.DataStrategy.encode(value) | |
| encoder.state.sink.writeString(encoded) | |
| } | |
| @inlinable | |
| func encode(_ value: URL, forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.writeString(value.absoluteString) | |
| } | |
| // Specialized array overloads - bypass UnkeyedContainer protocol overhead | |
| @inlinable | |
| func encode(_ value: [Int], forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.beginArray() | |
| for element in value { | |
| encoder.state.sink.writeNumber(String(element)) | |
| } | |
| encoder.state.sink.endArray() | |
| } | |
| @inlinable | |
| func encode(_ value: [String], forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.beginArray() | |
| for element in value { | |
| encoder.state.sink.writeString(element) | |
| } | |
| encoder.state.sink.endArray() | |
| } | |
| @inlinable | |
| func encode(_ value: [Double], forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.beginArray() | |
| for element in value { | |
| if !element.isFinite { | |
| if Strategies.FloatingPointStrategy.throwOnNonConformingFloat { | |
| throw EncodingError.invalidValue(element, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value")) | |
| } else { | |
| encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(element)) | |
| } | |
| } else { | |
| encoder.state.sink.writeNumber(String(element)) | |
| } | |
| } | |
| encoder.state.sink.endArray() | |
| } | |
| @inlinable | |
| func encode(_ value: [Bool], forKey key: Key) throws { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.sink.beginArray() | |
| for element in value { | |
| encoder.state.sink.writeBool(element) | |
| } | |
| encoder.state.sink.endArray() | |
| } | |
| @inlinable | |
| func nestedContainer<NestedKey>( | |
| keyedBy keyType: NestedKey.Type, | |
| forKey key: Key | |
| ) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.codingPath.append(key) | |
| let container = _StaticJSONKeyedContainer<NestedKey, Sink, Strategies>(encoder: encoder) | |
| return KeyedEncodingContainer(container) | |
| } | |
| @inlinable | |
| func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.codingPath.append(key) | |
| return _StaticJSONUnkeyedContainer<Sink, Strategies>(encoder: encoder) | |
| } | |
| @inlinable | |
| func superEncoder() -> Encoder { | |
| let key = _StaticJSONKey(stringValue: "super") | |
| encoder.state.sink.writeKey("super") | |
| encoder.state.codingPath.append(key) | |
| return encoder | |
| } | |
| @inlinable | |
| func superEncoder(forKey key: Key) -> Encoder { | |
| let transformedKey = Strategies.KeyTransform.transform(key.stringValue) | |
| encoder.state.sink.writeKey(transformedKey) | |
| encoder.state.codingPath.append(key) | |
| return encoder | |
| } | |
| } | |
| // MARK: - Unkeyed Container | |
| @usableFromInline | |
| internal struct _StaticJSONUnkeyedContainer<Sink: JSONSink, Strategies: JSONEncodingStrategies>: UnkeyedEncodingContainer { | |
| @usableFromInline let encoder: _StaticJSONEncoder<Sink, Strategies> | |
| @usableFromInline var codingPath: [CodingKey] { encoder.state.codingPath } | |
| @usableFromInline var count: Int = 0 | |
| @usableFromInline | |
| init(encoder: _StaticJSONEncoder<Sink, Strategies>) { | |
| self.encoder = encoder | |
| encoder.state.sink.beginArray() | |
| } | |
| @inlinable | |
| mutating func encodeNil() throws { | |
| encoder.state.sink.writeNull() | |
| count += 1 | |
| } | |
| @inlinable | |
| mutating func encode<T>(_ value: T) throws where T: Encodable { | |
| defer { count += 1 } | |
| let key = _StaticJSONKey(intValue: count) | |
| encoder.state.codingPath.append(key) | |
| defer { encoder.state.codingPath.removeLast() } | |
| // Check for less common primitive types (Int8, UInt*, etc.) | |
| // Common types (Int, String, Bool, Date, Data, URL, arrays) are handled by specialized overloads | |
| if let primitive = value as? _StaticJSONPrimitiveEncodable { | |
| try primitive.encodeToJSON(sink: &encoder.state.sink, strategies: Strategies.self) | |
| } else { | |
| // Fallback to container encoding for custom types | |
| try value.encode(to: encoder) | |
| } | |
| } | |
| // Specialized overloads for common types (no dynamic casts) | |
| @inlinable | |
| mutating func encode(_ value: String) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeString(value) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Int) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Int8) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Int16) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Int32) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Bool) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeBool(value) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Double) throws { | |
| defer { count += 1 } | |
| if !value.isFinite { | |
| if Strategies.FloatingPointStrategy.throwOnNonConformingFloat { | |
| throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value")) | |
| } else { | |
| encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(value)) | |
| } | |
| } else { | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Int64) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: UInt) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: UInt8) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: UInt16) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: UInt32) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: UInt64) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Float) throws { | |
| defer { count += 1 } | |
| let double = Double(value) | |
| if !double.isFinite { | |
| if Strategies.FloatingPointStrategy.throwOnNonConformingFloat { | |
| throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value")) | |
| } else { | |
| encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(double)) | |
| } | |
| } else { | |
| encoder.state.sink.writeNumber(String(value)) | |
| } | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Date) throws { | |
| defer { count += 1 } | |
| let encoded = try Strategies.DateStrategy.encode(value) | |
| encoder.state.sink.writeString(encoded) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: Data) throws { | |
| defer { count += 1 } | |
| let encoded = try Strategies.DataStrategy.encode(value) | |
| encoder.state.sink.writeString(encoded) | |
| } | |
| @inlinable | |
| mutating func encode(_ value: URL) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.writeString(value.absoluteString) | |
| } | |
| // Specialized array overloads - bypass UnkeyedContainer protocol overhead | |
| @inlinable | |
| mutating func encode(_ value: [Int]) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.beginArray() | |
| for element in value { | |
| encoder.state.sink.writeNumber(String(element)) | |
| } | |
| encoder.state.sink.endArray() | |
| } | |
| @inlinable | |
| mutating func encode(_ value: [String]) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.beginArray() | |
| for element in value { | |
| encoder.state.sink.writeString(element) | |
| } | |
| encoder.state.sink.endArray() | |
| } | |
| @inlinable | |
| mutating func encode(_ value: [Double]) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.beginArray() | |
| for element in value { | |
| if !element.isFinite { | |
| if Strategies.FloatingPointStrategy.throwOnNonConformingFloat { | |
| throw EncodingError.invalidValue(element, EncodingError.Context(codingPath: [], debugDescription: "Non-conforming floating point value")) | |
| } else { | |
| encoder.state.sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(element)) | |
| } | |
| } else { | |
| encoder.state.sink.writeNumber(String(element)) | |
| } | |
| } | |
| encoder.state.sink.endArray() | |
| } | |
| @inlinable | |
| mutating func encode(_ value: [Bool]) throws { | |
| defer { count += 1 } | |
| encoder.state.sink.beginArray() | |
| for element in value { | |
| encoder.state.sink.writeBool(element) | |
| } | |
| encoder.state.sink.endArray() | |
| } | |
| @inlinable | |
| mutating func nestedContainer<NestedKey>( | |
| keyedBy keyType: NestedKey.Type | |
| ) -> KeyedEncodingContainer<NestedKey> where NestedKey: CodingKey { | |
| let key = _StaticJSONKey(intValue: count) | |
| count += 1 | |
| encoder.state.codingPath.append(key) | |
| let container = _StaticJSONKeyedContainer<NestedKey, Sink, Strategies>(encoder: encoder) | |
| return KeyedEncodingContainer(container) | |
| } | |
| @inlinable | |
| mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { | |
| let key = _StaticJSONKey(intValue: count) | |
| count += 1 | |
| encoder.state.codingPath.append(key) | |
| return _StaticJSONUnkeyedContainer<Sink, Strategies>(encoder: encoder) | |
| } | |
| @inlinable | |
| mutating func superEncoder() -> Encoder { | |
| let key = _StaticJSONKey(intValue: count) | |
| count += 1 | |
| encoder.state.codingPath.append(key) | |
| return encoder | |
| } | |
| } | |
| // MARK: - Single Value Container | |
| @usableFromInline | |
| internal struct _StaticJSONSingleValueContainer<Sink: JSONSink, Strategies: JSONEncodingStrategies>: SingleValueEncodingContainer { | |
| @usableFromInline let encoder: _StaticJSONEncoder<Sink, Strategies> | |
| @usableFromInline var codingPath: [CodingKey] { encoder.state.codingPath } | |
| @usableFromInline | |
| init(encoder: _StaticJSONEncoder<Sink, Strategies>) { | |
| self.encoder = encoder | |
| } | |
| @inlinable | |
| func encodeNil() throws { | |
| encoder.state.sink.writeNull() | |
| } | |
| @inlinable | |
| func encode<T>(_ value: T) throws where T: Encodable { | |
| // Note: SingleValueEncodingContainer doesn't have specialized overloads, | |
| // so we need these checks here. Most usage goes through keyed/unkeyed containers. | |
| if let primitive = value as? _StaticJSONPrimitiveEncodable { | |
| try primitive.encodeToJSON(sink: &encoder.state.sink, strategies: Strategies.self) | |
| } else if let date = value as? Date { | |
| let encoded = try Strategies.DateStrategy.encode(date) | |
| encoder.state.sink.writeString(encoded) | |
| } else if let data = value as? Data { | |
| let encoded = try Strategies.DataStrategy.encode(data) | |
| encoder.state.sink.writeString(encoded) | |
| } else if let url = value as? URL { | |
| encoder.state.sink.writeString(url.absoluteString) | |
| } else { | |
| // Fallback to container encoding | |
| try value.encode(to: encoder) | |
| } | |
| } | |
| } | |
| // MARK: - Helper Types | |
| @usableFromInline | |
| internal struct _StaticJSONKey: CodingKey { | |
| @usableFromInline var stringValue: String | |
| @usableFromInline var intValue: Int? | |
| @usableFromInline | |
| init(stringValue: String) { | |
| self.stringValue = stringValue | |
| self.intValue = nil | |
| } | |
| @usableFromInline | |
| init(intValue: Int) { | |
| self.stringValue = "\(intValue)" | |
| self.intValue = intValue | |
| } | |
| } | |
| @usableFromInline | |
| internal protocol _StaticJSONPrimitiveEncodable { | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws | |
| } | |
| extension String: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeString(self) | |
| } | |
| } | |
| extension Int: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension Int8: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension Int16: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension Int32: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension Int64: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension UInt: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension UInt8: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension UInt16: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension UInt32: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension UInt64: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| extension Float: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| let double = Double(self) | |
| if !double.isFinite { | |
| if Strategies.FloatingPointStrategy.throwOnNonConformingFloat { | |
| throw EncodingError.invalidValue( | |
| self, | |
| EncodingError.Context( | |
| codingPath: [], | |
| debugDescription: "Non-conforming floating point value" | |
| ) | |
| ) | |
| } else { | |
| sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(double)) | |
| } | |
| } else { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| } | |
| extension Double: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| if !self.isFinite { | |
| if Strategies.FloatingPointStrategy.throwOnNonConformingFloat { | |
| throw EncodingError.invalidValue( | |
| self, | |
| EncodingError.Context( | |
| codingPath: [], | |
| debugDescription: "Non-conforming floating point value" | |
| ) | |
| ) | |
| } else { | |
| sink.writeNumber(Strategies.FloatingPointStrategy.encodeNonConformingFloat(self)) | |
| } | |
| } else { | |
| sink.writeNumber(String(self)) | |
| } | |
| } | |
| } | |
| extension Bool: _StaticJSONPrimitiveEncodable { | |
| @usableFromInline | |
| func encodeToJSON<Sink: JSONSink, Strategies: JSONEncodingStrategies>( | |
| sink: inout Sink, | |
| strategies: Strategies.Type | |
| ) throws { | |
| sink.writeBool(self) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import Testing | |
| import Foundation | |
| import StaticEncoder | |
| @Suite("Static JSON Encoder Tests") | |
| struct StaticJSONEncoderTests { | |
| // MARK: - Simple Model Tests | |
| @Test("JSON encoder encodes simple model") | |
| func simpleModel() throws { | |
| struct Person: Encodable { | |
| let name: String | |
| let age: Int | |
| } | |
| let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>() | |
| let person = Person(name: "Alice", age: 30) | |
| let data = try encoder.encode(person) | |
| let json = String(data: data, encoding: .utf8)! | |
| #expect(json.contains("\"name\"")) | |
| #expect(json.contains("\"Alice\"")) | |
| #expect(json.contains("\"age\"")) | |
| #expect(json.contains("30")) | |
| } | |
| @Test("JSON encoder encodes nested model") | |
| func nestedModel() throws { | |
| struct Address: Encodable { | |
| let street: String | |
| let city: String | |
| } | |
| struct Person: Encodable { | |
| let name: String | |
| let address: Address | |
| } | |
| let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>() | |
| let person = Person( | |
| name: "Bob", | |
| address: Address(street: "Main St", city: "NYC") | |
| ) | |
| let data = try encoder.encode(person) | |
| let json = String(data: data, encoding: .utf8)! | |
| #expect(json.contains("\"name\"")) | |
| #expect(json.contains("\"Bob\"")) | |
| #expect(json.contains("\"address\"")) | |
| #expect(json.contains("\"street\"")) | |
| #expect(json.contains("\"Main St\"")) | |
| #expect(json.contains("\"city\"")) | |
| #expect(json.contains("\"NYC\"")) | |
| } | |
| @Test("JSON encoder encodes array") | |
| func arrayEncoding() throws { | |
| struct Model: Encodable { | |
| let tags: [String] | |
| } | |
| let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>() | |
| let model = Model(tags: ["swift", "ios", "macos"]) | |
| let data = try encoder.encode(model) | |
| let json = String(data: data, encoding: .utf8)! | |
| #expect(json.contains("\"tags\"")) | |
| #expect(json.contains("[")) | |
| #expect(json.contains("\"swift\"")) | |
| #expect(json.contains("\"ios\"")) | |
| #expect(json.contains("\"macos\"")) | |
| } | |
| // MARK: - Primitive Type Tests | |
| @Test("JSON encoder handles all primitive types") | |
| func primitiveTypes() throws { | |
| struct AllTypes: Encodable { | |
| let string: String | |
| let int: Int | |
| let double: Double | |
| let bool: Bool | |
| } | |
| let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>() | |
| let model = AllTypes( | |
| string: "test", | |
| int: 42, | |
| double: 3.14, | |
| bool: true | |
| ) | |
| let data = try encoder.encode(model) | |
| let json = String(data: data, encoding: .utf8)! | |
| #expect(json.contains("\"string\"")) | |
| #expect(json.contains("\"test\"")) | |
| #expect(json.contains("\"int\"")) | |
| #expect(json.contains("42")) | |
| #expect(json.contains("\"double\"")) | |
| #expect(json.contains("3.14")) | |
| #expect(json.contains("\"bool\"")) | |
| #expect(json.contains("true")) | |
| } | |
| // MARK: - Snake Case Strategy Tests | |
| @Test("Snake case strategy transforms keys") | |
| func snakeCaseStrategy() throws { | |
| struct Person: Encodable { | |
| let firstName: String | |
| let lastName: String | |
| } | |
| let encoder = StaticJSONEncoder<JSONDataSink, SnakeCaseJSONEncodingStrategies>() | |
| let person = Person(firstName: "John", lastName: "Doe") | |
| let data = try encoder.encode(person) | |
| let json = String(data: data, encoding: .utf8)! | |
| #expect(json.contains("\"first_name\"")) | |
| #expect(json.contains("\"last_name\"")) | |
| #expect(!json.contains("firstName")) | |
| #expect(!json.contains("lastName")) | |
| } | |
| // MARK: - Data Sink Tests | |
| @Test("JSON Data sink produces valid JSON") | |
| func dataSink() throws { | |
| struct Model: Encodable { | |
| let value: Int | |
| } | |
| let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>() | |
| let model = Model(value: 123) | |
| let data = try encoder.encode(model) | |
| let string = String(data: data, encoding: .utf8) | |
| #expect(string != nil) | |
| #expect(string!.contains("\"value\"")) | |
| #expect(string!.contains("123")) | |
| } | |
| // MARK: - Performance Tests | |
| @Test("StaticJSONEncoder performance", .timeLimit(.minutes(1))) | |
| func encodingPerformanceIntoString() throws { | |
| @inline(never) | |
| @_optimize(none) | |
| func blackHole<T>(_ x: T) { | |
| withUnsafePointer(to: x) { ptr in | |
| _ = ptr.pointee | |
| } | |
| } | |
| struct Model: Encodable { | |
| let id: Int | |
| let name: String | |
| let tags: [String] | |
| let metadata: Metadata | |
| struct Metadata: Encodable { | |
| let created: String | |
| let updated: String | |
| } | |
| } | |
| let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>() | |
| for i in 0..<1_000_000 { | |
| let model = Model( | |
| id: i, | |
| name: "Test", | |
| tags: ["swift", "ios", "macos"], | |
| metadata: .init(created: "2024", updated: "2025") | |
| ) | |
| let output = try encoder.encode(model) | |
| blackHole(output) | |
| } | |
| } | |
| @Test("Foundation JSON Encoder performance", .timeLimit(.minutes(1))) | |
| func FoundationJSONEncoderEncodingPerformance() throws { | |
| @inline(never) | |
| @_optimize(none) | |
| func blackHole<T>(_ x: T) { | |
| withUnsafePointer(to: x) { ptr in | |
| _ = ptr.pointee | |
| } | |
| } | |
| struct Model: Encodable { | |
| let id: Int | |
| let name: String | |
| let tags: [String] | |
| let metadata: Metadata | |
| struct Metadata: Encodable { | |
| let created: String | |
| let updated: String | |
| } | |
| } | |
| let encoder = Foundation.JSONEncoder() | |
| encoder.dateEncodingStrategy = .iso8601 | |
| encoder.keyEncodingStrategy = .useDefaultKeys | |
| encoder.dataEncodingStrategy = .base64 | |
| encoder.nonConformingFloatEncodingStrategy = .throw | |
| for i in 0..<1_000_000 { | |
| let model = Model( | |
| id: i, | |
| name: "Test", | |
| tags: ["swift", "ios", "macos"], | |
| metadata: .init(created: "2024", updated: "2025") | |
| ) | |
| let output = try encoder.encode(model) | |
| blackHole(output) | |
| } | |
| } | |
| // MARK: - Type Erasure Tests | |
| @Test("Type-erased JSON encoder works") | |
| func typeErasure() throws { | |
| struct Model: Encodable { | |
| let value: String | |
| } | |
| let encoder = StaticJSONEncoder<JSONDataSink, StandardJSONEncodingStrategies>() | |
| .eraseToAnyEncoder() | |
| let model = Model(value: "test") | |
| let data: Data = try encoder.encode(model) | |
| let json = String(data: data, encoding: .utf8)! | |
| #expect(json.contains("\"value\"")) | |
| #expect(json.contains("\"test\"")) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // MARK: - Convenient Type Aliases | |
| /// Standard query string encoder with brackets notation. | |
| /// | |
| /// Equivalent to `StaticURLParameterEncoder<Sink, BracketsEncodingStrategies>` | |
| /// | |
| /// - Arrays: `items[]=1&items[]=2` | |
| /// - Objects: `user[name]=Ana` | |
| /// - Dates: ISO8601 | |
| public typealias StaticQueryEncoder<Sink: URLParameterSink> = | |
| StaticURLParameterEncoder<Sink, BracketsEncodingStrategies> | |
| /// URL template encoder with dot notation. | |
| /// | |
| /// Equivalent to `StaticURLParameterEncoder<Sink, DotNotationEncodingStrategies>` | |
| /// | |
| /// - Arrays: `items[]=1&items[]=2` | |
| /// - Objects: `user.name=Ana` (dot notation) | |
| /// - Dates: ISO8601 | |
| public typealias StaticTemplateEncoder<Sink: URLParameterSink> = | |
| StaticURLParameterEncoder<Sink, DotNotationEncodingStrategies> | |
| /// Query encoder with snake_case key transformation. | |
| /// | |
| /// Equivalent to `StaticURLParameterEncoder<Sink, SnakeCaseEncodingStrategies>` | |
| /// | |
| /// - Arrays: `items[]=1&items[]=2` | |
| /// - Objects: `user[home_address]=value` | |
| /// - Keys: Transformed to snake_case | |
| /// - Dates: ISO8601 | |
| public typealias StaticSnakeCaseQueryEncoder<Sink: URLParameterSink> = | |
| StaticURLParameterEncoder<Sink, SnakeCaseEncodingStrategies> | |
| /// Template encoder with dot notation and snake_case keys. | |
| /// | |
| /// Equivalent to `StaticURLParameterEncoder<Sink, DotNotationSnakeCaseStrategies>` | |
| /// | |
| /// Common for REST APIs. | |
| public typealias StaticRESTEncoder<Sink: URLParameterSink> = | |
| StaticURLParameterEncoder<Sink, DotNotationSnakeCaseStrategies> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment