can get/set properties via keypath?
alexlee002 opened this issue · 12 comments
How can I do that?
What do you mean? Using the KeyPath
type?
e.g.:
let info = try typeInfo(of: User.self)
let property = try info.property(named: \.userName)
There's not a way builtin to the library. But you could just get the offset of the KeyPath and find the property that has the same offset.
let offset = MemoryLayout<Foo>.offset(of: \.bar)
let property = info.properties.first{ $0.offset == offset }
OK, I'll try it ASAP, Thanks!
Hi, I found this issue as I'm trying to get the property name from a keyPath. Using the offset above works for a normal keyPath (\.bar
), but not a nested one (\.bar.foo
). Ideally in this case I'd like to return the string "bar.foo".
@wickwirew any tips?
This seems to do the trick as a brute force approach, though has to traverse the whole tree:
import Runtime
extension KeyPath {
var propertyName: String? {
guard let offset = MemoryLayout<Root>.offset(of: self) else {
return nil
}
guard let info = try? typeInfo(of: Root.self) else {
return nil
}
func getPropertyName(for info: TypeInfo, path: [String]) -> String? {
if let property = info.properties.first(where: { $0.offset == offset }) {
return (path + [property.name]).joined(separator: ".")
} else {
for property in info.properties {
if let info = try? typeInfo(of: property.type),
let propertyName = getPropertyName(for: info, path: path + [property.name]) {
return propertyName
}
}
return nil
}
}
return getPropertyName(for: info, path: [])
}
}
There really isn't a great way to do this as far as I'm aware. The solution above doesn't work for me for a few cases. The offset when it is in a child object will be relative to it's offset in the parent.
This may work better:
extension KeyPath {
var propertyName: String? {
guard let offset = MemoryLayout<Root>.offset(of: self) else {
return nil
}
guard let info = try? typeInfo(of: Root.self) else {
return nil
}
func getPropertyName(for info: TypeInfo, baseOffset: Int, path: [String]) -> String? {
for property in info.properties {
// Make sure to check the type as well as the offset. In the case of
// something like \Foo.bar.baz, if baz is the first property of bar, they
// will have the same offset since it will be at the top (offset 0).
if property.offset == offset - baseOffset && property.type == Value.self {
return (path + [property.name]).joined(separator: ".")
}
guard let propertyTypeInfo = try? typeInfo(of: property.type) else { continue }
let trueOffset = baseOffset + property.offset
let byteRange = trueOffset..<(trueOffset + propertyTypeInfo.size)
if byteRange.contains(offset) {
// The property is not this property but is within the byte range used by the value.
// So check its properties for the value at the offset.
return getPropertyName(
for: propertyTypeInfo,
baseOffset: property.offset + baseOffset,
path: path + [property.name]
)
}
}
return nil
}
return getPropertyName(for: info, baseOffset: 0, path: [])
}
}
This still won't always work though. If the child object is a class or a computed property it will fail, but if its all structs it seems to work.
Example:
struct Foo {
let a: Int
let bar: Bar
}
struct Bar {
let b: Int
let baz: Baz
}
struct Baz {
let c: Int
}
let path = \Foo.bar.baz.c
print(path.propertyName) // prints "bar.baz.c"
Oh that’s much better, many thanks! 🙏
@wickwirew it seems not work in a class
class C {
var x: Int = 0
var y: Int = 0
var z: Int = 0
}
print(MemoryLayout<C>.offset(of: \.x)) // nil
print(MemoryLayout<C>.offset(of: \.y)) // nil
print(MemoryLayout<C>.offset(of: \.z)) // nil
here is the KeyPaths implementation of Apple/swift:
@usableFromInline // Exposed as public API by MemoryLayout<Root>.offset(of:)
internal var _storedInlineOffset: Int? {
return withBuffer {
var buffer = $0
// The identity key path is effectively a stored keypath of type Self
// at offset zero
if buffer.data.isEmpty { return 0 }
var offset = 0
while true {
let (rawComponent, optNextType) = buffer.next()
switch rawComponent.header.kind {
case .struct:
offset += rawComponent._structOrClassOffset
case .class, .computed, .optionalChain, .optionalForce, .optionalWrap, .external:
return .none
}
if optNextType == nil { return .some(offset) }
}
}
}
}
@alexlee002 yea that is actually the expected behavior due to the way MemoryLayout.offset(of:)
works