├── swift-linux.sh ├── .github └── workflows │ └── main.yml ├── SourceCode └── JsonProtection │ ├── MultipleKeyProtection.swift │ ├── URLProtection.swift │ ├── UIImageProtection.swift │ ├── NumberArrayProtection.swift │ ├── NumbersProtection.swift │ ├── BoolProtection.swift │ ├── MissingKeyProtection.swift │ ├── ObjectProtection.swift │ ├── AESDecoder.swift │ ├── DateProtection.swift │ ├── NumberProtection.swift │ └── DTAES.swift ├── Package.swift ├── Tests └── JsonProtectionTests │ ├── DecimalTest.swift │ ├── NumbersProtectionTest.swift │ ├── MissingKeyProtectionTest.swift │ ├── DateProtectionTest.swift │ ├── BoolProtectionTest.swift │ ├── URLProtectionTest.swift │ ├── NumberArrayProtectionTest.swift │ ├── AESDecoderTest.swift │ ├── NumberProtectionTest.swift │ └── ObjectProtectionTest.swift ├── .gitignore ├── README.md ├── en └── README.md └── LICENSE /swift-linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | colima start 3 | docker run -it --rm \ 4 | -v $(pwd):/workspace \ 5 | -w /workspace \ 6 | swift:latest bash -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: macos-15 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup Swift 17 | uses: swift-actions/setup-swift@v2 18 | with: 19 | swift-version: "6.1.0" 20 | 21 | - name: Build package 22 | run: swift build 23 | 24 | - name: Run tests 25 | run: swift test 26 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/MultipleKeyProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MultipleKeyProtection.swift 3 | // 4 | // Created by Darktt on 2022/12/6. 5 | // Copyright © 2022 Darktt. All rights reserved. 6 | // 7 | 8 | @propertyWrapper 9 | public 10 | struct MultipleKeysProtection: Decodable where DecodeType: Decodable 11 | { 12 | // MARK: - Properties - 13 | 14 | public 15 | var wrappedValue: DecodeType? 16 | 17 | // MARK: - Methods - 18 | // MARK: Initial Method 19 | 20 | public 21 | init() { } 22 | } 23 | 24 | extension MultipleKeysProtection 25 | { 26 | 27 | } 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "JsonProtection", 8 | platforms: [.iOS(.v14), .macOS(.v12)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, making them visible to other packages. 11 | .library( 12 | name: "JsonProtection", 13 | targets: ["JsonProtection"]), 14 | ], 15 | targets: [ 16 | // Targets are the basic building blocks of a package, defining a module or a test suite. 17 | // Targets can depend on other targets in this package and products from dependencies. 18 | .target( 19 | name: "JsonProtection", 20 | path: "SourceCode" 21 | ), 22 | .testTarget( 23 | name: "JsonProtectionTests", 24 | dependencies: ["JsonProtection"]), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/DecimalTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DecimalTest.swift 3 | // JsonDecodeProtectionTests 4 | // 5 | // Created by Darktt on 2023/10/13. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | struct TestObject: Decodable 14 | { 15 | @NumberProtection 16 | var value: Decimal? 17 | } 18 | 19 | struct DecimalTest 20 | { 21 | @Test("小數保護應該正確處理浮點數精度") 22 | func float0_0001AndRoundDown() throws 23 | { 24 | // Arrange 25 | let jsonString = """ 26 | { 27 | "value": 122.999999999999999 28 | } 29 | """ 30 | 31 | let jsonData: Data = jsonString.data(using: .utf8)! 32 | let jsonDecoder = JSONDecoder() 33 | 34 | // Act 35 | let object = try jsonDecoder.decode(TestObject.self, from: jsonData) 36 | let value: Decimal = object.value ?? .zero 37 | 38 | // Assert 39 | let result: String = "\(value)" 40 | 41 | #expect(result == "122.999999999999999") 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/NumbersProtectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumbersProtectionTest.swift 3 | // JsonProtectionTests 4 | // 5 | // Created by Darktt on 2022/12/16. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | struct NumbersObject: Decodable 14 | { 15 | @NumbersProtection 16 | private(set) 17 | var index: Array? 18 | } 19 | 20 | struct NumbersProtectionTest 21 | { 22 | @Test("數字們保護應該正確解碼字串數字陣列") 23 | func numbersProtectionSuccess() throws 24 | { 25 | // Arrange 26 | let jsonString = """ 27 | { 28 | "index": ["10", "20", "50", "100"] 29 | } 30 | """ 31 | let jsonData: Data = jsonString.data(using: .utf8)! 32 | let jsonDecoder = JSONDecoder() 33 | 34 | // Act 35 | let object = try jsonDecoder.decode(NumbersObject.self, from: jsonData) 36 | 37 | // Assert 38 | let actual: Int? = object.index.map({ $0[2] }) 39 | let expect: Int = 50 40 | 41 | #expect(actual == expect) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/MissingKeyProtectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MissingKeyProtectionTest.swift 3 | // JsonProtectionTests 4 | // 5 | // Created by Darktt on 2022/12/2. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | private 14 | struct MissingKeyObject: Decodable 15 | { 16 | @MissingKeyProtection 17 | private(set) 18 | var existKey: String? 19 | 20 | @MissingKeyProtection 21 | private(set) 22 | var missingKey: String? 23 | } 24 | 25 | struct MissingKeyProtectionTest 26 | { 27 | @Test("缺失鍵保護應該優雅地處理缺失的鍵") 28 | func missingKeyProtectionSuccess() throws 29 | { 30 | // Arrange 31 | let jsonString = """ 32 | { 33 | "existKey": "Something" 34 | } 35 | """ 36 | let jsonData: Data = jsonString.data(using: .utf8)! 37 | let jsonDecoder = JSONDecoder() 38 | 39 | // Act 40 | let object = try jsonDecoder.decode(MissingKeyObject.self, from: jsonData) 41 | 42 | // Assert 43 | #expect(object.existKey == "Something") 44 | #expect(object.missingKey == nil) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/URLProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLProtection.swift 3 | // 4 | // Created by Darktt on 2022/7/7. 5 | // Copyright © 2022 Darktt. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper 11 | public 12 | struct URLProtection 13 | { 14 | // MARK: - Properties - 15 | 16 | public 17 | var wrappedValue: URL? 18 | 19 | // MARK: - Methods - 20 | // MARK: Initial Method 21 | 22 | public 23 | init() { } 24 | } 25 | 26 | // MARK: - Conform Protocols - 27 | 28 | extension URLProtection: Decodable 29 | { 30 | public 31 | init(from decoder: Decoder) throws 32 | { 33 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 34 | 35 | let urlString: String = try container.decode(String.self) 36 | let url = URL(string: urlString) 37 | 38 | self.wrappedValue = url 39 | } 40 | } 41 | 42 | extension URLProtection: CustomStringConvertible 43 | { 44 | public 45 | var description: String 46 | { 47 | self.wrappedValue.map { 48 | 49 | "\($0)" 50 | } ?? "nil" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/UIImageProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageProtection.swift 3 | // 4 | // Created by Darktt on 2022/7/5. 5 | // Copyright © 2022 Darktt. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | #if canImport(UIKit) 11 | 12 | import UIKit.UIImage 13 | 14 | @propertyWrapper 15 | public 16 | struct UIImageProtection 17 | { 18 | // MARK: - Properties - 19 | 20 | public 21 | var wrappedValue: UIImage? 22 | 23 | // MARK: - Methods - 24 | // MARK: Initial Method 25 | 26 | public 27 | init() { } 28 | } 29 | 30 | // MARK: - Conform Protocols - 31 | 32 | extension UIImageProtection: Decodable 33 | { 34 | public 35 | init(from decoder: Decoder) throws 36 | { 37 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 38 | let imageName = try container.decode(String.self) 39 | let image = UIImage(named: imageName) 40 | 41 | self.wrappedValue = image 42 | } 43 | } 44 | 45 | extension UIImageProtection: CustomStringConvertible 46 | { 47 | public var description: String 48 | { 49 | self.wrappedValue.map { 50 | 51 | "\($0)" 52 | } ?? "nil" 53 | } 54 | } 55 | 56 | #endif // canImport(UIKit) 57 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/DateProtectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateProtectionTest.swift 3 | // JsonProtectionTests 4 | // 5 | // Created by Darktt on 2023/9/8. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | struct DateObject: Decodable 14 | { 15 | let updateTime: Date? 16 | 17 | @DateProtection(configuration: DateConfiguration.self) 18 | private(set) 19 | var expiredDate: Date? 20 | } 21 | 22 | extension DateObject 23 | { 24 | struct DateConfiguration: DateConfigurate 25 | { 26 | static var option: DateConfigurateOption { 27 | 28 | .dateFormat("yyyyMMddhhss", timeZone: TimeZone(abbreviation: "GMT+0800")) 29 | } 30 | } 31 | } 32 | 33 | struct DateProtectionTest 34 | { 35 | @Test("日期保護應該使用自定義格式正確解碼日期") 36 | func dateProtectionSuccess() throws 37 | { 38 | // Arrange 39 | let jsonString = """ 40 | { 41 | "updateTime": 1694102400, 42 | "expiredDate": "202309080100" 43 | } 44 | """ 45 | let jsonData: Data = jsonString.data(using: .utf8)! 46 | let jsonDecoder = JSONDecoder() 47 | jsonDecoder.dateDecodingStrategy = .secondsSince1970 48 | 49 | // Act 50 | let object = try jsonDecoder.decode(DateObject.self, from: jsonData) 51 | let updateTime = Date(timeIntervalSince1970: 1694102400.0) 52 | let expiredDate = Date(timeIntervalSince1970: 1694106000.0) 53 | 54 | // Assert 55 | #expect(object.updateTime == updateTime) 56 | #expect(object.expiredDate == expiredDate) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/BoolProtectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolProtectionTest.swift 3 | // JsonProtectionTests 4 | // 5 | // Created by Darktt on 2022/11/30. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | private 14 | struct BoolObject: Decodable 15 | { 16 | @BoolProtection 17 | private(set) 18 | var `true`: Bool? 19 | 20 | @BoolProtection 21 | private(set) 22 | var `false`: Bool? 23 | } 24 | 25 | struct BoolProtectionTest 26 | { 27 | @Test("布林保護應該正確解碼布林值") 28 | func boolProtectionSuccess() throws 29 | { 30 | // Arrange 31 | let jsonString = """ 32 | { 33 | "true": true, 34 | "false": "FALSE" 35 | } 36 | """ 37 | let jsonData: Data = jsonString.data(using: .utf8)! 38 | let jsonDecoder = JSONDecoder() 39 | 40 | // Act 41 | let object = try jsonDecoder.decode(BoolObject.self, from: jsonData) 42 | 43 | // Assert 44 | #expect(object.true == true) 45 | #expect(object.false == false) 46 | } 47 | 48 | @Test("布林保護應該對無效的布林值拋出錯誤") 49 | func boolProtectionFailure() throws 50 | { 51 | // Arrange 52 | let jsonString = """ 53 | { 54 | "true": 4, 55 | "false": "FALSE" 56 | } 57 | """ 58 | let jsonData: Data = jsonString.data(using: .utf8)! 59 | let jsonDecoder = JSONDecoder() 60 | 61 | // Act & Assert 62 | #expect(throws: DecodingError.self) { 63 | try jsonDecoder.decode(BoolObject.self, from: jsonData) 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/URLProtectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLProtectionTest.swift 3 | // JsonProtectionTests 4 | // 5 | // Created by Darktt on 2022/12/16. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | struct URLObject: Decodable 14 | { 15 | @URLProtection 16 | private(set) 17 | var homePageUrl: URL? 18 | 19 | private(set) 20 | var detailPageUrl: URL? 21 | } 22 | 23 | struct URLProtectionTest 24 | { 25 | @Test("URL 保護在遇到空字串時應該對受保護屬性回傳 nil") 26 | func urlProtectionSuccess() throws 27 | { 28 | // Arrange 29 | let jsonString = """ 30 | { 31 | "homePageUrl": "", 32 | "detailPageUrl": "https://www.google.com" 33 | } 34 | """ 35 | let jsonData: Data = jsonString.data(using: .utf8)! 36 | let jsonDecoder = JSONDecoder() 37 | 38 | // Act 39 | let object = try jsonDecoder.decode(URLObject.self, from: jsonData) 40 | 41 | // Assert 42 | let actualOne: URL? = object.homePageUrl 43 | let actualTwo: URL? = object.detailPageUrl 44 | let expect: String = "https://www.google.com" 45 | 46 | #expect(actualOne?.absoluteString == nil) 47 | #expect(actualTwo?.absoluteString == expect) 48 | } 49 | 50 | @Test("URL 保護在未受保護屬性接收空字串時應該拋出錯誤") 51 | func urlProtectionFailure() throws 52 | { 53 | // Arrange 54 | let jsonString = """ 55 | { 56 | "homePageUrl": "https://www.google.com", 57 | "detailPageUrl": "" 58 | } 59 | """ 60 | let jsonData: Data = jsonString.data(using: .utf8)! 61 | let jsonDecoder = JSONDecoder() 62 | 63 | // Act & Assert 64 | #expect(throws: (any Error).self) { 65 | try jsonDecoder.decode(URLObject.self, from: jsonData) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/NumberArrayProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberArrayProtection.swift 3 | // 4 | // Created by Darktt on 2024/7/3. 5 | // Copyright © 2024 Darktt. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | @propertyWrapper 11 | public 12 | struct NumberArrayProtection where Element: Decodable, Element: NumberType 13 | { 14 | // MARK: - Properties - 15 | 16 | public 17 | typealias WarppedValue = Array 18 | 19 | public 20 | var wrappedValue: WarppedValue? 21 | 22 | // MARK: - Methods - 23 | // MARK: Initial Method 24 | 25 | public 26 | init() { } 27 | } 28 | 29 | // MARK: - Private Methods - 30 | 31 | private 32 | extension NumberArrayProtection 33 | { 34 | private 35 | func decode(with string: String) throws -> Array? 36 | { 37 | guard !string.isEmpty, 38 | let data: Data = string.data(using: .utf8) else { 39 | 40 | return nil 41 | } 42 | 43 | let jsonDecoder = JSONDecoder() 44 | let wrapperValue = try jsonDecoder.decode(NumbersProtection.self, from: data) 45 | 46 | return wrapperValue.wrappedValue 47 | } 48 | } 49 | 50 | // MARK: - Conform Protocols - 51 | 52 | extension NumberArrayProtection: Decodable 53 | { 54 | public 55 | init(from decoder: Decoder) throws 56 | { 57 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 58 | 59 | if container.decodeNil() { 60 | 61 | self.wrappedValue = nil 62 | return 63 | } 64 | 65 | let jsonString: String = try container.decode(String.self) 66 | let wrappedValue: Array? = try self.decode(with: jsonString) 67 | 68 | self.wrappedValue = wrappedValue 69 | } 70 | } 71 | 72 | extension NumberArrayProtection: CustomStringConvertible 73 | { 74 | public 75 | var description: String 76 | { 77 | self.wrappedValue.map { 78 | 79 | "\($0)" 80 | } ?? "nil" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/NumberArrayProtectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberArrayProtectionTest.swift 3 | // JsonProtectionTests 4 | // 5 | // Created by Darktt on 2024/7/3. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | private 14 | struct SomeObject: Decodable 15 | { 16 | @ObjectProtection 17 | var subObjects: Array? 18 | 19 | @NumberArrayProtection 20 | var dices: Array? 21 | } 22 | 23 | extension SomeObject 24 | { 25 | struct SubObject: Decodable 26 | { 27 | private(set) 28 | var name: String? 29 | 30 | private(set) 31 | var number: Int? 32 | } 33 | } 34 | 35 | struct NumberArrayProtectionTest 36 | { 37 | @Test("數字陣列保護應該正確解碼數字陣列") 38 | func numberObjectProtectionSuccess() throws 39 | { 40 | // Arrange 41 | let jsonString = """ 42 | { 43 | "subObjects": "[{\\"name\\": \\"Jo\\", \\"number\\": 233}, {\\"name\\": \\"Ana\\", \\"number\\": 4565}]", 44 | "dices": "[1, 5.2, 1.0]" 45 | } 46 | """ 47 | let jsonData: Data = jsonString.data(using: .utf8)! 48 | let jsonDecoder = JSONDecoder() 49 | 50 | // Act 51 | let object = try jsonDecoder.decode(SomeObject.self, from: jsonData) 52 | 53 | // Assert 54 | let actual: Array? = object.dices 55 | let expect: Array = [1, 5, 1] 56 | 57 | #expect(actual == expect) 58 | } 59 | 60 | @Test("數字陣列保護在遇到空字串時應該回傳 nil") 61 | func numberObjectProtectionWithEmptyString() throws 62 | { 63 | // Arrange 64 | let jsonString = """ 65 | { 66 | "subObjects": "[{\\"name\\": \\"Jo\\", \\"number\\": 233}, {\\"name\\": \\"Ana\\", \\"number\\": 4565}]", 67 | "dices": "" 68 | } 69 | """ 70 | let jsonData: Data = jsonString.data(using: .utf8)! 71 | let jsonDecoder = JSONDecoder() 72 | 73 | // Act 74 | let object = try jsonDecoder.decode(SomeObject.self, from: jsonData) 75 | 76 | // Assert 77 | let actual: Array? = object.dices 78 | let expect: Array? = nil 79 | 80 | #expect(actual == expect) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/NumbersProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumbersProtection.swift 3 | // 4 | // Created by Darktt on 2022/7/14. 5 | // Copyright © 2022 Darktt. All rights reserved. 6 | // 7 | 8 | @propertyWrapper 9 | public 10 | struct NumbersProtection: MissingKeyProtecting where Element: Decodable, Element: NumberType 11 | { 12 | // MARK: - Properties - 13 | 14 | public 15 | var wrappedValue: Array? 16 | 17 | // MARK: - Methods - 18 | // MARK: Initial Method 19 | 20 | public 21 | init(wrappedValue: Array?) 22 | { 23 | self.wrappedValue = wrappedValue 24 | } 25 | } 26 | 27 | // MARK: - Conform Protocol - 28 | 29 | extension NumbersProtection: Decodable 30 | { 31 | public 32 | init(from decoder: Decoder) throws 33 | { 34 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 35 | 36 | if container.decodeNil() { 37 | 38 | self.wrappedValue = nil 39 | return 40 | } 41 | 42 | let wrappedValue: Array? = self.decode(from: container) 43 | 44 | self.wrappedValue = wrappedValue 45 | } 46 | 47 | func decode(from container: SingleValueDecodingContainer) -> Array? 48 | { 49 | var wrappedValue: Array? 50 | 51 | if let strings = try? container.decode(Array.self) { 52 | 53 | wrappedValue = strings.compactMap(Element.init(_:)) 54 | } 55 | 56 | if let integers = try? container.decode(Array.self) { 57 | 58 | wrappedValue = integers.compactMap(Element.init(_:)) 59 | } 60 | 61 | if let floats = try? container.decode(Array.self) { 62 | 63 | wrappedValue = floats.compactMap(Element.init(_:)) 64 | } 65 | 66 | if let doubles = try? container.decode(Array.self) { 67 | 68 | wrappedValue = doubles.compactMap(Element.init(_:)) 69 | } 70 | 71 | return wrappedValue 72 | } 73 | } 74 | 75 | extension NumbersProtection: CustomStringConvertible 76 | { 77 | public 78 | var description: String 79 | { 80 | self.wrappedValue.map { 81 | 82 | "\($0)" 83 | } ?? "nil" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/AESDecoderTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AESDecoderTest.swift 3 | // JsonProtectionTests 4 | // 5 | // Created by Darktt on 2023/4/10. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | #if canImport(CommonCrypto) 14 | 15 | struct AESObject: Decodable 16 | { 17 | @AESDecoder(adopter: AESAdopting.self) 18 | private(set) 19 | var url: String? 20 | 21 | @AESDecoder(adopter: AESAdopting.self) 22 | private(set) 23 | var urls: Array? 24 | } 25 | 26 | struct AESAdopting: AESAdopter 27 | { 28 | static var key: String { 29 | 30 | "MnoewgUZrgt5Rk08MtESwHvgzY7ElaEq" 31 | } 32 | 33 | static var iv: String? { 34 | 35 | "rtCG5mdgtlCtbyI4" 36 | } 37 | 38 | static var options: DTAES.Options { 39 | 40 | [.pkc7Padding, .zeroPadding] 41 | } 42 | } 43 | 44 | struct AESDecoderTest 45 | { 46 | private 47 | func createTestObject() throws -> AESObject 48 | { 49 | let jsonString: String = """ 50 | { 51 | "url": "0NhMzVQIsjShyNnck3huFVjVCcku2a+iAQVfY3CDrUw=", 52 | "urls": [ 53 | "0NhMzVQIsjShyNnck3huFVjVCcku2a+iAQVfY3CDrUw=", 54 | "0NhMzVQIsjShyNnck3huFVjVCcku2a+iAQVfY3CDrUw=" 55 | ] 56 | } 57 | """ 58 | let jsonData: Data = jsonString.data(using: .utf8)! 59 | let jsonDecoder = JSONDecoder() 60 | 61 | return try jsonDecoder.decode(AESObject.self, from: jsonData) 62 | } 63 | 64 | @Test("AES 解碼器應該正確解密字串值") 65 | func aesDecoderSuccess() throws 66 | { 67 | // Arrange 68 | let object = try createTestObject() 69 | 70 | // Act 71 | let url: String? = object.url 72 | let expect: String = "https://www.apple.com" 73 | 74 | // Assert 75 | #expect(url == expect) 76 | } 77 | 78 | @Test("AES 解碼器應該正確解密字串陣列值") 79 | func decoderStringArraySuccess() throws 80 | { 81 | // Arrange 82 | let object = try createTestObject() 83 | 84 | // Act 85 | let urls: Array? = object.urls 86 | let expect: Array = ["https://www.apple.com", "https://www.apple.com"] 87 | 88 | // Assert 89 | #expect(urls == expect) 90 | } 91 | } 92 | 93 | #endif // canImport(CommonCrypto) 94 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/BoolProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BoolProtection.swift 3 | // 4 | // Created by Darktt on 22/6/9. 5 | // Copyright © 2022 Darktt. All rights reserved. 6 | // 7 | 8 | /** 9 | Automatic convert to bool value. 10 | */ 11 | @propertyWrapper 12 | public 13 | struct BoolProtection 14 | { 15 | // MARK: - Properties - 16 | 17 | public 18 | var wrappedValue: Bool? 19 | 20 | // MARK: - Methods - 21 | // MARK: Initial Method 22 | 23 | public 24 | init() { } 25 | } 26 | 27 | // MARK: - Conform Protocols - 28 | 29 | extension BoolProtection: Decodable 30 | { 31 | public 32 | init(from decoder: Decoder) throws 33 | { 34 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 35 | 36 | if let boolValue = try? container.decode(Bool.self) { 37 | 38 | self.wrappedValue = boolValue 39 | return 40 | } 41 | 42 | if let stringValue = try? container.decode(String.self).lowercased() { 43 | 44 | switch stringValue { 45 | 46 | case "no", "false", "0": 47 | self.wrappedValue = false 48 | 49 | case "yes", "true", "1": 50 | self.wrappedValue = true 51 | 52 | default: 53 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expect true/false, yes/no or 1/0 but`\(stringValue)` instead") 54 | } 55 | return 56 | } 57 | 58 | if let integerValue = try? container.decode(Int.self) { 59 | 60 | switch integerValue { 61 | 62 | case 0: 63 | self.wrappedValue = false 64 | 65 | case 1: 66 | self.wrappedValue = true 67 | 68 | default: 69 | throw DecodingError.dataCorruptedError(in: container, debugDescription: "Expect `0` or `1` but found `\(integerValue)` instead") 70 | } 71 | 72 | return 73 | } 74 | 75 | self.wrappedValue = nil 76 | } 77 | } 78 | 79 | extension BoolProtection: CustomStringConvertible 80 | { 81 | public 82 | var description: String 83 | { 84 | self.wrappedValue.map { 85 | 86 | "\($0)" 87 | } ?? "nil" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/NumberProtectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberProtectionTest.swift 3 | // JsonDecodeProtectionTests 4 | // 5 | // Created by Darktt on 2022/12/7. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | struct NumberObject: Decodable 14 | { 15 | @NumberProtection 16 | private(set) 17 | var index: Int? 18 | 19 | @NumberProtection 20 | private(set) 21 | var profit: Decimal? 22 | 23 | @NumberProtection 24 | private(set) 25 | var amount: Int? 26 | } 27 | 28 | struct NumberProtectionTest 29 | { 30 | @Test("數字保護應該成功解碼字串數字") 31 | func numberProtectionSuccess() throws 32 | { 33 | // Arrange 34 | let jsonString = """ 35 | { 36 | "index": "99" 37 | } 38 | """ 39 | let jsonData: Data = jsonString.data(using: .utf8)! 40 | let jsonDecoder = JSONDecoder() 41 | 42 | // Act 43 | let object = try jsonDecoder.decode(NumberObject.self, from: jsonData) 44 | 45 | // Assert 46 | let actual: Int? = object.index 47 | let expect: Int = 99 48 | 49 | #expect(actual == expect) 50 | } 51 | 52 | @Test("數字保護應該成功解碼 Decimal 類型") 53 | func numberProtectionSuccessForDecimalType() throws 54 | { 55 | // Arrange 56 | let jsonString = """ 57 | { 58 | "profit": "0.04" 59 | } 60 | """ 61 | let jsonData: Data = jsonString.data(using: .utf8)! 62 | let jsonDecoder = JSONDecoder() 63 | 64 | // Act 65 | let object = try jsonDecoder.decode(NumberObject.self, from: jsonData) 66 | 67 | // Assert 68 | let actual: Decimal? = object.profit 69 | let expect: Decimal = 0.04 70 | 71 | #expect(actual == expect) 72 | } 73 | 74 | @Test("數字保護應該成功處理小數字串") 75 | func numberProtectionSuccessForFractionString() throws 76 | { 77 | // Arrange 78 | let jsonString = """ 79 | { 80 | "amount": "122.999999999999999" 81 | } 82 | """ 83 | let jsonData: Data = jsonString.data(using: .utf8)! 84 | let jsonDecoder = JSONDecoder() 85 | 86 | // Act 87 | let object = try jsonDecoder.decode(NumberObject.self, from: jsonData) 88 | 89 | // Assert 90 | let actual: Int? = object.amount 91 | let expect: Int = 122 92 | 93 | #expect(actual == expect) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/MissingKeyProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MissingKeyProtection.swift 3 | // 4 | // Created by Darktt on 2022/6/14. 5 | // Copyright © 2022 Darktt. All rights reserved. 6 | // 7 | 8 | // MARK: - MissingKeyProtecting - 9 | 10 | public protocol MissingKeyProtecting: Decodable 11 | { 12 | associatedtype WrappedType: ExpressibleByNilLiteral 13 | 14 | init(wrappedValue: WrappedType) 15 | } 16 | 17 | // MARK: - MissingKeyProtection - 18 | 19 | /** 20 | Handle json key maybe not exist issue. 21 | 22 | Usage: 23 | ``` 24 | struct Foo 25 | { 26 | var foo: [String]? 27 | 28 | @MissingKeyProtection 29 | var third: String? 30 | } 31 | 32 | extension Foo: Decodable 33 | { 34 | enum CodingKeys: String, CodingKey 35 | { 36 | case foo 37 | 38 | case third 39 | } 40 | } 41 | 42 | // --------------- 43 | 44 | let jsonString = """ 45 | { 46 | "foo": "["1", "2", "3", "4", "5", "6"] 47 | } 48 | """ 49 | 50 | do { 51 | 52 | try jsonString.data(using: .utf8).map { 53 | 54 | let jsonDeocder = JSONDecoder() 55 | let jsonObject = try jsonDeocder.decode(Foo.self, from: $0) 56 | 57 | print("third: \(jsonObject.third ?? "nil")") 58 | } 59 | } catch { 60 | 61 | print("\(error)") 62 | } 63 | ``` 64 | */ 65 | @propertyWrapper 66 | public 67 | struct MissingKeyProtection: MissingKeyProtecting where Value: Decodable 68 | { 69 | // MARK: - Properties - 70 | 71 | public 72 | var wrappedValue: Value? 73 | 74 | // MARK: - Methods - 75 | // MARK: Initial Method 76 | 77 | public 78 | init(wrappedValue: Value?) 79 | { 80 | self.wrappedValue = wrappedValue 81 | } 82 | } 83 | 84 | // MARK: - Conform Protocols - 85 | 86 | extension MissingKeyProtection: Decodable 87 | { 88 | public 89 | init(from decoder: Decoder) throws 90 | { 91 | let container = try decoder.singleValueContainer() 92 | let wrappedValue: Value? = try? container.decode(Value.self) 93 | 94 | self.wrappedValue = wrappedValue 95 | } 96 | } 97 | 98 | extension MissingKeyProtection: CustomStringConvertible 99 | { 100 | public 101 | var description: String 102 | { 103 | self.wrappedValue.map { 104 | 105 | "\($0)" 106 | } ?? "nil" 107 | } 108 | } 109 | 110 | // MARK: - KeyedDecodingContainer extension - 111 | 112 | extension KeyedDecodingContainer 113 | { 114 | public 115 | func decode(_ type: T.Type, forKey key: KeyedDecodingContainer.Key) throws -> T where T: MissingKeyProtecting 116 | { 117 | let result: T = try self.decodeIfPresent(T.self, forKey: key) ?? T(wrappedValue: nil) 118 | 119 | return result 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/ObjectProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectProtection.swift 3 | // 4 | // Created by Darktt on 2022/6/9. 5 | // Copyright © 2022 Darktt. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | /** 11 | Automatic convert string style json to object. 12 | 13 | Usage: 14 | ``` 15 | struct Foo 16 | { 17 | @ObjectProtection 18 | var bar: Bar? 19 | } 20 | 21 | extension Foo: Decodable 22 | { 23 | enum CodingKeys: String, CodingKey 24 | { 25 | case bar 26 | } 27 | } 28 | 29 | struct Bar 30 | { 31 | let foo: String? 32 | } 33 | 34 | extension Bar: Decodable 35 | { 36 | enum CodingKeys: String, CodingKey 37 | { 38 | case foo 39 | } 40 | } 41 | 42 | // --------------- 43 | 44 | let jsonString = """ 45 | { 46 | "bar": "{\\"foo\\":\\"bar\\"}" 47 | } 48 | """ 49 | 50 | do { 51 | 52 | try jsonString.data(using: .utf8).map { 53 | 54 | let jsonDeocder = JSONDecoder() 55 | let foo = try jsonDeocder.decode(Foo.self, from: $0) 56 | 57 | foo.bar.map { 58 | 59 | print("bar: \($0)") 60 | } 61 | } 62 | } catch { 63 | 64 | print("\(error)") 65 | } 66 | ``` 67 | */ 68 | @propertyWrapper 69 | public 70 | struct ObjectProtection where Value: Decodable 71 | { 72 | // MARK: - Properties - 73 | 74 | public 75 | var wrappedValue: Value? 76 | 77 | // MARK: - Methods - 78 | // MARK: Initial Method 79 | 80 | public 81 | init() { } 82 | } 83 | 84 | // MARK: - Private Methods - 85 | 86 | private 87 | extension ObjectProtection 88 | { 89 | func decode(with string: String) throws -> Value? 90 | { 91 | guard !string.isEmpty, 92 | let data: Data = string.data(using: .utf8) else { 93 | 94 | return nil 95 | } 96 | 97 | let jsonDecoder = JSONDecoder() 98 | let wrapperValue = try jsonDecoder.decode(Value.self, from: data) 99 | 100 | return wrapperValue 101 | } 102 | } 103 | 104 | // MARK: - Conform Protocols - 105 | 106 | extension ObjectProtection: Decodable 107 | { 108 | public 109 | init(from decoder: Decoder) throws 110 | { 111 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 112 | 113 | let jsonString: String = try container.decode(String.self) 114 | let wrappedValue: Value? = try self.decode(with: jsonString) 115 | 116 | self.wrappedValue = wrappedValue 117 | } 118 | } 119 | 120 | extension ObjectProtection: CustomStringConvertible 121 | { 122 | public 123 | var description: String 124 | { 125 | self.wrappedValue.map { 126 | 127 | "\($0)" 128 | } ?? "nil" 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/AESDecoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AESDecoder.swift 3 | // 4 | // Created by Darktt on 2023/4/10. 5 | // Copyright © 2023 Darktt. All rights reserved. 6 | // 7 | 8 | #if canImport(CommonCrypto) 9 | 10 | // MARK: - AESAdopter - 11 | 12 | public 13 | protocol AESAdopter 14 | { 15 | static var key: String { get } 16 | 17 | static var iv: String? { get } 18 | 19 | static var options: DTAES.Options { get } 20 | } 21 | 22 | // MARK: - AESDecodable - 23 | 24 | public 25 | protocol AESDecodable: Decodable { } 26 | 27 | extension String: AESDecodable { } 28 | 29 | extension Array: AESDecodable where Element == String { } 30 | 31 | // MARK: - AESDecoder - 32 | 33 | @propertyWrapper 34 | public 35 | struct AESDecoder where Adopter: AESAdopter, Value: AESDecodable 36 | { 37 | // MARK: - Properties - 38 | 39 | public 40 | var wrappedValue: Value? 41 | 42 | // MARK: - Methods - 43 | // MARK: Initial Method 44 | 45 | public 46 | init(adopter: Adopter.Type) { } 47 | } 48 | 49 | // MARK: - Private Methods - 50 | 51 | private 52 | extension AESDecoder 53 | { 54 | func decodeString(_ string: String) throws -> String 55 | { 56 | let aes = DTAES(string) 57 | aes.setKey(Adopter.key) 58 | Adopter.iv.map { 59 | 60 | aes.setIv($0) 61 | } 62 | aes.operation = .decrypt 63 | aes.options = Adopter.options 64 | 65 | let decryptedString: String = try aes.result() 66 | 67 | return decryptedString 68 | } 69 | 70 | func decodeArray(_ array: Array) throws -> Array 71 | { 72 | let decryptedArray: Array = try array.map { 73 | 74 | try self.decodeString($0) 75 | } 76 | 77 | return decryptedArray 78 | } 79 | } 80 | 81 | 82 | // MARK: - Conform Protocols - 83 | 84 | extension AESDecoder: Decodable 85 | { 86 | public 87 | init(from decoder: Decoder) throws 88 | { 89 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 90 | let encryptedValue: Value? = try? container.decode(Value.self) 91 | 92 | if let encryptedString = encryptedValue as? String { 93 | 94 | let decodeValue: String? = try? self.decodeString(encryptedString) 95 | self.wrappedValue = decodeValue as? Value 96 | 97 | return 98 | } 99 | 100 | if let encryptedArray = encryptedValue as? Array { 101 | 102 | let decodeValue: Array? = try? self.decodeArray(encryptedArray) 103 | self.wrappedValue = decodeValue as? Value 104 | 105 | return 106 | } 107 | } 108 | } 109 | 110 | #endif // canImport(CommonCrypto) 111 | -------------------------------------------------------------------------------- /Tests/JsonProtectionTests/ObjectProtectionTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectProtectionTest.swift 3 | // JsonProtectionTests 4 | // 5 | // Created by Darktt on 2022/12/5. 6 | // 7 | 8 | import Foundation 9 | import Testing 10 | @testable 11 | import JsonProtection 12 | 13 | private 14 | struct SomeObject: Decodable 15 | { 16 | @ObjectProtection 17 | var subObjects: Array? 18 | 19 | @ObjectProtection 20 | var dices: Array? 21 | } 22 | 23 | extension SomeObject 24 | { 25 | struct SubObject: Decodable, Equatable 26 | { 27 | private(set) 28 | var name: String? 29 | 30 | private(set) 31 | var number: Int? 32 | 33 | static 34 | func == (lhs: SomeObject.SubObject, rhs: SomeObject.SubObject) -> Bool 35 | { 36 | lhs.name == rhs.name && lhs.number == rhs.number 37 | } 38 | } 39 | } 40 | 41 | struct ObjectProtectionTest 42 | { 43 | @Test("物件保護在有效 JSON 字串時應該成功解碼") 44 | func objectProtectionSuccess() throws 45 | { 46 | // Arrange 47 | let jsonString = """ 48 | { 49 | "subObjects": "[{\\"name\\": \\"Jo\\", \\"number\\": 233}, {\\"name\\": \\"Ana\\", \\"number\\": 4565}]", 50 | "dices": "[1,5,1]" 51 | } 52 | """ 53 | let jsonData: Data = jsonString.data(using: .utf8)! 54 | let jsonDecoder = JSONDecoder() 55 | 56 | // Act 57 | let object = try jsonDecoder.decode(SomeObject.self, from: jsonData) 58 | 59 | // Assert 60 | let actual: String? = object.subObjects?.last?.name 61 | let expect: String = "Ana" 62 | 63 | #expect(actual == expect) 64 | } 65 | 66 | @Test("物件保護應該正確解碼骰子陣列中的數字") 67 | func objectProtectionDicesSuccess() throws 68 | { 69 | // Arrange 70 | let jsonString = """ 71 | { 72 | "subObjects": "[{\\"name\\": \\"Jo\\", \\"number\\": 233}, {\\"name\\": \\"Ana\\", \\"number\\": 4565}]", 73 | "dices": "[1, 5, 1]" 74 | } 75 | """ 76 | 77 | let jsonData: Data = jsonString.data(using: .utf8)! 78 | let jsonDecoder = JSONDecoder() 79 | 80 | // Act 81 | let object = try jsonDecoder.decode(SomeObject.self, from: jsonData) 82 | 83 | // Assert 84 | let actual: Int? = object.dices?[1] 85 | let expect: Int = 5 86 | 87 | #expect(actual == expect) 88 | } 89 | 90 | @Test("物件保護在遇到空字串時應該回傳 nil") 91 | func objectProtectionWithEmptyString() throws 92 | { 93 | // Arrange 94 | let jsonString = """ 95 | { 96 | "subObjects": "", 97 | "dices": "[1, 5, 1]" 98 | } 99 | """ 100 | 101 | let jsonData: Data = jsonString.data(using: .utf8)! 102 | let jsonDecoder = JSONDecoder() 103 | 104 | // Act 105 | let object = try jsonDecoder.decode(SomeObject.self, from: jsonData) 106 | 107 | // Assert 108 | let actual: Array? = object.subObjects 109 | let expect: Array? = nil 110 | 111 | #expect(actual == expect) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JsonProtection 2 | [![Swift-6.0](https://img.shields.io/badge/Swift-6.2-red.svg?style=plastic&logo=Swift&logoColor=white&link=)](https://developer.apple.com/swift/) 3 | [![example workflow](https://github.com/Darktt/JsonProtection/actions/workflows/main.yml/badge.svg)]() 4 | 5 | [English](/en/README.md) 6 | 7 | 處理後端提供各種神奇 Json 資料,而做的解析保護 8 | 9 | ## 安裝方法 10 | 11 | 使用 Swift Package Manager: 12 | 13 | * File > Swift Packages > Add Package Dependency 14 | * Add https://github.com/Darktt/JsonProtection 15 | * Select "Up to Next Major" with "1.2.2" 16 | 17 | ## 功能說明 18 | 19 | ### BoolProtection 20 | 處理 json 資料應該為**布林值**,但是實際上並非**布林值**的問題 21 | ```json 22 | { 23 | "true": true, 24 | "false": "FALSE" 25 | } 26 | ``` 27 | 28 | 將要解析成 Bool 型態的 property 套上 `@BoolProtection` 進行型態保護 29 | ```swift 30 | struct BoolObject: Decodable 31 | { 32 | @BoolProtection 33 | var `true`: Bool? 34 | 35 | @BoolProtection 36 | var `false`: Bool? 37 | } 38 | ``` 39 | 40 | > 支援型態(字串不分大小寫): 41 | > * true: 1、“true”、“yes” 42 | > * false: 0、“false”、“no” 43 | 44 | --- 45 | 46 | ### MissingKeyProtection 47 | 保護 json key 時有時無的情況 48 | ```json 49 | 正常情況: 50 | { 51 | "data": { 52 | "name": "Some one", 53 | "age": 11 54 | } 55 | } 56 | 57 | 錯誤情況: 58 | { 59 | "error": "token is expired." 60 | } 61 | ``` 62 | 63 | 將 key 可能消失的 property 加上 `@MissingKeyProtection` 進行保護 64 | ```swift 65 | struct UserData: Decodable 66 | { 67 | @MissingKeyProtection 68 | var data: UserInfo? 69 | 70 | @MissingKeyProtection 71 | var error: String? 72 | } 73 | ``` 74 | 75 | ---- 76 | 77 | ### NumberProtection 78 | 處理 json 資料應該為**數字型態**,但是實際上可能為**字串型態**的問題 79 | > 包含 MissingKeyProtection 的功能 80 | ```json 81 | { 82 | "city": "高雄市", 83 | "lat": "22.78806", 84 | "lng": "120.24257" 85 | } 86 | ``` 87 | 88 | 將要解析成數字型態的 property 套上 `@NumberProtection` 進行型態保護 89 | ```swift 90 | struct BikeLocation: Decodable 91 | { 92 | var city: String? 93 | 94 | @NumberProtection 95 | var lat: CLLocationDegrees? 96 | 97 | @NumberProtection 98 | var lng: CLLocationDegrees? 99 | } 100 | ``` 101 | 102 | > 支援的數字型態: 103 | > * Int 104 | > * Float 105 | > * Double 106 | > * Decimal 107 | > * 含以上型態的列舉 108 | eg: `enum Type: Int` 109 | (需套用 NumberType 這個 protocol) 110 | 111 | --- 112 | 113 | ### NumbersProtection 114 | 處理 json 資料應該為**數字陣列型態**,但是實際上可能為**字串陣列型態**的問題 115 | ```json 116 | { 117 | "index": ["10", "20", "50", "100"] 118 | } 119 | ``` 120 | 121 | 將要解析成數字陣列型態的 property 套上 `@NumbersProtection` 進行型態保護 122 | ```swift 123 | struct Library: Decodable 124 | { 125 | @NumbersProtection 126 | var index: [Int]? 127 | } 128 | ``` 129 | 130 | > 支援的數字型態: 131 | > * 與 NumberProtection 相同 132 | 133 | ---- 134 | 135 | ### NumberArrayProtection 136 | 處理 json 裡應為數字內用的 jsonArray 資料,但實際上是 String 型態的問題 137 | ```json 138 | { 139 | "dices": "[1, 5.2, 1.0]" 140 | } 141 | ``` 142 | 143 | 將要解析成數字陣列型態的 property 套上 `@NumberArrayProtection` 進行型態保護 144 | ```swift 145 | struct NumberArray: Decodable 146 | { 147 | @NumberArrayProtection 148 | var dices: [Double]? 149 | } 150 | ``` 151 | 152 | > 支援的數字型態 (陣列內的數字型態必定要相同,不能混入字串): 153 | > * 與 NumberProtection 相同 154 | 155 | ---- 156 | 157 | ### ObjectProtection 158 | 處理 json 裡應為 jsonObject 資料,但實際上是 String 型態的問題 159 | ```json 160 | { 161 | "num": 1, 162 | "sub": "{\"num\": 22}" 163 | } 164 | ``` 165 | 166 | 將要解析成物件的 property 套上 `@ObjectProtection` 進行型態保護 167 | ```swift 168 | struct Info: Decodable 169 | { 170 | var num: Int? 171 | 172 | @ObjectProtection 173 | var sub: Sub? 174 | } 175 | 176 | extension Info 177 | { 178 | struct Sub: Decodable 179 | { 180 | var num: Int? 181 | } 182 | } 183 | ``` 184 | 185 | --- 186 | 187 | ### DateProtection 188 | 解決 json 資料裡的時間各式不統一的問題, 189 | 並且避免 json 資料型態不正確的問題 190 | ```json 191 | { 192 | "updateTime": 1694102400, 193 | "expiredDate": "202309080100" 194 | } 195 | ``` 196 | 197 | 將要解析成日期陣列型態的 property 套上 `@DateProtection` 進行型態保護 198 | ```swift 199 | struct DateObject: Decodable 200 | { 201 | let updateTime: Date? 202 | 203 | @DateProtection(configuration: DateConfiguration.self) 204 | private(set) 205 | var expiredDate: Date? 206 | } 207 | ``` 208 | 209 | 並且繼承 DateConfigurate 提供相關設定 210 | ```swift 211 | extension DateObject 212 | { 213 | struct DateConfiguration: DateConfigurate 214 | { 215 | static var option: DateConfigurateOption { 216 | 217 | .dateFormat("yyyyMMddhhss", timeZone: TimeZone(abbreviation: "GMT+0800")) 218 | } 219 | } 220 | } 221 | ``` 222 | 223 | > 設定選項: 224 | > * 時間格式 (yyyyMMdd 等格式) 與時區 225 | > * 時間戳(基本單位為秒(Second)) 226 | > * 時間戳(基本單位為毫秒(millisecond)) 227 | > * iso8601(是否有毫秒) 228 | 229 | > 支援型態: 230 | > * 字串 231 | > * 任何數字型態 232 | 233 | --- 234 | 235 | ### UIImageProtection 236 | 將 json 裡的字串解析成為 UIImage 型態 237 | > ⚠️:專案內需有同名的圖片檔案 238 | 239 | --- 240 | 241 | ### URLProtection 242 | 避免 json 裡的字串為空時,解析成 URL 型態會解析失敗的問題 243 | ```json 244 | { 245 | "homePageUrl": "https://www.google.com", 246 | "detailPageUrl": "" 247 | } 248 | ``` 249 | 250 | 將要解析成 URL 的 property 套上 `@URLProtection` 進行型態保護 251 | ```swift 252 | struct URLObject: Decodable 253 | { 254 | private(set) 255 | var homePageUrl: URL? 256 | 257 | @URLProtection 258 | var detailPageUrl: URL? 259 | } 260 | ``` 261 | 262 | --- 263 | 264 | ### AESDecoder 265 | 這是唯一不做保護的功能,僅做解析 Json 資料時同時做 AES256 解密 266 | 267 | 使用需繼承 AESAdopter 提供相關解密資訊 268 | ```swift 269 | struct AESAdopting: AESAdopter 270 | { 271 | static var key: String { 272 | 273 | "MnoewgUZrgt5Rk08MtESwHvgzY7ElaEq" 274 | } 275 | 276 | static var iv: String? { 277 | 278 | "rtCG5mdgtlCtbyI4" 279 | } 280 | 281 | static var options: DTAES.Options { 282 | 283 | [.pkc7Padding, .zeroPadding] 284 | } 285 | } 286 | ``` 287 | 288 | 然後在需解密的 property 時加上 `@AESDecoder`,即可在完成解析後並且做解密的動作 289 | ```swift 290 | struct AESObject: Decodable 291 | { 292 | @AESDecoder(adopter: AESAdopting.self) 293 | var url: String? 294 | } 295 | ``` 296 | 297 | > 支援型態: 298 | > * 字串 299 | > * 字串陣列 300 | 301 | --- 302 | ### MultipleKeysProtection (未完成) 303 | 保護同變數在不同 json 資料情境下,有複數的 key 存在的問題 304 | 305 | ## 作者 306 | Created by Darktt. 307 | 308 | ## License 309 | 310 | This project is licensed under the MIT License. 311 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/DateProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DateProtection.swift 3 | // 4 | // Created by Darktt on 2023/9/7. 5 | // Copyright © 2023 Darktt. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - DateConfiguration - 11 | 12 | public 13 | protocol DateConfigurate 14 | { 15 | static 16 | var option: DateConfigurateOption { get } 17 | } 18 | 19 | // MARK: - DateConfigurateOption - 20 | 21 | public 22 | enum DateConfigurateOption 23 | { 24 | /// 時間格式,時區未給的話,即預設系統時區 25 | case dateFormat(_ format: String, timeZone: TimeZone?) 26 | 27 | /// 時間戳(基本單位為秒(Second)) 28 | case secondsSince1970 29 | 30 | /// 時間戳(基本單位為毫秒(millisecond)) 31 | case millisecondsSince1970 32 | 33 | /// iso8601 34 | case iso8601(hasFractionalSeconds: Bool) 35 | } 36 | 37 | // MARK: - DateProtection - 38 | 39 | @propertyWrapper 40 | public 41 | struct DateProtection where Configurate: DateConfigurate 42 | { 43 | // MARK: - Properties - 44 | 45 | public 46 | var wrappedValue: Date? 47 | 48 | // MARK: - Methods - 49 | // MARK: Initial Method 50 | 51 | public 52 | init(configuration: Configurate.Type) { } 53 | } 54 | 55 | // MARK: - Conform Protocols - 56 | 57 | extension DateProtection: Decodable 58 | { 59 | public 60 | init(from decoder: Decoder) throws 61 | { 62 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 63 | 64 | if let stringValue = try? container.decode(String.self) { 65 | 66 | let date: Date? = self.date(from: stringValue) 67 | 68 | self.wrappedValue = date 69 | return 70 | } 71 | 72 | if let integerValue = try? container.decode(Int.self) { 73 | 74 | let date: Date? = self.date(from: integerValue) 75 | 76 | self.wrappedValue = date 77 | 78 | return 79 | } 80 | 81 | if let doubleValue = try? container.decode(Double.self) { 82 | 83 | let date: Date? = self.date(from: doubleValue) 84 | 85 | self.wrappedValue = date 86 | 87 | return 88 | } 89 | 90 | self.wrappedValue = nil 91 | } 92 | } 93 | 94 | // MARK: - Private Methods - 95 | 96 | private 97 | extension DateProtection 98 | { 99 | func date(from string: String) -> Date? 100 | { 101 | var date: Date? = nil 102 | let option: DateConfigurateOption = Configurate.option 103 | 104 | if case let .dateFormat(format, timeZone: timeZone) = option { 105 | 106 | let formatter = DateFormatter.privateShare 107 | formatter.dateFormat = format 108 | formatter.timeZone = timeZone ?? .current 109 | 110 | date = formatter.date(from: string) 111 | } 112 | 113 | if case .secondsSince1970 = option, 114 | let timeInterval = TimeInterval(string) { 115 | 116 | date = Date(timeIntervalSince1970: timeInterval) 117 | } 118 | 119 | if case .millisecondsSince1970 = option, 120 | var timeInterval = TimeInterval(string) { 121 | 122 | timeInterval /= 1000.0 123 | 124 | date = Date(timeIntervalSince1970: timeInterval) 125 | } 126 | 127 | if case let .iso8601(hasFractionalSeconds) = option { 128 | 129 | let options: ISO8601DateFormatter.Options = hasFractionalSeconds ? .withFractionalSeconds : [] 130 | 131 | let formatter = ISO8601DateFormatter() 132 | formatter.formatOptions = options 133 | 134 | date = formatter.date(from: string) 135 | } 136 | 137 | return date 138 | } 139 | 140 | func date(from integer: Int) -> Date? 141 | { 142 | var date: Date? 143 | let option: DateConfigurateOption = Configurate.option 144 | 145 | if case let .dateFormat(format, timeZone) = option { 146 | 147 | let formatter = DateFormatter.privateShare 148 | formatter.dateFormat = format 149 | formatter.timeZone = timeZone ?? .current 150 | 151 | date = formatter.date(from: String(integer)) 152 | } 153 | 154 | if case .secondsSince1970 = option { 155 | 156 | let timeInterval = TimeInterval(integer) 157 | 158 | date = Date(timeIntervalSince1970: timeInterval) 159 | } 160 | 161 | if case .millisecondsSince1970 = option { 162 | 163 | let timeInterval = TimeInterval(integer) / 1000.0 164 | 165 | date = Date(timeIntervalSince1970: timeInterval) 166 | } 167 | 168 | if case let .iso8601(hasFractionalSeconds) = option { 169 | 170 | let options: ISO8601DateFormatter.Options = hasFractionalSeconds ? .withFractionalSeconds : [] 171 | 172 | let formatter = ISO8601DateFormatter() 173 | formatter.formatOptions = options 174 | 175 | date = formatter.date(from: String(integer)) 176 | } 177 | 178 | return date 179 | } 180 | 181 | func date(from double: Double) -> Date? 182 | { 183 | var date: Date? 184 | let option: DateConfigurateOption = Configurate.option 185 | 186 | if case let .dateFormat(format, timeZone) = option { 187 | 188 | let formatter = DateFormatter.privateShare 189 | formatter.dateFormat = format 190 | formatter.timeZone = timeZone ?? .current 191 | 192 | date = formatter.date(from: String(double)) 193 | } 194 | 195 | if case .secondsSince1970 = option { 196 | 197 | date = Date(timeIntervalSince1970: double) 198 | } 199 | 200 | if case .millisecondsSince1970 = option { 201 | 202 | let timeInterval = double / 1000.0 203 | 204 | date = Date(timeIntervalSince1970: timeInterval) 205 | } 206 | 207 | if case let .iso8601(hasFractionalSeconds) = option { 208 | 209 | let options: ISO8601DateFormatter.Options = hasFractionalSeconds ? .withFractionalSeconds : [] 210 | 211 | let formatter = ISO8601DateFormatter() 212 | formatter.formatOptions = options 213 | 214 | date = formatter.date(from: String(double)) 215 | } 216 | 217 | return date 218 | } 219 | } 220 | 221 | // MARK: - Private Extension - 222 | 223 | fileprivate 224 | extension DateFormatter 225 | { 226 | static let privateShare: DateFormatter = .init() 227 | } 228 | -------------------------------------------------------------------------------- /en/README.md: -------------------------------------------------------------------------------- 1 | # JsonProtection 2 | [![Swift-6.0](https://img.shields.io/badge/Swift-6.2-red.svg?style=plastic&logo=Swift&logoColor=white&link=)](https://developer.apple.com/swift/) 3 | [![example workflow](https://github.com/Darktt/JsonProtection/actions/workflows/main.yml/badge.svg)]() 4 | 5 | A solution for handling and protecting the parsing of various unusual JSON data provided by the backend. 6 | 7 | ## Installation 8 | 9 | Using Swift Package Manager: 10 | 11 | 1. Go to **File > Swift Packages > Add Package Dependency** 12 | 2. Add `https://github.com/Darktt/JsonProtection` 13 | 3. Select **"Up to Next Major"** with version **"1.2.2"** 14 | 15 | ## Features 16 | 17 | ### BoolProtection 18 | Handles the issue where JSON data should be **Boolean**, but in reality, it is not a **Boolean**. 19 | 20 | ```json 21 | { 22 | "true": true, 23 | "false": "FALSE" 24 | } 25 | ``` 26 | 27 | Apply `@BoolProtection` to properties that need to be parsed as `Bool` type for type protection: 28 | 29 | ```swift 30 | struct BoolObject: Decodable 31 | { 32 | @BoolProtection 33 | var `true`: Bool? 34 | 35 | @BoolProtection 36 | var `false`: Bool? 37 | } 38 | ``` 39 | 40 | > Supported values (case-insensitive for strings): 41 | > - **true:** `1`, `"true"`, `"yes"` 42 | > - **false:** `0`, `"false"`, `"no"` 43 | 44 | --- 45 | 46 | ### MissingKeyProtection 47 | Protects against cases where JSON keys are sometimes missing. 48 | 49 | ```json 50 | Normal case: 51 | { 52 | "data": { 53 | "name": "Some one", 54 | "age": 11 55 | } 56 | } 57 | 58 | Error case: 59 | { 60 | "error": "Missing data" 61 | } 62 | ``` 63 | 64 | When parsing, apply `@MissingKeyProtection` to ensure the property does not cause parsing failures: 65 | 66 | ```swift 67 | struct DataObject: Decodable 68 | { 69 | @MissingKeyProtection 70 | var name: String? 71 | 72 | @MissingKeyProtection 73 | var age: Int? 74 | } 75 | ``` 76 | 77 | --- 78 | 79 | ### NumberProtection 80 | Handles JSON values that should be **numbers** but are actually **strings**. 81 | 82 | ```json 83 | { 84 | "price": "100", 85 | "discount": "10.5" 86 | } 87 | ``` 88 | 89 | Use `@NumberProtection` for automatic conversion: 90 | 91 | ```swift 92 | struct Product: Decodable 93 | { 94 | @NumberProtection 95 | var price: Double? 96 | 97 | @NumberProtection 98 | var discount: Double? 99 | } 100 | ``` 101 | 102 | > Supported types: 103 | > * Int 104 | > * Float 105 | > * Double 106 | > * Decimal 107 | > * And enum with `RawValue` type of Int, Float, Double, Decimal 108 | (Needs to conform to `NumberType` protocol) 109 | 110 | --- 111 | 112 | ### NumbersProtection 113 | Handles cases where **numeric values array** in JSON may be stored as **strings array** or incorrect formats. 114 | ```json 115 | { 116 | "index": ["10", "20", "50", "100"] 117 | } 118 | ``` 119 | 120 | Use `@NumbersProtection` to ensure the values are correctly converted: 121 | 122 | ```swift 123 | struct Library: Decodable 124 | { 125 | @NumbersProtection 126 | var index: [Int]? 127 | } 128 | ``` 129 | 130 | > Supported types: 131 | > * Same as `NumberProtection` 132 | 133 | ---- 134 | 135 | ### NumberArrayProtection 136 | Handles cases where **numeric array** in JSON may be stored as **string** or incorrect formats. 137 | 138 | ```json 139 | { 140 | "dices": "[1, 5.2, 1.0]" 141 | } 142 | ``` 143 | 144 | Use `@NumberArrayProtection` to ensure the values are correctly converted: 145 | 146 | ```swift 147 | struct NumberArray: Decodable 148 | { 149 | @NumberArrayProtection 150 | var dices: [Double]? 151 | } 152 | ``` 153 | 154 | > Supported types: 155 | > * Same as `NumberProtection` 156 | 157 | ---- 158 | 159 | ### ObjectProtection 160 | Handles cases where **JSON object** is stored as a **string**. 161 | 162 | ```json 163 | { 164 | "data": "{\"name\":\"Some one\",\"age\":11}" 165 | } 166 | ``` 167 | 168 | Use `@ObjectProtection` to ensure the value is correctly converted: 169 | 170 | ```swift 171 | struct Info: Decodable 172 | { 173 | var num: Int? 174 | 175 | @ObjectProtection 176 | var sub: Sub? 177 | } 178 | 179 | extension Info 180 | { 181 | struct Sub: Decodable 182 | { 183 | var num: Int? 184 | } 185 | } 186 | ``` 187 | 188 | --- 189 | 190 | ### DateProtection 191 | Handles cases where **date values** in JSON are stored as **strings** or **UNIX timestamps**. 192 | 193 | ```json 194 | { 195 | "updateTime": 1694102400, 196 | "expiredDate": "202309080100" 197 | } 198 | ``` 199 | 200 | Use `@DateProtection` to ensure correct parsing: 201 | 202 | ```swift 203 | struct DateObject: Decodable 204 | { 205 | let updateTime: Date? 206 | 207 | @DateProtection(configuration: DateConfiguration.self) 208 | private(set) 209 | var expiredDate: Date? 210 | } 211 | ``` 212 | 213 | And set the `DateConfiguration` for the date format: 214 | 215 | ```swift 216 | sextension DateObject 217 | { 218 | struct DateConfiguration: DateConfigurate 219 | { 220 | static var option: DateConfigurateOption { 221 | 222 | .dateFormat("yyyyMMddhhss", timeZone: TimeZone(abbreviation: "GMT+0800")) 223 | } 224 | } 225 | } 226 | ``` 227 | 228 | > Options: 229 | > * `dateFormat`: Date format string, and time zone. 230 | > * `secondsSince1970`: Convert the value to a time interval. 231 | > * `millisecondsSince1970`: Convert the value to a time interval in milliseconds. 232 | > * `iso8601`: Convert the value to an ISO8601 date format. 233 | 234 | > Supported types: 235 | > * Strings 236 | > * Any numeric type 237 | 238 | --- 239 | 240 | ### UIImageProtection 241 | Converts **strings** in JSON to `UIImage` type. 242 | > ⚠️: The project must contain the same name image files. 243 | 244 | --- 245 | 246 | ### URLProtection 247 | Avoids parsing failures when JSON data contains **Empty string**. 248 | 249 | ```json 250 | { 251 | "homePageUrl": "https://www.google.com", 252 | "detailPageUrl": "" 253 | } 254 | ``` 255 | 256 | Use `@URLProtection` to ensure the URL is correctly parsed: 257 | ```swift 258 | struct URLObject: Decodable 259 | { 260 | private(set) 261 | var homePageUrl: URL? 262 | 263 | @URLProtection 264 | var detailPageUrl: URL? 265 | } 266 | ``` 267 | 268 | --- 269 | 270 | ### AESDecoder 271 | This feature is used to decode the data that has been encrypted with AES. 272 | 273 | Use `AESAdopter` to provide the decryption information: 274 | ```swift 275 | struct AESAdopting: AESAdopter 276 | { 277 | static var key: String { 278 | 279 | "MnoewgUZrgt5Rk08MtESwHvgzY7ElaEq" 280 | } 281 | 282 | static var iv: String? { 283 | 284 | "rtCG5mdgtlCtbyI4" 285 | } 286 | 287 | static var options: DTAES.Options { 288 | 289 | [.pkc7Padding, .zeroPadding] 290 | } 291 | } 292 | ``` 293 | 294 | Then use `@AESDecoder` to decode the data: 295 | ```swift 296 | struct AESObject: Decodable 297 | { 298 | @AESDecoder(adopter: AESAdopting.self) 299 | var url: String? 300 | } 301 | ``` 302 | 303 | > Supported types: 304 | > * Strings 305 | > * Strings array 306 | 307 | ## Author 308 | Created by Darktt. 309 | 310 | ## License 311 | 312 | This project is licensed under the MIT License. 313 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/NumberProtection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberProtection.swift 3 | // 4 | // Created by Darktt on 2022/6/21. 5 | // Copyright © 2022 Darktt. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | // MARK: - NumberType - 11 | 12 | public 13 | protocol NumberType 14 | { 15 | init?(_ source: String) 16 | 17 | init?(_ source: Decimal) 18 | 19 | init?(_ source: T) where T: BinaryInteger 20 | 21 | init?(_ source: Float) 22 | 23 | init?(_ source: Double) 24 | } 25 | 26 | extension NumberType where Self: RawRepresentable, RawValue: NumberType 27 | { 28 | public 29 | init?(_ source: String) 30 | { 31 | guard let rawValue = RawValue(source) else { 32 | 33 | return nil 34 | } 35 | 36 | self.init(rawValue: rawValue) 37 | } 38 | 39 | public 40 | init?(_ source: Decimal) 41 | { 42 | guard let rawValue = RawValue(source) else { 43 | 44 | return nil 45 | } 46 | 47 | self.init(rawValue: rawValue) 48 | } 49 | 50 | public 51 | init?(_ source: T) where T : BinaryInteger 52 | { 53 | guard let rawValue = RawValue(source) else { 54 | 55 | return nil 56 | } 57 | 58 | self.init(rawValue: rawValue) 59 | } 60 | 61 | public 62 | init?(_ source: Float) 63 | { 64 | guard let rawValue = RawValue(source) else { 65 | 66 | return nil 67 | } 68 | 69 | self.init(rawValue: rawValue) 70 | } 71 | 72 | public 73 | init?(_ source: Double) 74 | { 75 | guard let rawValue = RawValue(source) else { 76 | 77 | return nil 78 | } 79 | 80 | self.init(rawValue: rawValue) 81 | } 82 | } 83 | 84 | extension Decimal: NumberType 85 | { 86 | public 87 | init?(_ source: String) 88 | { 89 | self.init(string: source) 90 | } 91 | 92 | public 93 | init?(_ source: Decimal) 94 | { 95 | self = source 96 | } 97 | 98 | public 99 | init?(_ source: T) where T : BinaryInteger 100 | { 101 | guard let value = source as? Int else { 102 | 103 | return nil 104 | } 105 | 106 | self.init(string: "\(value)") 107 | } 108 | 109 | public 110 | init?(_ source: Float) 111 | { 112 | let value = String(source) 113 | 114 | self.init(string: value) 115 | } 116 | } 117 | 118 | extension Int: NumberType 119 | { 120 | public 121 | init?(_ source: String) 122 | { 123 | guard let decimal = Decimal(source) else { 124 | 125 | return nil 126 | } 127 | 128 | self.init(decimal) 129 | } 130 | 131 | public 132 | init?(_ source: Decimal) 133 | { 134 | let number = NSDecimalNumber(decimal: source) 135 | 136 | self.init(truncating: number) 137 | } 138 | } 139 | 140 | extension Int32: NumberType 141 | { 142 | public 143 | init?(_ source: String) 144 | { 145 | guard let decimal = Decimal(source) else { 146 | 147 | return nil 148 | } 149 | 150 | self.init(decimal) 151 | } 152 | 153 | public 154 | init?(_ source: Decimal) 155 | { 156 | let number = NSDecimalNumber(decimal: source) 157 | 158 | self.init(truncating: number) 159 | } 160 | } 161 | 162 | extension UInt: NumberType 163 | { 164 | public 165 | init?(_ source: String) 166 | { 167 | guard let decimal = Decimal(source) else { 168 | 169 | return nil 170 | } 171 | 172 | self.init(decimal) 173 | } 174 | 175 | public 176 | init?(_ source: Decimal) 177 | { 178 | let number = NSDecimalNumber(decimal: source) 179 | 180 | self.init(truncating: number) 181 | } 182 | } 183 | 184 | extension UInt32: NumberType 185 | { 186 | public 187 | init?(_ source: String) 188 | { 189 | guard let decimal = Decimal(source) else { 190 | 191 | return nil 192 | } 193 | 194 | self.init(decimal) 195 | } 196 | 197 | public 198 | init?(_ source: Decimal) 199 | { 200 | let number = NSDecimalNumber(decimal: source) 201 | 202 | self.init(truncating: number) 203 | } 204 | } 205 | 206 | extension Float: NumberType 207 | { 208 | public 209 | init?(_ source: Decimal) 210 | { 211 | let number = NSDecimalNumber(decimal: source) 212 | 213 | self.init(truncating: number) 214 | } 215 | } 216 | 217 | extension Double: NumberType 218 | { 219 | public 220 | init?(_ source: Decimal) 221 | { 222 | let number = NSDecimalNumber(decimal: source) 223 | 224 | self.init(truncating: number) 225 | } 226 | } 227 | 228 | // MARK: - NumberProtection - 229 | 230 | @propertyWrapper 231 | public 232 | struct NumberProtection: MissingKeyProtecting where DecodeType: Decodable, DecodeType: NumberType 233 | { 234 | // MARK: - Properties - 235 | 236 | public 237 | var wrappedValue: DecodeType? 238 | 239 | // MARK: - Methods - 240 | // MARK: Initial Method 241 | 242 | public 243 | init(wrappedValue: DecodeType?) 244 | { 245 | self.wrappedValue = wrappedValue 246 | } 247 | } 248 | 249 | extension NumberProtection 250 | { 251 | public 252 | init(from decoder: Decoder) throws 253 | { 254 | let container: SingleValueDecodingContainer = try decoder.singleValueContainer() 255 | 256 | let wrappedValue: DecodeType? = self.decode(from: container) 257 | 258 | self.wrappedValue = wrappedValue 259 | } 260 | 261 | private 262 | func decode(from container: SingleValueDecodingContainer) -> DecodeType? 263 | { 264 | var wrappedValue: DecodeType? 265 | 266 | if let string = try? container.decode(String.self) { 267 | 268 | wrappedValue = DecodeType(string) 269 | } 270 | 271 | if let integer = try? container.decode(Int.self) { 272 | 273 | wrappedValue = DecodeType(integer) 274 | } 275 | 276 | if let float = try? container.decode(Float.self) { 277 | 278 | wrappedValue = DecodeType(float) 279 | } 280 | 281 | if let double = try? container.decode(Double.self) { 282 | 283 | wrappedValue = DecodeType(double) 284 | } 285 | 286 | if let decimal = try? container.decode(Decimal.self), DecodeType.self == Decimal.self { 287 | 288 | wrappedValue = DecodeType(decimal) 289 | } 290 | 291 | return wrappedValue 292 | } 293 | } 294 | 295 | // MARK: - Conform Protocol - 296 | 297 | extension NumberProtection: CustomStringConvertible 298 | { 299 | public var description: String 300 | { 301 | self.wrappedValue.map { 302 | 303 | "\($0)" 304 | } ?? "nil" 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /SourceCode/JsonProtection/DTAES.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DTAES.swift 3 | // 4 | // Created by Darktt on 18/11/9. 5 | // Copyright © 2018 Darktt. All rights reserved. 6 | // 7 | 8 | #if canImport(CommonCrypto) 9 | 10 | import CommonCrypto 11 | import Foundation 12 | 13 | /// AES 128 encrypt and decrypt 14 | public 15 | class DTAES 16 | { 17 | // MARK: - Properties - 18 | 19 | internal 20 | var operation: Operation 21 | 22 | internal 23 | var options: Options 24 | 25 | internal 26 | var key: Data = Data() 27 | 28 | internal 29 | var iv: Data? 30 | 31 | private 32 | var contentString: String? 33 | 34 | private 35 | var contentData: Data 36 | 37 | private 38 | var ccOperation: CCOperation { 39 | 40 | return CCOperation(self.operation.rawValue) 41 | } 42 | 43 | private 44 | var ccOptions: CCOptions { 45 | 46 | return CCOptions(self.options.rawValue) 47 | } 48 | 49 | // MARK: - Methods - 50 | // MARK: Initial Method 51 | 52 | internal 53 | convenience init(_ string: String) 54 | { 55 | self.init(Data()) 56 | self.contentString = string 57 | } 58 | 59 | internal 60 | init(_ data: Data) 61 | { 62 | self.operation = .encrypt 63 | self.options = [] 64 | self.contentData = data 65 | } 66 | 67 | internal 68 | convenience init(_ bytes: UnsafeRawPointer, length: Int) 69 | { 70 | let contentData = Data(bytes: bytes, count: length) 71 | 72 | self.init(contentData) 73 | } 74 | 75 | // MARK: - internal Methods - 76 | 77 | internal 78 | func setKey(_ keyString: String) 79 | { 80 | guard let key: Data = keyString.data(using: .utf8) else { 81 | 82 | return 83 | } 84 | 85 | self.key = key 86 | } 87 | 88 | internal 89 | func setIv(_ ivString: String) 90 | { 91 | guard let iv: Data = ivString.data(using: .utf8) else { 92 | 93 | return 94 | } 95 | 96 | self.iv = iv 97 | } 98 | 99 | internal 100 | func result() throws -> String 101 | { 102 | let data: Data = try self.result() 103 | 104 | var result: String! 105 | 106 | if self.operation == .encrypt { 107 | 108 | result = data.base64EncodedString() 109 | } 110 | 111 | if self.operation == .decrypt { 112 | 113 | result = String(data: data, encoding: .utf8) 114 | } 115 | 116 | return result 117 | } 118 | 119 | internal 120 | func result() throws -> Data 121 | { 122 | if let contentString = self.contentString { 123 | 124 | var contentData: Data! 125 | 126 | if self.operation == .encrypt { 127 | 128 | contentData = contentString.data(using: .utf8) 129 | } 130 | 131 | if self.operation == .decrypt { 132 | 133 | contentData = Data(base64Encoded: contentString) 134 | } 135 | 136 | self.contentData = contentData 137 | } 138 | 139 | let key: Array = Array(self.key) 140 | let length: Int = kCCKeySizeAES128 141 | var iv: Array = Array(repeating: 0, count: length) 142 | 143 | if let _iv = self.iv { 144 | 145 | iv = Array(_iv[0 ..< length]) 146 | } 147 | 148 | let result: Data = try self.aes(withKey: key, iv: iv) 149 | 150 | return result 151 | } 152 | } 153 | 154 | // MARK: - Private Methods - 155 | 156 | private 157 | extension DTAES 158 | { 159 | func aes(withKey key: Array, iv: Array) throws -> Data 160 | { 161 | let algorithm = CCAlgorithm(kCCAlgorithmAES) 162 | var contentData: Data = self.contentData 163 | let hasZeroPadding: Bool = self.options.contains(.zeroPadding) 164 | 165 | var currentOptions = self.options 166 | 167 | if hasZeroPadding && self.operation == .encrypt { 168 | 169 | // Reset to ECB mode for local use only 170 | currentOptions = [.ecbMode] 171 | 172 | // Add zero padding. 173 | var zeroByte: UInt8 = 0x00 174 | let blockSize: Int = kCCBlockSizeAES128 175 | let paddingBytes: Int = blockSize - (contentData.count % blockSize) 176 | 177 | (0 ..< paddingBytes).forEach { 178 | 179 | _ in 180 | 181 | contentData.append(&zeroByte, count: 1) 182 | } 183 | } 184 | 185 | let bufferSize: Int = contentData.count + kCCBlockSizeAES128 186 | var buffer: Array = Array(repeating: 0, count: bufferSize) 187 | 188 | var numberBytesCrypto: Int = 0 189 | 190 | let cryptStatus: CCCryptorStatus = CCCrypt(self.ccOperation, 191 | algorithm, 192 | CCOptions(currentOptions.rawValue), 193 | key, 194 | key.count, 195 | iv, 196 | contentData._bytes, 197 | contentData.count, 198 | &buffer, 199 | bufferSize, 200 | &numberBytesCrypto) 201 | 202 | if cryptStatus != kCCSuccess { 203 | 204 | throw Error.cryptFailed(code: Int(cryptStatus)) 205 | } 206 | 207 | let data = Data(bytes: buffer, count: numberBytesCrypto) 208 | 209 | return data 210 | } 211 | } 212 | 213 | // MARK: Enumerator, Options, Error 214 | 215 | extension DTAES 216 | { 217 | public 218 | enum Operation: Int 219 | { 220 | case encrypt = 0 221 | 222 | case decrypt = 1 223 | } 224 | 225 | public 226 | struct Options: OptionSet, Sendable 227 | { 228 | public typealias RawValue = UInt8 229 | 230 | public static let pkc7Padding: Options = Options(rawValue: 0x0001) 231 | 232 | public static let ecbMode: Options = Options(rawValue: 0x0002) 233 | 234 | public static let zeroPadding: Options = Options(rawValue: 0x0004) 235 | 236 | public let rawValue: UInt8 237 | 238 | public init(rawValue: UInt8) 239 | { 240 | self.rawValue = rawValue 241 | } 242 | } 243 | 244 | enum Error: Swift.Error 245 | { 246 | case cryptFailed(code: Int) 247 | } 248 | } 249 | 250 | extension DTAES.Error: LocalizedError 251 | { 252 | internal 253 | var errorDescription: String? 254 | { 255 | return "\(self)" 256 | } 257 | } 258 | 259 | extension DTAES.Error: CustomStringConvertible 260 | { 261 | internal 262 | var description: String 263 | { 264 | let description: String! 265 | 266 | switch self { 267 | 268 | case let .cryptFailed(code: code): 269 | description = "Crypt failed, code: \(code)" 270 | } 271 | 272 | return description 273 | } 274 | } 275 | 276 | // MARK: - Extensions - 277 | 278 | private extension Data 279 | { 280 | // MARK: - Properties - 281 | 282 | var _bytes: Array { 283 | 284 | return Array(self) 285 | } 286 | } 287 | 288 | #endif // canImport(CommonCrypto) 289 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------