Skip to content
This repository has been archived by the owner on Feb 8, 2021. It is now read-only.

Commit

Permalink
Merge pull request #363 from hkellaway/chore/use-codable
Browse files Browse the repository at this point in the history
Codable helper methods
  • Loading branch information
hkellaway authored Aug 31, 2020
2 parents 3f6fd53 + 36ef75e commit 84ce7a1
Show file tree
Hide file tree
Showing 18 changed files with 333 additions and 21 deletions.
14 changes: 12 additions & 2 deletions Gloss.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

/* Begin PBXBuildFile section */
2FA4CE9F1C947E3000EB8AB8 /* Gloss.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DC39BF611B8A7AD200088CE0 /* Gloss.framework */; };
DC424CD124FAA173001AD4E7 /* ExtensionEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC424CD024FAA173001AD4E7 /* ExtensionEncodable.swift */; };
DC424CD224FAA173001AD4E7 /* ExtensionEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC424CD024FAA173001AD4E7 /* ExtensionEncodable.swift */; };
DC424CD324FAA173001AD4E7 /* ExtensionEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC424CD024FAA173001AD4E7 /* ExtensionEncodable.swift */; };
DC424CD424FAA173001AD4E7 /* ExtensionEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC424CD024FAA173001AD4E7 /* ExtensionEncodable.swift */; };
DC47CAE01FAE578D006E6F9A /* Comparators.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC47CAC91FAE572D006E6F9A /* Comparators.swift */; };
DC47CAE11FAE578F006E6F9A /* DecoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC47CACA1FAE572D006E6F9A /* DecoderTests.swift */; };
DC47CAE21FAE5792006E6F9A /* EncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC47CACB1FAE572D006E6F9A /* EncoderTests.swift */; };
Expand Down Expand Up @@ -76,6 +80,7 @@
2FA4CE9A1C947E3000EB8AB8 /* GlossTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GlossTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
A04168E71C0DF69900269A6A /* Gloss.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Gloss.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DC39BF611B8A7AD200088CE0 /* Gloss.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Gloss.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DC424CD024FAA173001AD4E7 /* ExtensionEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionEncodable.swift; sourceTree = "<group>"; };
DC47CAA61FAE56D0006E6F9A /* Decoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decoder.swift; sourceTree = "<group>"; };
DC47CAA71FAE56D0006E6F9A /* Encoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Encoder.swift; sourceTree = "<group>"; };
DC47CAA81FAE56D0006E6F9A /* ExtensionArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionArray.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -157,10 +162,11 @@
DC47CAA71FAE56D0006E6F9A /* Encoder.swift */,
DC47CAA81FAE56D0006E6F9A /* ExtensionArray.swift */,
DC47CAA91FAE56D0006E6F9A /* ExtensionDecodable.swift */,
DC424CD024FAA173001AD4E7 /* ExtensionEncodable.swift */,
DC47CAAA1FAE56D0006E6F9A /* ExtensionDictionary.swift */,
DC47CAAE1FAE56D0006E6F9A /* Operators.swift */,
DC47CAAC1FAE56D0006E6F9A /* Gloss.swift */,
DC47CAAD1FAE56D0006E6F9A /* Info.plist */,
DC47CAAE1FAE56D0006E6F9A /* Operators.swift */,
);
path = Gloss;
sourceTree = "<group>";
Expand Down Expand Up @@ -207,8 +213,8 @@
DC47CAA51FAE56D0006E6F9A /* Sources */ = {
isa = PBXGroup;
children = (
07400D1C2354B261008D701C /* GlossTests */,
07400D1B2354B248008D701C /* Gloss */,
07400D1C2354B261008D701C /* GlossTests */,
);
path = Sources;
sourceTree = "<group>";
Expand Down Expand Up @@ -496,6 +502,7 @@
DC47CB0F1FAE57BF006E6F9A /* Operators.swift in Sources */,
DC47CB081FAE57BF006E6F9A /* Encoder.swift in Sources */,
DC47CB071FAE57BF006E6F9A /* Decoder.swift in Sources */,
DC424CD324FAA173001AD4E7 /* ExtensionEncodable.swift in Sources */,
DC47CB091FAE57BF006E6F9A /* ExtensionArray.swift in Sources */,
DC47CB0A1FAE57BF006E6F9A /* ExtensionDecodable.swift in Sources */,
);
Expand All @@ -510,6 +517,7 @@
DC47CAFD1FAE57BE006E6F9A /* Operators.swift in Sources */,
DC47CAF61FAE57BE006E6F9A /* Encoder.swift in Sources */,
DC47CAF51FAE57BE006E6F9A /* Decoder.swift in Sources */,
DC424CD124FAA173001AD4E7 /* ExtensionEncodable.swift in Sources */,
DC47CAF71FAE57BE006E6F9A /* ExtensionArray.swift in Sources */,
DC47CAF81FAE57BE006E6F9A /* ExtensionDecodable.swift in Sources */,
);
Expand All @@ -524,6 +532,7 @@
DC47CB181FAE57BF006E6F9A /* Operators.swift in Sources */,
DC47CB111FAE57BF006E6F9A /* Encoder.swift in Sources */,
DC47CB101FAE57BF006E6F9A /* Decoder.swift in Sources */,
DC424CD424FAA173001AD4E7 /* ExtensionEncodable.swift in Sources */,
DC47CB121FAE57BF006E6F9A /* ExtensionArray.swift in Sources */,
DC47CB131FAE57BF006E6F9A /* ExtensionDecodable.swift in Sources */,
);
Expand All @@ -538,6 +547,7 @@
DC47CB061FAE57BE006E6F9A /* Operators.swift in Sources */,
DC47CAFF1FAE57BE006E6F9A /* Encoder.swift in Sources */,
DC47CAFE1FAE57BE006E6F9A /* Decoder.swift in Sources */,
DC424CD224FAA173001AD4E7 /* ExtensionEncodable.swift in Sources */,
DC47CB001FAE57BE006E6F9A /* ExtensionArray.swift in Sources */,
DC47CB011FAE57BE006E6F9A /* ExtensionDecodable.swift in Sources */,
);
Expand Down
63 changes: 63 additions & 0 deletions Sources/Gloss/ExtensionArray.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,60 @@

