瓢虫 2.0.0

瓢虫 2.0.0

测试已测试
Lang语言 SwiftSwift
许可 MIT
发布最后发布2017年9月
SwiftSwift 版本4.0
SPM支持 SPM

杰夫·休里 维护。



瓢虫 2.0.0

瓢虫🐞

瓢虫使您能够在 Swift 4 中轻松编写模型或数据模型层。无需头疼即可完整符合 Codable

快速链接

CodableJSONCodable

瓢虫提供了 JSONCodable 协议,该协议是 Codable 协议的一个子协议。让我们比较一下使用 Codable 和使用 JSONCodable 创建对象的方式。

让我们用 Tree 来模拟一个模型。我想让这个对象符合 Codable,以便可以将其从 JSON 解析出来并将其编码为 JSON。

这里有一些 JSON

{
    "tree_names": {
        "colloquial": ["pine", "big green"],
        "scientific": ["piniferous scientificus"]
    },
    "age": 121,
    "family": 1,
    "planted_at": "7-4-1896",
    "leaves": [
        {
            "size": "large",
            "is_attached": true
        },
        {
            "size": "small",
            "is_attached": false
        }
    ]
}

使用 Codable😱

Tree: Codable 实现

struct Tree: Codable {
    
    enum Family: Int, Codable {
        case deciduous, coniferous
    }
    
    let name: String
    let family: Family
    let age: Int
    let plantedAt: Date
    let leaves: [Leaf]
    
    enum CodingKeys: String, CodingKey {
        case names = "tree_names"
        case family
        case age
        case plantedAt = "planted_at"
        case leaves
    }
    
    enum NameKeys: String, CodingKey {
        case name = "colloquial"
    }
    
    enum DecodingError: Error {
        case emptyColloquialNames
    }
    
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        let namesContainer = try values.nestedContainer(keyedBy: NameKeys.self, forKey: .names)
        let names = try namesContainer.decode([String].self, forKey: .name)
        guard let firstColloquialName = names.first else {
            throw DecodingError.emptyColloquialNames
        }
        name = firstColloquialName
        family = try values.decode(Family.self, forKey: .family)
        age = try values.decode(Int.self, forKey: .age)
        plantedAt = try values.decode(Date.self, forKey: .plantedAt)
        leaves = try values.decode([Leaf].self, forKey: .leaves)
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        var nameContainer = container.nestedContainer(keyedBy: NameKeys.self, forKey: .names)
        let colloquialNames = [name]
        try nameContainer.encode(colloquialNames, forKey: .name)
        try container.encode(family, forKey: .family)
        try container.encode(age, forKey: .age)
        try container.encode(plantedAt, forKey: .plantedAt)
        try container.encode(leaves, forKey: .leaves)
    }
    
    struct Leaf: Codable {
        
        enum Size: String, Codable {
            case small, medium, large
        }
        
        let size: Size
        let isAttached: Bool
        
        enum CodingKeys: String, CodingKey {
            case isAttached = "is_attached"
            case size
        }
        
        init(from decoder: Decoder) throws {
            let values = try decoder.container(keyedBy: CodingKeys.self)
            size = try values.decode(Size.self, forKey: .size)
            isAttached = try values.decode(Bool.self, forKey: .isAttached)
        }
        
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
            try container.encode(size, forKey: .size)
            try container.encode(isAttached, forKey: .isAttached)
        }
    }
}

Codable 是 Swift 的一大步,但要如你所见,它可以非常迅速地变得复杂。

解码 Tree: Codable

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
let tree = try decoder.decode(Tree_Codable.self, from: jsonData)

编码 Tree: Codable

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "MM-dd-yyyy"
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .formatted(dateFormatter)
let data = try encoder.encode(tree)

🐞拯救!

通过符合 JSONCodable 协议,您可以在仍然获得 Codable 符合性的同时跳过所有与 Codable 一起带来的样板代码。

Tree: JSONCodable 实现

