Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

can get/set properties via keypath? #107

Open
alexlee002 opened this issue Jul 17, 2022 · 12 comments
Open

can get/set properties via keypath? #107

alexlee002 opened this issue Jul 17, 2022 · 12 comments

Comments

@alexlee002
Copy link

How can I do that?

@wickwirew
Copy link
Owner

What do you mean? Using the KeyPath type?

@alexlee002
Copy link
Author

e.g.:

let info = try typeInfo(of: User.self)
let property = try info.property(named: \.userName)

@wickwirew
Copy link
Owner

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 }

@alexlee002
Copy link
Author

OK, I'll try it ASAP, Thanks!

@yonaskolb
Copy link

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?

@yonaskolb
Copy link

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: [])
    }
}

@wickwirew
Copy link
Owner

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"

@yonaskolb
Copy link

Oh that’s much better, many thanks! 🙏

@alexlee002
Copy link
Author

@wickwirew it seems not work in a class

@alexlee002
Copy link
Author

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

@alexlee002
Copy link
Author

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) }
      }
    }
  }
}

@wickwirew
Copy link
Owner

wickwirew commented Feb 2, 2023

@alexlee002 yea that is actually the expected behavior due to the way MemoryLayout.offset(of:) works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants