HappyCodable.CommandLine 1.0.9

HappyCodable.CommandLine 1.0.9

mikun 维护。



  • 作者
  • mikun

HappyCodable

中文介绍

一个更快乐的 Codable 框架,使用 SourceKittenFramework 自动生成与 Codable 相关的代码。

有什么问题吗?

  1. 不支持对单个编码键进行更改,一旦更改一个编码键,就需要设置所有编码键。
  2. 不支持忽略特定的编码键。
  3. 不支持非 RawRepresentable 枚举的自动合成,即使每个元素都是可编码的。
  4. 不支持在解码时将多个键映射到一个属性。
  5. 调试困难。
  6. 在 json 数据中缺少相应的键时,无法自动使用定义中的默认值。
  7. 不支持自动映射类型,如将 0/1 转换为 false/true。

使用 HappyCodable 可以解决所有这些问题。

安装

这似乎有些麻烦,但我可以保证这是值得的。

您需要将这些添加到您的项目中

  1. HappyCodable - 提供您所需的协议和功能
  2. HappyCodable/CommandLine,基于 SourceKitten,用于生成所需的 Codable 代码

准备

创建 HappyCodable/CommandLine 的主机命令行,这里命名为 HappyCodableCommandLine,并选择 Swift 作为语言

替换 HappyCodableCommandLine 由 Xcode 创建的 main.swift

import Foundation
import HappyCodable

let path: String = CommandLine.arguments[1]

let createdFilePath: String = CommandLine.arguments[2]

main(path: path, createdFilePath: createdFilePath)
dispatchMain()

在 Xcode 中打开您的项目目标,切换到“构建阶段”选项卡

打开依赖项,然后将其添加到主目标。

在文件列表中单击您的项目,选择您的目标下的“TARGETS”,点击“构建阶段”选项卡,单击左侧上角的小加号创建新的运行脚本书签,将其拖放到编译源代码阶段上方和 Check Pods Manifest.lock 下方,展开它并粘贴以下脚本

# The finish complied HappyCodableCommandLine path, no need to change
commandLine="${SYMROOT}/${CONFIGURATION}/HappyCodableCommandLine"

# The scan path, ${SRCROOT}/${PRODUCT_NAME} mean scan the whole project by default, change if you need
scanPath="${SRCROOT}/${PRODUCT_NAME}"

# The save path of grenerated code, change if you need
generatedPath="${SRCROOT}/HappyCodable.generated.swift"

echo "${commandLine} ${scanPath} ${generatedPath}"
${commandLine} ${scanPath} ${generatedPath}

CocoaPods

  1. 在 Podfile 的主目标中添加 `pod 'HappyCodable'
  2. 在 Podfile 的 HappyCodableCommandLine 目标中添加 `pod 'HappyCodable/CommandLine'

完成之后

target 'HappyCodableDemo' do
	pod 'HappyCodable'
end

target 'HappyCodableCommandLine' do
	pod 'HappyCodable/CommandLine'
end
  1. 运行 pod install

在你的项目中使用

  1. 首次构建可能会花费一些时间,因为需要编译 HappyCodableCommandLine,完成后,应在您的选择路径中看到 HappyCodable.generated.swift

  2. HappyCodable.generated.swift 文件拖入您的项目,如果需要,取消选中复制项

    提示:将 *.generated.swift 模式添加到您的 .gitignore 文件中,以防止不必要的冲突。

  3. HappyCodable 添加到类似 struct/class/enum 的结构中

import HappyCodable
struct Person: HappyCodable {
	var name: String = "abc"
	
	@Happy.codingKeys("🆔")
	var id: String = "abc"
	
	@Happy.codingKeys("secret_number", "age") // the first key will be the coding key
	var age: Int = 18
	
	@Happy.uncoding
	var secret_number: String = "3.1415" // Build fail if coded, in this case, we can "uncoding" it.
}

HappyCodableCommandLine 将自动创建代码

extension Person {
	enum CodingKeys: String, CodingKey {
		case name
		case id = "🆔"
		case age = "secret_number"
	}
	mutating
	func decode(happyFrom decoder: Decoder) throws {
		let container = try decoder.container(keyedBy: StringCodingKey.self)
		var errors = [Error]()
		
		do { self.name = try container.decode(key: "name") } catch { errors.append(error) }
		do { self.id = try container.decode(key: "🆔") } catch { errors.append(error) }
		do { self.age = try container.decode(key: "secret_number", alterKeys: { ["age"] }) } catch { errors.append(error) }
		
		if !Self.allowHappyDecodableSkipMissing, !errors.isEmpty {
			throw errors
		}
	}
	func encode(happyTo encoder: Encoder) throws {
		var container = encoder.container(keyedBy: CodingKeys.self)
		var errors = [Error]()
		do { try container.encodeIfPresent(self.name, forKey: .name) } catch { errors.append(error) }
		do { try container.encodeIfPresent(self.id, forKey: .id) } catch { errors.append(error) }
		do { try container.encodeIfPresent(self.age, forKey: .age) } catch { errors.append(error) }
		if !Self.allowHappyEncodableSafely, !errors.isEmpty {
			throw errors
		}
	}
}

也支持非 RawRepresentable Enum(您需要先确保参数类型是 Codable)

import HappyCodable
enum EnumTest: HappyCodableEnum {
	case value(num: Int, name: String)
//	case call(() -> Void) // Build fails if added, since (() -> Void) can't be codable
	case name0(String)
	case name1(String, last: String)
	case name2(first: String, String)
	case name3(_ first: String, _ last: String)
}

生成代码