struct Tree: JSONCodable {
    
    enum Family: Int, Codable {
        case deciduous, coniferous
    }
    
    let name: String
    let family: Family
    let age: Int
    let plantedAt: Date
    let leaves: [Leaf]
    
    static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
        "name": JSONKeyPath("tree_names", "colloquial", 0),
        "plantedAt": "planted_at" <- format("MM-dd-yyyy"),
        "leaves": [Leaf].transformer,
    ]
    
    struct Leaf: JSONCodable {
        
        enum Size: String, Codable {
            case small, medium, large
        }
        
        let size: Size
        let isAttached: Bool
        
        static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
            "isAttached": "is_attached"
        ]
    }
}

如你所见,您只需要提供不显式映射到属性名称的 JSON 键的映射。

解码 Tree: JSONCodable

let tree = try Tree(data: jsonData)

编码 Tree: JSONCodable

let data = try tree.toData()

Ladybug可以让你在Swift中创建模型时节省时间和精力,因为它提供了无需烦恼的Codable遵循。

解码

你可以将从JSON对象或Data中解码任何遵循JSONCodable的对象或对象数组。

/// Decode the given object from a JSON object
init(json: Any) throws
/// Decode the given object from `Data`
init(data: Data) throws

示例

let tree = try Tree(json: treeJSON)
let forest = try Array<Tree>(json: [treeJSON, treeJSON, treeJSON])

如果解码失败,两个初始化器都将抛出异常。

编码

你可以将任何遵循JSONCodable的对象或对象数组编码为JSON对象或Data

/// Encode the object into a JSON object
func toJSON() throws -> Any
/// Encode the object into Data
func toData() throws -> Data

示例

let jsonObject = try tree.toJSON()
let jsonData = try forest.toData()

如果编码失败,这两个函数都将抛出异常。

将JSON键映射到属性

通过遵循Ladybug提供的JSONCodable协议,你可以使用Data或JSON对象来初始化任何structfinal class。如果你的JSON结构与数据模型不一致,你可以覆盖静态transformersByPropertyKey属性来提供自定义映射。

struct Flight: JSONCodable {
    
    enum Airline: String, Codable {
        case delta, united, jetBlue, spirit, other
    }
    
    let airline: Airline
    let number: Int
    
    static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
    	"number": JSONKeyPath("flight_number")
    ]
}
...
let flightJSON = [
  "airline": "united",
  "flight_number": 472,
]
...
let directFlight = try Flight(json: flightJSON)
let flightWithLayover = try Array<Flight>(json: [flightJSON, otherFlightJSON])

directFlightflightWithLayover状态完全初始化,可以编码和解码。就这么简单。

**注意**:任何嵌套枚举都必须遵循CodableRawRepresentable,其中RawValue必须是Codable

**注意**:PropertyKeyString的自定义别名。

您可以通过遵循JSONTransformer的不同对象将JSON键与属性相关联。

变换器通过JSONCodable协议的只读static属性提供,并按PropertyKey索引。

static var transformersByPropertyKey: [PropertyKey: JSONTransformer] { get }

好吧,事情会变得有点复杂,但请相信我,这很容易。

访问JSON值:JSONKeyPath

在开头示例中,我们使用JSONKeyPathtree_name字段关联的值映射到Treename属性。

JSONKeyPath用于访问JSON中的值。它可以初始化为一系列JSON子脚本(IntString)的可变数量列表。

在下面的示例中

  • JSONKeyPath("foo")映射到{"hello": "world"}
  • 同样,"foo"映射到{"hello": "world"}
  • JSONKeyPath("foo", "hello")映射到"world"
  • JSONKeyPath("bar", 0)映射到"lorem"
{
  "foo": {
     "hello": "world"
  },
  "bar": [ 
  	 "lorem",
  	 "ipsum"
  ]  
}

**注意**:这些键路径在符合JSONTransformer的对象中作为可选使用,当要映射的属性名与JSON结构不匹配时。如果属性名与键路径相同,则不需要包含键路径。