import Foundation

// MARK: - Migration to Codable

public extension Array where Element: JSONDecodable & Decodable {

/**
Returns array of new objects created from provided JSON array.
If any decodings fail, nil is returned.

- parameter jsonArray: Array of JSON representations of objects.

- returns: Array of objects created from JSON.
*/
static func from(decodableJSONArray jsonArray: [JSON], jsonDecoder: JSONDecoder = JSONDecoder(), serializer: JSONSerializer = GlossJSONSerializer(), options: JSONSerialization.WritingOptions? = nil, logger: Logger = GlossLogger()) -> [Element]? {
var models: [Element] = []

for json in jsonArray {
let model: Element? = .from(decodableJSON: json, jsonDecoder: jsonDecoder, serializer: serializer, options: options, logger: logger)

if let model = model {
models.append(model)
} else {
return nil
}
}

return models
}

}

public extension Array where Element: JSONEncodable & Encodable {

/**
Encodes array of objects as JSON array.
If any encodings fail, nil is returned.

- returns: Array of JSON created from objects.
*/
func toEncodableJSONArray(jsonEncoder: JSONEncoder = JSONEncoder(), serializer: JSONSerializer = GlossJSONSerializer(), options: JSONSerialization.ReadingOptions = .mutableContainers, logger: Logger = GlossLogger()) -> [JSON]? {
var jsonArray: [JSON] = []

for json in self {
if let json = json.toEncodableJSON(jsonEncoder: jsonEncoder, serializer: serializer, options: options, logger: logger) {
jsonArray.append(json)
} else {
return nil
}
}

return jsonArray
}

}

// MARK: - JSONDecodable

