"genericizing" a keypath

Probably a bad topic title, but I couldn't figure an appropriate one.

Take the following:

import Foundation

func applyFrom<Type>(data: Data, to instance: inout Type, at: PartialKeyPath<Type>, encoding: String.Encoding? = nil) {
    switch at {
    case let path as WritableKeyPath<Type, String>:
        if let str = String(data: data, encoding: encoding ?? .utf8) {
            instance[keyPath: path] = str
        }
    case let path as WritableKeyPath<Type, UInt32>:
        instance[keyPath: path] = data.withUnsafeBytes { $0.pointee } as UInt32
    default:
        break
    }
}

Is there some way to make the second case "generic" such that said generic name can be used with the as clause that currently says "as UInt32"? Thus making it so I don't have to code a case for each of the different integer types?

Below is an example usage:

struct Stuff {
    var name: String = ""
    var nbr: UInt32 = 0
}

let asciiData = Data([0x30, 0x31, 0x32, 0x33])
let uint32Data = Data([0x01, 0x02, 0x03, 0x04])
var s = Stuff()
print(s)
for (rawData, stuffProperty) in zip([asciiData, uint32Data], [\Stuff.name, \Stuff.nbr]) {
    applyFrom(data: rawData, to: &s, at: stuffProperty)
    print(s)
}

Thanks,
Frank

Casting to a protocol with associated types is subject to opened existentials which, unfortunately, are yet to be implemented.
You could go around this by overloading the function, provided that you use a KeyPath instead to be able to impose requirements on the key type. The compiler will then resolve a call to the best match among the generic signatures. In your case, strings will resolve to the first function, numeric types - to the second.

func applyFrom<Type, Key>(... at: KeyPath<Type, Key> ...) {...}

func applyFrom<Type, Key: Numeric>(... at: KeyPath<Type, Key> ...) {...}

It doesn't appear that this will work in my situation. I'm passing it an element from a heterogeneous array of keypaths, which makes each element be of type PartialKeyPath, even though they still have a "value" of a full WritableKeyPath.

Further ideas welcome! :slight_smile:

class Assignable<To> { // Oh for generic protocols!
    /* abstract */ func from(data: Data, to: inout To) {
        fatalError("Must override `from`.")
    }
}
/* struct */ final class Assign<From, To>: Assignable<To> {
    let at: WritableKeyPath<To, From>
    init(at: WritableKeyPath<To, From>) {
        assert(!(at is WritableKeyPath<To, String>), "Use `AssignString` for `String`s, not `Assign`.")
        self.at = at
    }
    override func from(data: Data, to: inout To) {
        to[keyPath: at] = data.withUnsafeBytes { $0.pointee } as From
    }
}
/* struct */ final class AssignString<To>: Assignable<To> {
    let at: WritableKeyPath<To, String>
    let encoding: String.Encoding
    init(at: WritableKeyPath<To, String>, encoding: String.Encoding = .utf8) {
        self.at = at
        self.encoding = encoding
    }
    override func from(data: Data, to: inout To) {
        if let str = String(data: data, encoding: encoding) {
            to[keyPath: at] = str
        }
    }
}
s = Stuff()
print(s)
for (rawData, stuffProperty) in zip([asciiData, uint32Data], [AssignString(at: \Stuff.name), Assign(at: \Stuff.nbr)]) {
    stuffProperty.from(data: rawData, to: &s)
    print(s)
}

Works, but it would be nicer with generic protocols!

Note that key paths aren't covariant. This means case let path as WritableKeyPath<Type, T> will not succeed if you are trying to cast to a superclass of the actual key type or a protocol it conforms to, the key type has to match the type of the value it represents.

These are fundamental rules, so there is no workaround to what you're trying to do. Neither is the code I shared. Instead, it illustrates one way of actually doing it. You have two options: either hardcode it or take advantage of the generic approach and try to modify your code to avoid using heterogeneous array literals that enumerate member key paths just to loop an operation. You can of course also consider the solutions other people offer. The bottom line is not taking it for a workaround.

applyFrom(data: asciiData, to: &s, at: \Stuff.name)
applyFrom(data: uint32Data, to: &s, at: \Stuff.nbr)