**注意**:JSONKeyPath也可以表示为一个字符串字面量。

JSONKeyPath("some_key") == "some_key"

**注意**:你也可以使用Objective-C的键路径表示法。

JSONKeyPath("foo", "hello") == JSONKeyPath("foo.hello") == "foo.hello"

这对于Int索引不适用

JSONKeyPath("bar", 1) != JSONKeyPath("bar.1")

嵌套对象

让我们添加一个嵌套类,Passenger。航班有乘客。很好。

您可以通过遵循JSONCodable的任何对象或对象数组的静态transformer属性来表示嵌套对象。

可以使用 <- 运算符来组合变压器。在这种情况下,对于 airMarshal 属性,既要指定键路径,也要指定嵌套对象需要进行显式的转换。

{
  "airline": "united",
  "flight_number": 472,
  "air_marshal" {
     "name": "50 Cent",
  },
  "passengers": [
  	  {
  	  "name": "Jennifer Lawrence",
  	  },
  	  {
  	  "name": "Chris Pratt"
  	  },
  	  ... 
  ]
}
struct Flight: JSONCodable {
	...
    let passengers: [Passenger]
    let airMarshal: Passenger
    ...
    static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
    	...
    	"passengers": [Passenger].transformer,
    	"airMarshal": "air_marshal" <- Passenger.transformer
    ]
    
    struct Passenger: JSONCodable {
        let name: String
    }
}

注意:在使用 <- 运算符时,始终先将 JSONKeyPath 变换器放在第一位。

日期

最后,让我们把日期添加进去。Ladybug 提供了多个日期转换器

  • secondsSince1970:将日期解码为自 1970 年以来的 JSON 数字形式的 UNIX 时间戳。
  • millisecondsSince1970:将日期解码为自 1970 年以来的 UNIX 毫秒时间戳形式的 JSON 数字。
  • iso8601:将日期解码为 ISO-8601 格式的字符串(采用 RFC 3339 格式)。
  • format(_ format: String):使用自定义日期格式字符串解码日期。
  • custom(_ adapter: @escaping (Any?) -> Date?):从 JSON 值返回一个 Date
{
"july4th": "7/4/1776",
"y2k": 946684800,
}
struct SomeDates: JSONCodable {
    let july4th: Date
    let Y2K: Date
    let createdAt: Date
    
    static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
        "july4th": format("MM/dd/yyyy"),
        "Y2K": "y2k" <- secondsSince1970,
        "createdAt": custom { _ in return Date() }
    ]
}

注意:如果使用 custom 将其映射到非可选的 Date,则返回 nil 将导致在解码过程中抛出异常。

其他映射:Map<T: Codable>

如果您需要从 JSON 值到属性的简单映射,请使用 MapTransformer。使用该转换器将字符串转换为整数是一个很好的例子。

{
"count": "100"
}
...
struct BottlesOfBeerOnTheWall: JSONCodable {
    let count: Int
    
    static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
    	"count": Map<Int> { return Int($0 as! String) }
    ]
}

默认值/迁移:默认值

如果您想为一个属性提供默认值,您可以使用 Default。您提供默认值,还可以控制是否覆盖 JSON 有效负载中存在的属性键。

init(value: Any, override: Bool = false)

当 API 发生变化时,默认转换器非常有用,它可以帮助将缓存的 JSON 数据迁移到具有新属性的 JSONCodable 对象。

使用 JSONCodable 作为泛型约束

因为 Array 并未显式遵循 JSONCodable,所以 JSONCodable 在用作泛型约束时,不支持列表类型。如果您需要这项支持,可以使用 List<T: JSONCodable> 包装类型。

struct Tweet: JSONCodable { ... }
class ContentProvider<T: JSONCodable> { ... }

let tweetDetailProvider = ContentProvider<Tweet>()
let timelineProvider = ContentProvider<List<Tweet>>()