public extension Array where Element: JSONDecodable {
Expand Down Expand Up @@ -55,6 +109,15 @@ public extension Array where Element: JSONDecodable {
return models
}

/**
Returns array of new objects created from provided JSON array.
If any decodings fail, nil is returned.

- parameter jsonArray: Array of JSON representations of objects.

- returns: Array of objects created from JSON.
*/

/**
Initializes array of model objects from provided data.

Expand Down
50 changes: 49 additions & 1 deletion Sources/Gloss/ExtensionDecodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public extension JSONDecodable {

- parameter data: Raw JSON data.
- parameter serializer: Serializer to use when creating JSON from data.
- parameter ooptions: Options for reading the JSON data.
- parameter options: Options for reading the JSON data.

- returns: Object or nil.
*/
Expand All @@ -44,4 +44,52 @@ public extension JSONDecodable {
return nil
}

// MARK: - Migration to Swift.Decodable

/**
Attempts to create a Gloss model using `Swift.Decodable`.
Will fallback to Gloss initialization in case of error.

- parameter data: Raw JSON data.
- parameter jsonDecoder: A `Swift.JSONDecoder`.
- parameter serializer: Serializes JSON to data and vice versa.
- parameter options: Options for reading the JSON data.
- parameter logger: Logs issues with `Swift.Decodable`.

- returns: Object or nil.
*/
static func from<T: Decodable & JSONDecodable>(decodableData data: Data, jsonDecoder: JSONDecoder = JSONDecoder(), serializer: JSONSerializer = GlossJSONSerializer(), options: JSONSerialization.ReadingOptions = .mutableContainers, logger: Logger = GlossLogger()) -> T? {
do {
return try jsonDecoder.decode(T.self, from: data)
} catch {
logger.log(message: "Swift.Decodable error: \(error)")
return T(data: data, serializer: serializer, options: options)
}
}

/**
Attempts to create a Gloss model using `Swift.Decodable`.
Will fallback to Gloss initialization in case of error.

- parameter json: JSON to create model from.
- parameter jsonDecoder: A `Swift.JSONDecoder`.
- parameter serializer: Serializes JSON to data and vice versa.
- parameter options: Options for writing the JSON data.
- parameter logger: Logs issues with `Swift.Decodable`.

- returns: Object or nil.
*/
static func from<T: Decodable & JSONDecodable>(decodableJSON json: JSON, jsonDecoder: JSONDecoder = JSONDecoder(), serializer: JSONSerializer = GlossJSONSerializer(), options: JSONSerialization.WritingOptions? = nil, logger: Logger = GlossLogger()) -> T? {
do {
if let data = serializer.data(from: json, options: options) {
return try jsonDecoder.decode(T.self, from: data)
} else {
return T(json: json)
}
} catch {
logger.log(message: "Swift.Decodable error: \(error)")
return T(json: json)
}
}

}
52 changes: 52 additions & 0 deletions Sources/Gloss/ExtensionEncodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// ExtensionEncodable.swift
// Gloss
//
// Copyright (c) 2020 Harlan Kellaway
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//

import Foundation

// MARK: - Migration to Swift.Encodable

public extension JSONEncodable where Self:Encodable {

/**
Encodes provided model as JSON.

- parameter model: Model to encode as JSon.
- parameter serializer: Serializer to use when creating JSON from data.
- parameter options: Options for reading the JSON data.
- parameter logger: Logs issues with `Swift.Encodable`.

- returns: Object or nil.
*/
func toEncodableJSON(jsonEncoder: JSONEncoder = JSONEncoder(), serializer: JSONSerializer = GlossJSONSerializer(), options: JSONSerialization.ReadingOptions = .mutableContainers, logger: Logger = GlossLogger()) -> JSON? {
do {
let data = try jsonEncoder.encode(self)
return serializer.json(from: data, options: options)
} catch {
logger.log(message: "Swift.Encodable error: \(error)")
return self.toJSON()
}
}

}
25 changes: 25 additions & 0 deletions Sources/Gloss/Gloss.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ import Foundation

public typealias JSON = [String : Any]

// MARK: Errors

public enum GlossError: Error {
case decodableMigrationUnimplemented(context: String)
case encodableMigrationUnimplemented(context: String)
}

// MARK: - Protocols

/**
Expand Down Expand Up @@ -120,6 +127,16 @@ public protocol JSONSerializer {
*/
func jsonArray(from data: Data, options: JSONSerialization.ReadingOptions) -> [JSON]?

/**
Converts provided JSON into data.

- parameter json: JSON to convert to data.
- parameter options: Options for writing the JSON data.

- returns: Data if converted successfully, nil otherwise.
*/
func data(from json: JSON, options: JSONSerialization.WritingOptions?) -> Data?

}

