├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── JSONCodable │ ├── CodableFormat.swift │ └── JSONCodable.swift └── Tests ├── JSONCodableTests ├── JSONCodableTests.swift └── XCTestManifests.swift └── LinuxMain.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | macos: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | Linux: 21 | runs-on: ubuntu-latest 22 | container: 23 | image: swift:5.1 24 | steps: 25 | - uses: actions/checkout@v1 26 | - name: Build 27 | run: swift build -v 28 | - name: Run tests 29 | run: swift test -v 30 | 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | .DS_Store 6 | .build 7 | .swiftpm 8 | 9 | ## User settings 10 | xcuserdata/ 11 | 12 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 13 | *.xcscmblueprint 14 | *.xccheckout 15 | 16 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 17 | build/ 18 | DerivedData/ 19 | *.moved-aside 20 | *.pbxuser 21 | !default.pbxuser 22 | *.mode1v3 23 | !default.mode1v3 24 | *.mode2v3 25 | !default.mode2v3 26 | *.perspectivev3 27 | !default.perspectivev3 28 | 29 | ## Obj-C/Swift specific 30 | *.hmap 31 | 32 | ## App packaging 33 | *.ipa 34 | *.dSYM.zip 35 | *.dSYM 36 | 37 | ## Playgrounds 38 | timeline.xctimeline 39 | playground.xcworkspace 40 | 41 | # Swift Package Manager 42 | # 43 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 44 | # Packages/ 45 | # Package.pins 46 | # Package.resolved 47 | # *.xcodeproj 48 | # 49 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 50 | # hence it is not needed unless you have added a package configuration file to your project 51 | # .swiftpm 52 | 53 | .build/ 54 | 55 | # CocoaPods 56 | # 57 | # We recommend against adding the Pods directory to your .gitignore. However 58 | # you should judge for yourself, the pros and cons are mentioned at: 59 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 60 | # 61 | # Pods/ 62 | # 63 | # Add this line if you want to avoid checking in source code from the Xcode workspace 64 | # *.xcworkspace 65 | 66 | # Carthage 67 | # 68 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 69 | # Carthage/Checkouts 70 | 71 | Carthage/Build/ 72 | 73 | # Accio dependency management 74 | Dependencies/ 75 | .accio/ 76 | 77 | # fastlane 78 | # 79 | # It is recommended to not store the screenshots in the git repo. 80 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 81 | # For more information about the recommended setup visit: 82 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 83 | 84 | fastlane/report.xml 85 | fastlane/Preview.html 86 | fastlane/screenshots/**/*.png 87 | fastlane/test_output 88 | 89 | # Code Injection 90 | # 91 | # After new code Injection tools there's a generated folder /iOSInjectionProject 92 | # https://github.com/johnno1962/injectionforxcode 93 | 94 | iOSInjectionProject/ 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Marcin Krzyzanowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "JSONCodable", 6 | products: [ 7 | .library( 8 | name: "JSONCodable", 9 | targets: ["JSONCodable"]), 10 | ], 11 | targets: [ 12 | .target( 13 | name: "JSONCodable", 14 | dependencies: []), 15 | .testTarget( 16 | name: "JSONCodableTests", 17 | dependencies: ["JSONCodable"]), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSONCodable 2 | 3 | JSON Codable is what we need 90% of the time. 4 | 5 | ## Usage 6 | 7 | ```swift 8 | struct Filter: JSONCodable { 9 | let id: String 10 | } 11 | 12 | let jsonString = try Filter(id: "foo").toJSON() 13 | ``` 14 | 15 | or use `CodableFormat`: 16 | 17 | ```swift 18 | struct Filter: Codable { 19 | let id: String 20 | } 21 | 22 | let data = try Filter(id: "foo").to(.json) 23 | let filter = try Filter.from(data, format: .json) 24 | ``` 25 | 26 | ### Custom format 27 | 28 | To add custom format, add it to `CodableFormat` like this: 29 | 30 | ```swift 31 | extension CodableFormat { 32 | 33 | private static var jsonSnakeCaseEncoder: JSONEncoder { 34 | let encoder = JSONEncoder() 35 | encoder.keyEncodingStrategy = .convertToSnakeCase 36 | return encoder 37 | } 38 | 39 | private static var jsonSnakeCaseDecoder: JSONDecoder { 40 | let decoder = JSONDecoder() 41 | decoder.keyDecodingStrategy = .convertFromSnakeCase 42 | return decoder 43 | } 44 | 45 | // Custom format 46 | static let jsonSnakeCase = CodableFormat("jsonSnakeCase", jsonSnakeCaseEncoder, jsonSnakeCaseDecoder) 47 | } 48 | 49 | // use jsonSnakeCase format 50 | let json = try Filter(id: "foo").to(.jsonSnakeCase) 51 | ``` 52 | 53 | ## Installation 54 | 55 | Copy `JSONCodable.swift` to your project, or 56 | 57 | ### Swift Package Manager 58 | 59 | To depend on the package, you need to declare your dependency in your `Package.swift`: 60 | 61 | ```swift 62 | .package(url: "https://github.com/krzyzanowskim/JSONCodable.git", from: "1.2.0"), 63 | ``` 64 | 65 | and to your application/library target, add "JSONCodable" to your dependencies, e.g. like this: 66 | 67 | ```swift 68 | .target(name: "BestExampleApp", dependencies: ["JSONCodable"]), 69 | ``` 70 | -------------------------------------------------------------------------------- /Sources/JSONCodable/CodableFormat.swift: -------------------------------------------------------------------------------- 1 | // Created by Marcin Krzyzanowski on 06/01/2020. 2 | // 3 | // struct Filter: Codable { 4 | // let id: String 5 | // } 6 | // 7 | // let data = try Filter(id: "foo").to(.json) 8 | // let filter = try Filter.from(data, format: .json) 9 | // 10 | 11 | import Foundation 12 | 13 | public protocol FoundationEncoder { 14 | func encode(_ value: T) throws -> Data 15 | } 16 | extension JSONEncoder: FoundationEncoder { } 17 | extension PropertyListEncoder: FoundationEncoder { } 18 | 19 | public protocol FoundationDecoder { 20 | func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable 21 | } 22 | extension JSONDecoder: FoundationDecoder { } 23 | extension PropertyListDecoder: FoundationDecoder { } 24 | 25 | 26 | public struct CodableFormat: Identifiable { 27 | public let id: String 28 | let encoder: FoundationEncoder 29 | let decoder: FoundationDecoder 30 | 31 | public init(_ id: ID, _ encoder: @autoclosure () -> F, _ decoder: @autoclosure () -> D) { 32 | self.id = id 33 | self.encoder = encoder() 34 | self.decoder = decoder() 35 | } 36 | 37 | public static let json = CodableFormat("json", JSONEncoder(), JSONDecoder()) 38 | public static let plist = CodableFormat("plist", PropertyListEncoder(), PropertyListDecoder()) 39 | } 40 | 41 | public extension CodableFormat { 42 | 43 | private static var jsonSnakeCaseEncoder: JSONEncoder { 44 | let encoder = JSONEncoder() 45 | encoder.keyEncodingStrategy = .convertToSnakeCase 46 | return encoder 47 | } 48 | 49 | private static var jsonSnakeCaseDecoder: JSONDecoder { 50 | let decoder = JSONDecoder() 51 | decoder.keyDecodingStrategy = .convertFromSnakeCase 52 | return decoder 53 | } 54 | 55 | // Custom format 56 | static let jsonSnakeCase = CodableFormat("jsonSnakeCase", jsonSnakeCaseEncoder, jsonSnakeCaseDecoder) 57 | } 58 | 59 | public extension Encodable { 60 | func to(_ format: CodableFormat) throws -> Data { 61 | try format.encoder.encode(self) 62 | } 63 | } 64 | 65 | public extension Decodable { 66 | static func from(_ data: Data, format: CodableFormat) throws -> Self { 67 | try format.decoder.decode(Self.self, from: data) 68 | } 69 | 70 | static func from(_ string: String, format: CodableFormat) throws -> Self { 71 | try self.from(Data(string.utf8), format: format) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/JSONCodable/JSONCodable.swift: -------------------------------------------------------------------------------- 1 | // Created by Marcin Krzyzanowski 2 | 3 | import Foundation 4 | 5 | public protocol JSONEncodable: Encodable { } 6 | 7 | public extension JSONEncodable { 8 | func toJSON(using encoder: @autoclosure () -> JSONEncoder = JSONEncoder()) throws -> String { 9 | try String(decoding: encoder().encode(self), as: UTF8.self) 10 | } 11 | } 12 | 13 | public protocol JSONDecodable: Decodable { } 14 | 15 | public extension JSONDecodable { 16 | static func from(json data: Data, using decoder: @autoclosure () -> JSONDecoder = JSONDecoder()) throws -> Self { 17 | try decoder().decode(Self.self, from: data) 18 | } 19 | 20 | static func from(json string: String, using decoder: @autoclosure () -> JSONDecoder = JSONDecoder()) throws -> Self { 21 | try self.from(json: Data(string.utf8), using: decoder()) 22 | } 23 | } 24 | 25 | public protocol JSONCodable: JSONEncodable & JSONDecodable { } 26 | -------------------------------------------------------------------------------- /Tests/JSONCodableTests/JSONCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import JSONCodable 3 | 4 | final class JSONCodableTests: XCTestCase { 5 | 6 | func testJSONCodable1() throws { 7 | struct F: JSONCodable, Identifiable, Equatable { 8 | let id: Int 9 | } 10 | 11 | let f1 = F(id: 1) 12 | let json = try f1.toJSON() 13 | XCTAssertEqual(json, "{\"id\":1}") 14 | 15 | let f2 = try F.from(json: json) 16 | XCTAssertEqual(f1, f2) 17 | } 18 | 19 | func testCodableFormat1() throws { 20 | 21 | struct F: Codable, Identifiable, Equatable { 22 | let id: Int 23 | } 24 | 25 | let f1 = F(id: 1) 26 | let data = try f1.to(.json) 27 | let json = String(decoding: data, as: UTF8.self) 28 | XCTAssertEqual(json, "{\"id\":1}") 29 | 30 | let f2 = try F.from(data, format: .json) 31 | XCTAssertEqual(f1, f2) 32 | } 33 | 34 | func testCodableFormatCustom() throws { 35 | 36 | struct F: Codable, Equatable { 37 | let myIdentifier: Int 38 | } 39 | 40 | let f1 = F(myIdentifier: 1) 41 | let data = try f1.to(.jsonSnakeCase) 42 | let json = String(decoding: data, as: UTF8.self) 43 | XCTAssertEqual(json, "{\"my_identifier\":1}") 44 | 45 | let f2 = try F.from(data, format: .jsonSnakeCase) 46 | XCTAssertEqual(f1, f2) 47 | } 48 | 49 | static var allTests = [ 50 | ("testCodableFormat1", testCodableFormat1), 51 | ("testCodableFormat2", testCodableFormatCustom), 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /Tests/JSONCodableTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | #if !canImport(ObjectiveC) 2 | import XCTest 3 | 4 | extension JSONCodableTests { 5 | // DO NOT MODIFY: This is autogenerated, use: 6 | // `swift test --generate-linuxmain` 7 | // to regenerate. 8 | static let __allTests__JSONCodableTests = [ 9 | ("testCodableFormat1", testCodableFormat1), 10 | ("testCodableFormatCustom", testCodableFormatCustom), 11 | ("testJSONCodable1", testJSONCodable1), 12 | ] 13 | } 14 | 15 | public func __allTests() -> [XCTestCaseEntry] { 16 | return [ 17 | testCase(JSONCodableTests.__allTests__JSONCodableTests), 18 | ] 19 | } 20 | #endif 21 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import JSONCodableTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += JSONCodableTests.__allTests() 7 | 8 | XCTMain(tests) 9 | --------------------------------------------------------------------------------