Thanks for the comments. I am going to add a more "fleshed out" usage example so it can hopefully become more apparent why I want to be able to create an array to handle these.

import Foundation                                                                                                        
                                                                                                                         
func applyFrom<Type>(data: Data, 
                     to instance: inout Type, 
                     at: PartialKeyPath<Type>, 
                     encoding: String.Encoding? = nil) {  
    switch at {                                                                                                          
    case let path as WritableKeyPath<Type, String>:                                                                      
        if let str = String(data: data, encoding: encoding ?? .ascii) {                                                 
            instance[keyPath: path] = str                                                                                
        }                                                                                                                
    case let path as WritableKeyPath<Type, UInt32>:                                                                      
        instance[keyPath: path] = data.withUnsafeBytes { $0.pointee } as UInt32                                          
    default:                                                                                                             
        break                                                                                                            
    }                                                                                                                    
}                                                                                                                        
                                                                                                                         
public struct PropertyMap<Type> {                                                                                        
    let propertyKey: PartialKeyPath<Type>                                                                                
    let dataRange: CountableClosedRange<Int>                                                                             
    init(key: PartialKeyPath<Type>, 
         range: CountableClosedRange<Int>) {                                                  
        propertyKey = key                                                                                                
        dataRange = range                                                                                                
    }                                                                                                                    
}                                                                                                                        
                                                                                                                         
public func apply<Type>(data: Data, 
                        to instance: inout Type, 
                        using propertyMaps: [PropertyMap<Type>]) {                  
    for pmap in propertyMaps {                                                                                           
        applyFrom(data: data[pmap.dataRange], to: &instance, at: pmap.propertyKey)                                       
    }                                                                                                                    
}                                                                                                                        

                                                                                                                         
struct Stuff {                                                                                                           
    var name: String = ""                                                                                                
    var nbr: UInt32 = 0                                                                                                  
}                                                                                                                        

                                                                             
let stuffPropertyMaps = [                                                     
    PropertyMap(key: \Stuff.name, range: 0...4),                              
    PropertyMap(key: \Stuff.nbr, range: 5...8)                                
]                                                                             
var s = Stuff()                                                               
let rawData = Data([0x31, 0x32, 0x33, 0x34, 0x35, 0x01, 0x02, 0x03, 0x04])    
apply(data: rawData, to: &s, using: stuffPropertyMaps)                        
print(s)                                                                      

Essentially, I want to be able to create an array of "Property Maps" the map a particular range of the input data to a particular properly. For each rawData "record" I would apply this array to the data and get a usable "Stuff" struct.

Basically, I want the array of PropertyMap to define the mapping of the raw data to Swift properties. I don't want to have to code an "applyFrom" function manually for each property. There could be hundreds.

I am a mainframe COBOL programmer by trade, so I don't work much in this "new modern world", so its very possible I'm going about this the wrong way entirely. Any thoughts/suggestions welcome!

Although you can't do a cast that introduces genericity yet, you could build a PropertyMap type that captures the key path in its original generic state. Something like this:

import Foundation

/// A protocol for types that can read themselves from data
protocol DataReadable {
  init(fromData data: Data)
}

extension String: DataReadable {
  init(fromData data: Data) {
    self = String(data: data, encoding: .ascii)!
  }
}

extension UInt32: DataReadable {
  init(fromData data: Data) {
    self = data.withUnsafeBytes { $0.pointee }
  }
}

struct PropertyMap<Root> {
  let writeOperation: (inout Root, Data) -> Void
  
  /// Initialize PropertyMap with a generic initializer that preserves the original
  /// WritableKeyPath type, capturing the write operation in a nongeneric closure
  init<Value: DataReadable>(keyPath: WritableKeyPath<Root, Value>,
                            range: Range<Int>) {
    writeOperation = { root, data in
      root[keyPath: keyPath] = Value(fromData: data[range])
    }
  }
}

func apply<Type>(data: Data,
                 to instance: inout Type,
                 using propertyMaps: [PropertyMap<Type>]) {
  for pmap in propertyMaps {
    pmap.writeOperation(&instance, data)
  }
}
1 Like

