import Foundation extension CSVDecoder { /// Configuration for how to read CSV data. @dynamicMemberLookup public struct Configuration { /// The underlying `CSVReader` configurations. @usableFromInline private(set) var readerConfiguration: CSVReader.Configuration /// The strategy to use when decoding a `nil` representation. public var nilStrategy: Strategy.NilDecoding /// The strategy to use when decoding Boolean values. public var boolStrategy: Strategy.BoolDecoding /// The strategy to use when dealing with non-conforming numbers. public var nonConformingFloatStrategy: Strategy.NonConformingFloat /// The strategy to use when decoding decimal values. public var decimalStrategy: Strategy.DecimalDecoding /// The strategy to use when decoding dates. public var dateStrategy: Strategy.DateDecoding /// The strategy to use when decoding binary data. public var dataStrategy: Strategy.DataDecoding /// The amount of CSV rows kept in memory after decoding to allow the random-order jumping exposed by keyed containers. public var bufferingStrategy: Strategy.DecodingBuffer /// Designated initializer setting the default values. public init() { self.readerConfiguration = CSVReader.Configuration() self.nilStrategy = .empty self.boolStrategy = .insensitive self.nonConformingFloatStrategy = .throw self.decimalStrategy = .locale(nil) self.dateStrategy = .deferredToDate self.dataStrategy = .base64 self.bufferingStrategy = .keepAll } /// Gives direct access to all CSV reader's configuration values. /// - parameter member: Writable key path for the reader's configuration value. public subscript(dynamicMember member: WritableKeyPath) -> V { @inlinable get { self.readerConfiguration[keyPath: member] } set { self.readerConfiguration[keyPath: member] = newValue } } } } extension Strategy { /// The strategy to use for decoding `nil` representations. public enum NilDecoding { /// An empty string is considered a `nil` value. /// /// An empty string can be both the absence of characters between field delimiters and an empty escaped field (e.g. `""`). case empty /// Decodes the `nil` as a custom value decoded by the given closure. /// /// Custom `nil` decoding adheres to the same behavior as a custom `Decodable` type. For example: /// /// let decoder = CSVDecoder() /// decoder.nilStrategy = .custom({ /// let container = try $0.singleValueContainer() /// let string = try container.decode(String.self) /// return string == "-" /// }) /// /// - parameter decoding: Function receiving the CSV decoder used to parse a custom `nil` value. /// - parameter decoder: The decoder on which to fetch a single value container to obtain the underlying `String` value. /// - returns: Boolean indicating whether the encountered value was a `nil` representation. If the value is not supported, return `false`. case custom(_ decoding: (_ decoder: Decoder) -> Bool) } /// The strategy to use for decoding `Bool` values. public enum BoolDecoding { /// Defer to `Bool`'s `LosslessStringConvertible` initializer. /// /// For a value to be considered `true` or `false`, it must be a string with the exact value of `"true"` or `"false"`. case deferredToBool /// Decodes a Boolean from an underlying string value by transforming `true`/`false` and `yes`/`no` disregarding case sensitivity. /// /// The value: `True`, `TRUE`, `TruE` or `YES`are accepted. case insensitive /// Decodes the `Bool` from an underlying `0` or `1` case numeric /// Decodes the `Bool` as a custom value decoded by the given closure. If the closure fails to decode a value from the given decoder, the error will be bubled up. /// /// Custom `Bool` decoding adheres to the same behavior as a custom `Decodable` type. For example: /// /// let decoder = CSVDecoder() /// decoder.boolStrategy = .custom({ /// let container = try $0.singleValueContainer() /// switch try container.decode(String.self) { /// case "si": return true /// case "no": return false /// default: throw CSVError(...) /// } /// }) /// /// - parameter decoding: Function receiving the CSV decoder used to parse a custom `Bool` value. /// - parameter decoder: The decoder on which to fetch a single value container to obtain the underlying `String` value. /// - returns: Boolean value decoded from the underlying storage. case custom(_ decoding: (_ decoder: Decoder) throws -> Bool) } /// The strategy to use for decoding `Decimal` values. public enum DecimalDecoding { /// The locale used to interpret the number (specifically `decimalSeparator`). case locale(Locale? = nil) /// Decode the `Decimal` as a custom value decoded by the given closure. If the closure fails to decode a value from the given decoder, the error will be bubled up. /// /// Custom `Decimal` decoding adheres to the same behavior as a custom `Decodable` type. For example: /// /// let decoder = CSVDecoder() /// decoder.decimalStrategy = .custom({ /// let value = try Float(from: decoder) /// return Decimal(value) /// }) /// /// - parameter decoding: Function receiving the CSV decoder used to parse a custom `Decimal` value. /// - parameter decoder: The decoder on which to fetch a single value container to obtain the underlying `String` value. /// - returns: `Decimal` value decoded from the underlying storage. case custom(_ decoding: (_ decoder: Decoder) throws -> Decimal) } /// The strategy to use for decoding `Date` values. public enum DateDecoding { /// Defer to `Date` for decoding. case deferredToDate /// Decode the `Date` as a UNIX timestamp from a number. case secondsSince1970 /// Decode the `Date` as UNIX millisecond timestamp from a number. case millisecondsSince1970 /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). case iso8601 /// Decode the `Date` as a string parsed by the given formatter. case formatted(DateFormatter) /// Decode the `Date` as a custom value decoded by the given closure. If the closure fails to decode a value from the given decoder, the error will be bubled up. /// /// Custom `Date` decoding adheres to the same behavior as a custom `Decodable` type. For example: /// /// let decoder = CSVDecoder() /// decoder.dateStrategy = .custom({ /// let container = try $0.singleValueContainer() /// let string = try container.decode(String.self) /// // Now returns the date represented by the custom string or throw an error if the string cannot be converted to a date. /// }) /// /// - parameter decoding: Function receiving the CSV decoder used to parse a custom `Date` value. /// - parameter decoder: The decoder on which to fetch a single value container to obtain the underlying `String` value. /// - returns: `Date` value decoded from the underlying storage. case custom(_ decoding: (_ decoder: Decoder) throws -> Date) } /// The strategy to use for decoding `Data` values. public enum DataDecoding { /// Defer to `Data` for decoding. case deferredToData /// Decode the `Data` from a Base64-encoded string. case base64 /// Decode the `Data` as a custom value decoded by the given closure. If the closure fails to decode a value from the given decoder, the error will be bubled up. /// /// Custom `Data` decoding adheres to the same behavior as a custom `Decodable` type. For example: /// /// let decoder = CSVDecoder() /// decoder.dataStrategy = .custom({ /// let container = try $0.singleValueContainer() /// let string = try container.decode(String.self) /// // Now returns the data represented by the custom string or throw an error if the string cannot be converted to a data. /// }) /// /// - parameter decoding: Function receiving the CSV decoder used to parse a custom `Data` value. /// - parameter decoder: The decoder on which to fetch a single value container to obtain the underlying `String` value. /// - returns: `Data` value decoded from the underlying storage. case custom(_ decoding: (_ decoder: Decoder) throws -> Data) } /// Indication of how many rows are cached for reuse by the decoder. /// /// CSV decoding is an inherently sequential operation; i.e. row 2 must be decoded after row 1. This due to the string encoding, the field/row delimiter usage, and by not setting the underlying row width. /// On the other hand, the `Decodable` protocol allows CSV rows to be decoded in random-order through the keyed containers. For example, a user can ask for a row at position 24 and then ask for the CSV row at index 3. /// /// A buffer is used to marry the sequential needs of the CSV decoder and `Decodable`'s _random-access_ nature. This buffer stores all decoded CSV rows (starts with none and gets filled as more rows are being decoded). /// The `DecodingBuffer` strategy gives you the option to control the buffer's memory usage and whether rows are being discarded after being decoded. public enum DecodingBuffer { /// All decoded CSV rows are cached. /// Forward/Backwards decoding jumps are allowed. A row that has been previously decoded can be decoded again. /// - remark: This strategy consumes the largest amount of memory from all the supported options. case keepAll // /// Only CSV fields that have been decoded but not requested by the user are being kept in memory. // /// // /// _Keyed containers_ can be used to read rows/fields unordered. However, previously requested rows cannot be requested again or an error will be thrown. // /// - remark: This strategy tries to keep the cache to a minimum, but memory usage may be big if the user doesn't request intermediate rows. // case unrequested /// No rows are kept in memory (except for the CSV row being decoded at the moment) /// /// _Keyed containers_ can be used, but at a file-level any forward jump will discard the in-between rows. At a row-level _keyed containers_ may still be used for random-order reading. /// - remark: This strategy provides the smallest usage of memory from them all. case sequential } } // MARK: - extension CSVDecoder: Failable { /// The type of error raised by the CSV decoder. public enum Error: Int { /// Some of the configuration values provided are invalid. case invalidConfiguration = 1 /// The decoding coding path is invalid. case invalidPath = 2 /// An error occurred on the encoder buffer. case bufferFailure = 4 } public static var errorDomain: String { "Decoder" } public static func errorDescription(for failure: Error) -> String { switch failure { case .invalidConfiguration: return "Invalid configuration" case .invalidPath: return "Invalid path" case .bufferFailure: return "Invalid buffer state" } } }