├── .gitignore
├── Tests
├── Migration
│ ├── .gitignore
│ ├── Package.swift
│ └── Sources
│ │ └── main.swift
├── HappyCodableTests
│ ├── TestObjects
│ │ ├── DynamicDefault.swift
│ │ ├── HappyCodable.swift
│ │ ├── CodingKeysExist.swift
│ │ ├── Uncoding.swift
│ │ ├── WithAttribute.swift
│ │ ├── DataStrategy.swift
│ │ ├── NonConformingFloatStrategy.swift
│ │ ├── DateStrategy.swift
│ │ ├── Struct.swift
│ │ ├── ForKeyedDecodingContainer.swift
│ │ └── Class.swift
│ ├── CodingKeysExistTests.swift
│ ├── UncodingTest.swift
│ ├── Info.plist
│ ├── DynamicDefaultTest.swift
│ ├── ArrayTest.swift
│ ├── ArrayNullTest.swift
│ ├── DataStrategyTest.swift
│ ├── AttributesTest.swift
│ ├── TypeMismatchTests.swift
│ ├── NonConformingFloatStrategyTest.swift
│ ├── DateStrategyTest.swift
│ ├── KeyedDecodingContainerTests.swift
│ └── CommonTests.swift
└── HappyCodablePluginTests
│ └── HappyCodableTests.swift
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Sources
├── HappyCodableShared
│ └── Warnings.swift
├── HappyCodable
│ ├── types
│ │ ├── DecodeError.swift
│ │ ├── Datable.swift
│ │ ├── AnyCodable.swift
│ │ └── ThreadLocalStorage.swift
│ ├── extension
│ │ ├── Array+Extension.swift
│ │ ├── String+CodingKey.swift
│ │ ├── Encodable.swift
│ │ ├── Decodable.swift
│ │ ├── KeyedEncodingContainer.swift
│ │ └── KeyedDecodingContainer.swift
│ └── interface
│ │ ├── HappyCodable.swift
│ │ ├── HappyCodable+Helper.swift
│ │ └── HappyCodable+Macro.swift
├── HappyCodablePlugin
│ ├── extension
│ │ ├── LabeledExprListSyntax.swift
│ │ └── SyntaxStringInterpolation.swift
│ ├── Plugin.swift
│ ├── types
│ │ ├── SimpleDiagnosticMessage.swift
│ │ ├── DeclSyntaxProtocolHelper.swift
│ │ └── CodableItem.swift
│ └── Macros.swift
└── HappyCodable.podspec
├── HappyCodable.podspec
├── Package@swift-5.9.swift
├── Package@swift-5.swift
├── Plugins
└── Lint
│ ├── LintPlugin.swift
│ ├── CommonBuildToolPlugin.swift
│ └── swiftlint.yaml
├── Package.swift
├── README.cn.md
├── README.md
└── LICENSE
/.gitignore:
--------------------------------------------------------------------------------
1 | Pods/
2 | *.xcuserdatad
3 | .DS_Store
4 | /.build
5 | /Packages
6 | xcuserdata/
7 | Package.resolved
8 | .swiftpm
9 | .build
--------------------------------------------------------------------------------
/Tests/Migration/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/HappyCodableShared/Warnings.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Warnings.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | public enum Warnings: String {
10 | case noInitializer
11 | case all
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/types/DecodeError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DecodeError.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | public enum DecodeError: String, Swift.Error {
10 | case inputEmpty
11 | case invalidDesignatedPath
12 | case typeError
13 | case datableFail
14 | case unsupportedDecoder
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/HappyCodablePlugin/extension/LabeledExprListSyntax.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LabeledExprListSyntax.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import SwiftSyntax
8 |
9 | extension LabeledExprListSyntax {
10 | subscript(label: String) -> ExprSyntax? {
11 | self.first {
12 | $0.label?.text == label
13 | }?.expression
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/extension/Array+Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Array+Extension.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | extension Array {
10 | @discardableResult
11 | @inlinable
12 | mutating func removeFirstIfHas() -> Element? {
13 | if self.isEmpty {
14 | return nil
15 | } else {
16 | return removeFirst()
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/DynamicDefault.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DynamicDefault.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 | import SwiftData
10 |
11 | @HappyCodable(disableWarnings: [.noInitializer])
12 | struct TestStruct_dynamicDefault: HappyCodable {
13 | var intDynamic: Int = Int.random(in: 0...100)
14 | var intStatic: Int = 1
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/HappyCodablePlugin/Plugin.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Plugin.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | #if canImport(SwiftCompilerPlugin)
8 | import SwiftCompilerPlugin
9 | import SwiftSyntaxMacros
10 |
11 | @main
12 | struct MyPlugin: CompilerPlugin {
13 | let providingMacros: [Macro.Type] = [
14 | AttributePlaceholderMacro.self,
15 | HappyCodableMemberMacro.self,
16 | ]
17 | }
18 | #endif
19 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/extension/String+CodingKey.swift:
--------------------------------------------------------------------------------
1 | //
2 | // String+CodingKey.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | extension String: CodingKey {
10 | public init?(stringValue: String) {
11 | self = stringValue
12 | }
13 |
14 | @inlinable
15 | @inline(__always)
16 | public var stringValue: String { self }
17 |
18 | public init?(intValue: Int) { return nil }
19 | public var intValue: Int? { nil }
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/HappyCodablePlugin/extension/SyntaxStringInterpolation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SyntaxStringInterpolation.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import SwiftSyntaxBuilder
9 |
10 | extension SyntaxStringInterpolation {
11 | mutating func appendInterpolation(raw value: T, enable: Bool) where T: CustomStringConvertible, T: TextOutputStreamable {
12 | if enable {
13 | appendLiteral("\(value)")
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/HappyCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HappyCodable.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | extension HappyEncodable {
11 | public static var encodeHelper: EncodeHelper {
12 | .init()
13 | }
14 | }
15 |
16 | extension HappyDecodable {
17 | public static var decodeHelper: DecodeHelper {
18 | #if DEBUG1
19 | return .init { error in
20 | print(error)
21 | }
22 | #else
23 | return .init()
24 | #endif
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/interface/HappyCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HappyCodable.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | // MARK: - Type
10 | // MARK: - HappyEncodable
11 | public protocol HappyEncodable: Encodable {
12 | mutating func willStartEncoding()
13 | static var encodeHelper: EncodeHelper { get }
14 | }
15 |
16 | public protocol HappyDecodable: Decodable {
17 | mutating func didFinishDecoding()
18 | static var decodeHelper: DecodeHelper { get }
19 | }
20 |
21 | public typealias HappyCodable = HappyEncodable & HappyDecodable
22 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/CodingKeysExist.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodingKeysExist.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | @HappyCodable
11 | struct TestStruct_codingKeysExist: HappyCodable, Equatable {
12 | var int: Int = 0
13 | var string: String = ""
14 | var bool: Bool = false
15 | enum CodingKeys: String, CodingKey {
16 | case int = "int_alter"
17 | case string = "string_alter"
18 | case bool
19 | }
20 |
21 | init(int: Int, string: String, bool: Bool) {
22 | self.int = int
23 | self.string = string
24 | self.bool = bool
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/HappyCodable.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 |
3 | spec.name = "HappyCodable"
4 | spec.version = "0.0.1"
5 | spec.summary = "快乐使用Codable"
6 | spec.homepage = "http://apple.com"
7 | spec.license = { :"type" => "Copyright", :"text" => " Copyright 2020 mikun \n"}
8 | spec.author = { "mikun" => "v.v1958@qq.com" }
9 | spec.source = { :git => './', :tag => "0.0.1" }
10 |
11 | spec.swift_version = '5.0'
12 |
13 | spec.ios.deployment_target = "9.0"
14 | spec.osx.deployment_target = "10.14"
15 | spec.watchos.deployment_target = "2.0"
16 | spec.tvos.deployment_target = "9.0"
17 | spec.source_files = "HappyCodable/*/*.swift"
18 | end
--------------------------------------------------------------------------------
/Sources/HappyCodablePlugin/types/SimpleDiagnosticMessage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SimpleDiagnosticMessage.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import SwiftDiagnostics
8 | import SwiftSyntax
9 |
10 | struct SimpleDiagnosticMessage: DiagnosticMessage, Error {
11 | let message: String
12 | let diagnosticID: MessageID
13 | let severity: DiagnosticSeverity
14 | }
15 |
16 | extension SimpleDiagnosticMessage: FixItMessage {
17 | var fixItID: MessageID { diagnosticID }
18 | }
19 |
20 | enum CustomError: Error, CustomStringConvertible {
21 | case message(String)
22 |
23 | var description: String {
24 | switch self {
25 | case .message(let text):
26 | return text
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/Uncoding.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Uncoding.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | @HappyCodable
11 | struct TestStruct_uncoding: HappyCodable {
12 | @Uncoding
13 | var uncoing: NotCodable = NotCodable(int: Self.fakeData_int)
14 |
15 | @Uncoding
16 | var uncoingOptional: NotCodable?
17 |
18 | static let fakeData_int: Int = Int.random(in: 0...1000000)
19 |
20 | struct NotCodable: Equatable {
21 | let int: Int
22 | }
23 |
24 | init(uncoing: NotCodable = NotCodable(int: Self.fakeData_int), uncoingOptional: NotCodable? = nil) {
25 | self.uncoing = uncoing
26 | self.uncoingOptional = uncoingOptional
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/Migration/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.7.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 | let package = Package(
6 | name: "Migration",
7 | platforms: [
8 | .macOS("13")
9 | ],
10 | targets: [
11 | // Targets are the basic building blocks of a package, defining a module or a test suite.
12 | // Targets can depend on other targets in this package and products from dependencies.
13 | .executableTarget(
14 | name: "Migration",
15 | path: "Sources",
16 | swiftSettings: [
17 | .unsafeFlags(["-enable-bare-slash-regex"])
18 | ]
19 | )
20 | ]
21 | )
22 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/WithAttribute.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WithAttribute.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | @HappyCodable
11 | struct TestStruct_withAttribute: HappyCodable {
12 | @AlterCodingKeys("codingKey1", "codingKey2", "🍉")
13 | var codingKeys: Int = 0
14 |
15 | var optional_allow: Int?
16 |
17 | var optional_notAllow: Int?
18 |
19 | @Uncoding
20 | var uncoding: Int = 0
21 |
22 | init(codingKeys: Int = 0, optional_allow: Int? = nil, optional_notAllow: Int? = nil, uncoding: Int = 0) {
23 | self.codingKeys = codingKeys
24 | self.optional_allow = optional_allow
25 | self.optional_notAllow = optional_notAllow
26 | self.uncoding = uncoding
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/CodingKeysExistTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodingKeysExistTests.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | class CodingKeysExistTests: XCTestCase {
11 | func test() {
12 | let fakeData_int = Int.random(in: 0...1000)
13 | let fakeData_bool = Bool.random()
14 | let fakeData_string = "\(fakeData_int)\(fakeData_bool)"
15 | let object = TestStruct_codingKeysExist(int: fakeData_int, string: fakeData_string, bool: fakeData_bool)
16 | assert(try object.toJSON() as NSDictionary == [
17 | "int_alter": fakeData_int,
18 | "string_alter": fakeData_string,
19 | "bool": fakeData_bool
20 | ])
21 | assert(try TestStruct_codingKeysExist.decode(from: try object.toJSON()) == object)
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/UncodingTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UncodingTest.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | // swiftlint:disable empty_collection_literal
11 |
12 | class UncodingTest: XCTestCase {
13 | func test() throws {
14 | let fakeData_Int = Int.random(in: 0...1000000000)
15 | let json: NSDictionary = [
16 | "uncoing": fakeData_Int,
17 | "uncoingOptional": [
18 | "int": fakeData_Int
19 | ]
20 | ]
21 | let object = try TestStruct_uncoding.decode(from: json)
22 | assert(object.uncoingOptional == nil)
23 |
24 | assert(object.uncoing == TestStruct_uncoding().uncoing)
25 |
26 | assert((try object.toJSON() as NSDictionary) == [:])
27 | }
28 | }
29 |
30 | // swiftlint:enable empty_collection_literal
31 |
--------------------------------------------------------------------------------
/HappyCodable.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 |
3 | spec.name = "HappyCodable"
4 | spec.version = "3.0.2"
5 | spec.summary = "快乐使用Codable"
6 | spec.homepage = "https://github.com/miku1958/HappyCodable"
7 | spec.license = "Mozilla"
8 | spec.author = { "mikun" => "v.v1958@qq.com" }
9 |
10 | spec.source = {
11 | :git => "https://github.com/miku1958/HappyCodable.git",
12 | :tag => spec.version
13 | }
14 | spec.source_files = "Sources/HappyCodable/**/*.swift"
15 |
16 | spec.requires_arc = true
17 | spec.swift_version = "5.1"
18 |
19 | spec.ios.deployment_target = '8.0'
20 | spec.osx.deployment_target = "10.10"
21 | # spec.watchos.deployment_target = "2.0"
22 | # spec.tvos.deployment_target = "9.0"
23 | end
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/types/Datable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Datable.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | public protocol Datable {
10 | var data: Data { get throws }
11 | }
12 |
13 | extension Data: Datable {
14 | public var data: Data { self }
15 | }
16 |
17 | extension [String: Any]: Datable {
18 | public var data: Data {
19 | get throws {
20 | try JSONSerialization.data(withJSONObject: self)
21 | }
22 | }
23 | }
24 |
25 | extension String: Datable {
26 | public var data: Data {
27 | get throws {
28 | guard let data = data(using: .utf8) else {
29 | throw DecodeError.datableFail
30 | }
31 | return data
32 | }
33 | }
34 | }
35 |
36 | extension NSDictionary: Datable {
37 | public var data: Data {
38 | get throws {
39 | try JSONSerialization.data(withJSONObject: self)
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/DynamicDefaultTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DynamicDefaultTest.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 | // swiftlint:disable identical_operands
10 | class DynamicDefaultTest: XCTestCase {
11 |
12 | func test() {
13 | let fakeData_Int = Int.random(in: 0...1000000000)
14 | let json: NSDictionary = [
15 | "intDynamic": fakeData_Int,
16 | "intStatic": fakeData_Int
17 | ]
18 |
19 | assert(try TestStruct_dynamicDefault.decode(from: json).toJSON() as NSDictionary == json)
20 |
21 | assert((try TestStruct_dynamicDefault.decode(from: [:])).intDynamic != (try TestStruct_dynamicDefault.decode(from: [:])).intDynamic)
22 |
23 | assert((try TestStruct_dynamicDefault.decode(from: [:])).intStatic == (try TestStruct_dynamicDefault.decode(from: [:])).intStatic)
24 | }
25 | }
26 | // swiftlint:enable identical_operands
27 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/interface/HappyCodable+Helper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HappyCodable+Helper.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | public struct DecodeHelper {
10 | public let errorsReporter: (([Error]) -> Void)?
11 |
12 | /// initializer
13 | /// - Parameter errorsCatcher: Used to catch errors in Decodeing
14 | public init(errorsReporter: (([Error]) -> Void)? = nil) {
15 | self.errorsReporter = errorsReporter
16 | }
17 | }
18 |
19 | public struct EncodeHelper {
20 | public let errorsReporter: (([Error]) -> Void)?
21 |
22 | /// initializer
23 | /// - Parameter errorsCatcher: Used to catch errors in Encodeing
24 | public init(errorsReporter: (([Error]) -> Void)? = nil) {
25 | self.errorsReporter = errorsReporter
26 | }
27 | }
28 |
29 | extension HappyEncodable {
30 | public func willStartEncoding() { }
31 | }
32 |
33 | extension HappyDecodable {
34 | public func didFinishDecoding() { }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/ArrayTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArrayTest.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | class ArrayTest: XCTestCase {
11 | @HappyCodable(disableWarnings: [.noInitializer])
12 | struct Model: HappyCodable {
13 | var value: Int = 0
14 | }
15 |
16 | func test() throws {
17 | let json: NSDictionary = [
18 | "target": [
19 | "value": 12
20 | ],
21 | "code": 0
22 | ]
23 | let jsonData = try JSONSerialization.data(withJSONObject: json)
24 | let jsonStr = String(data: jsonData, encoding: .utf8)
25 |
26 | assert(try Model.decode(from: json, designatedPath: "target").value == 12)
27 | assert(try Model.decode(from: json as? [String: Any], designatedPath: "target").value == 12)
28 | assert(try Model.decode(from: jsonData, designatedPath: "target").value == 12)
29 | assert(try Model.decode(from: jsonStr, designatedPath: "target").value == 12)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/HappyCodablePlugin/types/DeclSyntaxProtocolHelper.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DeclSyntaxProtocolHelper.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import SwiftSyntax
8 |
9 | protocol DeclSyntaxProtocolHelper {
10 | var inheritanceClause: InheritanceClauseSyntax? { get }
11 | }
12 |
13 | extension DeclSyntaxProtocolHelper {
14 | var inheritedTypes: Set? {
15 | guard let types = inheritanceClause?
16 | .inheritedTypes
17 | .compactMap({
18 | $0.type.as(IdentifierTypeSyntax.self)?.name.text
19 | })
20 | else {
21 | return []
22 | }
23 | return Set(types)
24 | }
25 | }
26 |
27 | extension StructDeclSyntax: DeclSyntaxProtocolHelper {
28 |
29 | }
30 |
31 | extension ClassDeclSyntax: DeclSyntaxProtocolHelper {
32 |
33 | }
34 |
35 | // Swift complier can handle enum correctly, no need to get involved
36 | // extension EnumDeclSyntax: DeclSyntaxProtocolHelper { }
37 |
38 | extension ActorDeclSyntax: DeclSyntaxProtocolHelper {
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/ArrayNullTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ArrayNullTest.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | @testable import HappyCodable
10 | import XCTest
11 |
12 | class ArrayNullTest: XCTestCase {
13 | @HappyCodable(disableWarnings: [.noInitializer])
14 | final class HappyField1: HappyCodable {
15 | @ElementNullable
16 | var data: [String] = []
17 | }
18 | func test() throws {
19 | let json =
20 | """
21 | {
22 | "data": ["a", "b", null]
23 | }
24 | """
25 | let data: [String] = try HappyField1.decode(from: json).data
26 | XCTAssertEqual(data, ["a", "b"])
27 | }
28 |
29 | @HappyCodable(disableWarnings: [.noInitializer])
30 | final class HappyField2: HappyCodable {
31 | @AlterCodingKeys("data1")
32 | @ElementNullable
33 | var data: [String] = []
34 | }
35 | func testWithAlter() throws {
36 | let json =
37 | """
38 | {
39 | "data1": ["a", "b", null]
40 | }
41 | """
42 | let data: [String] = try HappyField2.decode(from: json).data
43 | XCTAssertEqual(data, ["a", "b"])
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/types/AnyCodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AnyCodable.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | enum AnyDecodableStorage {
10 | @ThreadLocalStorage
11 | static var designatedPaths: [String] = []
12 | }
13 |
14 | indirect enum AnyDecodable: Decodable {
15 | case anyDecodable(AnyDecodable)
16 | case value(R)
17 |
18 | init(from decoder: Decoder) throws {
19 | if let key = AnyDecodableStorage.designatedPaths.removeFirstIfHas() {
20 | let container = try decoder.container(keyedBy: Swift.String.self)
21 | if container.allKeys.contains(key) {
22 | self = try container.decode(AnyDecodable.self, forKey: key)
23 | } else {
24 | throw DecodeError.invalidDesignatedPath
25 | }
26 | } else if let value = try? decoder.singleValueContainer().decode(R.self) {
27 | self = .value(value)
28 | } else {
29 | throw DecodeError.invalidDesignatedPath
30 | }
31 | }
32 |
33 | func getSingleValue() -> R {
34 | switch self {
35 | case .anyDecodable(let anyDecodable):
36 | return anyDecodable.getSingleValue()
37 | case .value(let value):
38 | return value
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/DataStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataStrategy.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | @HappyCodable
11 | struct TestStruct_dataStrategy: HappyCodable, Equatable {
12 | @DataStrategy(decode: .deferredToData, encode: .deferredToData)
13 | var data_deferredToData: Data = Self.defaultData
14 |
15 | @DataStrategy(decode: .base64, encode: .base64)
16 | var data_base64: Data = Self.defaultData
17 |
18 | @DataStrategy(decode: .custom {
19 | _ = try $0.singleValueContainer().decode(Data.self)
20 | return Self.customData
21 | }, encode: .custom { _, encoder in
22 | try Self.customData.encode(to: encoder)
23 | })
24 | var data_custom: Data = Self.defaultData
25 | // swiftlint:disable force_unwrapping
26 | static let customData = "\(Int.random(in: 0...1000))".data(using: .utf8)!
27 | static let defaultData = "\(Int.random(in: 0...1000))".data(using: .utf8)!
28 | // swiftlint:enable force_unwrapping
29 |
30 | init(data_deferredToData: Data, data_base64: Data, data_custom: Data) {
31 | self.data_deferredToData = data_deferredToData
32 | self.data_base64 = data_base64
33 | self.data_custom = data_custom
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/NonConformingFloatStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatStrategy.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | @HappyCodable
11 | struct TestStruct_floatStrategy: HappyCodable {
12 | @FloatStrategy(
13 | decode: .convertFromString(positiveInfinity: Data.positiveInfinity, negativeInfinity: Data.negativeInfinity, nan: Data.nan),
14 | encode: .convertToString(positiveInfinity: Data.positiveInfinity, negativeInfinity: Data.negativeInfinity, nan: Data.nan)
15 | ) var doubleConvertFromString: Double = Data.fakeData_double
16 |
17 | @FloatStrategy(decode: .throw, encode: .throw)
18 | var doubleThrow: Double = Data.fakeData_double
19 |
20 | enum Data {
21 | static let fakeData_double = Double(Int.random(in: 0...1000000))
22 | static let positiveInfinity = "positiveInfinity\(Int.random(in: 0...100))"
23 | static let negativeInfinity = "positiveInfinity\(Int.random(in: 0...100))"
24 | static let nan = "positiveInfinity\(Int.random(in: 0...100))"
25 | }
26 |
27 | init(doubleConvertFromString: Double, doubleThrow: Double) {
28 | self.doubleConvertFromString = doubleConvertFromString
29 | self.doubleThrow = doubleThrow
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Tests/HappyCodablePluginTests/HappyCodableTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HappyCodableTests.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodablePlugin
9 | import SwiftSyntax
10 | import SwiftSyntaxBuilder
11 | import SwiftSyntaxMacroExpansion
12 | import SwiftSyntaxMacros
13 | import XCTest
14 |
15 | enum TestEnum: Codable {
16 | case value1(arg1: Int, arg2: Int)
17 | case value2(Int, arg2: Int)
18 | case value3
19 | }
20 |
21 | final class HappyCodableTests: XCTestCase {
22 | func testStringify() {
23 | let testMacros: [String: Macro.Type] = [
24 | "HappyCodable": HappyCodableMemberMacro.self,
25 | ]
26 | let sf: SourceFileSyntax =
27 | #"""
28 | @HappyCodable(disableWarnings: [.noInitializer])
29 | struct TestStruct_dynamicDefault: HappyCodable {
30 | var intDynamic: Int = Int.random(in: 0...100)
31 | var intStatic: Int = 1
32 | }
33 | """#
34 | let context = BasicMacroExpansionContext(
35 | sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")]
36 | )
37 | let transformedSF = sf.expand(macros: testMacros, in: context)
38 | XCTAssertEqual(
39 | transformedSF.description,
40 | #"""
41 | let a = (x + y, "x + y")
42 | let b = ("Hello, \(name)", #""Hello, \(name)""#)
43 | """#
44 | )
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/types/ThreadLocalStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ThreadLocalStorage.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | // modified from https://github.com/nvzqz/Threadly
8 | import Foundation
9 |
10 | @propertyWrapper
11 | class ThreadLocalStorage {
12 | private final class Box {
13 | /// The boxed value.
14 | var value: Value
15 |
16 | /// Creates an instance that boxes `value`.
17 | init(_ value: Value) {
18 | self.value = value
19 | }
20 | }
21 |
22 | private var raw: pthread_key_t
23 |
24 | private var box: Box {
25 | guard let pointer = pthread_getspecific(raw) else {
26 | let box = Box(initialValue)
27 | pthread_setspecific(raw, Unmanaged.passRetained(box).toOpaque())
28 | return box
29 | }
30 | return Unmanaged>.fromOpaque(pointer).takeUnretainedValue()
31 | }
32 |
33 | let initialValue: V
34 | var wrappedValue: V {
35 | get {
36 | box.value
37 | }
38 | set {
39 | box.value = newValue
40 | }
41 | }
42 |
43 | init(wrappedValue: V) {
44 | raw = pthread_key_t()
45 | pthread_key_create(&raw) {
46 | // Cast required because argument is optional on some
47 | // platforms (Linux) but not on others (macOS)
48 | guard let rawPointer = ($0 as UnsafeMutableRawPointer?) else {
49 | return
50 | }
51 | Unmanaged.fromOpaque(rawPointer).release()
52 | }
53 | initialValue = wrappedValue
54 | }
55 |
56 | deinit {
57 | pthread_key_delete(raw)
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/DateStrategy.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateStrategy.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | @HappyCodable(disableWarnings: [.noInitializer])
11 | struct TestStruct_dateStrategy: HappyCodable, Equatable {
12 | @DateStrategy(decode: .deferredToDate, encode: .deferredToDate)
13 | var date_deferredToDate: Date = Self.defaultDate
14 |
15 | @DateStrategy(decode: .secondsSince1970, encode: .secondsSince1970)
16 | var date_secondsSince1970: Date = Self.defaultDate
17 |
18 | @DateStrategy(decode: .millisecondsSince1970, encode: .millisecondsSince1970)
19 | var date_millisecondsSince1970: Date = Self.defaultDate
20 |
21 | @DateStrategy(decode: .iso8601, encode: .iso8601)
22 | var date_iso8601: Date = Self.defaultDate
23 |
24 | @DateStrategy(decode: .formatted(Self.dateFormater), encode: .formatted(Self.dateFormater))
25 | var date_formatted: Date = Self.defaultDate
26 |
27 | @DateStrategy(decode: .custom { _ in
28 | return Self.customDate
29 | }, encode: .custom { _, encoder in
30 | try Self.customDate.encode(to: encoder)
31 | })
32 | var date_custom: Date = Self.defaultDate
33 |
34 | static var dateFormater: DateFormatter = {
35 | let formatter = DateFormatter()
36 | formatter.dateFormat = "EEE MMM dd HH:mm:ss Z yyyy"
37 | return formatter
38 | }()
39 |
40 | static let customDate = Date()
41 | static let defaultDate = Date(timeIntervalSince1970: TimeInterval.random(in: 0...100000000000))
42 | }
43 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/DataStrategyTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataStrategyTest.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | class DataCodingStrategyTest: XCTestCase {
11 | func test() throws {
12 | let fakeData_string = "\(Int.random(in: 0...1000000000))"
13 | // swiftlint:disable force_unwrapping
14 | let fakeData_data = fakeData_string.data(using: .utf8)!
15 | // swiftlint:enable force_unwrapping
16 |
17 | let json: NSMutableDictionary = [
18 |
19 | "data_deferredToData": Array(fakeData_data),
20 |
21 | "data_base64": fakeData_data.base64EncodedString(),
22 |
23 | "data_custom": Array(fakeData_data)
24 | ]
25 | let object = try TestStruct_dataStrategy.decode(from: json)
26 |
27 | assert(try object.toJSON() as NSDictionary != json)
28 | json["data_custom"] = Array(TestStruct_dataStrategy.customData)
29 | assert(try object.toJSON() as NSDictionary == json)
30 | }
31 | func testNull() throws {
32 | let json: NSMutableDictionary = [
33 |
34 | "data_deferredToDate": NSNull(),
35 |
36 | "data_base64": NSNull(),
37 |
38 | "data_custom": NSNull()
39 | ]
40 | let object = try TestStruct_dataStrategy.decode(from: json)
41 |
42 | assert(try object.toJSON() as NSDictionary != json)
43 |
44 | assert(object.data_deferredToData == TestStruct_dataStrategy.defaultData)
45 | assert(object.data_base64 == TestStruct_dataStrategy.defaultData)
46 | assert(object.data_custom == TestStruct_dataStrategy.defaultData)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/extension/Encodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Encodable.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | public enum EncodeError: String, Swift.Error {
10 | case stringEncodingFail
11 | case toJSONTypeFail
12 | }
13 |
14 | extension HappyEncodable {
15 | func getEncoder() -> JSONEncoder {
16 | let encoder = JSONEncoder()
17 | encoder.dataEncodingStrategy = .deferredToData
18 | encoder.dateEncodingStrategy = .deferredToDate
19 | return encoder
20 | }
21 | public func toJSON() throws -> [String: Any] {
22 | let encoder = getEncoder()
23 | let data = try encoder.encode(self)
24 | guard let object = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
25 | throw EncodeError.toJSONTypeFail
26 | }
27 | return object
28 | }
29 |
30 | public func toJSONString(prettyPrint: Bool = false) throws -> String {
31 | let encoder = getEncoder()
32 | if prettyPrint {
33 | encoder.outputFormatting = .prettyPrinted
34 | }
35 |
36 | let data = try encoder.encode(self)
37 | guard let string = String(data: data, encoding: .utf8) else {
38 | throw EncodeError.stringEncodingFail
39 | }
40 | return string
41 | }
42 | }
43 |
44 | extension Array where Element: HappyEncodable {
45 | public func toJSON() throws -> [[String: Any]] {
46 | try self.map {
47 | try $0.toJSON()
48 | }
49 | }
50 |
51 | public func toJSONString(prettyPrint: Bool = false) throws -> String {
52 | return """
53 | [
54 | \(try self.map {
55 | try $0.toJSONString(prettyPrint: prettyPrint)
56 | }
57 | .joined(separator: ",\(prettyPrint ? "\n\t" : "")"))
58 | ]
59 | """
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Package@swift-5.9.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import CompilerPluginSupport
5 | import PackageDescription
6 |
7 | let package = Package(
8 | name: "HappyCodable",
9 | platforms: [
10 | .iOS("8.0"),
11 | .macOS("10.15")
12 | ],
13 | products: [
14 | .library(
15 | name: "HappyCodable",
16 | targets: [
17 | "HappyCodable",
18 | "HappyCodableShared"
19 | ]
20 | ),
21 | ],
22 | dependencies: [
23 | .package(
24 | url: "https://github.com/apple/swift-syntax.git",
25 | from: "509.0.0"
26 | ),
27 | ],
28 | targets: [
29 | .target(
30 | name: "HappyCodableShared"
31 | ),
32 | .macro(
33 | name: "HappyCodablePlugin",
34 | dependencies: [
35 | "HappyCodableShared",
36 | .product(name: "SwiftSyntax", package: "swift-syntax"),
37 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
38 | .product(name: "SwiftOperators", package: "swift-syntax"),
39 | .product(name: "SwiftParser", package: "swift-syntax"),
40 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
41 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
42 | ],
43 | swiftSettings: [
44 | .enableUpcomingFeature("BareSlashRegexLiterals")
45 | ]
46 | ),
47 | .target(
48 | name: "HappyCodable",
49 | dependencies: [
50 | "HappyCodableShared",
51 | "HappyCodablePlugin",
52 | ]
53 | ),
54 | .testTarget(
55 | name: "HappyCodableTests",
56 | dependencies: [
57 | "HappyCodable",
58 | "HappyCodableShared",
59 | ]
60 | ),
61 | .testTarget(
62 | name: "HappyCodablePluginTests",
63 | dependencies: [
64 | "HappyCodablePlugin",
65 | "HappyCodableShared",
66 | .product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
67 | ]
68 | ),
69 | ]
70 | )
71 |
--------------------------------------------------------------------------------
/Package@swift-5.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import CompilerPluginSupport
5 | import PackageDescription
6 |
7 | let package = Package(
8 | name: "HappyCodable",
9 | platforms: [
10 | .iOS("8.0"),
11 | .macOS("10.15")
12 | ],
13 | products: [
14 | .library(
15 | name: "HappyCodable",
16 | targets: [
17 | "HappyCodable",
18 | "HappyCodableShared"
19 | ]
20 | ),
21 | ],
22 | dependencies: [
23 | .package(
24 | url: "https://github.com/apple/swift-syntax.git",
25 | from: "509.0.0"
26 | ),
27 | ],
28 | targets: [
29 | .target(
30 | name: "HappyCodableShared"
31 | ),
32 | .macro(
33 | name: "HappyCodablePlugin",
34 | dependencies: [
35 | "HappyCodableShared",
36 | .product(name: "SwiftSyntax", package: "swift-syntax"),
37 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
38 | .product(name: "SwiftOperators", package: "swift-syntax"),
39 | .product(name: "SwiftParser", package: "swift-syntax"),
40 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
41 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
42 | ],
43 | swiftSettings: [
44 | .enableUpcomingFeature("BareSlashRegexLiterals")
45 | ]
46 | ),
47 | .target(
48 | name: "HappyCodable",
49 | dependencies: [
50 | "HappyCodableShared",
51 | "HappyCodablePlugin",
52 | ]
53 | ),
54 | .testTarget(
55 | name: "HappyCodableTests",
56 | dependencies: [
57 | "HappyCodable",
58 | "HappyCodableShared",
59 | ]
60 | ),
61 | .testTarget(
62 | name: "HappyCodablePluginTests",
63 | dependencies: [
64 | "HappyCodablePlugin",
65 | "HappyCodableShared",
66 | .product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
67 | ]
68 | ),
69 | ]
70 | )
71 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/extension/Decodable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Decodable.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | extension Decodable {
10 | /// Finds the internal JSON field in `data` as the `designatedPath` specified, and converts it to a Model
11 | /// `designatedPath` is a string like `result.data.orderInfo`, which each element split by `.` represents key of each layer
12 | public static func decode(from datable: Datable?, designatedPath: String? = nil, allowsJSON5: Bool = false) throws -> Self {
13 | guard let datable else {
14 | throw DecodeError.inputEmpty
15 | }
16 | let data = try datable.data
17 |
18 | let decoder = JSONDecoder()
19 | decoder.dataDecodingStrategy = .deferredToData
20 | decoder.dateDecodingStrategy = .deferredToDate
21 | if #available(macOS 12.0, iOS 15.0, *) {
22 | decoder.allowsJSON5 = allowsJSON5
23 | }
24 | if let designatedPaths = designatedPath?.split(separator: ".") {
25 | AnyDecodableStorage.designatedPaths = designatedPaths.map(String.init)
26 | }
27 | let anyDecodable = try decoder.decode(AnyDecodable.self, from: data)
28 |
29 | return anyDecodable.getSingleValue()
30 | }
31 | }
32 |
33 | // MARK: - decode [[String: Any]]
34 | extension Array where Element: Decodable {
35 | /// deserialize model array from NSArray
36 | @inlinable
37 | @inline(__always)
38 | public static func decode(from array: NSArray?, allowsJSON5: Bool = false) throws -> [Element] {
39 | return try decode(from: array as? [Any], allowsJSON5: allowsJSON5)
40 | }
41 |
42 | /// deserializae model array from array
43 | @inlinable
44 | @inline(__always)
45 | public static func decode(from array: [Any]?, allowsJSON5: Bool = false) throws -> [Element] {
46 | guard let array = array as? [[String: Any]] else {
47 | throw DecodeError.typeError
48 | }
49 |
50 | return try array.compactMap {
51 | try Element.decode(from: $0, allowsJSON5: allowsJSON5)
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Plugins/Lint/LintPlugin.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import PackagePlugin
3 |
4 | @main
5 | struct LintPlugin: CommonBuildToolPlugin {
6 | func createBuildCommands(context: CommonPluginContext, targetDirectory: Path, inputFiles: [Path], name: String, runEverytime: Bool) throws -> [Command] {
7 |
8 | return [
9 | .buildCommand(
10 | displayName: "Running SwiftLint for \(name)",
11 | executable: try context.tool(named: "swiftlint").path,
12 | arguments: [
13 | "lint",
14 | "--config",
15 | configPath(directory: targetDirectory, fileName: "swiftlint.yaml", localFileName: ".swiftlint.yml"),
16 | "--cache-path",
17 | "\(context.pluginWorkDirectory.string)/SwiftLintCache",
18 | ] + inputFiles.map(\.string),
19 | inputFiles: runEverytime ? [] : inputFiles,
20 | outputFiles: []
21 | )
22 | ]
23 | }
24 |
25 | /// directory is a relative path
26 | private func configPath(directory: Path, fileName: String, localFileName: String) -> String {
27 | do {
28 | let configPath = directory.appending(localFileName).string
29 |
30 | if FileManager.default.fileExists(atPath: configPath) {
31 | return configPath
32 | }
33 | }
34 |
35 | // Swift Package Plugin doesn't support resource files.
36 | // To work around this, we directly find the config file.
37 |
38 | var url = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
39 |
40 | var testingPath: String {
41 | url.appendingPathComponent("HappyCodable/Plugins/Lint//\(fileName)").standardizedFileURL.path
42 | }
43 | while !url.pathComponents.isEmpty, !FileManager.default.fileExists(atPath: testingPath) {
44 | url.deleteLastPathComponent()
45 | }
46 |
47 | return testingPath
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/AttributesTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AttributesTest.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | func assert(_ condition: @autoclosure () throws -> Bool) {
11 | do {
12 | let result = try condition()
13 | XCTAssert(result)
14 | } catch {
15 | XCTFail("\(error)")
16 | }
17 | }
18 |
19 | class AttributesTest: XCTestCase {
20 | func test() {
21 | // MARK: - optional
22 | let fakeData_optional = Int.random(in: 0..<10000)
23 |
24 | // MARK: - codingKeys
25 | let fakeData_codingKeys = Int.random(in: 0..<10000)
26 |
27 | assert(try TestStruct_withAttribute.decode(from: ["codingKey1": fakeData_codingKeys]).codingKeys == fakeData_codingKeys)
28 | assert(try TestStruct_withAttribute.decode(from: ["codingKey2": fakeData_codingKeys]).codingKeys == fakeData_codingKeys)
29 | // emoji test, SourceKittenFramework does not use Unicode to parser swift files
30 | assert(try TestStruct_withAttribute.decode(from: ["🍉": fakeData_codingKeys]).codingKeys == fakeData_codingKeys)
31 |
32 | assert(try TestStruct_withAttribute.decode(from: ["codingKey": fakeData_codingKeys]).codingKeys != fakeData_codingKeys)
33 | assert(try TestStruct_withAttribute.decode(from: ["codingKey": fakeData_codingKeys]).codingKeys == TestStruct_withAttribute().codingKeys)
34 |
35 | // MARK: - uncoding
36 | let fakeData_uncoding = Int.random(in: 0..<10000)
37 | assert(try TestStruct_withAttribute.decode(from: ["uncoding": fakeData_uncoding]).codingKeys == TestStruct_withAttribute().uncoding)
38 | assert(try TestStruct_withAttribute.decode(from: ["uncoding": fakeData_uncoding]).codingKeys != fakeData_uncoding)
39 |
40 | // MARK: - encode
41 | let object = TestStruct_withAttribute(codingKeys: fakeData_codingKeys, optional_allow: fakeData_optional, optional_notAllow: fakeData_optional, uncoding: fakeData_uncoding)
42 | assert(try object.toJSON() as NSDictionary == [
43 | "codingKeys": fakeData_codingKeys,
44 | "optional_allow": fakeData_optional,
45 | "optional_notAllow": fakeData_optional
46 | ])
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TypeMismatchTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TypeMismatchTests.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | class TypeMismatchTests: XCTestCase {
11 | func testBaseDataType() {
12 | let fakeData_int = Int.random(in: 0...127)
13 | let fakeData_double: Double = Double(fakeData_int) / 100
14 | let fakeData_String: String = NSNumber(value: fakeData_double).stringValue
15 |
16 | let fakeData_bool = Bool.random()
17 |
18 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
19 | "Bool_2": fakeData_bool ? 1 : 0,
20 | ]).Bool == fakeData_bool)
21 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
22 | "Bool_2": fakeData_bool ? "1" : "0",
23 | ]).Bool == fakeData_bool)
24 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
25 | "Bool_2": fakeData_bool ? "true" : "false",
26 | ]).Bool == fakeData_bool)
27 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
28 | "String_2": fakeData_int,
29 | ]).String == "\(fakeData_int)")
30 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
31 | "String_2": fakeData_double,
32 | ]).String == fakeData_String)
33 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
34 | "String_2": fakeData_bool,
35 | ]).String == "\(fakeData_bool)")
36 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
37 | "String_2": UInt64.max,
38 | ]).String == "\(UInt64.max)")
39 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
40 | "Double_2": fakeData_String,
41 | ]).Double == fakeData_double)
42 |
43 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
44 | "Int_2": "\(fakeData_int)",
45 | ]).Int == fakeData_int)
46 |
47 | assert(try TestStruct_ForKeyedDecodingContainer.decode(from: [
48 | "UInt_2": "\(fakeData_int)",
49 | ]).UInt == fakeData_int)
50 | }
51 | func testBaseTypeWithoutPropertyWrapper() {
52 | assert(try TestStruct_TypeMismatch.decode(from: [
53 | "Int": Date().description,
54 | ]).Int == TestStruct_TypeMismatch().Int)
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Plugins/Lint/CommonBuildToolPlugin.swift:
--------------------------------------------------------------------------------
1 | import PackagePlugin
2 |
3 | #if canImport(XcodeProjectPlugin)
4 | import XcodeProjectPlugin
5 |
6 | extension XcodePluginContext: CommonPluginContext {}
7 |
8 | typealias CombinedBuildToolPlugin = BuildToolPlugin & XcodeBuildToolPlugin
9 |
10 | extension CommonBuildToolPlugin {
11 | func createBuildCommands(context: XcodePluginContext, target: XcodeTarget) throws -> [Command] {
12 | try createBuildCommands(
13 | context: context,
14 | targetDirectory: context.xcodeProject.directory,
15 | inputFiles: target.inputFiles
16 | .filter { $0.type == .source }
17 | .map(\.path),
18 | name: target.displayName,
19 | // Use empty array as input files for Xcode target since Xcode has a weird bug for App targets.
20 | // Xcode 14 beta 6 would complain entry point _main undefined when linking if input files are provided.
21 | // Try to set input files for new Xcode versions.
22 | runEverytime: true
23 | )
24 | }
25 | }
26 | #else
27 | typealias CombinedBuildToolPlugin = BuildToolPlugin
28 | #endif
29 |
30 | protocol CommonPluginContext {
31 | var pluginWorkDirectory: Path { get }
32 | func tool(named name: String) throws -> PluginContext.Tool
33 | }
34 |
35 | extension PluginContext: CommonPluginContext {}
36 |
37 | protocol CommonBuildToolPlugin: CombinedBuildToolPlugin {
38 | func createBuildCommands(context: CommonPluginContext, targetDirectory: Path, inputFiles: [Path], name: String, runEverytime: Bool) throws -> [Command]
39 | }
40 |
41 | extension CommonBuildToolPlugin {
42 | func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] {
43 | try createBuildCommands(
44 | context: context,
45 | targetDirectory: target.directory,
46 | inputFiles: (target as! SourceModuleTarget).sourceFiles
47 | .filter { $0.type == .source }
48 | .map(\.path),
49 | name: target.name,
50 | runEverytime: false
51 | )
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/NonConformingFloatStrategyTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatStrategyTest.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | class NonConformingFloatDecodingStrategyTest: XCTestCase {
11 |
12 | func test() {
13 | let fakeData_Double = Double(Int.random(in: 0...10000))
14 | var json: NSDictionary = [
15 | "doubleConvertFromString": fakeData_Double,
16 |
17 | "doubleThrow": fakeData_Double
18 | ]
19 |
20 | assert(try TestStruct_floatStrategy.decode(from: json).toJSON() as NSDictionary == json)
21 |
22 | do {
23 | let object = try TestStruct_floatStrategy.decode(from: [:])
24 | assert(object.doubleThrow == TestStruct_floatStrategy.Data.fakeData_double)
25 | assert(object.doubleConvertFromString == TestStruct_floatStrategy.Data.fakeData_double)
26 | } catch {
27 | assertionFailure()
28 | }
29 |
30 | json = [
31 | "doubleConvertFromString": TestStruct_floatStrategy.Data.nan
32 | ]
33 | // Double.nan 是不能对比的
34 | assert(try TestStruct_floatStrategy.decode(from: json).doubleConvertFromString.isNaN)
35 | assert(try TestStruct_floatStrategy.decode(from: json).toJSON() as NSDictionary == [
36 | "doubleConvertFromString": TestStruct_floatStrategy.Data.nan,
37 | "doubleThrow": TestStruct_floatStrategy.Data.fakeData_double
38 | ])
39 | json = [
40 | "doubleConvertFromString": TestStruct_floatStrategy.Data.positiveInfinity
41 | ]
42 |
43 | assert(try TestStruct_floatStrategy.decode(from: json).doubleConvertFromString == .infinity)
44 | assert(try TestStruct_floatStrategy.decode(from: json).toJSON() as NSDictionary == [
45 | "doubleConvertFromString": TestStruct_floatStrategy.Data.positiveInfinity,
46 | "doubleThrow": TestStruct_floatStrategy.Data.fakeData_double
47 | ])
48 |
49 | json = [
50 | "doubleConvertFromString": TestStruct_floatStrategy.Data.negativeInfinity
51 | ]
52 | assert(try TestStruct_floatStrategy.decode(from: json).doubleConvertFromString == -.infinity)
53 | assert(try TestStruct_floatStrategy.decode(from: json).toJSON() as NSDictionary == [
54 | "doubleConvertFromString": TestStruct_floatStrategy.Data.negativeInfinity,
55 | "doubleThrow": TestStruct_floatStrategy.Data.fakeData_double
56 | ])
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9.0
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import CompilerPluginSupport
5 | import PackageDescription
6 |
7 | let commonPlugins: [Target.PluginUsage] = [
8 | .plugin(name: "SwiftLint")
9 | ]
10 |
11 | let package = Package(
12 | name: "HappyCodable",
13 | platforms: [
14 | .iOS("8.0"),
15 | .macOS("10.15")
16 | ],
17 | products: [
18 | .library(
19 | name: "HappyCodable",
20 | targets: [
21 | "HappyCodable",
22 | "HappyCodableShared"
23 | ]
24 | ),
25 | ],
26 | dependencies: [
27 | .package(
28 | url: "https://github.com/apple/swift-syntax.git",
29 | from: "509.0.0"
30 | ),
31 | ],
32 | targets: [
33 | .target(
34 | name: "HappyCodableShared",
35 | plugins: commonPlugins
36 | ),
37 | .macro(
38 | name: "HappyCodablePlugin",
39 | dependencies: [
40 | "HappyCodableShared",
41 | .product(name: "SwiftSyntax", package: "swift-syntax"),
42 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
43 | .product(name: "SwiftOperators", package: "swift-syntax"),
44 | .product(name: "SwiftParser", package: "swift-syntax"),
45 | .product(name: "SwiftParserDiagnostics", package: "swift-syntax"),
46 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
47 | ],
48 | swiftSettings: [
49 | .enableUpcomingFeature("BareSlashRegexLiterals")
50 | ],
51 | plugins: commonPlugins
52 | ),
53 | .target(
54 | name: "HappyCodable",
55 | dependencies: [
56 | "HappyCodableShared",
57 | "HappyCodablePlugin",
58 | ],
59 | plugins: commonPlugins
60 | ),
61 | .testTarget(
62 | name: "HappyCodableTests",
63 | dependencies: [
64 | "HappyCodable",
65 | "HappyCodableShared",
66 | ],
67 | plugins: commonPlugins
68 | ),
69 | .testTarget(
70 | name: "HappyCodablePluginTests",
71 | dependencies: [
72 | "HappyCodablePlugin",
73 | "HappyCodableShared",
74 | .product(name: "SwiftSyntaxMacroExpansion", package: "swift-syntax"),
75 | ],
76 | plugins: commonPlugins
77 | ),
78 |
79 | // MARK: - Build Tools
80 | .binaryTarget(
81 | name: "SwiftLintBinary",
82 | url: "https://github.com/realm/SwiftLint/releases/download/0.52.2/SwiftLintBinary-macos.artifactbundle.zip",
83 | checksum: "89651e1c87fb62faf076ef785a5b1af7f43570b2b74c6773526e0d5114e0578e"
84 | ),
85 | .plugin(
86 | name: "SwiftLint",
87 | capability: .buildTool(),
88 | dependencies: ["SwiftLintBinary"],
89 | path: "Plugins/Lint"
90 | ),
91 | ]
92 | )
93 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/Struct.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Struct.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | // swiftlint:disable lower_acl_than_parent
11 |
12 | // MARK: - empty struct
13 | @HappyCodable(disableWarnings: [.noInitializer])
14 | struct TestStruct_empty: HappyCodable {
15 |
16 | }
17 | @HappyCodable
18 | public struct TestStruct_empty_public: HappyCodable {
19 | public init() { }
20 | }
21 | @HappyCodable(disableWarnings: [.noInitializer])
22 | private struct TestStruct_empty_private: HappyCodable {
23 |
24 | }
25 |
26 | // MARK: - not empty struct
27 | @HappyCodable
28 | struct TestStruct_notEmpty: HappyCodable {
29 | public var int: Int = 0
30 | var object: Class = Class()
31 | var objectNil: Struct?
32 | private var string: String = ""
33 |
34 | public var getter: Void { () }
35 | public func function() { }
36 |
37 | @HappyCodable
38 | class Class: HappyCodable {
39 | required init() { }
40 | var int: Int = 0
41 | var string: String = ""
42 | }
43 |
44 | @HappyCodable
45 | struct Struct: HappyCodable {
46 | var int: Int = 0
47 | var string: String = ""
48 |
49 | init() { }
50 | }
51 |
52 | init() { }
53 | }
54 | @HappyCodable
55 | public struct TestStruct_notEmpty_public: HappyCodable {
56 | public init() { }
57 | public var int: Int = 0
58 | var object: Class = Class()
59 | var objectNil: Struct?
60 | private var string: String = ""
61 |
62 | public var getter: Void { () }
63 | public func function() { }
64 |
65 | @HappyCodable
66 | class Class: HappyCodable {
67 | required init() { }
68 | var int: Int = 0
69 | var string: String = ""
70 | }
71 |
72 | @HappyCodable(disableWarnings: [.noInitializer])
73 | struct Struct: HappyCodable {
74 | var int: Int = 0
75 | var string: String = ""
76 | }
77 | }
78 | @HappyCodable(disableWarnings: [.noInitializer])
79 | private struct TestStruct_notEmpty_private: HappyCodable {
80 | public var int: Int = 0
81 | var object: Class = Class()
82 | var objectNil: Struct?
83 | private var string: String = ""
84 |
85 | public var getter: Void { () }
86 | public func function() { }
87 |
88 | @HappyCodable
89 | class Class: HappyCodable {
90 | required init() { }
91 | var int: Int = 0
92 | var string: String = ""
93 | }
94 |
95 | @HappyCodable(disableWarnings: [.noInitializer])
96 | struct Struct: HappyCodable {
97 | var int: Int = 0
98 | var string: String = ""
99 | }
100 | }
101 |
102 | // swiftlint:enable lower_acl_than_parent
103 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/DateStrategyTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateStrategyTest.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | class DateCodingStrategyTest: XCTestCase {
11 | func test() throws {
12 | let fakeData_date = Date(timeIntervalSince1970: TimeInterval.random(in: 100000000...10000000000))
13 | let encoder = Foundation.JSONEncoder()
14 | encoder.dateEncodingStrategy = .deferredToDate
15 | // swiftlint:disable force_unwrapping
16 | let fakeData_dateJSON = try String(data: encoder.encode(fakeData_date), encoding: .utf8)!
17 | // swiftlint:enable force_unwrapping
18 | let iso8601formatter = ISO8601DateFormatter()
19 | iso8601formatter.formatOptions = .withInternetDateTime
20 |
21 | let json: NSDictionary = [
22 | "date_deferredToDate": fakeData_dateJSON,
23 |
24 | "date_secondsSince1970": fakeData_date.timeIntervalSince1970,
25 |
26 | "date_millisecondsSince1970": fakeData_date.timeIntervalSince1970 * 1000,
27 |
28 | "date_iso8601": iso8601formatter.string(from: fakeData_date),
29 |
30 | "date_formatted": TestStruct_dateStrategy.dateFormater.string(from: fakeData_date),
31 |
32 | "date_custom": fakeData_dateJSON
33 | ]
34 | let object = try TestStruct_dateStrategy.decode(from: json)
35 |
36 | assert(object.date_deferredToDate == fakeData_date)
37 | assert(object.date_secondsSince1970 == fakeData_date)
38 | assert(object.date_millisecondsSince1970 == fakeData_date)
39 | assert("\(object.date_iso8601)" == "\(fakeData_date)") // 格式后会丢掉毫秒的信息
40 | assert("\(object.date_formatted)" == "\(fakeData_date)") // 格式后会丢掉毫秒的信息
41 | assert(object.date_custom == TestStruct_dateStrategy.customDate)
42 | }
43 | func testNSNull() throws {
44 | let json: NSDictionary = [
45 | "date_deferredToDate": NSNull(),
46 |
47 | "date_secondsSince1970": NSNull(),
48 |
49 | "date_millisecondsSince1970": NSNull(),
50 |
51 | "date_iso8601": NSNull(),
52 |
53 | "date_formatted": NSNull(),
54 |
55 | "date_custom": NSNull()
56 | ]
57 | let object = try TestStruct_dateStrategy.decode(from: json)
58 |
59 | assert(object.date_deferredToDate == TestStruct_dateStrategy.defaultDate)
60 | assert(object.date_secondsSince1970 == TestStruct_dateStrategy.defaultDate)
61 | assert(object.date_millisecondsSince1970 == TestStruct_dateStrategy.defaultDate)
62 | assert("\(object.date_iso8601)" == "\(TestStruct_dateStrategy.defaultDate)") // 格式后会丢掉毫秒的信息
63 | assert("\(object.date_formatted)" == "\(TestStruct_dateStrategy.defaultDate)") // 格式后会丢掉毫秒的信息
64 | assert(object.date_custom == TestStruct_dateStrategy.customDate)
65 | }
66 | }
67 |
68 | extension Date {
69 | // 由于单双精度切换的问题, 有时候6位小数后会有变化所以这里至今过滤掉了
70 | static func == (lhs: Date, rhs: Date) -> Bool {
71 | return Int(lhs.timeIntervalSince1970 * 100000) == Int(rhs.timeIntervalSince1970 * 100000)
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/interface/HappyCodable+Macro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HappyCodable+Macro.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | @_exported import HappyCodableShared
9 |
10 | @attached(member, conformances: HappyCodable, names: named(CodingKeys), named(encode(to:)), named(init(from:)))
11 | public macro HappyCodable(disableWarnings: [Warnings] = []) = #externalMacro(module: "HappyCodablePlugin", type: "HappyCodableMemberMacro")
12 |
13 | // MARK: - AlterCodingKeys
14 | @attached(peer)
15 | public macro AlterCodingKeys(_ keys: StaticString...) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
16 |
17 | // MARK: - DataStrategy
18 | @attached(peer)
19 | public macro DataStrategy(decode: Foundation.JSONDecoder.DataDecodingStrategy, encode: Foundation.JSONEncoder.DataEncodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
20 | @attached(peer)
21 | public macro DataStrategy(encode: Foundation.JSONEncoder.DataEncodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
22 | @attached(peer)
23 | public macro DataStrategy(decode: Foundation.JSONDecoder.DataDecodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
24 |
25 | // MARK: - FloatStrategy
26 | @attached(peer)
27 | public macro FloatStrategy(decode: Foundation.JSONDecoder.NonConformingFloatDecodingStrategy, encode: Foundation.JSONEncoder.NonConformingFloatEncodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
28 | @attached(peer)
29 | public macro FloatStrategy(decode: Foundation.JSONDecoder.NonConformingFloatDecodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
30 | @attached(peer)
31 | public macro FloatStrategy(encode: Foundation.JSONEncoder.NonConformingFloatEncodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
32 |
33 | // MARK: - DateStrategy
34 | @attached(peer)
35 | public macro DateStrategy(decode: Foundation.JSONDecoder.DateDecodingStrategy, encode: Foundation.JSONEncoder.DateEncodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
36 | @attached(peer)
37 | public macro DateStrategy(decode: Foundation.JSONDecoder.DateDecodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
38 | @attached(peer)
39 | public macro DateStrategy(encode: Foundation.JSONEncoder.DateEncodingStrategy) = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
40 |
41 | // MARK: - ElementNullable
42 | @attached(peer)
43 | public macro ElementNullable() = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
44 |
45 | // MARK: - Uncoding
46 | @attached(peer)
47 | public macro Uncoding() = #externalMacro(module: "HappyCodablePlugin", type: "AttributePlaceholderMacro")
48 |
--------------------------------------------------------------------------------
/Sources/HappyCodable/extension/KeyedEncodingContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyedEncodingContainer.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | // MARK: - Strategy implementation
10 | extension KeyedEncodingContainer {
11 | @inlinable
12 | @inline(__always)
13 | public mutating func encodeIfPresent(_ data: Data?, forKey key: KeyedEncodingContainer.Key, strategy: Foundation.JSONEncoder.DataEncodingStrategy) throws {
14 | guard let data else {
15 | return
16 | }
17 | switch strategy {
18 | case .deferredToData:
19 | try encode(data, forKey: key)
20 | case .base64:
21 | try encode(data.base64EncodedString(), forKey: key)
22 | case .custom(let closure):
23 | try closure(data, superEncoder(forKey: key))
24 | @unknown default:
25 | fatalError("not implemented")
26 | }
27 | }
28 |
29 | @inlinable
30 | @inline(__always)
31 | public mutating func encodeIfPresent(_ date: Date?, forKey key: KeyedEncodingContainer.Key, strategy: Foundation.JSONEncoder.DateEncodingStrategy) throws {
32 | guard let date else {
33 | return
34 | }
35 | switch strategy {
36 | case .deferredToDate:
37 | try encode(date.timeIntervalSinceReferenceDate, forKey: key)
38 | case .secondsSince1970:
39 | try encode(date.timeIntervalSince1970, forKey: key)
40 | case .millisecondsSince1970:
41 | try encode(1000.0 * date.timeIntervalSince1970, forKey: key)
42 | case .iso8601:
43 | try encode(_iso8601Formatter.string(from: date), forKey: key)
44 | case .formatted(let formatter):
45 | try encode(formatter.string(from: date), forKey: key)
46 | case .custom(let closure):
47 | try closure(date, superEncoder(forKey: key))
48 | @unknown default:
49 | fatalError("not implemented")
50 | }
51 | }
52 |
53 | // copy from https://github.com/apple/swift-corelibs-foundation/blob/0ac1a34ab76f6db3196a279c2626a0e4554ff592/Sources/Foundation/JSONEncoder.swift#L465
54 | @inlinable
55 | @inline(__always)
56 | public mutating func encodeIfPresent(_ float: F?, forKey key: KeyedEncodingContainer.Key, strategy: Foundation.JSONEncoder.NonConformingFloatEncodingStrategy) throws where F: BinaryFloatingPoint & Encodable & LosslessStringConvertible {
57 | guard let float else {
58 | return
59 | }
60 | guard !float.isNaN, !float.isInfinite else {
61 | if case .convertToString(let posInfString, let negInfString, let nanString) = strategy {
62 | let string: String
63 | switch float {
64 | case F.infinity:
65 | string = String(posInfString)
66 | case -F.infinity:
67 | string = String(negInfString)
68 | default:
69 | // must be nan in this case
70 | string = String(nanString)
71 | }
72 | try encode(string, forKey: key)
73 | return
74 | }
75 |
76 | throw EncodingError.invalidValue(float, .init(
77 | codingPath: codingPath + [key],
78 | debugDescription: "Unable to encode \(F.self).\(float) directly in JSON."
79 | ))
80 | }
81 |
82 | try encode(float, forKey: key)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/ForKeyedDecodingContainer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ForKeyedDecodingContainer.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 | import HappyCodable
9 |
10 | // swiftlint:disable identifier_name
11 | @HappyCodable(disableWarnings: [.noInitializer])
12 | struct TestStruct_ForKeyedDecodingContainer: HappyCodable {
13 | @AlterCodingKeys("Bool_1", "Bool_2") var Bool: Bool = false
14 | @AlterCodingKeys("String_1", "String_2") var String: String = ""
15 | @AlterCodingKeys("Double_1", "Double_2") var Double: Double = 0
16 | @AlterCodingKeys("Float_1", "Float_2") var Float: Float = 0
17 | @AlterCodingKeys("Int_1", "Int_2") var Int: Int = 0
18 | @AlterCodingKeys("Int8_1", "Int8_2") var Int8: Int8 = 0
19 | @AlterCodingKeys("Int16_1", "Int16_2") var Int16: Int16 = 0
20 | @AlterCodingKeys("Int32_1", "Int32_2") var Int32: Int32 = 0
21 | @AlterCodingKeys("Int64_1", "Int64_2") var Int64: Int64 = 0
22 | @AlterCodingKeys("UInt_1", "UInt_2") var UInt: UInt = 0
23 | @AlterCodingKeys("UInt8_1", "UInt8_2") var UInt8: UInt8 = 0
24 | @AlterCodingKeys("UInt16_1", "UInt16_2") var UInt16: UInt16 = 0
25 | @AlterCodingKeys("UInt32_1", "UInt32_2") var UInt32: UInt32 = 0
26 | @AlterCodingKeys("UInt64_1", "UInt64_2") var UInt64: UInt64 = 0
27 | @AlterCodingKeys("Data_1", "Data_2") var Data: Data? = .init(data: 10)
28 |
29 | @AlterCodingKeys("Bool_optional_1", "Bool_optional_2") var Bool_optional: Bool? = false
30 | @AlterCodingKeys("String_optional_1", "String_optional_2") var String_optional: String? = ""
31 | @AlterCodingKeys("Double_optional_1", "Double_optional_2") var Double_optional: Double? = 0
32 | @AlterCodingKeys("Float_optional_1", "Float_optional_2") var Float_optional: Float? = 0
33 | @AlterCodingKeys("Int_optional_1", "Int_optional_2") var Int_optional: Int? = 0
34 | @AlterCodingKeys("Int8_optional_1", "Int8_optional_2") var Int8_optional: Int8? = 0
35 | @AlterCodingKeys("Int16_optional_1", "Int16_optional_2") var Int16_optional: Int16? = 0
36 | @AlterCodingKeys("Int32_optional_1", "Int32_optional_2") var Int32_optional: Int32? = 0
37 | @AlterCodingKeys("Int64_optional_1", "Int64_optional_2") var Int64_optional: Int64? = 0
38 | @AlterCodingKeys("UInt_optional_1", "UInt_optional_2") var UInt_optional: UInt? = 0
39 | @AlterCodingKeys("UInt8_optional_1", "UInt8_optional_2") var UInt8_optional: UInt8? = 0
40 | @AlterCodingKeys("UInt16_optional_1", "UInt16_optional_2") var UInt16_optional: UInt16? = 0
41 | @AlterCodingKeys("UInt32_optional_1", "UInt32_optional_2") var UInt32_optional: UInt32? = 0
42 | @AlterCodingKeys("UInt64_optional_1", "UInt64_optional_2") var UInt64_optional: UInt64? = 0
43 | @AlterCodingKeys("Data_optional_1", "Data_optional_2") var Data_optional: Data?
44 |
45 | struct Data: HappyCodable {
46 | var data = 0
47 | init(data: Int = 0) {
48 | self.data = data
49 | }
50 | }
51 | }
52 |
53 | @HappyCodable
54 | struct TestStruct_TypeMismatch: HappyCodable {
55 | var Int: Int = 0
56 | init(Int: Int = 0) {
57 | self.Int = Int
58 | }
59 | }
60 | // swiftlint:enable identifier_name
61 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/TestObjects/Class.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Class.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | // 测试规则:
8 | // class/struct/enum
9 | // 空class测试
10 | // 各包含各一个sub class/struct/enum/复杂enum
11 | // 然后包含一个public, internal, private 属性
12 | // internal 包含var/let各一个类型为自己的复杂类型
13 | // internal 包含的var复制一遍改为可选
14 | // private的属性测试时得测试没效果才是正确的
15 | // 一个public的getter
16 | // 一个public方法
17 | // 把整个类复制改成public和private(包括空class)
18 | // private 的不需要复制基本数据类型的 enum, 因为Swift的编译器会强制要求基本数据类型的 Enum 实现协议方法, 不能用 extension 里的
19 | // 测试的时候, 每个类型都创建一个, 修改属性为随机数据, toJson, fromJson, 对比:
20 | // 新创建的对象随机数据是否一样
21 | // private的数据是否默认值
22 |
23 | import Foundation
24 | import HappyCodable
25 |
26 | // swiftlint:disable lower_acl_than_parent
27 | // MARK: - empty class
28 | @HappyCodable
29 | class TestClass_empty: HappyCodable {
30 | required init() { }
31 | }
32 | @HappyCodable
33 | public class TestClass_empty_public: HappyCodable {
34 | public required init() { }
35 | }
36 | @HappyCodable
37 | private class TestClass_empty_private: HappyCodable {
38 | required init() { }
39 | }
40 |
41 | // MARK: - not empty class
42 | @HappyCodable
43 | class TestClass_notEmpty: HappyCodable {
44 | required init() { }
45 | public var int: Int = 0
46 | var object: Class = Class()
47 | var objectNil: Struct?
48 | private var string: String = ""
49 |
50 | public var getter: Void { () }
51 | public func function() { }
52 |
53 | @HappyCodable
54 | class Class: HappyCodable {
55 | required init() { }
56 | var int: Int = 0
57 | var string: String = ""
58 | }
59 |
60 | @HappyCodable
61 | struct Struct: HappyCodable {
62 | var int: Int = 0
63 | var string: String = ""
64 |
65 | init() { }
66 | }
67 | }
68 |
69 | @HappyCodable
70 | public class TestClass_notEmpty_public: HappyCodable {
71 | public required init() { }
72 | public var int: Int = 0
73 | var object: Class = Class()
74 | var objectNil: Struct?
75 | private var string: String = ""
76 |
77 | public var getter: Void { () }
78 | public func function() { }
79 |
80 | @HappyCodable
81 | class Class: HappyCodable {
82 | required init() { }
83 | var int: Int = 0
84 | var string: String = ""
85 | }
86 |
87 | @HappyCodable(disableWarnings: [.noInitializer])
88 | struct Struct: HappyCodable {
89 | var int: Int = 0
90 | var string: String = ""
91 | }
92 | }
93 |
94 | @HappyCodable
95 | private class TestClass_notEmpty_private: HappyCodable {
96 | required init() { }
97 | public var int: Int = 0
98 | var object: Class = Class()
99 | var objectNil: Struct?
100 | private var string: String = ""
101 |
102 | public var getter: Void { () }
103 | public func function() { }
104 |
105 | @HappyCodable
106 | class Class: HappyCodable {
107 | required init() { }
108 | var int: Int = 0
109 | var string: String = ""
110 | }
111 |
112 | @HappyCodable(disableWarnings: [.noInitializer])
113 | struct Struct: HappyCodable {
114 | var int: Int = 0
115 | var string: String = ""
116 | }
117 | }
118 | // swiftlint:enable lower_acl_than_parent
119 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/KeyedDecodingContainerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyedDecodingContainerTests.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | class KeyedDecodingContainerTests: XCTestCase {
11 | func test() throws {
12 | // 到127是为了防止Int8转不了
13 | let fakeData_int = Int.random(in: 0...127)
14 | let fakeData_bool = Bool.random()
15 | let json: NSDictionary = [
16 | "Bool_2": fakeData_bool,
17 | "String_2": "\(fakeData_int)\(fakeData_bool)",
18 | "Double_2": fakeData_int,
19 | "Float_2": fakeData_int,
20 | "Int_2": fakeData_int,
21 | "Int8_2": fakeData_int,
22 | "Int16_2": fakeData_int,
23 | "Int32_2": fakeData_int,
24 | "Int64_2": fakeData_int,
25 | "UInt_2": fakeData_int,
26 | "UInt8_2": fakeData_int,
27 | "UInt16_2": fakeData_int,
28 | "UInt32_2": fakeData_int,
29 | "UInt64_2": fakeData_int,
30 |
31 | "Bool_optional_2": fakeData_bool,
32 | "String_optional_2": "\(fakeData_int)\(fakeData_bool)",
33 | "Double_optional_2": fakeData_int,
34 | "Float_optional_2": fakeData_int,
35 | "Int_optional_2": fakeData_int,
36 | "Int8_optional_2": fakeData_int,
37 | "Int16_optional_2": fakeData_int,
38 | "Int32_optional_2": fakeData_int,
39 | "Int64_optional_2": fakeData_int,
40 | "UInt_optional_2": fakeData_int,
41 | "UInt8_optional_2": fakeData_int,
42 | "UInt16_optional_2": fakeData_int,
43 | "UInt32_optional_2": fakeData_int,
44 | "UInt64_optional_2": fakeData_int,
45 | ]
46 | let object = try TestStruct_ForKeyedDecodingContainer.decode(from: json)
47 | assert(object.Bool == fakeData_bool)
48 | assert(object.String == "\(fakeData_int)\(fakeData_bool)")
49 | assert(object.Double == .init(fakeData_int))
50 | assert(object.Float == .init(fakeData_int))
51 | assert(object.Int == .init(fakeData_int))
52 | assert(object.Int8 == .init(fakeData_int))
53 | assert(object.Int16 == .init(fakeData_int))
54 | assert(object.Int32 == .init(fakeData_int))
55 | assert(object.Int64 == .init(fakeData_int))
56 | assert(object.UInt == .init(fakeData_int))
57 | assert(object.UInt8 == .init(fakeData_int))
58 | assert(object.UInt16 == .init(fakeData_int))
59 | assert(object.UInt32 == .init(fakeData_int))
60 | assert(object.UInt64 == .init(fakeData_int))
61 |
62 | assert(object.Bool_optional == fakeData_bool)
63 | assert(object.String_optional == "\(fakeData_int)\(fakeData_bool)")
64 | assert(object.Double_optional == .init(fakeData_int))
65 | assert(object.Float_optional == .init(fakeData_int))
66 | assert(object.Int_optional == .init(fakeData_int))
67 | assert(object.Int8_optional == .init(fakeData_int))
68 | assert(object.Int16_optional == .init(fakeData_int))
69 | assert(object.Int32_optional == .init(fakeData_int))
70 | assert(object.Int64_optional == .init(fakeData_int))
71 | assert(object.UInt_optional == .init(fakeData_int))
72 | assert(object.UInt8_optional == .init(fakeData_int))
73 | assert(object.UInt16_optional == .init(fakeData_int))
74 | assert(object.UInt32_optional == .init(fakeData_int))
75 | assert(object.UInt64_optional == .init(fakeData_int))
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/README.cn.md:
--------------------------------------------------------------------------------
1 | # HappyCodable
2 |
3 | 使用Swift Macro 实现优雅的 Codable
4 |
5 |
6 |
7 | ## 与 3.x 的区别
8 |
9 | 从`编译时`加`自定义的 Decoder` 的方式实现改为使用Swift Macro 实现(目前只支持Swift Package)
10 |
11 | 相比自定义的 Decoder有以下好处:
12 |
13 | 1. 不用再维护自定义的 Decoder
14 | 2. 更进一步地减少PropertyWrapper的运行时的开销
15 | 3. 更简单地实现
16 | 4. 因为不再依赖extension, 因此extension的限制将被解除, 比如混用其他第三方的Coder时, 不用手写CodingKeys了
17 | 5. 因为Swift Macro不支持命名空间, 所以@Happy.的前缀需要被移除
18 |
19 | ## 原生 JSON Codable 的问题 ?
20 |
21 | 1. 不支持自定义某个属性的 coding key, 一旦你有这种需求, 要么把所有的 coding key 手动实现一遍去修改想要的 coding key, 要么就得在 decode 的时候去设置 Decoder , 极其不方便
22 | 2. 不支持忽略掉某些不能 Codable 的属性, 还是需要手动实现 coding key 才行
23 | 3. decode 的时候不支持多个 coding key 映射同一个属性
24 | 4. 不能使用模型的默认值, 当 decode 的数据缺失时无法使用定义里的默认值而是 throw 数据缺失错误, 这个设计导致例如版本更新后, 服务端删掉了模型的某个过期字段, 然后旧版本 app 都会陷入错误, 即使不用这个字段旧版本客户端依旧是能正常工作的(只是无效的数据显示缺失而已), 这很明显是不合理的.
25 | 5. 不支持简单的类型转换, 比如转换 0/1 到 false/true, "123" 到 Int的123 或者反过来, 谁又能确保服务端的人员不会失手修改了字段类型导致 app 端故障呢?
26 |
27 | 而这些, 你全都可以用HappyCodable解决
28 |
29 | ## 安装
30 |
31 | ### Swift Package
32 |
33 | 1. 添加该repo到项目的Swift package中, 设置版本为 4.0.2 或以上
34 |
35 |
36 |
37 | 2. 添加 HappyCodable 到需要的target里
38 |
39 |
40 |
41 | 3. 编译一次后查看 warnings / errors, 选择 trust HappyCodable
42 |
43 |
44 |
45 |
46 | ### 在项目中使用
47 |
48 | 把 `HappyCodable` 应用到你的struct/class/enum:
49 |
50 | ```swift
51 | import HappyCodable
52 |
53 | extension HappyEncodable {
54 | static var decodeHelper: DecodeHelper {
55 | .init()
56 | }
57 | }
58 |
59 | extension HappyDecodable {
60 | static var encodeHelper: EncodeHelper {
61 | .init()
62 | }
63 | }
64 |
65 | @HappyCodable
66 | struct Person: HappyCodable {
67 | var name: String = "abc"
68 |
69 | @AlterCodingKeys("🆔")
70 | var id: String = "abc"
71 |
72 | @AlterCodingKeys("secret_number", "age")
73 | var age: Int = 18
74 |
75 | @DataStrategy(decode: .deferredToData, encode: .deferredToData)
76 | var data_deferredToData: Data = Data()
77 |
78 | @DateStrategy(decode: .secondsSince1970, encode: .secondsSince1970)
79 | var date_secondsSince1970: Date = Date(timeIntervalSince1970: 1234)
80 |
81 | @AlterCodingKeys("data1")
82 | @ElementNullable
83 | var data: [String] = []
84 |
85 | @Uncoding
86 | var secret_number: String = "3.1415"
87 |
88 | init() { }
89 | }
90 |
91 | ## 答疑
92 |
93 | 1. ### 你为什么会写这个库, 我为什么不用 HandyJSON
94 |
95 | 我之前项目是用 HandyJSON 的, 但由于 HandyJSON 是基于操作 Swift 底层数据结构实现的, 已经好几次 Swift 版本迭代后, 由于数据结构的改变 HandyJSON 都会出问题, 由于我不想手动解析模型, 促使了我写这个库, 配上足够多的测试用例总比手动安全一些
96 |
97 | 可能有人会说更新 HandyJSON 不就好了, 但是你既不能确保以后 Swift 不会更新底层数据结构后, 直接导致HandyJSON 死亡, 也不能确保你所开发的 APP 突然被迫停止开发后, 你的用户更新系统就不能用了对吧
98 |
99 | 为了迁移到 HappyCodable, HappyCodable 的 API 很大程度参考了 HandyJSON
100 |
101 | 2. ### 我的项目用了其他基于Codable的库(比如WCDB.swift), 能共存吗?
102 |
103 | 可以
104 |
105 | 3. 待补充...
106 |
--------------------------------------------------------------------------------
/Sources/HappyCodablePlugin/types/CodableItem.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodableItem.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | import Foundation
8 |
9 | struct CodableItem {
10 | let name: String
11 | enum TypeSyntax {
12 | case normal(String)
13 | case optional(String)
14 | var isOptional: Bool {
15 | if case .optional = self {
16 | return true
17 | } else {
18 | return false
19 | }
20 | }
21 | var string: String {
22 | switch self {
23 | case let .normal(string):
24 | return string
25 | case let .optional(string):
26 | return string
27 | }
28 | }
29 | }
30 |
31 | let defaultValue: String?
32 | var alterKeys: [String] = []
33 | var uncoding: Bool = false
34 |
35 | struct DateStrategy {
36 | let encode: String
37 | let decode: String
38 | }
39 | var dateStrategy: DateStrategy?
40 |
41 | struct DataStrategy {
42 | let encode: String
43 | let decode: String
44 | }
45 | var dataStrategy: DataStrategy?
46 |
47 | struct FloatStrategy {
48 | let encode: String
49 | let decode: String
50 | }
51 | var floatStrategy: FloatStrategy?
52 |
53 | var elementNullable: Bool = false
54 | }
55 |
56 | extension CodableItem {
57 | func codingKey(override: [String: String]) -> String? {
58 | if uncoding {
59 | return nil
60 | }
61 | return "case \(name) = \"\(override[name] ?? name)\""
62 | }
63 |
64 | var decode: String {
65 | if uncoding {
66 | // swiftlint:disable force_unwrapping
67 | return "self.\(name) = \(defaultValue!)"
68 | // swiftlint:enable force_unwrapping
69 | }
70 | let alterKeysArgument: String
71 | if alterKeys.isEmpty {
72 | alterKeysArgument = ""
73 | } else {
74 | alterKeysArgument = ", alterKeys: { [\(alterKeys.joined(separator: ","))] }"
75 | }
76 |
77 | let additionalArgument: String
78 | if let dataStrategy {
79 | additionalArgument = ", strategy: \(dataStrategy.decode)"
80 | } else if let dateStrategy {
81 | additionalArgument = ", strategy: \(dateStrategy.decode)"
82 | } else if let floatStrategy {
83 | additionalArgument = ", strategy: \(floatStrategy.decode)"
84 | } else if elementNullable {
85 | additionalArgument = ", strategy: ()"
86 | } else {
87 | additionalArgument = ""
88 | }
89 |
90 | let decodeExpression = "container.decode(key: CodingKeys.\(name).rawValue \(alterKeysArgument) \(additionalArgument))"
91 |
92 | func throwIfNoDefaultValue() -> String {
93 | if let defaultValue {
94 | return """
95 | errors.append(error)
96 | return \(defaultValue)
97 | """
98 | } else {
99 | return "throw error"
100 | }
101 | }
102 |
103 | return """
104 | self.\(name) = \(defaultValue == nil ? "try " : ""){
105 | do {
106 | return try \(decodeExpression)
107 | } catch {
108 | \(throwIfNoDefaultValue())
109 | }
110 | }()
111 | """
112 | }
113 |
114 | var encode: String {
115 | if uncoding {
116 | return ""
117 | }
118 |
119 | let additionalArgument: String
120 | if let dataStrategy {
121 | additionalArgument = ", strategy: \(dataStrategy.encode)"
122 | } else if let dateStrategy {
123 | additionalArgument = ", strategy: \(dateStrategy.encode)"
124 | } else if let floatStrategy {
125 | additionalArgument = ", strategy: \(floatStrategy.encode)"
126 | } else {
127 | additionalArgument = ""
128 | }
129 |
130 | let decodeExpression = "container.encodeIfPresent(self.\(name), forKey: CodingKeys.\(name).rawValue \(additionalArgument))"
131 |
132 | return """
133 | do {
134 | try \(decodeExpression)
135 | } catch {
136 | errors.append(error)
137 | }
138 | """
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/Tests/Migration/Sources/main.swift:
--------------------------------------------------------------------------------
1 | // The Swift Programming Language
2 | // https://docs.swift.org/swift-book
3 |
4 | import Foundation
5 |
6 | let path = "/Volumes/Common/HappyCodable/Tests/HappyCodableTests"
7 |
8 | typealias Fix = (_ content: inout String, _ checkIndex: inout String.Index) -> Void
9 |
10 | let fixs: [Fix] = [
11 | { (content, checkIndex) in
12 | let rex = /extension HappyCodable\s*?\{\n(.*)static\s+?var\s+?decodeOption\s*?:\s*? HappyCodableDecodeOption\s*?\{/
13 | guard let match = try? rex.firstMatch(in: content[checkIndex...]) else {
14 | checkIndex = content.endIndex
15 | return
16 | }
17 | let replacement: String =
18 | """
19 | extension HappyEncodable {
20 | static var encodeHelper: EncodeHelper {
21 | .init()
22 | }
23 | }
24 |
25 | extension HappyDecodable {
26 | \(match.1)static var decodeHelper: DecodeHelper {
27 | """
28 | content.replaceSubrange(match.range, with: replacement)
29 |
30 | let offset = replacement.distance(from: replacement.startIndex, to: replacement.endIndex)
31 |
32 | checkIndex = content.index(match.range.lowerBound, offsetBy: offset)
33 | },
34 |
35 | { (content, checkIndex) in
36 |
37 | let cases = [
38 | (/Happy\.alterCodingKeys/, "HappyAlterCodingKeys"),
39 | (/Happy\.dataStrategy/, "HappyDataStrategy"),
40 | (/Happy\.nonConformingFloatStrategy/, "HappyNonConformingFloatStrategy"),
41 | (/Happy\.dateStrategy/, "HappyDateStrategy"),
42 | (/Happy\.elementNullable/, "HappyElementNullable"),
43 | (/Happy\.uncoding/, "HappyUncoding"),
44 | ]
45 |
46 | for (rex, replacement) in cases {
47 | var checkIndex = checkIndex
48 | while let match = try? rex.firstMatch(in: content[checkIndex...]) {
49 | content.replaceSubrange(match.range, with: replacement)
50 |
51 | let offset = replacement.distance(from: replacement.startIndex, to: replacement.endIndex)
52 | checkIndex = content.index(match.range.lowerBound, offsetBy: offset)
53 | }
54 | }
55 |
56 | while let match = try? /\n[\s|\t]*?@Happy.dynamicDefault/.firstMatch(in: content[checkIndex...]) {
57 | content.replaceSubrange(match.range, with: "")
58 |
59 | checkIndex = match.range.lowerBound
60 | }
61 | checkIndex = content.endIndex
62 | },
63 |
64 | { (content, checkIndex) in
65 | let rex = /\n(.*?)(struct|class|actor)(.*)\s*?:\s*?HappyCodable/
66 | guard let match = try? rex.firstMatch(in: content[checkIndex...]) else {
67 | checkIndex = content.endIndex
68 | return
69 | }
70 | let spaces = match.1.prefix {
71 | $0.unicodeScalars.first.map {
72 | CharacterSet.whitespaces.contains($0)
73 | } ?? false
74 | }
75 | let replacement: String = "\n\(spaces)@HappyCodable\(match.0)"
76 |
77 | content.replaceSubrange(match.range, with: replacement)
78 |
79 | let offset = replacement.distance(from: replacement.startIndex, to: replacement.endIndex)
80 |
81 | checkIndex = content.index(match.range.lowerBound, offsetBy: offset)
82 | },
83 |
84 | { (content, checkIndex) in
85 | let rex = /@HappyCodable\n([\s|\t]*?)@HappyCodable/
86 | guard let match = try? rex.firstMatch(in: content[checkIndex...]) else {
87 | checkIndex = content.endIndex
88 | return
89 | }
90 | let replacement: String = "@HappyCodable"
91 |
92 | content.replaceSubrange(match.range, with: replacement)
93 |
94 | let offset = replacement.distance(from: replacement.startIndex, to: replacement.endIndex)
95 |
96 | checkIndex = content.index(match.range.lowerBound, offsetBy: offset)
97 | },
98 | ]
99 |
100 |
101 | let enumerator = FileManager.default.enumerator(at: URL(filePath: path), includingPropertiesForKeys: nil)
102 | let url = URL(fileURLWithPath: "/Volumes/Common/HappyCodable/Tests/HappyCodableTests/ArrayNullTest.swift")
103 | while let url = enumerator?.nextObject() as? URL {
104 | guard url.pathExtension == "swift" else {
105 | continue
106 | }
107 | var content = try String(contentsOf: url)
108 |
109 |
110 | for fix in fixs {
111 | var checkIndex = content.startIndex
112 | while checkIndex < content.endIndex {
113 | fix(&content, &checkIndex)
114 | }
115 | }
116 | try? content.write(to: url, atomically: true, encoding: .utf8)
117 | }
118 |
119 |
120 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HappyCodable
2 |
3 | Elegant Codable implementation using Swift Macro
4 |
5 | ## Differences from version 3.x
6 |
7 | The way of implementing a custom decoder at compile time has been changed to using Swift Macro (currently only supported by Swift Package). This has the following benefits over a custom decoder:
8 |
9 | 1. No need to maintain a custom decoder
10 | 2. Further reduces the runtime overhead of PropertyWrapper
11 | 3. Simpler implementation
12 | 4. Since it no longer relies on extensions, extension restrictions will be lifted. For example, when using other third-party Coders, you no longer need to manually write CodingKeys
13 | 5. Because Swift Macro does not support namespaces, the @Happy prefix needs to be removed
14 |
15 | ## Issues with native JSON Codable?
16 |
17 | 1. Does not support custom coding keys for a property. Once you have this requirement, you either have to manually implement all the coding keys and modify the ones you want, or set the Decoder during decoding, which is extremely inconvenient.
18 | 2. Does not support ignoring properties that cannot be Codable. You still need to manually implement coding keys.
19 | 3. Does not support multiple coding keys mapping to the same property during decoding.
20 | 4. Cannot use the model's default values. When decoding data is missing, you cannot use the default values defined in the model and instead throw a data missing error. This design leads to errors when, for example, a server deletes an outdated field of a model after a version update, and old version apps will all be in error, even if they don't use this field (only invalid data is missing). This is clearly unreasonable.
21 | 5. Does not support simple type conversion, such as converting 0/1 to false/true, "123" to Int's 123 or vice versa. Who can guarantee that the server personnel will not accidentally modify the field type and cause a failure on the app side?
22 |
23 | All of these can be solved with HappyCodable.
24 |
25 | ## Installation
26 |
27 | ### Swift Package
28 |
29 | 1. Add this repo to your project's Swift package, set the version to 4.0.2 or higher
30 |
31 |
32 |
33 | 2. Add HappyCodable to the desired project
34 |
35 |
36 |
37 | 3. View warnings and errors after compiling, trust HappyCodable
38 |
39 |
40 |
41 | ### Using in your project
42 |
43 | Apply `HappyCodable` to your struct/class/enum:
44 |
45 | ```swift
46 | import HappyCodable
47 |
48 | extension HappyEncodable {
49 | static var decodeHelper: DecodeHelper {
50 | .init()
51 | }
52 | }
53 |
54 | extension HappyDecodable {
55 | static var encodeHelper: EncodeHelper {
56 | .init()
57 | }
58 | }
59 |
60 | @HappyCodable
61 | struct Person: HappyCodable {
62 | var name: String = "abc"
63 |
64 | @AlterCodingKeys("🆔")
65 | var id: String = "abc"
66 |
67 | @AlterCodingKeys("secret_number", "age")
68 | var age: Int = 18
69 |
70 | @DataStrategy(decode: .deferredToData, encode: .deferredToData)
71 | var data_deferredToData: Data = Data()
72 |
73 | @DateStrategy(decode: .secondsSince1970, encode: .secondsSince1970)
74 | var date_secondsSince1970: Date = Date(timeIntervalSince1970: 1234)
75 |
76 | @AlterCodingKeys("data1")
77 | @ElementNullable
78 | var data: [String] = []
79 |
80 | @Uncoding
81 | var secret_number: String = "3.1415"
82 |
83 | init() { }
84 | }
85 | ```
86 |
87 | ## Q&A
88 |
89 | 1. ### Why did you write this library, and why not use HandyJSON?
90 |
91 | I used HandyJSON before, but because HandyJSON is based on operating on Swift's underlying data structures, it has had problems several times after Swift version updates due to changes in data structures. Since I don't want to manually parse models, it prompted me to write this library. Having enough test cases with HappyCodable is safer than manual implementation.
92 |
93 | Some people might say that updating HandyJSON is enough, but you can't guarantee that Swift won't update the underlying data structure in the future, causing HandyJSON to die. Nor can you guarantee that your app suddenly stops development and your users can't use it after updating the system.
94 |
95 | To migrate to HappyCodable, HappyCodable's API is largely based on HandyJSON.
96 |
97 | 2. ### Can it coexist with other Codable-based libraries in my project (such as WCDB.swift)?
98 |
99 | Yes.
100 |
101 | 3. To be continued...
102 |
--------------------------------------------------------------------------------
/Tests/HappyCodableTests/CommonTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CommonTests.swift
3 | // HappyCodable
4 | //
5 | //
6 |
7 | @testable import HappyCodable
8 | import XCTest
9 |
10 | class CommonTests: XCTestCase {
11 | // 空的类型decode会直接报错所以不测了
12 | // Top-level TestClass_empty did not encode any values.
13 | func testClass() throws {
14 | let fakeData_object_int = Int.random(in: (0 ... 10000))
15 |
16 | let fakeData_object_class_int = Int.random(in: (0 ... 10000))
17 | let fakeData_object_class_string = "\(Int.random(in: (0 ... 10000)))"
18 |
19 | let fakeData_object_struct_int = Int.random(in: (0 ... 10000))
20 | let fakeData_object_struct_string = "\(Int.random(in: (0 ... 10000)))"
21 |
22 | let object = TestClass_notEmpty()
23 |
24 | object.int = fakeData_object_int
25 |
26 | object.object.int = fakeData_object_class_int
27 | object.object.string = fakeData_object_class_string
28 |
29 | object.objectNil = .init()
30 | object.objectNil?.int = fakeData_object_struct_int
31 | object.objectNil?.string = fakeData_object_struct_string
32 |
33 | let json = try object.toJSON() as NSDictionary
34 | let jsonString = try object.toJSONString(prettyPrint: false)
35 |
36 | struct Package