├── .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 | image 36 | 37 | 2. 添加 HappyCodable 到需要的target里 38 | 39 | image 40 | 41 | 3. 编译一次后查看 warnings / errors, 选择 trust HappyCodable 42 | 43 | Screenshot 2023-09-22 at 19 31 45 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 | image 32 | 33 | 2. Add HappyCodable to the desired project 34 | 35 | image 36 | 37 | 3. View warnings and errors after compiling, trust HappyCodable 38 | 39 | Screenshot 2023-09-22 at 19 31 45 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: Codable where Object: Codable { 37 | let data: Package 38 | struct Package: Codable { 39 | let object: Object 40 | } 41 | } 42 | 43 | let encoder = Foundation.JSONEncoder() 44 | let json1 = try encoder.encode(Package(data: .init(object: object))) 45 | 46 | let object1 = try TestClass_notEmpty.decode(from: json1, designatedPath: "data.object") 47 | 48 | assert(try object1.toJSONString() == jsonString) 49 | 50 | assert(try (try [TestClass_notEmpty].decode(from: try [object].toJSONString()))[0].toJSONString() == jsonString) 51 | assert(try (try [TestClass_notEmpty].decode(from: try [object].toJSON()))[0].toJSONString() == jsonString) 52 | assert(try (try [TestClass_notEmpty].decode(from: try encoder.encode(Package(data: .init(object: [object]))), designatedPath: "data.object"))[0].toJSONString() == jsonString) 53 | 54 | assert(json == [ 55 | "int": fakeData_object_int, 56 | "object": [ 57 | "int": fakeData_object_class_int, 58 | "string": fakeData_object_class_string 59 | ], 60 | "objectNil": [ 61 | "int": fakeData_object_struct_int, 62 | "string": fakeData_object_struct_string 63 | ], 64 | "string": "" 65 | ]) 66 | assert(jsonString == "{\"object\":{\"int\":\(fakeData_object_class_int),\"string\":\"\(fakeData_object_class_string)\"},\"string\":\"\",\"int\":\(fakeData_object_int),\"objectNil\":{\"int\":\(fakeData_object_struct_int),\"string\":\"\(fakeData_object_struct_string)\"}}") 67 | assert(try object.toJSONString(prettyPrint: true) == "{\n \"object\" : {\n \"int\" : \(fakeData_object_class_int),\n \"string\" : \"\(fakeData_object_class_string)\"\n },\n \"string\" : \"\",\n \"int\" : \(fakeData_object_int),\n \"objectNil\" : {\n \"int\" : \(fakeData_object_struct_int),\n \"string\" : \"\(fakeData_object_struct_string)\"\n }\n}") 68 | let fromJSON = try TestClass_notEmpty.decode(from: json) 69 | let fromString = try TestClass_notEmpty.decode(from: jsonString) 70 | 71 | assert(fromJSON.int == fakeData_object_int) 72 | assert(fromJSON.object.int == fakeData_object_class_int) 73 | assert(fromJSON.objectNil?.int == fakeData_object_struct_int) 74 | 75 | assert(fromJSON.object.string == fakeData_object_class_string) 76 | assert(fromJSON.objectNil?.string == fakeData_object_struct_string) 77 | 78 | assert(fromString.int == fakeData_object_int) 79 | assert(fromString.object.int == fakeData_object_class_int) 80 | assert(fromString.objectNil?.int == fakeData_object_struct_int) 81 | 82 | assert(fromString.object.string == fakeData_object_class_string) 83 | assert(fromString.objectNil?.string == fakeData_object_struct_string) 84 | } 85 | 86 | func testStruct() throws { 87 | let fakeData_object_int = Int.random(in: (0 ... 10000)) 88 | 89 | let fakeData_object_class_int = Int.random(in: (0 ... 10000)) 90 | let fakeData_object_class_string = "\(Int.random(in: (0 ... 10000)))" 91 | 92 | let fakeData_object_struct_int = Int.random(in: (0 ... 10000)) 93 | let fakeData_object_struct_string = "\(Int.random(in: (0 ... 10000)))" 94 | 95 | var object = TestStruct_notEmpty() 96 | 97 | object.int = fakeData_object_int 98 | 99 | object.object.int = fakeData_object_class_int 100 | object.object.string = fakeData_object_class_string 101 | 102 | object.objectNil = .init() 103 | object.objectNil?.int = fakeData_object_struct_int 104 | object.objectNil?.string = fakeData_object_struct_string 105 | 106 | let json = try object.toJSON() as NSDictionary 107 | let jsonString = try object.toJSONString(prettyPrint: false) 108 | 109 | assert(json == [ 110 | "int": fakeData_object_int, 111 | "object": [ 112 | "int": fakeData_object_class_int, 113 | "string": fakeData_object_class_string 114 | ], 115 | "objectNil": [ 116 | "int": fakeData_object_struct_int, 117 | "string": fakeData_object_struct_string 118 | ], 119 | "string": "" 120 | ]) 121 | assert(jsonString == "{\"object\":{\"int\":\(fakeData_object_class_int),\"string\":\"\(fakeData_object_class_string)\"},\"string\":\"\",\"int\":\(fakeData_object_int),\"objectNil\":{\"int\":\(fakeData_object_struct_int),\"string\":\"\(fakeData_object_struct_string)\"}}") 122 | assert(try object.toJSONString(prettyPrint: true) == "{\n \"object\" : {\n \"int\" : \(fakeData_object_class_int),\n \"string\" : \"\(fakeData_object_class_string)\"\n },\n \"string\" : \"\",\n \"int\" : \(fakeData_object_int),\n \"objectNil\" : {\n \"int\" : \(fakeData_object_struct_int),\n \"string\" : \"\(fakeData_object_struct_string)\"\n }\n}") 123 | let fromJSON = try TestStruct_notEmpty.decode(from: json) 124 | let fromString = try TestStruct_notEmpty.decode(from: jsonString) 125 | 126 | assert(fromJSON.int == fakeData_object_int) 127 | assert(fromJSON.object.int == fakeData_object_class_int) 128 | assert(fromJSON.objectNil?.int == fakeData_object_struct_int) 129 | 130 | assert(fromJSON.object.string == fakeData_object_class_string) 131 | assert(fromJSON.objectNil?.string == fakeData_object_struct_string) 132 | 133 | assert(fromString.int == fakeData_object_int) 134 | assert(fromString.object.int == fakeData_object_class_int) 135 | assert(fromString.objectNil?.int == fakeData_object_struct_int) 136 | 137 | assert(fromString.object.string == fakeData_object_class_string) 138 | assert(fromString.objectNil?.string == fakeData_object_struct_string) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Sources/HappyCodablePlugin/Macros.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Macros.swift 3 | // HappyCodable 4 | // 5 | // 6 | 7 | import Foundation 8 | import HappyCodableShared 9 | import SwiftDiagnostics 10 | import SwiftSyntax 11 | import SwiftSyntaxBuilder 12 | import SwiftSyntaxMacros 13 | 14 | public struct AttributePlaceholderMacro: PeerMacro { 15 | public static func expansion( 16 | of node: SwiftSyntax.AttributeSyntax, 17 | providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, 18 | in context: some SwiftSyntaxMacros.MacroExpansionContext 19 | ) throws -> [SwiftSyntax.DeclSyntax] { 20 | [] 21 | } 22 | } 23 | 24 | public struct HappyCodableMemberMacro: MemberMacro { 25 | class Conditions { 26 | var hasCodingKeys = false 27 | var hasInitFrom = false 28 | var hasInit = false 29 | var hasEncodeTo = false 30 | var codingKeysOverride: [String: String] = [:] 31 | } 32 | 33 | static func handleCodingKeys( 34 | decl: EnumDeclSyntax, 35 | into conditions: Conditions 36 | ) { 37 | conditions.hasCodingKeys = true 38 | decl.memberBlock.members.compactMap { 39 | $0.decl.as(EnumCaseDeclSyntax.self)?.elements 40 | } 41 | .joined() 42 | .forEach { `case` in 43 | let name = `case`.name.text 44 | let alterName: String 45 | guard let value = `case`.rawValue?.value else { 46 | return 47 | } 48 | if let stringValue = value.as(StringLiteralExprSyntax.self) { 49 | alterName = stringValue.segments.trimmedDescription 50 | } else if let intValue = value.as(IntegerLiteralExprSyntax.self) { 51 | alterName = intValue.trimmedDescription 52 | } else { 53 | return 54 | } 55 | conditions.codingKeysOverride[name] = alterName 56 | } 57 | } 58 | 59 | @available(macOS 13.0, iOS 16.0, *) 60 | static func checkCustomCodable( 61 | of member: MemberBlockItemSyntax, 62 | into conditions: Conditions 63 | ) { 64 | if let enumDecl = member.decl.as(EnumDeclSyntax.self), enumDecl.name.text == "CodingKeys" { 65 | handleCodingKeys(decl: enumDecl, into: conditions) 66 | } 67 | 68 | if let syntax = member.decl.as(InitializerDeclSyntax.self) { 69 | let rex = /\s*?\(\s*?from\s+?decoder\s*?:\s*?(Foundation\.)?Decoder\s*?\)\s+?throws\s*?/ 70 | let match = (try? rex.firstMatch(in: syntax.signature.description)) != nil 71 | if match { 72 | conditions.hasInitFrom = true 73 | } else { 74 | conditions.hasInit = true 75 | } 76 | } 77 | 78 | if ({ 79 | guard 80 | let syntax = member.decl.as(FunctionDeclSyntax.self), 81 | syntax.name.text == "encode" 82 | else { 83 | return false 84 | } 85 | 86 | let rex = /\s*?\(\s*?to\s+?encoder\s*?:\s*?(Foundation\.)?Encoder\s*?\)\s+?throws\s*?/ 87 | return (try? rex.firstMatch(in: syntax.signature.description)) != nil 88 | }()) { 89 | conditions.hasEncodeTo = true 90 | } 91 | } 92 | 93 | static func generateCodableItems( 94 | from memberList: MemberBlockItemListSyntax, 95 | in context: some MacroExpansionContext, 96 | into conditions: Conditions 97 | ) -> [CodableItem] { 98 | memberList.compactMap { member -> CodableItem? in 99 | // is a property 100 | guard 101 | let property = member.decl.as(VariableDeclSyntax.self), 102 | !property.modifiers.contains(where: { 103 | $0.name.tokenKind == .keyword(.static) 104 | }), 105 | property.bindings.count == 1, 106 | let propertyBinding = property.bindings.first, 107 | propertyBinding.accessorBlock?.is(AccessorBlockSyntax.self) != true, 108 | let propertyName = propertyBinding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text 109 | else { 110 | if #available(macOS 13.0, iOS 16.0, *) { 111 | checkCustomCodable(of: member, into: conditions) 112 | } 113 | return nil 114 | } 115 | 116 | var defaultValue: String? 117 | 118 | if let value = propertyBinding.initializer?.value { 119 | defaultValue = value.description 120 | } 121 | 122 | if defaultValue == nil, 123 | let propertyType = propertyBinding.typeAnnotation?.type.description.trimmingCharacters(in: .whitespacesAndNewlines), 124 | (propertyType.hasSuffix("?") || propertyType.hasPrefix("Optional<")) { 125 | defaultValue = "nil" 126 | } 127 | if defaultValue == nil, 128 | propertyBinding.accessorBlock?.is(CodeBlockSyntax.self) != true { 129 | context.diagnose( 130 | Diagnostic( 131 | node: Syntax(property), 132 | message: SimpleDiagnosticMessage( 133 | message: "property \(propertyName) has no default value, decode failure will throw an error, consider giving it a default value or changing it to Optional?", 134 | diagnosticID: MessageID(domain: "HappyCodable", id: "warning"), 135 | severity: .warning 136 | ) 137 | ) 138 | ) 139 | } 140 | 141 | var item = CodableItem(name: propertyName, defaultValue: defaultValue) 142 | let attributes = property.attributes.compactMap { 143 | $0.as(AttributeSyntax.self) 144 | } 145 | for attribute in attributes { 146 | let arguments = attribute.arguments?.as(LabeledExprListSyntax.self) 147 | let encodeArgument = arguments?["encode"]?.description 148 | let decodeArgument = arguments?["decode"]?.description 149 | 150 | switch "\(attribute.attributeName)" { 151 | case "AlterCodingKeys": 152 | guard let arguments = arguments?.map(\.expression), !arguments.isEmpty else { 153 | context.diagnose( 154 | Diagnostic( 155 | node: Syntax(attribute), 156 | message: SimpleDiagnosticMessage( 157 | message: "Alter keys is empty, consider removing it", 158 | diagnosticID: MessageID(domain: "HappyCodable", id: "warning"), 159 | severity: .warning 160 | ) 161 | ) 162 | ) 163 | break 164 | } 165 | item.alterKeys = arguments.map { 166 | "\($0)" 167 | } 168 | case "DataStrategy": 169 | item.dataStrategy = .init( 170 | encode: encodeArgument ?? ".deferredToData", 171 | decode: decodeArgument ?? ".deferredToData" 172 | ) 173 | case "DateStrategy": 174 | item.dateStrategy = .init( 175 | encode: encodeArgument ?? ".deferredToDate", 176 | decode: decodeArgument ?? ".deferredToDate" 177 | ) 178 | case "FloatStrategy": 179 | item.floatStrategy = .init( 180 | encode: encodeArgument ?? ".throw", 181 | decode: decodeArgument ?? ".throw" 182 | ) 183 | case "ElementNullable": 184 | item.elementNullable = true 185 | case "Uncoding": 186 | item.uncoding = true 187 | if item.defaultValue == nil { 188 | context.diagnose( 189 | Diagnostic( 190 | node: Syntax(attribute), 191 | message: SimpleDiagnosticMessage( 192 | message: "Uncoding requires a default value", 193 | diagnosticID: MessageID(domain: "HappyCodable", id: "warning"), 194 | severity: .error 195 | ) 196 | ) 197 | ) 198 | } 199 | default: 200 | break 201 | } 202 | } 203 | return item 204 | } 205 | } 206 | 207 | public static func expansion( 208 | of node: AttributeSyntax, 209 | providingMembersOf declaration: some DeclGroupSyntax, 210 | in context: some MacroExpansionContext 211 | ) throws -> [DeclSyntax] { 212 | var disableWarnings: Set = [] 213 | node.arguments?.as(LabeledExprListSyntax.self)?.forEach { 214 | if $0.label?.text == "disableWarnings" { 215 | guard let arrayExpr = $0.expression.as(ArrayExprSyntax.self) else { 216 | context.diagnose( 217 | Diagnostic( 218 | node: Syntax(node), 219 | message: SimpleDiagnosticMessage( 220 | message: "disableWarnings only support static Array literal", 221 | diagnosticID: MessageID(domain: "HappyCodable", id: "warning"), 222 | severity: .warning 223 | ) 224 | ) 225 | ) 226 | return 227 | } 228 | arrayExpr 229 | .elements 230 | .forEach { 231 | guard 232 | let name = $0.expression.description.split(separator: ".").last, 233 | let warning = Warnings(rawValue: String(name)) 234 | else { 235 | return 236 | } 237 | disableWarnings.insert(warning) 238 | } 239 | } 240 | } 241 | if declaration.is(EnumDeclSyntax.self) { 242 | context.diagnose( 243 | Diagnostic( 244 | node: Syntax(node), 245 | message: SimpleDiagnosticMessage( 246 | message: "HappyCodable has no effect on Enum, consider removing it", 247 | diagnosticID: MessageID(domain: "HappyCodable", id: "warning"), 248 | severity: .warning 249 | ) 250 | ) 251 | ) 252 | return [] 253 | } 254 | guard 255 | let inheritedTypes = (declaration as? DeclSyntaxProtocolHelper)?.inheritedTypes, 256 | let hasHappyEncodable = !inheritedTypes.isDisjoint(with: [ "HappyCodable", "HappyEncodable"]) as Bool?, 257 | let hasHappyDecodable = !inheritedTypes.isDisjoint(with: ["HappyCodable", "HappyDecodable"]) as Bool?, 258 | (hasHappyEncodable || hasHappyDecodable) 259 | else { 260 | context.diagnose( 261 | Diagnostic( 262 | node: Syntax(node), 263 | message: SimpleDiagnosticMessage( 264 | message: "Missing HappyCodable, HappyEncodable or HappyDecodable", 265 | diagnosticID: MessageID(domain: "HappyCodable", id: "error"), 266 | severity: .error 267 | ) 268 | ) 269 | ) 270 | return [] 271 | } 272 | 273 | let memberList = declaration.memberBlock.members 274 | 275 | let conditions = Conditions() 276 | 277 | let codableItems: [CodableItem] = generateCodableItems(from: memberList, in: context, into: conditions) 278 | 279 | let needCoder = !codableItems.allSatisfy { 280 | $0.uncoding 281 | } 282 | 283 | var codingKeys: DeclSyntax? 284 | var initcoder: DeclSyntax? 285 | var encodeto: DeclSyntax? 286 | 287 | let modifier: String 288 | if declaration.is(ClassDeclSyntax.self) { 289 | modifier = "required" 290 | } else { 291 | modifier = "" 292 | } 293 | 294 | if !conditions.hasInit, disableWarnings.isDisjoint(with: [.all, .noInitializer]) { 295 | context.diagnose( 296 | Diagnostic( 297 | node: Syntax(declaration), 298 | message: SimpleDiagnosticMessage( 299 | message: "Due to the limitation, @HappyCodable causes the autosynthesis initializer to fail, if you don't need a initializer, you can use @HappyCodable(disableWarnings: [.noInitializer])", 300 | diagnosticID: MessageID(domain: "HappyCodable", id: "warning"), 301 | severity: .warning 302 | ) 303 | ) 304 | ) 305 | } 306 | 307 | let itemCodingKeys = codableItems.compactMap { 308 | $0.codingKey(override: conditions.codingKeysOverride) 309 | } 310 | 311 | if !conditions.hasCodingKeys, !itemCodingKeys.isEmpty { 312 | codingKeys = """ 313 | 314 | enum CodingKeys: Swift.String, Swift.CodingKey { 315 | \(raw: itemCodingKeys.joined(separator: "\n")) 316 | } 317 | 318 | """ 319 | } 320 | 321 | if !conditions.hasInitFrom, hasHappyDecodable { 322 | initcoder = """ 323 | 324 | public \(raw: modifier) init(from decoder: Swift.Decoder) throws { 325 | \(raw: "let container = try decoder.container(keyedBy: Swift.String.self)", enable: needCoder) 326 | \(raw: "var errors = [Swift.Error]()", enable: needCoder) 327 | \(raw: codableItems.map(\.decode).joined(separator: "\n")) 328 | \(raw: "Self.decodeHelper.errorsReporter?(errors)", enable: needCoder) 329 | \(raw: "self.didFinishDecoding()") 330 | } 331 | 332 | """ 333 | } 334 | 335 | if !conditions.hasEncodeTo, hasHappyEncodable { 336 | // `_ = encoder.container(keyedBy: Swift.String.self)` is added for removing "\(type) did not encode any values" error 337 | encodeto = """ 338 | 339 | public func encode(to encoder: Swift.Encoder) throws { 340 | \(raw: "self.willStartEncoding()") 341 | \(raw: "var container = encoder.container(keyedBy: Swift.String.self)", enable: needCoder) 342 | \(raw: "_ = encoder.container(keyedBy: Swift.String.self)", enable: !needCoder) 343 | \(raw: "var errors = [Swift.Error]()", enable: needCoder) 344 | \(raw: codableItems.map(\.encode).joined(separator: "\n")) 345 | \(raw: "Self.encodeHelper.errorsReporter?(errors)", enable: needCoder) 346 | } 347 | 348 | """ 349 | } 350 | 351 | return [ 352 | codingKeys, 353 | initcoder, 354 | encodeto 355 | ].compactMap { 356 | $0 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /Plugins/Lint/swiftlint.yaml: -------------------------------------------------------------------------------- 1 | swiftlint_version: 0.52.2 2 | 3 | disabled_rules: 4 | # TODOs and FIXMEs should be avoided. 5 | - todo 6 | # SwiftUI use let _ frequently 7 | - redundant_discardable_let 8 | 9 | # Complexity of function bodies should be limited. 10 | # This can now be configured to ignore case statements, so we should enable 11 | - cyclomatic_complexity 12 | 13 | - function_parameter_count 14 | 15 | # Combine multiple pattern matching bindings by moving keywords out of tuples. 16 | - pattern_matching_keywords 17 | 18 | # Length limit for file/type/method 19 | - file_length 20 | - type_body_length 21 | - function_body_length 22 | 23 | # where clauses are preferred over a single if inside a for. 24 | - for_where 25 | 26 | # Trailing closure syntax should not be used when passing more than one closure argument. 27 | - multiple_closures_with_trailing_closure 28 | 29 | # Too many false alert, it has been marked as off by default in 0.50 30 | - weak_delegate 31 | 32 | # A doc comment should be attached to a declaration 33 | - orphaned_doc_comment 34 | # Enum cases should be sorted 35 | - sorted_enum_cases 36 | 37 | - trailing_comma 38 | 39 | analyzer_rules: 40 | # attempts to find internal/public/open/private/fileprivate declarations that are not used 41 | - unused_declaration 42 | 43 | # All imported modules should be required to make the file compile. 44 | - unused_import 45 | 46 | opt_in_rules: 47 | # prefer optional wrapping, optional chaining, or more constrained types within the codebase. 48 | - force_unwrapping 49 | 50 | # Prefer using Array(seq) than seq.map { $0 } to convert a sequence into an Array. 51 | - array_init 52 | 53 | # Closure end should have the same indentation as the line that started it. 54 | - closure_end_indentation 55 | 56 | # Closure expressions should have a single space inside each brace. 57 | - closure_spacing 58 | 59 | # All elements in a collection literal should be vertically aligned 60 | - collection_alignment 61 | 62 | # Conditional statements should always return on the next line 63 | - conditional_returns_on_newline 64 | 65 | # prefer `contains` over comparing `filter(where:).count` to 0 66 | - contains_over_filter_count 67 | 68 | # prefer `contains` over comparing `filter(where:).isEmpty` 69 | - contains_over_filter_is_empty 70 | 71 | # prefer contains(where:) over first(where:) != nil 72 | - contains_over_first_not_nil 73 | 74 | # prefer contains over comparison of `range(of:)` to `nil` 75 | - contains_over_range_nil_comparison 76 | 77 | # Types used for hosting only static members should be implemented as a caseless enum to avoid instantiation. 78 | - convenience_type 79 | 80 | # as we move to block based vs @objc based notification handling this will come in handy. 81 | - discarded_notification_center_observer 82 | 83 | # Encourages initializers over object literals 84 | - discouraged_object_literal 85 | 86 | # Prefer checking `isEmpty` over comparing collection to an empty array or dictionary literal 87 | - empty_collection_literal 88 | 89 | # Prefer checking `isEmpty` over comparing `count` to zero. 90 | - empty_count 91 | 92 | # Prefer checking `.isEmpty` on strings over comparing against "" 93 | - empty_string 94 | 95 | # Empty XCTest methods should be avoided. 96 | - empty_xctest_method 97 | 98 | # Explicitly calling .init() should be avoided. 99 | - explicit_init 100 | 101 | # should be avoided 102 | - fallthrough 103 | 104 | # ? 105 | - file_header 106 | 107 | # File name should not contain any whitespace. 108 | - file_name_no_space 109 | 110 | # Prefer using `.first(where:)` over `.filter { }.first` in collections. 111 | - first_where 112 | 113 | # Prefer handling thrown errors or optional binding instead of try! 114 | - force_try 115 | 116 | # Prefer using `flatMap` over `map { ... }.reduce([], +)` 117 | - flatmap_over_map_reduce 118 | 119 | # Comparing two identical operands is likely a mistake. 120 | - identical_operands 121 | 122 | # Discouraged explicit usage of the default separator. 123 | - joined_default_parameter 124 | 125 | # encourage the use of Swift 4.2's new hashing interface. 126 | - legacy_hashing 127 | 128 | # Prefer using `type.random(in:)` over legacy functions. 129 | - legacy_random 130 | 131 | # Prefer using the isMultiple(of:) function instead of using the remainder operator (%). 132 | - legacy_multiple 133 | 134 | # Array and dictionary literal end should have the same indentation as the line that started it. 135 | - literal_expression_end_indentation 136 | 137 | # Ensure declarations have a lower access control level than their enclosing parent 138 | - lower_acl_than_parent 139 | 140 | # MARK comment should be in valid format. 141 | - mark 142 | 143 | # Modifier order should be consistent 144 | - modifier_order 145 | 146 | # Chained function calls should be either on the same line, or one per line. 147 | - multiline_function_chains 148 | 149 | # nsobject subclasses should override isequals. not == 150 | - nsobject_prefer_isequal 151 | 152 | # Prefer not to use extension access modifiers 153 | - no_extension_access_modifier 154 | 155 | # Matching an enum case against an optional enum without '?' is supported on Swift 5.1 and above. 156 | - optional_enum_case_matching 157 | 158 | # Some overridden methods should always call super 159 | - overridden_super_call 160 | 161 | # Operators should be surrounded by a single whitespace when they are being used. 162 | - operator_usage_whitespace 163 | 164 | # Combine multiple pattern matching bindings by moving keywords out of tuples. 165 | - pattern_matching_keywords 166 | 167 | # Prefer `Self` over `type(of: self)` when accessing properties or calling methods. 168 | - prefer_self_type_over_type_of_self 169 | 170 | # Prefer `.zero` over explicit init with zero parameters (e.g. CGPoint(x: 0, y: 0)`) 171 | - prefer_zero_over_explicit_init 172 | 173 | # Encourages top level constants to be prefixed by k 174 | - prefixed_toplevel_constant 175 | 176 | # Checks that all IBActions are private rather than public 177 | - private_action 178 | 179 | # Creating views using Interface Builder should be avoided. 180 | - prohibited_interface_builder 181 | 182 | # Some methods should not call super 183 | - prohibited_super_call 184 | 185 | # prefer simpler constructs over reduce(Boolean). 186 | - reduce_boolean 187 | 188 | # mutable reference can be faster than repeated copying 189 | - reduce_into 190 | 191 | # nil coalescing operator is only evaluated if the lhs is nil, coalescing operator with nil as rhs is redundant 192 | - redundant_nil_coalescing 193 | 194 | # Test files should contain a single QuickSpec or XCTestCase class. 195 | - single_test_class 196 | 197 | # Prefer using min() or max() over sorted().first or sorted().last 198 | - sorted_first_last 199 | 200 | # Imports should be sorted. 201 | - sorted_imports 202 | 203 | # Operators should be declared as static functions, not free functions. 204 | - static_operator 205 | 206 | # Prefer someBool.toggle() over someBool = !someBool. 207 | - toggle_bool 208 | 209 | # Parentheses are not needed when declaring closure arguments. 210 | - unneeded_parentheses_in_closure_argument 211 | 212 | # Unimplemented functions should be marked as unavailable. 213 | - unavailable_function 214 | 215 | # Prefer capturing references as weak to avoid potential crashes. 216 | - unowned_variable_capture 217 | 218 | # Catch statements should not declare error variables without type casting. 219 | - untyped_error_in_catch 220 | 221 | # Checks for yoda conditions 222 | - yoda_condition 223 | 224 | # Identifier names should only contain alphanumeric characters and start with 225 | # a lowercase character or should only contain capital letters. In an 226 | # exception to the above, variable names may start with a capital letter when 227 | # they are declared static and immutable. Variable names should not be too 228 | # long or too short. 229 | # Gives us some extra optimization, so should be enabled once it is generating 230 | # less false positives 231 | # Currently failing due to upper case enum values 232 | - identifier_name 233 | 234 | # A fatalError call should have a message. 235 | - fatal_error_message 236 | 237 | # Multiline arguments should have their surrounding brackets in a new line. 238 | - multiline_arguments_brackets 239 | 240 | # Multiline literals should have their surrounding brackets in a new line. 241 | - multiline_literal_brackets 242 | 243 | # Multiline parameters should have their surrounding brackets in a new line. 244 | - multiline_parameters_brackets 245 | 246 | # Trailing closure syntax should be used whenever possible. 247 | - trailing_closure 248 | 249 | # Function parameters should be aligned vertically if they're in multiple lines in a method call. 250 | - vertical_parameter_alignment_on_call 251 | 252 | # Prefer specific XCTest matchers over XCTAssertEqual and XCTAssertNotEqual 253 | - xct_specific_matcher 254 | 255 | #Use commas to separate types in inheritance lists 256 | - comma_inheritance 257 | 258 | # Re-bind self to a consistent identifier name. 259 | - self_binding 260 | 261 | # Directly return the expression instead of assigning it to a variable first 262 | - direct_return 263 | 264 | # Errors thrown inside this task are not handled, which may be unexpected. Handle errors inside the task, or use try await to access the Tasks value and handle errors. See this forum thread for more details: https://forums.swift.org/t/task-initializer-with-throwing-closure-swallows-error/56066 265 | - unhandled_throwing_task 266 | 267 | identifier_name: 268 | min_length: 1 269 | max_length: 270 | warning: 60 271 | error: 80 272 | allowed_symbols: ["_"] 273 | validates_start_with_lowercase: error 274 | 275 | line_length: 276 | warning: 400 277 | error: 400 278 | 279 | nesting: 280 | type_level: 2 281 | function_level: 5 282 | 283 | type_name: 284 | max_length: 285 | warning: 75 286 | error: 75 287 | allowed_symbols: [_] 288 | 289 | file_header: 290 | required_pattern: | 291 | // 292 | // SWIFTLINT_CURRENT_FILENAME 293 | // HappyCodable 294 | // 295 | // 296 | 297 | # Make DynamicProperty always be private 298 | # Modified from https://gist.github.com/chriseidhof/d8c079ca97099a6122f37890a144e9b0 299 | # Using $value to get the Binding and modifying it in the external view will get this error: 300 | # Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update. 301 | # Bingding and ObservedObject were removed because both can be held separately outside of the View 302 | # Although the StateObject can be accessed outside of the View, but there will be an error: "Accessing StateObject's object without being installed on a View. This will create a new instance each time." 303 | custom_rules: 304 | DynamicProperty_private: 305 | included: .*\.swift 306 | name: "Private SwiftUI DynamicProperty" 307 | regex: "(?-s)@(AccessibilityFocusState|AppStorage|EnvironmentObject|FocusState|FocusedObject|FocusedObject|GestureState|Namespace|ScaledMetric|SceneStorage|State|StateObject)\\s*?\\n?(\\s|\\t)*?(var|public|internal|open)" 308 | message: "SwiftUI DynamicProperty variables should always be marked private." 309 | severity: error 310 | DynamicProperty_private_argument: 311 | included: .*\.swift 312 | name: "Private SwiftUI DynamicProperty" 313 | regex: "(?-s)@(Environment|FetchRequest|FocusedValue|GestureState|NSApplicationDelegateAdaptor|ScaledMetric|SectionedFetchRequest)\\(.*\\)\\s*?\\n?(\\s|\\t)*?(var|public|internal|open)" 314 | message: "SwiftUI DynamicProperty(with argument) variables should always be marked private." 315 | severity: error 316 | 317 | closure_parameter_parentheses: 318 | included: .*\.swift 319 | message: 320 | Closures with single parameters should not have these parameters enclosed 321 | in parentheses 322 | name: Closure Parameter Parentheses 323 | regex: \{ *\([^,:]*\) *in *$ 324 | severity: error 325 | dispatch_execute: 326 | included: .*\.swift 327 | message: 328 | Don't specify the closure argument explicitly when executing on a DispatchQueue. 329 | Use the implicit form instead. 330 | name: Dispatch Execute 331 | regex: '\.a?sync\(execute\: \{' 332 | severity: error 333 | equality_parens: 334 | included: .*\.swift 335 | message: Prefer != instead of wrapping it in parentheses 336 | name: Don't unnecessarily wrap inequality statements 337 | regex: '!\([^{&|]+? ===? [^}&|]+?\)' 338 | image_literals: 339 | included: .*\.swift 340 | message: Don't use image literals. 341 | name: Image Literals 342 | regex: \#imageLiteral\( 343 | severity: error 344 | implicit_unwrapped_method_parameter: 345 | included: .*\.swift 346 | message: Avoid use of Implicitly Unwrapped Optional parameters. 347 | name: Implicit Unwrapped Optional Method Parameter 348 | regex: '(\: [A-Z][a-zA-Z]+\!(,|\)+))|(\n\s+[a-zA-Z]+\: [a-zA-Z]+\!\n)|(\->\sA-Z][a-zA-Z]+\!)' 349 | severity: error 350 | init_space: 351 | included: .*\.swift 352 | message: Remove the extra space after `init` 353 | name: Extra space after init 354 | regex: init \( 355 | notification_name: 356 | included: .*\.swift 357 | message: 358 | Notification.Name(\"notification\") is the preferred way to define notification 359 | Names 360 | name: Use Notification.Name() 361 | regex: (Notification.Name\(rawValue)|(let\s\w+Notification = \"\w) 362 | severity: error 363 | notification_name_observer_shorthand: 364 | included: .*\.swift 365 | message: 366 | 'Prefer the more concise `.addObserver(name: .` vs `.addObserver(name: 367 | Notification.Name.`"' 368 | name: Use shorthand notification name 369 | regex: 'addObserver\(.*, name: Notification\.Name\.' 370 | severity: error 371 | notification_name_shorthand: 372 | included: .*\.swift 373 | message: "Prefer the more concise `.post(name: .` vs `.post(name: Notification.Name.`" 374 | name: Use shorthand notification name 375 | regex: '\.post\(name: Notification\.Name\.' 376 | severity: error 377 | ns_notification: 378 | included: .*\.swift 379 | message: Prefer Notification over NSNotification 380 | name: Notification over NSNotification 381 | regex: NSNotification 382 | severity: error 383 | opening_brace: 384 | included: .*\.swift 385 | match_kinds: 386 | - argument 387 | - identifier 388 | - keyword 389 | - number 390 | - objectliteral 391 | - parameter 392 | - placeholder 393 | - typeidentifier 394 | message: 395 | Opening braces should only be succeeded by a space, a new line or a closing 396 | brace. 397 | name: Opening Brace 398 | regex: \{[^ \r\n\}] 399 | severity: error 400 | prefer_modern_kvo: 401 | included: .*\.swift 402 | message: 403 | '"Instead of .addObserver(...) with string types prefer .observe(...) with 404 | strong Key-Path expressions."' 405 | name: Prefer modern KVO in swift 406 | regex: 'addObserver\(\w*,\sforKeyPath\:' 407 | severity: error 408 | prefer_ui_graphics_image_renderer: 409 | included: .*\.swift 410 | message: Prefer using `UIGraphicsImageRenderer` over `UIGraphicsBeginImageContextWithOptions`" 411 | name: Use UIGraphicsImageRenderer 412 | regex: UIGraphicsBeginImageContextWithOptions 413 | severity: error 414 | redundant_ternary: 415 | included: .*\.swift 416 | message: 417 | "Don't use a ternary where none is required. E.g. `X ? false : true` can 418 | be shortened to just `!X`" 419 | name: Redundant ternary 420 | regex: ' *? *(?:true|false) *\: *(?:true|false)' 421 | severity: error 422 | set_animations_enabled: 423 | included: .*\.swift 424 | message: 425 | Calling UIView.setAnimationsEnabled will enable/disable animations in the entire app. 426 | This should be avoided. 427 | name: Avoid calling UIView.setAnimationsEnabled 428 | regex: \.setAnimationsEnabled\( 429 | severity: error 430 | unnecessary_internal: 431 | included: .*\.swift 432 | message: Internal access level is the default, no need to specify it. 433 | name: Unnecessary use of internal 434 | regex: "internal (var|let|func|static) " 435 | unsafely_unwrapped: 436 | included: .*\.swift 437 | message: Avoid use of unsafelyUnwrapped. 438 | name: Unsafely Unwrapped 439 | regex: \.unsafelyUnwrapped 440 | severity: error 441 | use_concise_file_literals: 442 | excluded: .*(Test|Tests|TestCase|XCTAssertContains)\.swift 443 | included: .*\.swift 444 | message: 445 | "Avoid overly verbose filePath literals use #file #file is less verbose 446 | and results in smaller binaries" 447 | name: Avoid overly verbose filePath literals 448 | regex: "#filePath" 449 | severity: error 450 | xctassert_equal: 451 | included: .*\.swift 452 | message: 453 | XCTassertEqual will show the value that failed making it easier to debug 454 | failing test cases than XCTAssert( == ). 455 | name: Use XCTAssertEqual 456 | regex: (XCTAssert|XCTAssertTrue)\([^{\n]+? == .+?\) 457 | severity: error 458 | xctassert_fail: 459 | included: .*\.swift 460 | message: Prefer XCTFail over XCTAssertTrue(false). 461 | name: Use XCTFail 462 | regex: (XCTAssert|XCTAssertTrue)\(false 463 | severity: error 464 | xctassert_pass: 465 | included: .*\.swift 466 | message: Asserting true is unnecessary 467 | name: Unnecessary pass 468 | regex: (XCTAssert|XCTAssertTrue)\(true[,)]+ 469 | severity: error 470 | yyyy_date_format: 471 | included: .*\.swift 472 | message: Usage of 'YYYY' is usually incorrect. Use 'yyyy' instead. 473 | name: YYYYDateFormat 474 | regex: YYYY 475 | severity: error 476 | PreferIndentUsingSpace: 477 | included: .*\.swift 478 | name: "Indent using spaces" 479 | regex: "^ " 480 | message: "Indent using tab instead of space. Please change your Xcode default indentation to 4 width tab" 481 | severity: error 482 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Sources/HappyCodable/extension/KeyedDecodingContainer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyedDecodingContainer.swift 3 | // HappyCodable 4 | // 5 | // 6 | 7 | import Foundation 8 | 9 | // MARK: - Interfaces 10 | extension KeyedDecodingContainer { 11 | // MARK: - Decodable 12 | @inlinable 13 | @inline(__always) 14 | @_disfavoredOverload 15 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> T where T: Decodable { 16 | try decodeDecodable(mainKey: key, alterKeys: alterKeys, optional: false) 17 | } 18 | @inlinable 19 | @inline(__always) 20 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> T? where T: Decodable { 21 | try decodeDecodable(mainKey: key, alterKeys: alterKeys, optional: true) as T 22 | } 23 | 24 | // MARK: - Bool 25 | @inlinable 26 | @inline(__always) 27 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Bool? { 28 | try decodeBool(mainKey: key, alterKeys: alterKeys, optional: true) 29 | } 30 | @inlinable 31 | @inline(__always) 32 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Bool { 33 | try decodeBool(mainKey: key, alterKeys: alterKeys, optional: false) 34 | } 35 | 36 | // MARK: - String 37 | @inlinable 38 | @inline(__always) 39 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> String? { 40 | try decodeString(mainKey: key, alterKeys: alterKeys, optional: true) 41 | } 42 | @inlinable 43 | @inline(__always) 44 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> String { 45 | try decodeString(mainKey: key, alterKeys: alterKeys, optional: false) 46 | } 47 | 48 | // MARK: - Data 49 | @inlinable 50 | @inline(__always) 51 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Data? { 52 | try decodeData(mainKey: key, alterKeys: alterKeys, optional: true) 53 | } 54 | @inlinable 55 | @inline(__always) 56 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Data { 57 | try decodeData(mainKey: key, alterKeys: alterKeys, optional: false) 58 | } 59 | 60 | // MARK: - Double 61 | @inlinable 62 | @inline(__always) 63 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Double? { 64 | try decodeDouble(mainKey: key, alterKeys: alterKeys, optional: true, targetType: Double.self) 65 | } 66 | @inlinable 67 | @inline(__always) 68 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Double { 69 | try decodeDouble(mainKey: key, alterKeys: alterKeys, optional: false, targetType: Double.self) 70 | } 71 | 72 | // MARK: - Float 73 | @inlinable 74 | @inline(__always) 75 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Float? { 76 | try decodeDouble(mainKey: key, alterKeys: alterKeys, optional: true, targetType: Float.self) 77 | } 78 | @inlinable 79 | @inline(__always) 80 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Float { 81 | try decodeDouble(mainKey: key, alterKeys: alterKeys, optional: false, targetType: Float.self) 82 | } 83 | 84 | // MARK: - Int 85 | @inlinable 86 | @inline(__always) 87 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int? { 88 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: Int.self) 89 | } 90 | @inlinable 91 | @inline(__always) 92 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int { 93 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: Int.self) 94 | } 95 | 96 | // MARK: - Int8 97 | @inlinable 98 | @inline(__always) 99 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int8? { 100 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: Int8.self) 101 | } 102 | @inlinable 103 | @inline(__always) 104 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int8 { 105 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: Int8.self) 106 | } 107 | 108 | // MARK: - Int16 109 | @inlinable 110 | @inline(__always) 111 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int16? { 112 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: Int16.self) 113 | } 114 | @inlinable 115 | @inline(__always) 116 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int16 { 117 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: Int16.self) 118 | } 119 | 120 | // MARK: - Int32 121 | @inlinable 122 | @inline(__always) 123 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int32? { 124 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: Int32.self) 125 | } 126 | @inlinable 127 | @inline(__always) 128 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int32 { 129 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: Int32.self) 130 | } 131 | 132 | // MARK: - Int64 133 | @inlinable 134 | @inline(__always) 135 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int64? { 136 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: Int64.self) 137 | } 138 | @inlinable 139 | @inline(__always) 140 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> Int64 { 141 | try decodeInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: Int64.self) 142 | } 143 | 144 | // MARK: - UInt 145 | @inlinable 146 | @inline(__always) 147 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt? { 148 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: UInt.self) 149 | } 150 | @inlinable 151 | @inline(__always) 152 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt { 153 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: UInt.self) 154 | } 155 | 156 | // MARK: - UInt8 157 | @inlinable 158 | @inline(__always) 159 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt8? { 160 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: UInt8.self) 161 | } 162 | @inlinable 163 | @inline(__always) 164 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt8 { 165 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: UInt8.self) 166 | } 167 | 168 | // MARK: - UInt16 169 | @inlinable 170 | @inline(__always) 171 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt16? { 172 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: UInt16.self) 173 | } 174 | @inlinable 175 | @inline(__always) 176 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt16 { 177 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: UInt16.self) 178 | } 179 | 180 | // MARK: - UInt32 181 | @inlinable 182 | @inline(__always) 183 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt32? { 184 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: UInt32.self) 185 | } 186 | @inlinable 187 | @inline(__always) 188 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt32 { 189 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: UInt32.self) 190 | } 191 | 192 | // MARK: - UInt64 193 | @inlinable 194 | @inline(__always) 195 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt64? { 196 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: true, targetType: UInt64.self) 197 | } 198 | @inlinable 199 | @inline(__always) 200 | public func decode(key: String, alterKeys: (() -> [String])? = nil) throws -> UInt64 { 201 | try decodeUInt(mainKey: key, alterKeys: alterKeys, optional: false, targetType: UInt64.self) 202 | } 203 | } 204 | 205 | @usableFromInline var _iso8601Formatter: ISO8601DateFormatter = { 206 | let formatter = ISO8601DateFormatter() 207 | formatter.formatOptions = .withInternetDateTime 208 | return formatter 209 | }() 210 | 211 | // MARK: - Strategy implementation 212 | extension KeyedDecodingContainer { 213 | @inlinable 214 | @inline(__always) 215 | public func decode(key: String, alterKeys: (() -> [String])? = nil, strategy: Foundation.JSONDecoder.DataDecodingStrategy) throws -> Data { 216 | switch strategy { 217 | case .deferredToData: 218 | return try decode(key: key, alterKeys: alterKeys) 219 | case .base64: 220 | let string: String = try decode(key: key, alterKeys: alterKeys) 221 | guard let data = Data(base64Encoded: string) else { 222 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath + [key], debugDescription: "Encountered Data is not valid Base64.")) 223 | } 224 | return data 225 | case .custom(let closure): 226 | return try closure(superDecoder(forKey: key)) 227 | @unknown default: 228 | fatalError("not implemented") 229 | } 230 | } 231 | 232 | @inlinable 233 | @inline(__always) 234 | public func decode(key: String, alterKeys: (() -> [String])? = nil, strategy: Foundation.JSONDecoder.DateDecodingStrategy) throws -> Date { 235 | switch strategy { 236 | case .deferredToDate: 237 | return Date(timeIntervalSinceReferenceDate: try decode(key: key, alterKeys: alterKeys)) 238 | case .secondsSince1970: 239 | return Date(timeIntervalSince1970: try decode(key: key, alterKeys: alterKeys)) 240 | case .millisecondsSince1970: 241 | return Date(timeIntervalSince1970: try decode(key: key, alterKeys: alterKeys) / 1000.0) 242 | case .iso8601: 243 | let string = try decode(Swift.String.self, forKey: key) 244 | guard let date = _iso8601Formatter.date(from: string) else { 245 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath + [key], debugDescription: "Expected date string to be ISO8601-formatted.")) 246 | } 247 | 248 | return date 249 | case .formatted(let formatter): 250 | let string = try decode(Swift.String.self, forKey: key) 251 | guard let date = formatter.date(from: string) else { 252 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath + [key], debugDescription: "Date string does not match format expected by formatter.")) 253 | } 254 | 255 | return date 256 | case .custom(let closure): 257 | return try closure(superDecoder(forKey: key)) 258 | @unknown default: 259 | fatalError("not implemented") 260 | } 261 | } 262 | 263 | // copy from https://github.com/apple/swift-corelibs-foundation/blob/0ac1a34ab76f6db3196a279c2626a0e4554ff592/Sources/Foundation/JSONDecoder.swift#L413 264 | @inlinable 265 | @inline(__always) 266 | public func decode(key: String, alterKeys: (() -> [String])? = nil, strategy: Foundation.JSONDecoder.NonConformingFloatDecodingStrategy) throws -> T where T: BinaryFloatingPoint & LosslessStringConvertible { 267 | 268 | if let number: Double = try? decode(key: key, alterKeys: alterKeys) { 269 | let floatingPoint = T(number) 270 | guard floatingPoint.isFinite else { 271 | throw DecodingError.dataCorrupted( 272 | .init( 273 | codingPath: codingPath + [key], 274 | debugDescription: "Parsed JSON number <\(number)> does not fit in \(Float.self)." 275 | ) 276 | ) 277 | } 278 | 279 | return floatingPoint 280 | } 281 | 282 | if let string: String = try? decode(key: key, alterKeys: alterKeys), 283 | case .convertFromString(let posInfString, let negInfString, let nanString) = strategy { 284 | if string == posInfString { 285 | return T.infinity 286 | } else if string == negInfString { 287 | return -T.infinity 288 | } else if string == nanString { 289 | return T.nan 290 | } 291 | } 292 | 293 | throw DecodingError.typeMismatch(T.self, .init( 294 | codingPath: codingPath + [key], 295 | debugDescription: "Expected to decode \(T.self) but found Unknown type instead." 296 | )) 297 | } 298 | 299 | @inlinable 300 | @inline(__always) 301 | public func decode(key: String, alterKeys: (() -> [String])? = nil, strategy: Void) throws -> [T] where T: Decodable { 302 | let elements: [T?] = try decode(key: key, alterKeys: alterKeys) 303 | return elements.compactMap { $0 } 304 | } 305 | } 306 | 307 | // MARK: - Generic implementation 308 | extension KeyedDecodingContainer { 309 | // MARK: - Sub Interface 310 | // MARK: - Decodable 311 | @inlinable 312 | @inline(__always) 313 | func decodeDecodable(mainKey: String, alterKeys: (() -> [String])?, optional: Bool) throws -> T where T: Decodable { 314 | func _decode(key: String) throws -> T { 315 | if optional { 316 | if let value = try decodeIfPresent(T.self, forKey: key) { 317 | return value 318 | } else { 319 | throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [key], debugDescription: "Expected \(T.self) value but found nothing.")) 320 | } 321 | } else { 322 | return try decode(T.self, forKey: key) 323 | } 324 | } 325 | do { 326 | return try _decode(key: mainKey) 327 | } catch { 328 | if let alterKeys = alterKeys?() { 329 | for key in alterKeys where contains(key) { 330 | if let value = try? _decode(key: key) { 331 | return value 332 | } 333 | } 334 | } 335 | 336 | throw error 337 | } 338 | } 339 | // MARK: - Bool 340 | @inlinable 341 | @inline(__always) 342 | func decodeBool(mainKey: String, alterKeys: (() -> [String])?, optional: Bool) throws -> Bool { 343 | func _decode(key: String) throws -> Bool { 344 | do { 345 | if optional { 346 | if let value = try decodeIfPresent(Bool.self, forKey: key) { 347 | return value 348 | } else { 349 | throw DecodingError.valueNotFound(Bool.self, DecodingError.Context(codingPath: [key], debugDescription: "Expected Bool value but found nothing.")) 350 | } 351 | } else { 352 | return try decode(Bool.self, forKey: key) 353 | } 354 | } catch { 355 | if case DecodingError.typeMismatch = error { 356 | if let value = try? decodeIfPresent(Int.self, forKey: key) { 357 | return value == 1 358 | } else if let value = try? decodeIfPresent(Swift.String.self, forKey: key) { 359 | if let result = Bool(value) { 360 | return result 361 | } else if let result = Int(value) { 362 | return result == 1 363 | } 364 | } 365 | } 366 | throw error 367 | } 368 | } 369 | do { 370 | return try _decode(key: mainKey) 371 | } catch { 372 | if let alterKeys = alterKeys?() { 373 | for key in alterKeys where contains(key) { 374 | if let value = try? _decode(key: key) { 375 | return value 376 | } 377 | } 378 | } 379 | 380 | throw error 381 | } 382 | } 383 | // MARK: - String 384 | @inlinable 385 | @inline(__always) 386 | func decodeString(mainKey: String, alterKeys: (() -> [String])?, optional: Bool) throws -> String { 387 | func _decode(key: String) throws -> String { 388 | do { 389 | if optional { 390 | if let value = try decodeIfPresent(Swift.String.self, forKey: key) { 391 | return value 392 | } else { 393 | throw DecodingError.valueNotFound(Swift.String.self, DecodingError.Context(codingPath: [key], debugDescription: "Expected String value but found nothing.")) 394 | } 395 | } else { 396 | return try decode(Swift.String.self, forKey: key) 397 | } 398 | } catch { 399 | if case DecodingError.typeMismatch = error { 400 | if let value = try? decodeIfPresent(Bool.self, forKey: key) { 401 | return String(value) 402 | } 403 | if let value = try? decodeIfPresent(Int64.self, forKey: key) { 404 | return String(value) 405 | } 406 | if let value = try? decodeIfPresent(UInt64.self, forKey: key) { 407 | return String(value) 408 | } 409 | if let value = try? decodeIfPresent(Double.self, forKey: key) { 410 | return String(value) 411 | } 412 | } 413 | throw error 414 | } 415 | } 416 | do { 417 | return try _decode(key: mainKey) 418 | } catch { 419 | if let alterKeys = alterKeys?() { 420 | for key in alterKeys where contains(key) { 421 | if let value = try? _decode(key: key) { 422 | return value 423 | } 424 | } 425 | } 426 | 427 | throw error 428 | } 429 | } 430 | // MARK: - Data 431 | @inlinable 432 | @inline(__always) 433 | func decodeData(mainKey: String, alterKeys: (() -> [String])?, optional: Bool) throws -> Data { 434 | func _decode(key: String) throws -> Data { 435 | do { 436 | if optional { 437 | if let value = try decodeIfPresent(Data.self, forKey: key) { 438 | return value 439 | } else { 440 | throw DecodingError.valueNotFound(Swift.String.self, DecodingError.Context(codingPath: [key], debugDescription: "Expected String value but found nothing.")) 441 | } 442 | } else { 443 | return try decode(Data.self, forKey: key) 444 | } 445 | } catch { 446 | throw error 447 | } 448 | } 449 | do { 450 | return try _decode(key: mainKey) 451 | } catch { 452 | if let alterKeys = alterKeys?() { 453 | for key in alterKeys where contains(key) { 454 | if let value = try? _decode(key: key) { 455 | return value 456 | } 457 | } 458 | } 459 | 460 | throw error 461 | } 462 | } 463 | // MARK: - Double 464 | @inlinable 465 | @inline(__always) 466 | func decodeDouble(mainKey: String, alterKeys: (() -> [String])?, optional: Bool, targetType: T.Type) throws -> T where T: BinaryFloatingPoint { 467 | func convert(_ value: Double) -> T { 468 | value as? T ?? T(value) 469 | } 470 | func _decode(key: String) throws -> T { 471 | do { 472 | if optional { 473 | if let value = try decodeIfPresent(Double.self, forKey: key) { 474 | return convert(value) 475 | } else { 476 | throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [key], debugDescription: "Expected \(T.self) value but found nothing.")) 477 | } 478 | } else { 479 | return convert(try decode(Double.self, forKey: key)) 480 | } 481 | } catch { 482 | if case DecodingError.typeMismatch = error { 483 | if let value = try? decodeIfPresent(Swift.String.self, forKey: key), let result = Double(value) { 484 | return convert(result) 485 | } 486 | } 487 | throw error 488 | } 489 | } 490 | do { 491 | return try _decode(key: mainKey) 492 | } catch { 493 | if let alterKeys = alterKeys?() { 494 | for key in alterKeys where contains(key) { 495 | if let value = try? _decode(key: key) { 496 | return value 497 | } 498 | } 499 | } 500 | 501 | throw error 502 | } 503 | } 504 | 505 | // MARK: - Int 506 | @inlinable 507 | @inline(__always) 508 | func decodeInt(mainKey: String, alterKeys: (() -> [String])?, optional: Bool, targetType: T.Type) throws -> T where T: FixedWidthInteger { 509 | func boundsCheck(value: Int64, for key: String) throws -> T { 510 | guard value <= T.max, value >= T.min else { 511 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [key], debugDescription: "Parsed JSON number <\(value)> does not fit in \(T.self).")) 512 | } 513 | return T(value) 514 | } 515 | func _decode(key: String) throws -> T { 516 | do { 517 | if optional { 518 | if let value = try decodeIfPresent(Int64.self, forKey: key) { 519 | return try boundsCheck(value: value, for: key) 520 | } else { 521 | throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [key], debugDescription: "Expected \(T.self) value but found nothing.")) 522 | } 523 | } else { 524 | return try boundsCheck(value: try decode(Int64.self, forKey: key), for: key) 525 | } 526 | } catch { 527 | if case DecodingError.typeMismatch = error { 528 | if let value = try? decodeIfPresent(Swift.String.self, forKey: key), let int64 = Int64(value), let result = try? boundsCheck(value: int64, for: key) { 529 | return result 530 | } 531 | } 532 | throw error 533 | } 534 | } 535 | do { 536 | return try _decode(key: mainKey) 537 | } catch { 538 | if let alterKeys = alterKeys?() { 539 | for key in alterKeys where contains(key) { 540 | if let value = try? _decode(key: key) { 541 | return value 542 | } 543 | } 544 | } 545 | 546 | throw error 547 | } 548 | } 549 | 550 | // MARK: - UInt 551 | @inlinable 552 | @inline(__always) 553 | func decodeUInt(mainKey: String, alterKeys: (() -> [String])?, optional: Bool, targetType: T.Type) throws -> T where T: FixedWidthInteger { 554 | func boundsCheck(value: UInt64, for key: String) throws -> T { 555 | guard value <= T.max, value >= T.min else { 556 | throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [key], debugDescription: "Parsed JSON number <\(value)> does not fit in \(T.self).")) 557 | } 558 | return T(value) 559 | } 560 | func _decode(key: String) throws -> T { 561 | do { 562 | if optional { 563 | if let value = try decodeIfPresent(UInt64.self, forKey: key) { 564 | return try boundsCheck(value: value, for: key) 565 | } else { 566 | throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [key], debugDescription: "Expected \(T.self) value but found nothing.")) 567 | } 568 | } else { 569 | return try boundsCheck(value: try decode(UInt64.self, forKey: key), for: key) 570 | } 571 | } catch { 572 | if case DecodingError.typeMismatch = error { 573 | if let value = try? decodeIfPresent(Swift.String.self, forKey: key), let uint64 = UInt64(value), let result = try? boundsCheck(value: uint64, for: key) { 574 | return result 575 | } 576 | } 577 | throw error 578 | } 579 | } 580 | do { 581 | return try _decode(key: mainKey) 582 | } catch { 583 | if let alterKeys = alterKeys?() { 584 | for key in alterKeys where contains(key) { 585 | if let value = try? _decode(key: key) { 586 | return value 587 | } 588 | } 589 | } 590 | 591 | throw error 592 | } 593 | } 594 | } 595 | --------------------------------------------------------------------------------