From c79737d152a6a59638019479855e7d3157d3e8ea Mon Sep 17 00:00:00 2001 From: Bhargav Date: Sat, 16 Apr 2016 17:38:14 +0530 Subject: [PATCH] Add helpers to load and dump JSON files --- Banana.xcodeproj/project.pbxproj | 20 +--- BananaTests/GetTests.swift | 15 +-- BananaTests/NestedObjectTests.swift | 35 +++--- BananaTests/PerfTests.swift | 34 +++--- BananaTests/PlainObjectTests.swift | 26 ++--- BananaTests/TestUtils.swift | 22 ---- Example/Sources/AppDelegate.swift | 39 +------ Example/Sources/Models/Address.swift | 18 ++- Example/Sources/Models/Customer.swift | 25 +++- Example/Sources/Models/Gender.swift | 15 +++ Example/Sources/ViewController.swift | 24 ++-- .../Contents.swift | 48 +++----- .../Contents.swift | 46 +++----- .../Contents.swift | 40 ++----- .../Simple.xcplaygroundpage/Contents.swift | 43 ++++--- README.md | 71 +++++------ Sources/Banana.swift | 110 ++++++++++++++++++ Sources/Functions.swift | 77 ++++++++++-- Sources/Operators.swift | 7 ++ Sources/Types.swift | 10 +- Support/Info.plist | 2 +- 21 files changed, 403 insertions(+), 324 deletions(-) delete mode 100644 BananaTests/TestUtils.swift create mode 100644 Sources/Banana.swift diff --git a/Banana.xcodeproj/project.pbxproj b/Banana.xcodeproj/project.pbxproj index 0b0b1b2..1118f94 100644 --- a/Banana.xcodeproj/project.pbxproj +++ b/Banana.xcodeproj/project.pbxproj @@ -10,12 +10,12 @@ 7C2053DD1CBE792A00069051 /* PerfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C2053DC1CBE792A00069051 /* PerfTests.swift */; }; 7C76D4311CBE594A00363423 /* GetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C76D4301CBE594A00363423 /* GetTests.swift */; }; 7C76D4331CBE594A00363423 /* Banana.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7CC8CCA01CBC107200896C9C /* Banana.framework */; }; - 7C76D43C1CBE5A1A00363423 /* TestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C76D43B1CBE5A1A00363423 /* TestUtils.swift */; }; 7C76D43E1CBE692A00363423 /* PlainObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C76D43D1CBE692A00363423 /* PlainObjectTests.swift */; }; 7C76D4401CBE6F6C00363423 /* person.json in Resources */ = {isa = PBXBuildFile; fileRef = 7C76D43F1CBE6F6C00363423 /* person.json */; }; 7C76D4421CBE6FA100363423 /* personWithTODOItems.json in Resources */ = {isa = PBXBuildFile; fileRef = 7C76D4411CBE6FA100363423 /* personWithTODOItems.json */; }; 7C76D4441CBE700C00363423 /* NestedObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C76D4431CBE700C00363423 /* NestedObjectTests.swift */; }; 7CA20E9C1CBCFB8F0098EC8E /* Banana.h in Headers */ = {isa = PBXBuildFile; fileRef = 7CA20E9B1CBCFB8F0098EC8E /* Banana.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7CA2DF3B1CBFB3FA00E91622 /* Banana.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CA2DF3A1CBFB3FA00E91622 /* Banana.swift */; }; 7CC8CCB91CBC176600896C9C /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC8CCB61CBC176600896C9C /* Types.swift */; }; 7CC8CCBA1CBC176600896C9C /* Operators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC8CCB71CBC176600896C9C /* Operators.swift */; }; 7CC8CCBB1CBC176600896C9C /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CC8CCB81CBC176600896C9C /* Functions.swift */; }; @@ -36,12 +36,12 @@ 7C76D42E1CBE594A00363423 /* BananaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BananaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7C76D4301CBE594A00363423 /* GetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTests.swift; sourceTree = ""; }; 7C76D4321CBE594A00363423 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7C76D43B1CBE5A1A00363423 /* TestUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; 7C76D43D1CBE692A00363423 /* PlainObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlainObjectTests.swift; sourceTree = ""; }; 7C76D43F1CBE6F6C00363423 /* person.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = person.json; sourceTree = ""; }; 7C76D4411CBE6FA100363423 /* personWithTODOItems.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = personWithTODOItems.json; sourceTree = ""; }; 7C76D4431CBE700C00363423 /* NestedObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NestedObjectTests.swift; sourceTree = ""; }; 7CA20E9B1CBCFB8F0098EC8E /* Banana.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Banana.h; path = Support/Banana.h; sourceTree = SOURCE_ROOT; }; + 7CA2DF3A1CBFB3FA00E91622 /* Banana.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Banana.swift; path = Sources/Banana.swift; sourceTree = ""; }; 7CC8CCA01CBC107200896C9C /* Banana.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Banana.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7CC8CCA51CBC107200896C9C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = Info.plist; path = Support/Info.plist; sourceTree = ""; }; 7CC8CCB61CBC176600896C9C /* Types.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Types.swift; path = Sources/Types.swift; sourceTree = ""; }; @@ -68,18 +68,9 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 7C2053DB1CBE791000069051 /* Utils */ = { - isa = PBXGroup; - children = ( - 7C76D43B1CBE5A1A00363423 /* TestUtils.swift */, - ); - name = Utils; - sourceTree = ""; - }; 7C76D42F1CBE594A00363423 /* BananaTests */ = { isa = PBXGroup; children = ( - 7C2053DB1CBE791000069051 /* Utils */, 7C76D4301CBE594A00363423 /* GetTests.swift */, 7C76D43D1CBE692A00363423 /* PlainObjectTests.swift */, 7C76D4431CBE700C00363423 /* NestedObjectTests.swift */, @@ -126,6 +117,7 @@ 7CC8CCB61CBC176600896C9C /* Types.swift */, 7CC8CCB71CBC176600896C9C /* Operators.swift */, 7CC8CCB81CBC176600896C9C /* Functions.swift */, + 7CA2DF3A1CBFB3FA00E91622 /* Banana.swift */, ); name = Sources; sourceTree = ""; @@ -244,7 +236,6 @@ 7C76D4311CBE594A00363423 /* GetTests.swift in Sources */, 7C76D4441CBE700C00363423 /* NestedObjectTests.swift in Sources */, 7C2053DD1CBE792A00069051 /* PerfTests.swift in Sources */, - 7C76D43C1CBE5A1A00363423 /* TestUtils.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -252,6 +243,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7CA2DF3B1CBFB3FA00E91622 /* Banana.swift in Sources */, 7CC8CCB91CBC176600896C9C /* Types.swift in Sources */, 7CC8CCBA1CBC176600896C9C /* Operators.swift in Sources */, 7CC8CCBB1CBC176600896C9C /* Functions.swift in Sources */, @@ -327,7 +319,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -370,7 +362,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/BananaTests/GetTests.swift b/BananaTests/GetTests.swift index fbfe62b..30076be 100644 --- a/BananaTests/GetTests.swift +++ b/BananaTests/GetTests.swift @@ -45,21 +45,16 @@ class GetTests: XCTestCase { } func testKeyPath() { - let json = Utils.loadJSON("personWithTODOItems") - do { - let stringKey: String = try get(json) <~~ keyPath("address.home.street") + let rawJSON: [String: AnyObject] = try Banana.load(file: "personWithTODOItems", fileExtension: "json", bundle: NSBundle(forClass: GetTests.self)) + + let stringKey: String = try rawJSON <~~ keyPath("address.home.street") + let boolKey: Bool = try rawJSON <~~ keyPath("address.office.is_active") + XCTAssertTrue(stringKey == "17/B, Bank Road") - } catch { - XCTFail("Failed with error: \(error)") - } - - do { - let boolKey: Bool = try get(json) <~~ keyPath("address.office.is_active") XCTAssertTrue(boolKey == false) } catch { XCTFail("Failed with error: \(error)") } } - } diff --git a/BananaTests/NestedObjectTests.swift b/BananaTests/NestedObjectTests.swift index 06d7030..64da688 100644 --- a/BananaTests/NestedObjectTests.swift +++ b/BananaTests/NestedObjectTests.swift @@ -12,10 +12,9 @@ import Banana class NestedObjectTests: XCTestCase { func testParsing() { - let json = Utils.loadJSON("personWithTODOItems") - do { - let person = try get(json) <~~ Person.init + + let person = try Banana.load(file: "personWithTODOItems", fileExtension: "json", bundle: NSBundle(forClass: GetTests.self)) <~~ Person.fromJSON XCTAssert(person.name == "Bob") XCTAssert(person.age == 25) @@ -34,12 +33,12 @@ class NestedObjectTests: XCTestCase { let homePinCode: String? let todoItems: [TodoItem] - init(json: JSON) throws { - name = try get(json, key: "name") - age = try get(json, key: "age") - gender = try get(json, key: "gender") <~~ Gender.init - homePinCode = try? get(json, key: "address") <~~ keyPath("home.pincode") - todoItems = try get(json, key: "todo_items") <<~ TodoItem.init + static func fromJSON(json: JSON) throws -> Person { + return Person(name: try get(json, key: "name"), + age: try get(json, key: "age"), + gender: try get(json, key: "gender") <~~ Gender.parse, + homePinCode: try? get(json, key: "address") <~~ keyPath("home.pincode"), + todoItems: try get(json, key: "todo_items") <<~ TodoItem.fromJSON) } } @@ -48,24 +47,24 @@ class NestedObjectTests: XCTestCase { let title: String let isCompleted: Bool - init(json: JSON) throws { - id = try get(json, key: "id") - title = try get(json, key: "title") - isCompleted = try get(json, key: "is_completed") + static func fromJSON(json: JSON) throws -> TodoItem { + return TodoItem(id: try get(json, key: "id"), + title: try get(json, key: "title"), + isCompleted: try get(json, key: "is_completed")) } } enum Gender { case Male, Female - init(fromString: String) throws { - switch fromString { + static func parse(value: String) throws -> Gender { + switch value { case "male": - self = .Male + return .Male case "female": - self = .Female + return .Female default: - throw ParseError.Custom("Invalid Gender: \(fromString)") + throw BananaError.Custom("Invalid Gender: \(value)") } } } diff --git a/BananaTests/PerfTests.swift b/BananaTests/PerfTests.swift index 25c9a4a..06dab10 100644 --- a/BananaTests/PerfTests.swift +++ b/BananaTests/PerfTests.swift @@ -12,11 +12,11 @@ import Banana class PerfTests: XCTestCase { func testPerformanceExample() { - let json = Utils.loadJSON("personWithTODOItems") + let json: [String: AnyObject] = try! Banana.load(file: "personWithTODOItems", fileExtension: "json", bundle: NSBundle(forClass: PerfTests.self)) self.measureBlock { for _ in 0...1_000 { - let _ = try! get(json) <~~ Person.init + let _ = try! json <~~ Person.parse } } } @@ -29,12 +29,12 @@ class PerfTests: XCTestCase { let homePinCode: String? let todoItems: [TodoItem] - init(json: JSON) throws { - name = try get(json, key: "name") - age = try get(json, key: "age") - gender = try get(json, key: "gender") <~~ Gender.init - homePinCode = try? get(json, key: "address") <~~ keyPath("home.pincode") - todoItems = try get(json, key: "todo_items") <<~ TodoItem.init + static func parse(json: JSON) throws -> Person { + return Person(name: try get(json, key: "name"), + age: try get(json, key: "age"), + gender: try get(json, key: "gender") <~~ Gender.parse, + homePinCode: try? get(json, key: "address") <~~ keyPath("home.pincode"), + todoItems: try get(json, key: "todo_items") <<~ TodoItem.parse) } } @@ -43,24 +43,24 @@ class PerfTests: XCTestCase { let title: String let isCompleted: Bool - init(json: JSON) throws { - id = try get(json, key: "id") - title = try get(json, key: "title") - isCompleted = try get(json, key: "is_completed") + static func parse(json: JSON) throws -> TodoItem { + return TodoItem(id: try get(json, key: "id"), + title: try get(json, key: "title"), + isCompleted: try get(json, key: "is_completed")) } } enum Gender { case Male, Female - init(fromString: String) throws { - switch fromString { + static func parse(value: String) throws -> Gender { + switch value { case "male": - self = .Male + return .Male case "female": - self = .Female + return .Female default: - throw ParseError.Custom("Invalid Gender: \(fromString)") + throw BananaError.Custom("Invalid Gender: \(value)") } } } diff --git a/BananaTests/PlainObjectTests.swift b/BananaTests/PlainObjectTests.swift index af9f87a..043e391 100644 --- a/BananaTests/PlainObjectTests.swift +++ b/BananaTests/PlainObjectTests.swift @@ -12,10 +12,8 @@ import Banana class ParsingTests: XCTestCase { func testParsing() { - let json = Utils.loadJSON("person") - - do { - let person = try get(json) <~~ Person.init + do { + let person = try Banana.load(file: "person", fileExtension: "json", bundle: NSBundle(forClass: GetTests.self)) <~~ Person.fromJSON XCTAssert(person.name == "Bob") XCTAssert(person.age == 25) @@ -33,25 +31,25 @@ class ParsingTests: XCTestCase { let gender: Gender let address: String? - init(json: JSON) throws { - name = try get(json, key: "name") - age = try get(json, key: "age") - gender = try get(json, key: "gender") <~~ Gender.init - address = try? get(json, key: "address") + static func fromJSON(json: JSON) throws -> Person { + return Person(name: try get(json, key: "name"), + age: try get(json, key: "age"), + gender: try get(json, key: "gender") <~~ Gender.parse, + address: try? get(json, key: "address")) } } enum Gender { case Male, Female - init(fromString: String) throws { - switch fromString { + static func parse(value: String) throws -> Gender { + switch value { case "male": - self = .Male + return .Male case "female": - self = .Female + return .Female default: - throw ParseError.Custom("Invalid Gender: \(fromString)") + throw BananaError.Custom("Invalid Gender: \(value)") } } } diff --git a/BananaTests/TestUtils.swift b/BananaTests/TestUtils.swift deleted file mode 100644 index 0dd6111..0000000 --- a/BananaTests/TestUtils.swift +++ /dev/null @@ -1,22 +0,0 @@ - - -// -// TestUtils.swift -// Banana -// -// Created by Bhargav Gurlanka on 4/13/16. -// Copyright © 2016 Bhargav Gurlanka. All rights reserved. -// - -import Foundation - - -class Utils { - static func loadJSON(fileName: String) -> AnyObject { - - let path = NSBundle(forClass: Utils.self).pathForResource(fileName, ofType: "json")! - let data = NSData(contentsOfFile: path)! - let json = try? NSJSONSerialization.JSONObjectWithData(data, options: []) - return json! - } -} \ No newline at end of file diff --git a/Example/Sources/AppDelegate.swift b/Example/Sources/AppDelegate.swift index 1502bf7..9110646 100644 --- a/Example/Sources/AppDelegate.swift +++ b/Example/Sources/AppDelegate.swift @@ -1,46 +1,17 @@ -// -// AppDelegate.swift -// Example -// -// Created by Bhargav Gurlanka on 4/11/16. -// Copyright © 2016 Bhargav Gurlanka. All rights reserved. -// - import UIKit @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { - var window: UIWindow? - func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { - // Override point for customization after application launch. return true } - func applicationWillResignActive(application: UIApplication) { - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. - } - - func applicationDidEnterBackground(application: UIApplication) { - // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. - // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. - } - - func applicationWillEnterForeground(application: UIApplication) { - // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. - } - - func applicationDidBecomeActive(application: UIApplication) { - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - } - - func applicationWillTerminate(application: UIApplication) { - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. - } - - + func applicationWillResignActive(application: UIApplication) { } + func applicationDidEnterBackground(application: UIApplication) { } + func applicationWillEnterForeground(application: UIApplication){ } + func applicationDidBecomeActive(application: UIApplication) { } + func applicationWillTerminate(application: UIApplication) { } } diff --git a/Example/Sources/Models/Address.swift b/Example/Sources/Models/Address.swift index fc6ffad..aeec4a4 100644 --- a/Example/Sources/Models/Address.swift +++ b/Example/Sources/Models/Address.swift @@ -4,10 +4,20 @@ struct Address { let street: String let city: String let state: String? + + /// Mapping from JSON to this model + static func fromJSON(json: JSON) throws -> Address { + return Address(street: try get(json, key: "street"), + city: try get(json, key: "city"), + state: try? get(json, key: "state")) + } - init(json: JSON) throws { - street = try get(json, key: "street") - city = try get(json, key: "city") - state = try? get(json, key: "state") + /// Mapping from this model to JSON + static func toJSON(address: Address) throws -> JSON { + var json = JSON() + json["street"] = address.street + json["city"] = address.city + json["state"] = address.state + return json } } \ No newline at end of file diff --git a/Example/Sources/Models/Customer.swift b/Example/Sources/Models/Customer.swift index 6f8717e..517a8f3 100644 --- a/Example/Sources/Models/Customer.swift +++ b/Example/Sources/Models/Customer.swift @@ -9,12 +9,13 @@ struct Customer { let address: Address let skills: [String]? - init(json: JSON) throws { - name = try get(json, key: "name") <~~ Customer.getFirstLastNames <~~ Customer.createName - age = try get(json, key: "age") - gender = try get(json, keys: ["gender", "g"]) <~~ Gender.parseGender - address = try get(json, key: "address") <~~ Address.init - skills = try? get(json, key: "skills") + /// Mapping from JSON to this model + static func fromJSON(json: JSON) throws -> Customer { + return Customer(name: try get(json, key: "name") <~~ Customer.getFirstLastNames <~~ Customer.createName, + age: try get(json, key: "age"), + gender: try get(json, keys: ["gender", "g"]) <~~ Gender.parseGender, + address: try get(json, key: "address") <~~ Address.fromJSON, + skills: try? get(json, key: "skills")) } static func getFirstLastNames(json: [String: AnyObject]) throws -> (firstName: String, lastName: String) { @@ -26,4 +27,16 @@ struct Customer { static func createName(firstName: String, withLastName lastName: String) -> (String) { return [firstName, lastName].joinWithSeparator(", ") } + + + /// Mapping from this model to JSON + static func toJSON(customer: Customer) throws -> JSON { + var json = JSON() + json["name"] = customer.name + json["age"] = customer.age + json["gender"] = try customer.gender <~~ Gender.toJSON + json["address"] = try customer.address <~~ Address.toJSON + json["skills"] = customer.skills + return json + } } diff --git a/Example/Sources/Models/Gender.swift b/Example/Sources/Models/Gender.swift index b1747c4..378f703 100644 --- a/Example/Sources/Models/Gender.swift +++ b/Example/Sources/Models/Gender.swift @@ -1,6 +1,9 @@ +import Banana + enum Gender { case Unknown, Male, Female + /// Mapping from JSON to this model static func parseGender(gender: String) -> Gender { switch gender { case "male": @@ -11,4 +14,16 @@ enum Gender { return .Unknown } } + + /// Mapping from this model to JSON + static func toJSON(gender: Gender) -> String { + switch gender { + case .Male: + return "male" + case .Female: + return "female" + default: + return "unknown" + } + } } \ No newline at end of file diff --git a/Example/Sources/ViewController.swift b/Example/Sources/ViewController.swift index 3ecffa2..a46d888 100644 --- a/Example/Sources/ViewController.swift +++ b/Example/Sources/ViewController.swift @@ -1,11 +1,3 @@ -// -// ViewController.swift -// Example -// -// Created by Bhargav Gurlanka on 4/11/16. -// Copyright © 2016 Bhargav Gurlanka. All rights reserved. -// - import UIKit import Banana @@ -14,16 +6,16 @@ class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() - guard let path = NSBundle.mainBundle().pathForResource("customers", ofType: "json"), - let data = NSData(contentsOfFile: path), - let jsonData = try? NSJSONSerialization.JSONObjectWithData(data, options: []) else { - print("Couldn't load JSON file") - exit(1) - } - do { - let customers: [Customer]? = try get(jsonData) <~~ keyPath("response.customers") <<~ Customer.init + + /// Mapping from JSON to models + let customers: [Customer] = try Banana.load(file: "customers") <~~ keyPath("response.customers") <<~ Customer.fromJSON print(customers) + + /// Mapping from models to JSON + let jsonAsString = try customers <<~ Customer.toJSON <~~ Banana.dump(options: [.PrettyPrinted]) <~~ Banana.toString(encoding: NSUTF8StringEncoding) + print(jsonAsString) + } catch { // Failed to parse JSON print(error) diff --git a/PlayGround.playground/Pages/Array as root.xcplaygroundpage/Contents.swift b/PlayGround.playground/Pages/Array as root.xcplaygroundpage/Contents.swift index b29b300..201e7cc 100644 --- a/PlayGround.playground/Pages/Array as root.xcplaygroundpage/Contents.swift +++ b/PlayGround.playground/Pages/Array as root.xcplaygroundpage/Contents.swift @@ -1,45 +1,33 @@ import Banana - /*: - **Load JSON** + **Model** - The `array.json` file under resources is loaded for parsing + The values are retrieved using `get(_,key:String)` method. */ - -guard let path = NSBundle.mainBundle().pathForResource("array", ofType: "json"), - let data = NSData(contentsOfFile: path), - let json = try? NSJSONSerialization.JSONObjectWithData(data, options: []) else { - - print("Couldn't load JSON file") - exit(0) +struct Foo { + let key: String + + static func fromJSON(json: JSON) throws -> Foo { + return try Foo(key: get(json, key: "key")) + } + + static func toJSON(foo: Foo) throws -> JSON { + return ["key": foo.key] + } } /*: - **Parse** + **Map JSON to Model** - As json contains an array of `Foo` values, `<<~` operator is used for parsing + The `array.json` file under resources is loaded */ - -do { - let value: [Foo] = try get(json) <<~ Foo.init - print(value) -} catch { - print("Couldn't parse JSON, error: \(error)") -} - +let foos: [Foo] = try Banana.load(file: "array") <<~ Foo.fromJSON /*: - **Model** + **Map Model to JSON string** - The values are retrieved using `get(_,key:String)` method. + As json contains an array of `Foo` values, `<<~` operator is used for mapping */ -struct Foo { - let key: String - - init(json: JSON) throws { - key = try get(json, key: "key") - } -} - +let fooAsJSONString = try foos <<~ Foo.toJSON <~~ Banana.dump(options: []) <~~ Banana.toString(encoding: NSUTF8StringEncoding) \ No newline at end of file diff --git a/PlayGround.playground/Pages/Dictionary as root.xcplaygroundpage/Contents.swift b/PlayGround.playground/Pages/Dictionary as root.xcplaygroundpage/Contents.swift index ca5536b..1214c9a 100644 --- a/PlayGround.playground/Pages/Dictionary as root.xcplaygroundpage/Contents.swift +++ b/PlayGround.playground/Pages/Dictionary as root.xcplaygroundpage/Contents.swift @@ -1,41 +1,31 @@ import Banana /*: - **Load JSON** - - The `dictionary.json` file under resources is loaded for parsing + **Model** */ - -guard let path = NSBundle.mainBundle().pathForResource("dictionary", ofType: "json"), - let data = NSData(contentsOfFile: path), - let json = try? NSJSONSerialization.JSONObjectWithData(data, options: []) else { - - print("Couldn't load JSON file") - exit(0) +struct Foo { + let key: String + + static func fromJSON(json: JSON) throws -> Foo { + return Foo(key: try get(json, key: "key")) + } + + static func toJSON(foo: Foo) -> JSON { + return ["key": foo.key] + } } /*: - **Parse** + **Map JSON to Model** - As json contains a dictionary, `<~~` operator is used for parsing + The `array.json` file under resources is loaded */ - -do { - let value: Foo = try get(json) <~~ Foo.init - print(value) -} catch { - print("Couldn't parse JSON, error: \(error)") -} +let foo = try Banana.load(file: "dictionary") <~~ Foo.fromJSON /*: - **Model** + **Map Model to JSON string** + + As json contains `Foo` as dictionary, `<~~` operator is used for mapping */ -struct Foo { - let key: String - - init(json: JSON) throws { - key = try get(json, key: "key") - } -} - +let fooAsJSONString = try foo <~~ Foo.toJSON <~~ Banana.dump(options: []) <~~ Banana.toString(encoding: NSUTF8StringEncoding) \ No newline at end of file diff --git a/PlayGround.playground/Pages/Multi Keys.xcplaygroundpage/Contents.swift b/PlayGround.playground/Pages/Multi Keys.xcplaygroundpage/Contents.swift index d7c2b70..eaea610 100644 --- a/PlayGround.playground/Pages/Multi Keys.xcplaygroundpage/Contents.swift +++ b/PlayGround.playground/Pages/Multi Keys.xcplaygroundpage/Contents.swift @@ -1,45 +1,21 @@ import Banana - -/*: - **Load JSON** - - The `array.json` file under resources is loaded for parsing - */ - -guard let path = NSBundle.mainBundle().pathForResource("multi_keys", ofType: "json"), - let data = NSData(contentsOfFile: path), - let json = try? NSJSONSerialization.JSONObjectWithData(data, options: []) else { - - print("Couldn't load JSON file") - exit(0) -} - -/*: - **Parse** - - As json contains an array of `Foo` values, `<<~` operator is used for parsing - */ - -do { - let value: [Foo] = try get(json) <<~ Foo.init - print(value) -} catch { - print("Couldn't parse JSON, error: \(error)") -} - - /*: **Model** The values are retrieved using `get(_,keys:[String])` method, which allows us to specify multiple keys for a single value. */ - struct Foo { let key: String - init(json: JSON) throws { - key = try get(json, keys: ["key", "k"]) + static func fromJSON(json: JSON) throws -> Foo { + return Foo(key: try get(json, keys: ["key", "k"])) } } +/*: + **Map JSON to Model** + + The `multi_keys.json` file under resources is loaded for parsing + */ +let foos: [Foo] = try Banana.load(file: "multi_keys") <<~ Foo.fromJSON diff --git a/PlayGround.playground/Pages/Simple.xcplaygroundpage/Contents.swift b/PlayGround.playground/Pages/Simple.xcplaygroundpage/Contents.swift index 50f538d..4f6c684 100644 --- a/PlayGround.playground/Pages/Simple.xcplaygroundpage/Contents.swift +++ b/PlayGround.playground/Pages/Simple.xcplaygroundpage/Contents.swift @@ -1,31 +1,36 @@ import Banana - /*: - **Load JSON** - - The `simple.json` file under resources is loaded for parsing + **Model** */ - -guard let path = NSBundle.mainBundle().pathForResource("simple", ofType: "json"), - let data = NSData(contentsOfFile: path), - let json = try? NSJSONSerialization.JSONObjectWithData(data, options: []) else { - - print("Couldn't load JSON file") - exit(0) +struct Foo { + let key: String + + static func fromJSON(json: JSON) throws -> Foo { + return try Foo(key: get(json, key: "key")) + } + + static func toJSON(foo: Foo) -> JSON{ + return ["key": foo.key] + } } + /*: - **Parse** + **Map JSON to Model** - The value for `key` under `root` is retrieved using `keyPath` method + The `simple.json` file under resources is loaded for parsing */ +// Retrieve values without model classes +let value: String = try Banana.load(file: "simple") <~~ keyPath("root.key") -do { - let value: String = try get(json) <~~ keyPath("root.key") - print(value) -} catch { - print("Couldn't parse JSON, error: \(error)") -} +// Using models +let foo: Foo = try Banana.load(file: "simple") <~~ keyPath("root") <~~ Foo.fromJSON +/*: + **Map Model to JSON string** + + `Banana.dump` is used in combination with `Banana.toString` to convert our model to a JSON string + */ +let jsonAsString = try foo <~~ Foo.toJSON <~~ Banana.dump(options: [.PrettyPrinted]) <~~ Banana.toString(encoding: NSUTF8StringEncoding) diff --git a/README.md b/README.md index d929260..07528a9 100644 --- a/README.md +++ b/README.md @@ -35,65 +35,48 @@ and `carthage update`. For full list of command, please refer [Carthage document ## Examples -Let's say you a `Foo` struct +### Read a single value ```swift -struct Foo { - let x: String - let y: Int -} +let value: String = try Banana.load(file: "simple") <~~ keyPath("path.to.key") ``` -and the following JSON to parse: - +### Mapping to models and back ```json -{ - "x": "Hi", - "y": 5 -} +[ + { + "x": "hi", + "y": 5 + }, + { + "x": "yolo", + "yo": 6 + } +] ``` -With Banana, just add a method that takes `JSON` (in this case, `init` method) and get the values using `get` method. - ```swift struct Foo { let x: String let y: Int - init(json: JSON) throws { - x = get(json, key: "x") - y = get(json, key: "y") + static func fromJSON(json: JSON) throws -> Foo { + return Foo( + x: try get(json, key: "x"), + y: try get(json, keys: ["y", "yo"]) + ) } -} -``` -## Documentation -This library has a very tiny footprint of: -- 4 methods -- 2 Custom Operators -- 1 Type alias - -### `get(item: AnyObject)` -All this method does is, try to cast the `item` to a type that caller expects. This will throw error if it couldn't cast. - -### `get(box:key:)` -This method will try to retrive the value of `key` from the given dictionary and cast the value to what the caller expects. It can throw errors in two cases: -- If there is no such key (throws `ParseError.NilValue` error) -- If the value couldn't be casted (throws `ParseError.InvalidType` error) - -### `get(box:keys:)` -This is similar to above method in functionality, except that now it accepts an array of keys instead of a single key. - -### `keyPath(path:)` -This can be used to retrive value of given key path, like, `key1.key2.key3` - -### `<~~` -A transformation operator that applies the transformer function (rhs) on the given value (lhs) - -### `<<~` -A transformation operator that applies the transformer function (rhs) on each value of given array (rhs) + static func toJSON(foo: Foo) -> JSON { + return ["x": foo.x, "y": foo.y] + } +} +let foos: [Foo] = try Banana.load(file: "foos_file") <<~ Foo.fromJSON +print(foos) -For detailed usage, please open `Banana.xcworkspace` in Xcode. It contains a sample project along with a Playground. +let jsonString: String = try foos <<~ Foo.toJSON <~~ Banana.dump(options: [.PrettyPrinted]) <~~ Banana.toString(encoding: NSUTF8StringEncoding) +print(jsonString) +``` ## Todo: - [ ] CocoaPods Support diff --git a/Sources/Banana.swift b/Sources/Banana.swift new file mode 100644 index 0000000..1b52f11 --- /dev/null +++ b/Sources/Banana.swift @@ -0,0 +1,110 @@ +import Foundation + +public struct Banana { + + /** + Parse the given JSON file. + + It uses `NSJSONSerialization.JSONObjectWithData` for parsing. The parsed value, + which is of type `AnyObject` will be casted to a type that receiver is expecting. + + - parameter file: Name of the JSON file without extension + - parameter fileExtension: Optional extension of the file. Defaults to `.json` + - parameter bundle: Optional `NSBundle` in which the file exists. Defaults to `NSBundle.mainBundle` + + - returns: An object of type T. This T is determined by the receiver. It tries to cast the AnyObject from `JSONObjectWithData` to a type that receiver expects. + - throws: Throws `BananaError` if file couldn't be loaded or the value cannot be casted + */ + public static func load(file file: String, fileExtension: String = "json", bundle: NSBundle = NSBundle.mainBundle(), options: NSJSONReadingOptions = []) throws -> T { + guard let path = bundle.pathForResource(file, ofType: fileExtension), + let data = NSData(contentsOfFile: path) else { + throw BananaError.Custom("Couldn't load JSON file: \(file)") + } + + return try load(data: data, options: options) + } + + /** + Parse the given data into a JSON object. + + It uses `NSJSONSerialization.JSONObjectWithData` for parsing. The parsed value, + which is of type `AnyObject` will be casted to a type that receiver is expecting. + + - parameter data: `NSData` object that needs to be parsed + - parameter options: Optional `NSJSONReadingOptions` options for parsing + + - returns: An object of type T. This T is determined by the receiver. It tries to cast the AnyObject from `JSONObjectWithData` to a type that receiver expects. + - throws: Throws `BananaError` if JSON couldn't be parsed, or, the value cannot be casted + */ + public static func load(data data: NSData, options: NSJSONReadingOptions = []) throws -> T { + return try NSJSONSerialization.JSONObjectWithData(data, options: options) <~~ get + } + + /** + Encode the given `jsonObject` into NSData + + It uses `NSJSONSerialization.dataWithJSONObject` for encoding. + + - note: This is a curried function. It first takes the `NSJSONWritingOptions` and returns a closure. + This closure takes the `jsonObject` and encodes it into NSData with the options previously provided. + + - parameter options: `NSJSONWritingOptions` used for encoding + - parameter jsonObject: Object to encode + + - returns: Encoded value in the form of `NSData` + + - throws: `BananaError` if the value cannot be encoded + */ + public static func dump(options options: NSJSONWritingOptions) -> (jsonObject: AnyObject) throws -> NSData { + return { jsonObject in + return try Banana.dump(JSONObject: jsonObject, options: options) + } + } + + /** + Encode the given `jsonObject` into NSData + + It uses `NSJSONSerialization.dataWithJSONObject` for encoding. + + - parameter jsonObject: Object to encode + - parameter options: `NSJSONWritingOptions` used for encoding + + - returns: Encoded value in the form of `NSData` + + - throws: `BananaError` if the value cannot be encoded + */ + public static func dump(JSONObject jsonObject: AnyObject, options: NSJSONWritingOptions) throws -> NSData { + guard let jsonData = try? NSJSONSerialization.dataWithJSONObject(jsonObject, options: options) else { + throw BananaError.Custom("Couldn't convert JSON Object to NSData") + } + + return jsonData + } + + + /** + Convert the given `NSData` into an `NSString` with given `encoding` + + It uses `NSString(data:encoding:)` for the conversion. + + - note: This is a curried function. It first takes `encoding` parameter and returns a closure. + This closure takes the actual data to convert and returns the `NSString` of it. + + - parameter encoding: `NSStringEncoding` to be used for conversion + - parameter data: `NSData` that needs to be converted + + - returns: Converted value in the form of `NSString` + + - throws: `BananaError` if `NSString` object cannot be made + */ + public static func toString(encoding encoding: NSStringEncoding) -> (data: NSData) throws -> NSString { + return { data in + guard let string = NSString(data: data, encoding: encoding) else { + throw BananaError.Custom("Couldn't convert to NSString") + } + + return string + } + } + +} \ No newline at end of file diff --git a/Sources/Functions.swift b/Sources/Functions.swift index 3080040..3fc7327 100644 --- a/Sources/Functions.swift +++ b/Sources/Functions.swift @@ -1,30 +1,56 @@ + +/** + A function to get the value from given JSON dictonary and cast it to the type that + receiver expects. + + - parameter box: A JSON dictionary + - parameter key: A key whose value is required + - returns: The type casted value of `key` from `box` + + - throws: `BananaError` if the key is not found or the value cannot be casted. + */ public func get(box: JSON, key: String) throws -> T { - typealias ParseErrorType = ParseError + typealias BananaErrorType = BananaError guard let value = box[key] else { - throw ParseErrorType.NilValue(key) + throw BananaErrorType.NilValue(key) } return try get(value) } +/** + A function to get the value from given JSON dictonary and cast it to the type that + receiver expects. + + This function is exactly similar to `get(box:key:)`, except that now it takes an array + of keys instead of a single value. + + - parameter box: A JSON dictionary + - parameter keys: A set of keys to try for a value. All the keys will be tried + and the first successfull value will be returned. + - returns: The type casted value of `key` from `box` + + - throws: `BananaError` if non keys are provided, or, the JSON doesn't have any of the + keys, or, values could'nt be casted + */ public func get(box: JSON, keys: [String]) throws -> T { - typealias ParseErrorType = ParseError + typealias BananaErrorType = BananaError guard !keys.isEmpty else { - throw ParseErrorType.Custom("Should provide at least one key") + throw BananaErrorType.Custom("Should provide at least one key") } - var errors:[ParseErrorType] = [] + var errors:[BananaErrorType] = [] for key in keys { do { let value: T = try get(box, key: key) return value } catch { - if let err = error as? ParseErrorType { + if let err = error as? BananaErrorType { errors.append(err) } else { errors.append(.Custom("Unknown error occured while processing keys: \(keys)")) @@ -33,7 +59,7 @@ public func get(box: JSON, keys: [String]) throws -> T { } guard let lastError = errors.last else { - throw ParseErrorType.Custom("Unknown error occured while processing keys: \(keys)") + throw BananaErrorType.Custom("Unknown error occured while processing keys: \(keys)") } for error in errors { @@ -45,25 +71,52 @@ public func get(box: JSON, keys: [String]) throws -> T { } let joinedKeys = keys.map{ "\"" + $0 + "\"" }.joinWithSeparator(", ") - throw ParseErrorType.Custom("No values found for keys: \(joinedKeys) \nin: \n\(box)") + throw BananaErrorType.Custom("No values found for keys: \(joinedKeys) \nin: \n\(box)") } +/** + Cast the given input into required type. + + The required type is deduced from the receiver. + + - parameter item: Object to cast + - returns: Casted value + + - throws: Throws `BananaError` if the value cannot be casted. + */ public func get(item: AnyObject) throws -> T { guard let typedItem = item as? T else { - throw ParseError.InvalidType(item, expected: T.self) + throw BananaError.InvalidType(item, expected: T.self) } return typedItem } - +/** + Get the value of a keypath from given dictonary. + + This function works by casting the given dictionary into `NSDictionary` and using + `valueForKeyPath` to get the value. The retrieved value will be casted to the type + that receiver expects. + + - note: This is a curried function. First, it takes a keypath and returns a closure. + This returned closure takes a dictionary and returns the value of the keypath in that + dictionary. + + - parameter path: A key path + - parameter box: A dictionary + - returns: Value of the given key path, casted to required type + + - throws: `BananaError` if the dictionary doesn't have a key path or + the value cannot be casted. + */ public func keyPath(path: String) -> (box: JSON) throws -> T { - typealias ParseErrorType = ParseError + typealias BananaErrorType = BananaError return { box in guard let value = (box as NSDictionary).valueForKeyPath(path) else { - throw ParseErrorType.NilValue(path) + throw BananaErrorType.NilValue(path) } return try get(value) diff --git a/Sources/Operators.swift b/Sources/Operators.swift index a9899c7..41190f7 100644 --- a/Sources/Operators.swift +++ b/Sources/Operators.swift @@ -8,10 +8,17 @@ infix operator <<~ { precedence 110 } +/** + Custom operator that takes a vaule x, a function f and returns f(x) + */ + public func <~~(x: A, f: (A throws -> B)) throws -> B { return try f(x) } +/** + Custom operator that takes an array x, a function f and returns x.map(f) + */ public func <<~(x: [A], f: (A throws -> B)) throws -> [B] { return try x.map(f) } diff --git a/Sources/Types.swift b/Sources/Types.swift index 061171b..04942de 100644 --- a/Sources/Types.swift +++ b/Sources/Types.swift @@ -1,13 +1,17 @@ - +/// A type to represent typical JSON public typealias JSON = [String: AnyObject] -public enum ParseError: ErrorType { +/// Enumeration to represent Banana Errors. +public enum BananaError: ErrorType { + /// Case when the value is of different type than expected case InvalidType(T, expected: U) + /// Case when a value is nil case NilValue(T) + /// Custom error case case Custom(T) } -extension ParseError: CustomStringConvertible { +extension BananaError: CustomStringConvertible { public var description: String { switch self { case .InvalidType(let aThing, let expectedType): diff --git a/Support/Info.plist b/Support/Info.plist index d3de8ee..11d7458 100644 --- a/Support/Info.plist +++ b/Support/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 1.0 + 0.1 CFBundleSignature ???? CFBundleVersion