I don't think I could have ever come upon this idea myself (even though there's a very similar example on Stack Overflow that I studied even to get this far: swift4 - In Swift 4, how can you assign to a keypath when the type of the keypath and value are generic but the same? - Stack Overflow). It's a thing of beauty! It does seem to require me to "cast" the keyPath literal to WritableKeyPath, but that's a reasonable compromise for this to work. Thank you so much!!

struct Stuff {
    var name: String = ""
    var nbr: UInt32 = 0
}

let stuffPropertyMaps = [
    PropertyMap(keyPath: \Stuff.name as WritableKeyPath, range: 0..<5),
    PropertyMap(keyPath: \Stuff.nbr as WritableKeyPath, range: 5..<9)
]

var s = Stuff()
let rawData = Data([0x31, 0x32, 0x33, 0x34, 0x35, 0x01, 0x02, 0x03, 0x04])
apply(data: rawData, to: &s, using: stuffPropertyMaps)
print(s)
1 Like

Hm, that looks like a bug. You should be able to pass a key path for a mutable property as a WritableKeyPath without the as.

1 Like

Hmm, now its working for me. Not sure what I changed to make it work...?!

So here's what I have now, with several improvements. Going perhaps a bit off of the original topic, but...

import Foundation

public enum Endian {
    case big, little
}

public enum ConversionOption {
    case encoding(String.Encoding)
    case endian(Endian)
}

extension ConversionOption: Hashable {
    public var hashValue: Int {
        switch self { case .encoding: return 0; case .endian: return 1 }
    }
    public static func ==(lhs: ConversionOption, rhs: ConversionOption) -> Bool {
        return lhs.hashValue == rhs.hashValue
    }
}

/// A protocol for types that can read themselves from data
public protocol DataReadable {
    init(fromData data: Data, options: Set<ConversionOption>)
}

extension FixedWidthInteger {
    public init(fromData data: Data, options: Set<ConversionOption>) {
        self = data.withUnsafeBytes { $0.pointee }
        for option in options {
            if case .endian(let endian) = option {
                self = (endian == .big) ? bigEndian : littleEndian
            }
        }
    }
}

extension String: DataReadable {
    public init(fromData data: Data, options: Set<ConversionOption>) {
        var encoding = String.Encoding.ascii
        for option in options {
            if case .encoding(let newEncoding) = option {
                encoding = newEncoding
            }
        }
        guard let decodedString = String(data: data, encoding: encoding)
        else { fatalError("Decoding error") }
        self = decodedString
    }
}

extension Int: DataReadable {}
extension UInt: DataReadable {}
extension Int64: DataReadable {}
extension UInt64: DataReadable {}
extension Int32: DataReadable {}
extension UInt32: DataReadable {}
extension Int16: DataReadable {}
extension UInt16: DataReadable {}
extension Int8: DataReadable {}
extension UInt8: DataReadable {}

public struct PropertyMap<Root> {
    let writeOperation: (inout Root, Data) -> Void

    /// Initialize PropertyMap with a generic initializer that preserves the original
    /// WritableKeyPath type, capturing the write operation in a nongeneric closure
    init<Value: DataReadable>(keyPath: WritableKeyPath<Root, Value>,
                              range: Range<UInt>,
                              conversionOptions: Set<ConversionOption> = []) {
        writeOperation = { root, data in
            root[keyPath: keyPath] = Value(fromData: data[range], options: conversionOptions)
        }
    }
    init<Value: DataReadable>(keyPath: WritableKeyPath<Root, Value>,
                              position: UInt, length: UInt = 1,
                              conversionOptions: Set<ConversionOption> = []) {
        guard position > 0 else { fatalError("Starting position must be positive value") }
        guard length > 0 else { fatalError("Length must be positive value") }
        let offset = position - 1
        self.init(keyPath: keyPath,
                  range: offset ..< (offset + length),
                  conversionOptions: conversionOptions)
    }
}

public protocol PropertyMapable {
    static var propertyMaps: [PropertyMap<Self>] { get }
}

extension PropertyMapable {
    mutating func apply(data: Data) {
        for pmap in Self.propertyMaps {
            pmap.writeOperation(&self, data)
        }
    }
}