extension EnumTest {
	init(from decoder: Decoder) throws {
		let container = try decoder.singleValueContainer()
		let content = try container.decode([String: [String: String]].self)
		let error = DecodingError.typeMismatch(EnumTest.self, DecodingError.Context(codingPath: [], debugDescription: ""))
		guard let name = content.keys.first else {
			throw error
		}
		let decoder = JSONDecoder()
		switch name {
			case ".value(num:name:)":
				guard
					let _0 = content[name]?["num"]?.data(using: .utf8),
					let _1 = content[name]?["name"]?.data(using: .utf8)
				else {
					throw error
				}
				
				self = .value(
					num: try decoder.decode((Int).self, from: _0),
					name: try decoder.decode((String).self, from: _1)
				)
			case ".name0(_:)":
				guard
					let _0 = content[name]?["$0"]?.data(using: .utf8)
				else {
					throw error
				}
				
				self = .name0(
					try decoder.decode((String).self, from: _0)
				)
			case ".name1(_:last:)":
				guard
					let _0 = content[name]?["$0"]?.data(using: .utf8),
					let _1 = content[name]?["last"]?.data(using: .utf8)
				else {
					throw error
				}
				
				self = .name1(
					try decoder.decode((String).self, from: _0),
					last: try decoder.decode((String).self, from: _1)
				)
			case ".name2(first:_:)":
				guard
					let _0 = content[name]?["first"]?.data(using: .utf8),
					let _1 = content[name]?["$1"]?.data(using: .utf8)
				else {
					throw error
				}
				
				self = .name2(
					first: try decoder.decode((String).self, from: _0),
					try decoder.decode((String).self, from: _1)
				)
			case ".name3(_:_:)":
				guard
					let _0 = content[name]?["first"]?.data(using: .utf8),
					let _1 = content[name]?["last"]?.data(using: .utf8)
				else {
					throw error
				}
				
				self = .name3(
					try decoder.decode((String).self, from: _0),
					try decoder.decode((String).self, from: _1)
				)
		default:
			throw error
		}
	}
	func encode(to encoder: Encoder) throws {
		var container = encoder.singleValueContainer()
		let encoder = JSONEncoder()
		switch self {
			case let .value(_0, _1):
				try container.encode([
					".value(num:name:)": [
						"num": String(data: try encoder.encode(_0), encoding: .utf8),
						"name": String(data: try encoder.encode(_1), encoding: .utf8)
					]
				])
			case let .name0(_0):
				try container.encode([
					".name0(_:)": [
						"$0": String(data: try encoder.encode(_0), encoding: .utf8)
					]
				])
			case let .name1(_0, _1):
				try container.encode([
					".name1(_:last:)": [
						"$0": String(data: try encoder.encode(_0), encoding: .utf8),
						"last": String(data: try encoder.encode(_1), encoding: .utf8)
					]
				])
			case let .name2(_0, _1):
				try container.encode([
					".name2(first:_:)": [
						"first": String(data: try encoder.encode(_0), encoding: .utf8),
						"$1": String(data: try encoder.encode(_1), encoding: .utf8)
					]
				])
			case let .name3(_0, _1):
				try container.encode([
					".name3(_:_:)": [
						"first": String(data: try encoder.encode(_0), encoding: .utf8),
						"last": String(data: try encoder.encode(_1), encoding: .utf8)
					]
				])
		}
	}
}

限制

  1. 由于HappyCodable使用扩展文件来生成Codable的函数,因此它不能用于私有模型类型,也不能用于在函数中定义的模型。

    func getNumber() {
    	struct Package: Codable {
    		let result: Int
    	}
    }
    
  2. HappyCodable需要一个init()来为模型创建一个默认对象(HappyCodableEnum不需要),然后使用Codable更改属性,因此它要求编码属性可变。因此,它不能用于只读模型,例如,上面的Package结构体。

问答

  1. 为什么要创建HappyCodable,为什么不用HandyJSON呢?

    我的项目之前使用的是HandyJSON,但由于它是基于Swift的低级数据结构,并且Swift结构变动后,HandyJSON出现了问题,所以我写了HappyCodable。

    也许有人会说:更新HandyJSON就可以了,但你不能保证在Swift数据结构再次改变后HandyJSON不会停止更新,或者你的APP不会突然停止开发,然后你的用户在更新iOS后不能再用它了,对吧?

    对于迁移到HappyCodable,API主要参考HandyJSON,我实际上可以写一份关于此的指南(可能只有100字左右)。

  2. 我的项目使用基于Codable的另一个框架(如WCDB.swift),这是否可行?

    我测试了WCDB.swift,如果你手动实现了CodingKeys,HappyCodable将不会生成CodingKeys。你还可以在HappyCodable完成后扩展模型的CodingKeys以实现WCDB.swift的协议;这要简单得多。

    extension LevelInfo.CodingKeys: WCDBSwift.CodingTableKey {
    	typealias Root = LevelInfo
    	static let objectRelationalMapping = TableBinding(Self.self)
    	static var tableConstraintBindings: [TableConstraintBinding.Name: TableConstraintBinding]? {
    		return [
    			"MultiPrimaryConstraint": MultiPrimaryBinding(indexesBy: [subjectId, id])
    		]
    	}
    }
    
  3. 您的框架存在许多限制。您可以选择使用私有属性,同时也需要使属性可变

    因为Swift自己生成的代码在编译时直接插入定义中,然后如果其他基于Codable的库的协议在同一文件中编写,Swift要求您在定义中实现Codable的方法。然后HappyCodable在扩展中实现init(from decoder: Decoder),Swift将不会在模型扩展中使用init(from decoder: Decoder)...简而言之,经过多次测试,我最终选择了这个麻烦的解决方案,在init(from decoder: Decoder)中调用另一个方法来分配属性值,因此属性必须可变,对于像WCDB.swift这样需要数据映射的情况,属性也必须可变