/// Gloss JSON Serializer.
Expand All @@ -143,6 +160,14 @@ public struct GlossJSONSerializer: JSONSerializer {

return jsonArray
}

public func data(from json: JSON, options: JSONSerialization.WritingOptions?) -> Data? {
guard JSONSerialization.isValidJSONObject(json) else {
return nil
}

return try? JSONSerialization.data(withJSONObject: json, options: options ?? [])
}

}

Expand Down
4 changes: 2 additions & 2 deletions Sources/GlossTests/DecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,13 @@ class DecoderTests: XCTestCase {
}

func testInitializingFailableObjectsWithBadDataCanFail() {
let result = TestFailableModel(json: testFailableModelJSONInvalid!)
let result: TestFailableModel? = .from(decodableJSON: testFailableModelJSONInvalid!)

XCTAssertTrue(result == nil, "Expected initialization with bad data to fail, instead got \(String(describing: result))")
}

func testInitializingFailableObjectsWithValidDataCanSucceed() {
let result = TestFailableModel(json: testFailableModelJSONValid!)
let result: TestFailableModel? = .from(decodableJSON: testFailableModelJSONValid!)

XCTAssertTrue(result != nil, "Expected initialization with valid data to succeed, instead got \(String(describing: result))")
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/GlossTests/EncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ class EncoderTests: XCTestCase {
override func setUp() {
super.setUp()

testNestedModel1 = TestNestedModel(json: [ "id" : 1, "name" : "nestedModel1"])
testNestedModel2 = TestNestedModel(json: ["id" : 2, "name" : "nestedModel2"])
testNestedModel1 = .from(decodableJSON: [ "id" : 1, "name" : "nestedModel1"])
testNestedModel2 = .from(decodableJSON: ["id" : 2, "name" : "nestedModel2"])
}

override func tearDown() {
Expand Down
2 changes: 1 addition & 1 deletion Sources/GlossTests/FlowObjectCreationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class FlowObjectCreationTests: XCTestCase {
}

func testObjectDecodedFromJSONHasCorrectProperties() {
let result = TestModel(json: testJSON!)!
let result: TestModel = .from(decodableJSON: testJSON!)!

XCTAssertTrue((result.bool == true), "Model created from JSON should have correct property values")
XCTAssertTrue((result.boolArray! == [true, false, true]), "Model created from JSON should have correct property values")
Expand Down
10 changes: 5 additions & 5 deletions Sources/GlossTests/GlossTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ class GlossTests: XCTestCase {
"uuid" : "964F2FE2-0F78-4C2D-A291-03058C0B98AB"
]

let model = TestModel(json: testModelsJSON!)
let model: TestModel? = .from(decodableJSON: testModelsJSON!)
testModels = [model!, model!]
}

Expand Down Expand Up @@ -143,7 +143,7 @@ class GlossTests: XCTestCase {
}

func testModelsFromJSONArrayProducesValidModels() {
let result = [TestModel].from(jsonArray: testJSONArray!)
let result = [TestModel].from(decodableJSONArray: testJSONArray!)
let model1: TestModel = result![0]
let model2: TestModel = result![1]

Expand Down Expand Up @@ -206,13 +206,13 @@ class GlossTests: XCTestCase {
func testModelsFromJSONArrayReturnsNilIfDecodingFails() {
testJSONArray![0].removeValue(forKey: "bool")

let result = [TestModel].from(jsonArray: testJSONArray!)
let result = [TestModel].from(decodableJSONArray: testJSONArray!)

XCTAssertNil(result, "Model array from JSON array should be nil is any decoding fails.")
}

func testJSONArrayFromModelsProducesValidJSON() {
let result = testModels!.toJSONArray()
let result = testModels!.toEncodableJSONArray()
let json1 = result![0]
let json2 = result![1]

Expand Down Expand Up @@ -303,7 +303,7 @@ class GlossTests: XCTestCase {
invalidJSON.removeValue(forKey: "bool")
var jsonArray = testJSONArray!
jsonArray.append(invalidJSON)
let result = [TestModel].from(jsonArray: jsonArray)
let result = [TestModel].from(decodableJSONArray: jsonArray)

XCTAssertNil(result, "JSON array from model array should be nil is any encoding fails.")
}
Expand Down
Loading

0 comments on commit 84ce7a1

Please sign in to comment.