Example of attributes with current version of Swift

Having seen several posts about the desire for attributes in Swift and having the same desire myself, I thought I would play with implementing something with Swift as it is now.

public protocol Attribute { }

public protocol Attributable
{
  static func getAttributes() -> [Attribute]
}

public struct Serializable : Attribute
{
  public private(set) var targetType: Any.Type
  
  public init(_ targetType: Any.Type)
  {
    self.targetType = targetType
  }
}

public struct SerializableProperty : Attribute
{
  public private(set) var keyPath: AnyKeyPath
  
  public private(set) var isSerializable: Bool
  
  public init(keyPath: AnyKeyPath, isSerializable: Bool = true)
  {
    self.keyPath = keyPath
    
    self.isSerializable = isSerializable
  }
}

public struct Person
{
  public var firstName: String
  
  public var lastName: String
  
  public var fullname: String
  {
    return "\(firstName) \(lastName)"
  }
}

extension Person : Attributable
{
  public static func getAttributes() -> [Attribute]
  {
    return [
            Serializable(Person.self),
            SerializableProperty(keyPath: \Person.fullname, isSerializable: false)
           ]
  }
}

Then, to recover the attributes elsewhere:

  {
    let testType: Any.Type = Person.self
    
    if let attributableType = testType as? Attributable.Type
    {
      let attributes = attributableType.getAttributes()
      
      for attribute in attributes
      {
        switch attribute
        {
          case let serializableType as Serializable:
            print("\(serializableType.targetType)")
          
          case let serializableProperty as SerializableProperty:
            print("\(serializableProperty.isSerializable) : \(serializableProperty.keyPath)")
          
          default:
            print("default")
        }
      }
    }    
  }

I've had some thoughts around this area too, though for the purposes of customizing encoding using Codable rather than another mechanism. In that case it seemed interesting to extend CodingKeys:

extension Person.CodingKeys: CustomTreatmentForMyEncoder {
  func wantsCustomTreatmentForMyEncoder() -> Bool {
    switch self {
    case .firstName: return true
    case .lastName: return false
    }
  }
}

But that's limited to coding. I also like the use of key paths in your version, but I can see why the default CodingKeys implementation doesn't do that: most of the time it'd be unnecessary code bloat.

Hi Jordan

The problem with explicitly declaring CodingKeys is that, by so doing, you implicitly decide on which properties participate in the Codable process. Therefore, you wouldn't want to use wantsCustomTreatmentForMyEncoder() simply for flagging properties as Codable or not. I presume you were thinking of using it for "special" coding?

Anyhow, your idea got me thinking again and here is another technique for my example using the Serializable protocol, which you could use instead of attributes:

public protocol SerializableProperties
{
  static func isSerializable(for keyPath: AnyKeyPath) -> Bool
}

extension Person : SerializableProperties
{
  public static func isSerializable(for keyPath: AnyKeyPath) -> Bool
  {
    switch keyPath
    {
      case \Person.firstName:
        return true
      case \Person.lastName:
        return true
      case \Person.fullname:
        return false
      default:
        fatalError("keyPath not found")
    }
  }
}

Of course, it still doesn't address the feasibility of attributes, but it is an interesting side track :wink:

Or, to cater for Codable with keypaths, how about this:

protocol CodableForKeyPaths : Codable
{
  static func isCodable(for keyPath: AnyKeyPath) -> Bool
}

extension Person : CodableForKeyPaths
{
  static func isCodable(for keyPath: AnyKeyPath) -> Bool
  {
    return keyPath == \Person.fullName ? false : true
  }
}