错误处理

异常

Ladybug 是以错误驱动的,所有 JSONCodable 初始化器和编码机制在失败时都会抛出异常。如果发生类型转换错误,Ladybug 会抛出 JSONCodableError 类型,Ladybug 还会从 JSONSerializationJSONDecoderJSONEncoder 抛出异常。

可选值

如果 JSON 负载中的某个值是可选的,则在您的数据模型中也应该是可选的。Ladybug 只会在键丢失且属性将其映射到非可选属性时抛出异常。孩子们,为了保险起见,使用可选值。

对 Map 和自定义日期的安全性

有两个转换器可以返回 nil 值:`Map`) 和 `custom(_ adapter: @escaping (Any?) -> Date?)。

如果您正在从已经编码的 `JSONCodable` 对象中解码,返回 `nil` 是完全可以的。

如果您正在从 `URLResponse` 解码,返回 `nil` 可能会导致抛出异常。

类符合 JSONCodable

当您使一个 符合 JSONCodable 时,有 2 个小的注意事项需要记住

  1. 因为 Swift 中的类不像结构体那样具有内置的默认初始化器,所以您必须确保属性被初始化。您可以通过提供默认值或默认初始化器来初始化这些值来实现这一点。

您可以在 ClassConformanceTests.swift 中看到示例。

  1. 从符合 Codable 的对象派生子类将不起作用,因此它也不适用于 JSONCodable

由于这些注意事项,我建议您使用结构体来设计数据模型。

关于……的思考🐞

如果能生成其关联属性的字符串,那就相当棒了 AnyKeyPath

如果 Swift 4 中的键路径公开了一个字符串值,我们就可以使用 PartialKeyPath<Self> 作为我们的 PropertyKey typealias,而不是使用 String。这将是一个更安全的替代方案。

 typealias PropertyKey = PartialKeyPath<Self>
...
static var transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
	\Tree.name: JSONKeyPath("tree_name") 
]

关于这一点,在 SE-0161 中没有讨论。

如果 Mirror 可以为类型而不是实例创建,那就更棒了

这允许我们隐式映射符合 JSONCodable 的嵌套对象。

Codable 的问题是什么?

如前所述,Codable 是简化 Swift 中 JSON 解析的巨大进步,但使用 Codable 时仍然存在 O(n) 的样板代码,这是 Swift JSON 解析中的主要问题(例如,对于对象拥有的每个属性,您需要写 1 或多行代码来将 JSON 映射到相应的属性)。在 Apple 的关于 编码和自定义类型解码 的文档中,您可以看到,一旦 JSON 键与属性键不同,就需要编写大量样板代码来获取 Codable 的符合性。Ladybug 通常会跳过这个问题,并在底层为您完成大量工作。

小心使用 MapTransformer

MapTransformer 很容易过度使用。在下面的例子中,map transformer 被用来计算总和,而不是映射 JSON 值到 Codable 类型。对我来说,这促进了不好的数据建模。我坚信,数据模型应该与 JSON 响应紧密对应。如果使用不当,映射转换器可能会给数据模型太多的责任。

{
"values": [1, 1, 2, 3, 5, 8, 13]
}
... 
struct FibonacciSequence: JSONCodable {
	let values: [Int]
	let sum: Int
	
	static let transformersByPropertyKey: [PropertyKey: JSONTransformer] = [
		"sum": MapTransformer<Int>(keyPath: "values") { value in
			let values = value as! [Int]
			return values.reduce(0) { $0 + $1 }
		}
	]
}

致谢

Mantle 中的好人表示感谢,他们给了我一些关于这个项目的灵感。我很高兴这个类似的框架最终可以用于 Swift,而不需要混合 Obj-C 运行时。

联系信息和贡献

请随时通过电子邮件 [email protected] 或在推特上联系我 戳我。我很乐意听听您的想法,或者看到这个是如何被使用的示例。

MIT 许可