我们的 json-over-rest(ish( API 遵循一种编码 URL 模式,该 URL 可从特定对象以列表格式访问,在@links
键下。虚构示例:
{ "id": "whatever",
"height_at_birth": 38,
"@links": [
{ "name": "shield-activation-level",
"url": "https://example.com/some/other/path" },
{ "name": "register-genre-preference",
"url": "https://example.com/some/path" }
]
}
在 Swift 方面,我们使用 phantom 类型和可选性来实现类型安全。例如,上面的 json 可能对应于这样的结构:
struct Baby {
let id: String
let heightAtBirth: Int
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
}
幻像类型确保我们不会意外地将馈送计划发布到registerGenrePreference
URL,并且可选性指示格式正确的Baby
-json 将始终包含registerGenrePreference
的@links
条目,但其他两个链接可能存在,也可能不存在。目前为止,一切都好。
我想使用Decodable
来使用这种 json 格式,理想情况下最少使用init(decoder:Decoder)
个自定义实现。但我被@links
条目难倒了。
我想如果我手动完成整个解码,我会是什么样子:
- 拿到婴儿的容器,
- 从中获取一个嵌套的无键容器,用于
@links
键 - 遍历其值(应为
[String:String]
字典(并构建与 URL 匹配名称的字典 - 对于
Baby
期望的每个链接,请在字典中查找它(如果属性是非可选的并且链接丢失,则抛出(
但是步骤 2 和 3 对于遵循此模式的每个类都是相同的(不理想(,更糟糕的是,必须这样做也会阻止我使用编译器提供的Decodable
实现,所以我还必须手动解码Baby
的所有其他属性。
如果有帮助,我非常高兴重组Baby
;一个可能有帮助的明显步骤是:
struct Baby {
let id: String
let heightAtBirth: Int
let links: Links
struct Links {
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
}
}
当然,我希望我们将不得不添加编码键,即使仅用于蛇/骆驼壳转换和@
:
enum CodingKeys: String, CodingKey {
case id
case heightAtBirth = "height_at_birth"
case links = "@links"
}
我可能会按照上面的模式手动Decodable
Baby.Links
一致性,但这仍然意味着重复"获取未键入的集合,将其转换为字典,在字典中查找编码键"步骤对于遵循此模式的每个类。
有没有办法集中这个逻辑?
实际上,您的链接有一个定义良好的结构。它们是字典[String : String]
因此您可以在使用Decodable时利用它来发挥自己的优势。
您可能需要考虑设置如下结构。这些链接是从 JSON 解码的,扩展程序提供了您要查找的确切链接的可选性。
可链接协议可用于将一致性添加到需要它的任何类/结构中。
import Foundation
struct Link: Decodable {
let name: String
let url: String
}
protocol Linkable {
var links: [Link] { get }
}
extension Linkable {
func url(forName name: String) -> URL? {
guard let path = links.first(where: { $0.name == name })?.url else {
return nil
}
return URL(string: path)
}
}
struct Baby: Decodable, Linkable {
let id: String
let heightAtBirth: Int
let links: [Link]
enum CodingKeys: String, CodingKey {
case id = "id"
case heightAtBirth = "height_at_birth"
case links = "@links"
}
static func makeBaby(json: String) throws -> Baby {
guard let data = json.data(using: .utf8) else {
throw CocoaError.error(.fileReadUnknown)
}
return try JSONDecoder().decode(Baby.self, from: data)
}
}
extension Baby {
var registerGenrePreference: URL? {
return url(forName: "register-genre-preference")
}
var shieldActivationLevel: URL? {
return url(forName: "shield-activation-level")
}
}
let baby = try Baby.makeBaby(json: json)
baby.registerGenrePreference
baby.shieldActivationLevel
下面的模式为我提供了完整的类型安全,并且大多数实现都存在于一次性实用程序类UntypedLinks
中。每个模型类都需要定义一个嵌套类Links
自定义decode(from: Decoder)
,但这些实现完全是样板的(可能可以通过简单的代码生成工具自动化(并且可读性合理。
public struct Baby: Decodable {
public let id: String
public let heightAtBirth: Int
public let links: Links
enum CodingKeys: String, CodingKey {
case id
case heightAtBirth = "height_at_birth"
case links = "@links"
}
public struct Links: Decodable {
let registerGenrePreference: Link<POST<GenrePreference>>
let shieldActivationLevel: Link<GET<PowerLevel>>?
let magicPowers: Link<GET<[MagicPower]>>?
enum CodingKeys: String, CodingKey {
case registerGenrePreference = "register-genre-preference"
case shieldActivationLevel = "shield-activation-level"
case magicPowers = "magic-powers"
}
public init(from decoder: Decoder) throws {
let links = try UntypedLinks<CodingKeys>(from: decoder)
registerGenrePreference = try links.required(.registerGenrePreference)
shieldActivationLevel = links.optional(.shieldActivationLevel)
magicPowers = links.optional(.magicPowers)
}
}
}
public class UntypedLinks<CodingKeys> where CodingKeys: CodingKey {
let links: [String: String]
let codingPath: [CodingKey]
class UntypedLink: Codable {
let name: String
let url: String
}
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var links: [String: String] = [:]
while !container.isAtEnd {
let link = try container.decode(UntypedLink.self)
links[link.name] = link.url
}
self.links = links
self.codingPath = container.codingPath
}
func optional<Phantom>(_ name: CodingKeys) -> Link<Phantom>? {
return links[name.stringValue].map(Link.init)
}
func required<Phantom>(_ name: CodingKeys) throws -> Link<Phantom> {
guard let link: Link<Phantom> = optional(name) else {
throw DecodingError.keyNotFound(
name,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Link not found")
)
}
return link
}
}