My latest version. Added a corresponding feature to encode/convert back to a "Data buffer". Added the ability to deal with Optionals.
I'm pretty happy with it so far. All thoughts welcome.
import struct Foundation.Data
public enum Endian {
case big, little
}
public enum DataConversionOption {
case encoding(String.Encoding)
case endian(Endian)
}
extension DataConversionOption: Hashable {
public var hashValue: Int { switch self { case .encoding: return 0; case .endian: return 1 } }
public static func ==(lhs: DataConversionOption, rhs: DataConversionOption) -> Bool {
return lhs.hashValue == rhs.hashValue
}
}
extension String.Encoding {
public static let ebcdic = String.Encoding(rawValue: 0x8000_0C02)
#if os(zOS)
public static var native = String.Encoding.ebcdic
#else
public static var native = String.Encoding.ascii
#endif
fileprivate init(conversionOptions: Set<DataConversionOption>) {
var encoding = String.Encoding.native
for option in conversionOptions {
if case .encoding(let newEncoding) = option {
encoding = newEncoding
}
}
self = encoding
}
}
/// A protocol for types that can be converted to and from a Data "record buffer".
public protocol DataRecordConvertable {
init?(buffer: Data, conversionOptions: Set<DataConversionOption>)
func toDataRecord(buffer: inout Data, conversionOptions: Set<DataConversionOption>)
}
extension String: DataRecordConvertable {
public init?(buffer: Data, conversionOptions options: Set<DataConversionOption>) {
/* check for first "non-nul" (binary zero) byte; if none, buffer is all nuls, so set to nil */
guard buffer.first(where: { $0 != 0 } ) != nil else { return nil }
guard var decodedString = String(data: buffer, encoding: String.Encoding(conversionOptions: options))
else { fatalError("Decoding error") }
/* remove trailing spaces */
while decodedString.hasSuffix(" ") {
decodedString.removeLast()
}
self = decodedString
}
public func toDataRecord(buffer: inout Data, conversionOptions options: Set<DataConversionOption>) {
/* make sure string is "same size" as buffer */
func rightSize() -> String {
var str = self
let diff = buffer.count - count
if diff > 0 {
str.append(String(repeating: " ", count: diff)) // append trailing spaces
}
else {
str.removeLast(0 - diff) // truncate to proper length
}
return str
}
if let dataRecord = rightSize().data(using: String.Encoding(conversionOptions: options)) {
buffer = dataRecord
}
}
}
extension Character: DataRecordConvertable {
public init?(buffer: Data, conversionOptions options: Set<DataConversionOption>) {
guard let asString = String(buffer: buffer, conversionOptions: options) else { return nil }
self = Character(asString.isEmpty ? " " : asString)
}
public func toDataRecord(buffer: inout Data, conversionOptions options: Set<DataConversionOption>) {
String(self).toDataRecord(buffer: &buffer, conversionOptions: options)
}
}
extension Int: DataRecordConvertable {}
extension UInt: DataRecordConvertable {}
extension Int64: DataRecordConvertable {}
extension UInt64: DataRecordConvertable {}
extension Int32: DataRecordConvertable {}
extension UInt32: DataRecordConvertable {}
extension Int16: DataRecordConvertable {}
extension UInt16: DataRecordConvertable {}
extension Int8: DataRecordConvertable {}
extension UInt8: DataRecordConvertable {}
/* DataRecordConvertable implementation for all integer types */
extension FixedWidthInteger {
public init?(buffer: Data, conversionOptions options: Set<DataConversionOption>) {
self = Self.endianConverted(buffer.withUnsafeBytes { $0.pointee }, conversionOptions: options)
}
public func toDataRecord(buffer: inout Data, conversionOptions options: Set<DataConversionOption>) {
var myself = Self.endianConverted(self, conversionOptions: options)
buffer = Data(bytes: &myself, count: MemoryLayout<Self>.size)
}
private static func endianConverted(_ number: Self, conversionOptions: Set<DataConversionOption>) -> Self {
var number = number
for option in conversionOptions {
if case .endian(let endian) = option {
number = (endian == .big) ? number.bigEndian : number.littleEndian
}
}
return number
}
}
public struct RecordPropertyMap<Root> {
fileprivate let writeOperation: (inout Root, Data) -> Void
fileprivate let readOperation: (Root, inout Data) -> Void
/// Initialize RecordPropertyMap with a generic initializer that preserves the original
/// WritableKeyPath type, capturing the read and write operations in nongeneric closures.
init<Value: DataRecordConvertable>(keyPath: WritableKeyPath<Root, Value>, range: Range<Int>,
conversionOptions options: Set<DataConversionOption> = []) {
writeOperation = { root, buffer in
if let value = Value(buffer: buffer[range], conversionOptions: options) {
root[keyPath: keyPath] = value
}
}
readOperation = { root, buffer in
root[keyPath: keyPath].toDataRecord(buffer: &buffer[range], conversionOptions: options)
}
}
init<Value: DataRecordConvertable>(keyPath: WritableKeyPath<Root, Value>,
position: Int, length: Int = 1,
conversionOptions options: Set<DataConversionOption> = []) {
precondition(position > 0, "Starting position must be positive value")
precondition(length > 0, "Length must be positive value")
let offset = position - 1
self.init(keyPath: keyPath, range: offset ..< (offset + length), conversionOptions: options)
}
/* inits for Optionals */
init<Value: DataRecordConvertable>(keyPath: WritableKeyPath<Root, Value?>, range: Range<Int>,
conversionOptions options: Set<DataConversionOption> = []) {
writeOperation = { root, buffer in
root[keyPath: keyPath] = Value(buffer: buffer[range], conversionOptions: options)
}
readOperation = { root, buffer in
root[keyPath: keyPath]?.toDataRecord(buffer: &buffer[range], conversionOptions: options)
}
}
init<Value: DataRecordConvertable>(keyPath: WritableKeyPath<Root, Value?>,
position: Int, length: Int = 1,
conversionOptions options: Set<DataConversionOption> = []) {
precondition(position > 0, "Starting position must be positive value")
precondition(length > 0, "Length must be positive value")
let offset = position - 1
self.init(keyPath: keyPath, range: offset ..< (offset + length), conversionOptions: options)
}
}
public protocol RecordPropertyMapable {
init()
static var recordPropertyMaps: [RecordPropertyMap<Self>] { get }
}
extension RecordPropertyMapable {
public init(recordBuffer buffer: Data) {
self.init()
self.apply(recordBuffer: buffer)
}
public mutating func apply(recordBuffer buffer: Data) {
Self.recordPropertyMaps.forEach { $0.writeOperation(&self, buffer) }
}
public func recordBuffer(basedOn buffer: Data) -> Data {
var buffer = buffer
Self.recordPropertyMaps.forEach { $0.readOperation(self, &buffer) }
return buffer
}
public func recordBuffer(size: Int) -> Data {
return recordBuffer(basedOn: Data(count: size))
}
}
public struct Stuff {
var name: String = "wxyz"
var nbr: UInt32 = 0
var char: Character = "?"
var optStr: String? = "not empty"
public init() {} // declare default initializer public
}
extension Stuff: RecordPropertyMapable {
#if os(zOS) // z/OS is an EBCDIC big endian platform
public static let recordPropertyMaps: [RecordPropertyMap<Stuff>] = [
RecordPropertyMap(keyPath: \.name, position: 1, length: 5),
RecordPropertyMap(keyPath: \.nbr, position: 6, length: 4, conversionOptions: [.endian(.little)]),
RecordPropertyMap(keyPath: \.char, position: 10, conversionOptions: [.encoding(.ascii)]),
RecordPropertyMap(keyPath: \.optStr, position: 11, length: 4),
]
#else
public static let recordPropertyMaps: [RecordPropertyMap<Stuff>] = [
RecordPropertyMap(keyPath: \.name, position: 1, length: 5, conversionOptions: [.encoding(.ebcdic)]),
RecordPropertyMap(keyPath: \.nbr, position: 6, length: 4, conversionOptions: [.endian(.little)]),
RecordPropertyMap(keyPath: \.char, position: 10),
RecordPropertyMap(keyPath: \.optStr, position: 11, length: 4),
]
#endif
}
let stuffBuffer = Data([
0xF9, 0xF8, 0xF7, 0xF6, 0x40, // EBCDIC "9876 "
0xD2, 0x02, 0x96, 0x49, // little-endian 1234567890 / 0x499602D2
0x21, // ASCII "!"
0x00, 0x00, 0x00, 0x00
])
print(Array(stuffBuffer))
/* two step initialization */
var stuff1 = Stuff()
stuff1.apply(recordBuffer: stuffBuffer)
print(stuff1)
/* one step initialization */
let stuff2 = Stuff(recordBuffer: stuffBuffer)
print(stuff2)
/* convert back and print */
let stuffBufferNew = stuff1.recordBuffer(size: 14)
print(Array(stuffBufferNew))
print("match: \(stuffBufferNew == stuffBuffer)")
stuff1.name = "abcdefg" // will be truncated when converted
stuff1.optStr = "2112"
let stuffBuffer3 = stuff1.recordBuffer(size: 14)
print(Array(stuffBuffer3))
stuff1.name = "vwxyz"
stuff1.optStr = nil // will not update record when converted
print(Array(stuff1.recordBuffer(basedOn: stuffBuffer3)))