public struct Stuff {
    var name: String = ""
    var nbr: UInt32 = 0
    var char: Character = Character(" ")
}

extension Stuff: PropertyMapable {
    public static let propertyMaps = [
        PropertyMap(keyPath: \Stuff.name, position: 1, length: 5,
                    conversionOptions: [.encoding(.ascii)]),
        PropertyMap(keyPath: \Stuff.nbr, position: 6, length: 4,
                    conversionOptions: [.endian(.big)]),
//      PropertyMap(keyPath: \Stuff.char, position: 10, length: 1), // this one doesn't work???
    ]
}

var s = Stuff()
let rawData = Data([0x31, 0x32, 0x33, 0x34, 0x35, 0x01, 0x02, 0x03, 0x04, 0x21])
s.apply(data: rawData)
print(s)

The simple hashValue in ConversionOption was quite intentional. I want the client to be able to use at most one of each option.

Might there be a more elegant way to check for the relevant ConversionOption and extract its associated value? See init in FixedWidthInteger and String extensions for what I'm asking about.

Any idea why I'm getting the following error?

keypath1.swift:110:37: error: type '_' has no member 'char'
    PropertyMap(keyPath: \Stuff.char, position: 10, length: 1), // this one doesn't work???

Thanks!

The problem is that the error is very misleading, you're getting it because Character doesn't conform to DataReadable.

Oh! Haha. Thanks.
Should I file a bug for the misleading message?

Should I file a bug for the misleading message?

Sure, go ahead. The issue actually covers a whole bunch of cases, apparently when an argument type is not directly a generic parameter type, but a compound or generic type including it.

1 Like

The solution you are presenting doesn't solve your original "generic" problem; as demonstrated by your Character error. In particular you have to manually add extension XXX: DataReadable {} to all types; it is not generic at all. The solution I presented "genericizing" a keypath - #4 by hlovatt however is generic.

In either approach, you have to specify the per-type reading behavior, either with a protocol conformance or a subclass. They're equally expressive.

It sounds like you've found an approach your happy with. If you wanted to refine this slightly more, you could also make ConversionOptions be an associated type of the DataReadable protocol, so that different types can present different conversion options that make sense for each type:

/// A protocol for types that can read themselves from data
public protocol DataReadable {
    associatedtype ConversionOptions
    static var defaultOptions: ConversionOptions { get }

    init(fromData data: Data, options: ConversionOptions)
}

extension FixedWidthInteger {
    enum ConversionEndian {
      case .littleEndian, .bigEndian
    }

    static var defaultOptions: ConversionEndian { return .littleEndian }

    public init(fromData data: Data, options: ConversionEndian) {
        self = data.withUnsafeBytes { $0.pointee }
        switch options {
        case .littleEndian: self = littleEndian
        case .bigEndian: self = bigEndian
        }
    }
}

extension String: DataReadable {
    static var defaultOptions: String.Encoding = .ascii
    public init(fromData data: Data, options: String.Encoding) {
        guard let decodedString = String(data: data, encoding: encoding)
        else { fatalError("Decoding error") }
        self = decodedString
    }
}

public struct PropertyMap<Root> {
    let writeOperation: (inout Root, Data) -> Void

    /// Initialize PropertyMap with a generic initializer that preserves the original
    /// WritableKeyPath type, capturing the write operation in a nongeneric closure
    init<Value: DataReadable>(keyPath: WritableKeyPath<Root, Value>,
                              range: Range<UInt>,
                              conversionOptions: Value.ConversionOptions = Value.defaultOptions) {
        writeOperation = { root, data in
            root[keyPath: keyPath] = Value(fromData: data[range], options: conversionOptions)
        }
    }
}

Thanks. I've been considering that. I haven't fully fleshed out all of my options, but I think some will apply to all (or many) types. I'll keep this in mind as I proceed. I also want to make it so that I can add conversion options later, without recompiling client applications. This is the main reason I went with using a Set. Perhaps there are better alternatives?

A Set is a reasonable approach if you're still experimenting and haven't solidified the options you're going to support. Swift's resilient ABI model does support adding new fields to existing structs without recompiling client code, so using type-specific options structs could still be a viable option if you've established that having different options per type is the right final model.

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)))
2 Likes