Skip to content

Instantly share code, notes, and snippets.

@edwardaux
Created September 30, 2015 22:49
Show Gist options
  • Save edwardaux/93e3108ae0dc43c1c3e7 to your computer and use it in GitHub Desktop.
Save edwardaux/93e3108ae0dc43c1c3e7 to your computer and use it in GitHub Desktop.
Patches for Argo #237
From bf726b714c977045974a92d91d561a72c3045924 Mon Sep 17 00:00:00 2001
From: Tony DiPasquale <[email protected]>
Date: Fri, 13 Mar 2015 13:08:22 -0400
Subject: [PATCH] Update the documentation for Swift 2
This also breaks up the main README.md into a Documentations directory
with separate documents for different topics. This will be expanded upon
with new PRs going forward.
---
ArgoTests/JSON/JSONFileReader.swift | 3 +-
Documentation/.gitkeep | 0
Documentation/Basic-Usage.md | 137 ++++++++++++++++++
Documentation/Functional-Concepts.md | 26 ++++
Documentation/Ideology.md | 37 +++++
Documentation/README.md | 24 ++++
README.md | 261 +----------------------------------
7 files changed, 231 insertions(+), 257 deletions(-)
create mode 100644 Documentation/.gitkeep
create mode 100644 Documentation/Basic-Usage.md
create mode 100644 Documentation/Functional-Concepts.md
create mode 100644 Documentation/Ideology.md
create mode 100644 Documentation/README.md
diff --git a/ArgoTests/JSON/JSONFileReader.swift b/ArgoTests/JSON/JSONFileReader.swift
index 1b3c8e6..0f50ee7 100644
--- a/ArgoTests/JSON/JSONFileReader.swift
+++ b/ArgoTests/JSON/JSONFileReader.swift
@@ -7,8 +7,7 @@ func JSONFromFile(file: String) -> AnyObject? {
}
private func JSONObjectWithData(data: NSData) -> AnyObject? {
- do { return try NSJSONSerialization.JSONObjectWithData(data, options: []) }
- catch { return .None }
+ return try? NSJSONSerialization.JSONObjectWithData(data, options: [])
}
private class JSONFileReader { }
diff --git a/Documentation/.gitkeep b/Documentation/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/Documentation/Basic-Usage.md b/Documentation/Basic-Usage.md
new file mode 100644
index 0000000..e9e86bd
--- /dev/null
+++ b/Documentation/Basic-Usage.md
@@ -0,0 +1,137 @@
+## Usage
+
+The first thing you need to do when you receive JSON data is convert it from
+`NSData` to an `AnyObject` using `Foundation`'s `NSJSONSerialization` API.
+Once you have the `AnyObject`, you can call the global `decode` function to get
+back the decoded model.
+
+```swift
+let json: AnyObject? = try? NSJSONSerialization.JSONObjectWithData(responseData, options: [])
+
+if let j: AnyObject = json {
+ let user: User? = decode(j) // ignore error info or
+ let decodedUser: Decoded<User> = decode(j) // preserve error info
+}
+```
+
+Argo 1.0 introduces a new type: `Decoded<T>`. This is the type returned from
+the `decode` function that you implement as part of the `Decodable` protocol.
+This new type allows you to preserve information about why a decoding failed.
+You can choose to either ignore the `Decoded` type and just get back the
+optional value or keep the `Decoded` type and use it to debug decoding errors.
+When you decode an `AnyObject` into a model using the global `decode` function,
+you can specify whether you want an `Optional` model or a `Decoded` model by
+specifying the return type as seen in the code block above.
+
+Next, you need to make sure that models that you wish to decode from JSON
+conform to the `Decodable` protocol:
+
+```swift
+public protocol Decodable {
+ typealias DecodedType = Self
+ class func decode(JSON) -> Decoded<DecodedType>
+}
+```
+
+You will need to implement the `decode` function to perform any kinds of
+transformations you need to transform your model from a JSON value. The power
+of Argo can be seen when decoding actual model objects. To illustrate this, we
+will decode the simple `User` object.
+
+Create your `User` model:
+
+```swift
+struct User {
+ let id: Int
+ let name: String
+}
+```
+
+We will be using [`Curry`] to help with decoding our `User` model. Currying
+allows us to partially apply the `init` function over the course of the
+decoding process. If you'd like to learn more about currying, we recommend the
+following articles:
+
+[`Curry`]: https://github.com/thoughtbot/Curry
+
+- [Apple's documentation of curried functions](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html#//apple_ref/doc/uid/TP40014097-CH34-XID_615)
+- [Introduction to Function Currying in Swift](http://robots.thoughtbot.com/introduction-to-function-currying-in-swift)
+
+Now, we make `User` conform to `Decodable` and implement the required `decode`
+function. We will implement this function by using `map` (`<^>`) and `apply`
+(`<*>`) to conditionally pass the required parameters to the curried init
+function. The common pattern will look like:
+
+```swift
+return curry(Model.init) <^> paramOne <*> paramTwo <*> paramThree
+```
+
+and so on. If any of those parameters are an error, the entire creation process
+will fail, and the function will return the first error. If all of the
+parameters are successful, the value will be unwrapped and passed to the
+`init` function.
+
+In order to help with the decoding process, Argo introduces two new operators
+for parsing a value out of the JSON:
+
+- `<|` will attempt to parse a single value from the JSON
+- `<||` will attempt to parse an array of values from the JSON
+
+The usage of these operators is the same regardless:
+
+- `json <| "key"` is analogous to `json["key"]`
+- `json <| ["key", "nested"]` is analogous to `json["key"]["nested"]`
+
+Both operators will attempt to parse the value from the JSON and will also
+attempt to cast the value to the expected type. If it can't find a value, the
+function will return a `Decoded.MissingKey(message: String)` error. If the
+value it finds is the wrong type, the function will return a
+`Decoded.TypeMismatch(message: String)` error.
+
+There are also Optional versions of these operators:
+
+- `<|?` will attempt to parse an optional value from the JSON
+- `<||?` will attempt to parse an optional array of values from the JSON
+
+Usage is the same as the non-optionals. The difference is that if these fail
+parsing, the parsing continues. This is useful for including parameters that
+truly are optional values. For example, if your system doesn't require someone
+to supply an email address, you could have an optional property on `User`: `let
+email: String?` and parse it with `json <|? "email"`.
+
+So to implement our `decode` function, we can use the JSON parsing operator in
+conjunction with `map` and `apply`:
+
+```swift
+extension User: Decodable {
+ static func decode(j: JSON) -> Decoded<User> {
+ return curry(User.init)
+ <^> j <| "id"
+ <*> j <| "name"
+ }
+}
+```
+
+For comparison, this same function without Argo would look like so:
+
+```swift
+extension User {
+ static func decode(j: NSDictionary) -> User? {
+ if let id = j["id"] as Int,
+ let name = j["name"] as String
+ {
+ return User(id: id, name: name)
+ }
+
+ return .None
+ }
+}
+```
+
+You could see how this would get much worse with a more complex model.
+
+You can decode custom types the same way, as long as the type also conforms to
+`Decodable`.
+
+For more examples on how to use Argo, please check out the tests.
+
diff --git a/Documentation/Functional-Concepts.md b/Documentation/Functional-Concepts.md
new file mode 100644
index 0000000..ba3e09c
--- /dev/null
+++ b/Documentation/Functional-Concepts.md
@@ -0,0 +1,26 @@
+## Functional Concepts
+
+Argo really wants to be used with patterns borrowed from functional programming
+such as `map` and `apply`. We feel that these patterns greatly reduce the pain
+felt in trying to use JSON (an inherently loosely typed format) with Swift (a
+strictly typed language). It also gives us a way to succinctly maintain Argo's
+core concept, and short circuit the decoding process if any part of it fails.
+
+Additionally, we feel that the use of operators for these functions greatly
+improves the readability of the code we're suggesting. Using named functions
+would lead to nested functions and a confusing number of parenthesis.
+
+If you aren't familiar with how these functions work (or just aren't
+comfortable with using operators), that's totally OK. It's possible to use the
+library without using them, although it might be a little more painful.
+
+If you're looking to learn more about these functions, we would recommend
+reading the following articles:
+
+- [Functional Swift for Dealing with Optional Values](http://robots.thoughtbot.com/functional-swift-for-dealing-with-optional-values)
+- [Railway Oriented Programming](http://fsharpforfunandprofit.com/posts/recipe-part2/)
+
+And check out this talk:
+
+- [How I Learned To Stop Worrying And Love The Functor](https://github.com/gfontenot/talks/tree/master/Functors)
+
diff --git a/Documentation/Ideology.md b/Documentation/Ideology.md
new file mode 100644
index 0000000..80c6fb6
--- /dev/null
+++ b/Documentation/Ideology.md
@@ -0,0 +1,37 @@
+## Ideology
+
+Argo's core concept is that in order to maintain type safety, you should only
+be able to successfully decode an object if all parameters are satisfied
+properly. So if you have a model that looks like this:
+
+```swift
+struct User {
+ let id: Int
+ let name: String
+}
+```
+
+but the JSON you receive from the server looks like this:
+
+```json
+{
+ "user": {
+ "id": "this isn't a number",
+ "name": "Gob Bluth"
+ }
+}
+```
+
+then ideally, JSON parsing would fail, and you'd get an error state instead of
+a `User` object. In Argo, if JSON parsing succeeds you'll receive the `User`
+object and you can be sure that it is full and valid. If it fails, you will
+instead be given the reason why the `User` couldn't be constructed.
+
+If you're interested in learning more about the concepts and ideology that
+went into building Argo, we recommend reading the series of articles that were
+written alongside its development:
+
+- [Efficient JSON in Swift with Functional Concepts and Generics](http://robots.thoughtbot.com/efficient-json-in-swift-with-functional-concepts-and-generics)
+- [Real World JSON Parsing with Swift](http://robots.thoughtbot.com/real-world-json-parsing-with-swift)
+- [Parsing Embedded JSON and Arrays in Swift](http://robots.thoughtbot.com/parsing-embedded-json-and-arrays-in-swift)
+
diff --git a/Documentation/README.md b/Documentation/README.md
new file mode 100644
index 0000000..d2725b7
--- /dev/null
+++ b/Documentation/README.md
@@ -0,0 +1,24 @@
+# Documentation #
+
+Argo allows you to easily decode loosely typed structures into strongly typed
+models. When paired with functional programming concepts, Argo becomes a
+beautiful way to decode JSON from network responses into your application
+models. The following guides will teach you how to use Argo and how powerful it
+can be.
+
+## High Level Concepts ##
+
+- [Overarching ideology](Ideology.md)
+- [Functional concepts](Functional-Concepts.md)
+
+## Basic Usage ##
+
+- [Decoding your first model](Basic-Usage.md)
+- Relationships // TODO
+
+## Advanced Usage ##
+
+- Understanding the Decode operators // TODO
+- Interacting with the `JSON` enum // TODO
+- Writing your own custom parser // TODO
+- More complex parsers // TODO
diff --git a/README.md b/README.md
index 69577eb..338339e 100644
--- a/README.md
+++ b/README.md
@@ -84,280 +84,30 @@ extension User: Decodable {
// Wherever you receive JSON data:
-let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions(0), error: nil)
+let json: AnyObject? = try? NSJSONSerialization.JSONObjectWithData(data, options: [])
if let j: AnyObject = json {
let user: User? = decode(j)
}
```
-## Ideology
+For more information, see the [Documentation](Documentation/)
-Argo's core concept is that in order to maintain type safety, you should only
-be able to successfully decode an object if all parameters are satisfied
-properly. So if you have a model that looks like this:
-
-```swift
-struct User {
- let id: Int
- let name: String
-}
-```
-
-but the JSON you receive from the server looks like this:
-
-```json
-{
- "user": {
- "id": "this isn't a number",
- "name": "Gob Bluth"
- }
-}
-```
-
-then ideally, JSON parsing would fail, and you'd get an error state instead of
-a `User` object. In Argo, if JSON parsing succeeds you'll receive the `User`
-object and you can be sure that it is full and valid. If it fails, you will
-instead be given the reason why the `User` couldn't be constructed.
-
-If you're interested in learning more about the concepts and ideology that
-went into building Argo, we recommend reading the series of articles that were
-written alongside its development:
-
-- [Efficient JSON in Swift with Functional Concepts and Generics](http://robots.thoughtbot.com/efficient-json-in-swift-with-functional-concepts-and-generics)
-- [Real World JSON Parsing with Swift](http://robots.thoughtbot.com/real-world-json-parsing-with-swift)
-- [Parsing Embedded JSON and Arrays in Swift](http://robots.thoughtbot.com/parsing-embedded-json-and-arrays-in-swift)
-
-## Functional Concepts
-
-Argo really wants to be used with patterns borrowed from functional
-programming such as `map` and `apply`. We feel that these patterns greatly
-reduce the pain felt in trying to use JSON (an inherently loosely typed
-format) with Swift (a strictly typed language). It also gives us a way to
-succinctly maintain the core concept described above, and short circuit the
-decoding process if any part of it fails.
-
-Additionally, we feel that the use of operators for these functions greatly
-improves the readability of the code we're suggesting. Using named functions
-would lead to nested functions and a confusing number of parenthesis.
-
-If you aren't familiar with how these functions work (or just aren't
-comfortable with using operators), that's totally OK. It's possible to use the
-library without using them, although it might be a little more painful.
-
-If you're looking to learn more about these functions, we would recommend
-reading the following articles:
-
-- [Functional Swift for Dealing with Optional Values](http://robots.thoughtbot.com/functional-swift-for-dealing-with-optional-values)
-- [Railway Oriented Programming](http://fsharpforfunandprofit.com/posts/recipe-part2/)
-
-And check out this talk:
-
-- [How I Learned To Stop Worrying And Love The Functor](https://github.com/gfontenot/talks/tree/master/Functors)
-
-## Usage
-
-The first thing you need to do when you receive JSON data is convert it from
-`NSData` to an `AnyObject` using the built-in `NSJSONSerialization` API. Once
-you have the `AnyObject`, you can call the global `decode` function to get back
-the decoded model.
-
-```swift
-var error: NSError?
-let json: AnyObject? = NSJSONSerialization.JSONObjectWithData(responseData, options: NSJSONReadingOptions(0), error: &error)
-
-if let j: AnyObject = json {
- let user: User? = decode(j) // ignore error info or
- let decodedUser: Decoded<User> = decode(j) // preserve error info
-} else {
- // handle error
-}
-```
-
-Note that you probably want to use an error pointer to track errors from
-`NSJSONSerialization`.
-
-The `JSON` enum exists to help with some of the type inference, and also wraps
-up some of the casting that you'll need to do to transform the JSON into native
-types.
-
-Argo 1.0 introduces a new type: `Decoded<T>`. This is now the type returned
-from the `decode` function that you implement as part of the `Decodable`
-protocol. This new type allows you to preserve information about why a decoding
-failed. You can choose to either ignore the `Decoded` type and just get back
-the optional value or keep the `Decoded` type and use it to debug decoding
-errors. When you decode an `AnyObject` into a model using the global `decode`
-function, you can specify whether you want an `Optional` model or a `Decoded`
-model by specifying the return type as seen in the code block above.
-
-Next, you need to make sure that models that you wish to decode from JSON
-conform to the `Decodable` protocol:
-
-```swift
-public protocol Decodable {
- typealias DecodedType = Self
- class func decode(JSON) -> Decoded<DecodedType>
-}
-```
-
-You will need to implement the `decode` function to perform any kinds of
-transformations you need to transform your model from a JSON value. A simple
-implementation for an enum value might look like:
-
-```swift
-enum RoleType: String {
- case Admin = "Admin"
- case User = "User"
-}
-
-extension RoleType: Decodable {
- static func decode(j: JSON) -> Decoded<RoleType> {
- switch j {
- case let .String(s): return .fromOptional(RoleType(rawValue: s))
- default: return .TypeMismatch("\(j) is not a String") // Provide an Error message for a string type mismatch
- }
- }
-}
-```
-
-The real power of Argo can be seen when decoding actual model objects. To
-illustrate this, we will decode the simple `User` object that we used earlier.
-
-Create your model normally:
-
-```swift
-struct User {
- let id: Int
- let name: String
-}
-```
-
-You will also need a curried function used to construct your model object. If
-you'd like to do this manually, it would look like so:
-
-```swift
-extension User {
- static func create(id: Int)(name: String) -> User {
- return User(id: id, name: name)
- }
-}
-```
-
-Alternatively, you can use a `curry` function to curry the object's normal
-initializer:
-
-```swift
-curry(User.init)
-```
-
-We recommend using a shared dependency such as [Curry.framework] to introduce
-this function to avoid collisions.
-
-[Curry.framework]: https://github.com/thoughtbot/Curry
-
-Using this curried syntax will allow us to partially apply the function over
-the course of the decoding process. If you'd like to learn more about
-currying, we recommend the following articles:
-
-- [Apple's documentation of curried functions](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Declarations.html#//apple_ref/doc/uid/TP40014097-CH34-XID_615)
-- [Introduction to Function Currying in Swift](http://robots.thoughtbot.com/introduction-to-function-currying-in-swift)
-
-The last thing to do will be to conform to `Decodable` and implement the
-required `decode` function. We will implement this function by using `map`
-(`<^>`) and `apply` (`<*>`) to conditionally pass the required parameters to
-the curried creation function. The common pattern will look like:
-
-```swift
-return curry(Model.init) <^> paramOne <*> paramTwo <*> paramThree
-```
-
-and so on. If any of those parameters are an error, the entire creation process
-will fail, and the function will return the first error. If all of the
-parameters are successful, the value will be unwrapped and passed to the
-constructor function.
-
-In order to help with the decoding process, Argo introduces two new operators
-for parsing a value out of the JSON:
-
-- `<|` will attempt to parse a single value from the JSON
-- `<||` will attempt to parse an array of values from the JSON
-
-The usage of these operators is the same regardless:
-
-- `json <| "key"` is analogous to `json["key"]`
-- `json <| ["key", "nested"]` is analogous to `json["key"]["nested"]`
-
-Both operators will attempt to parse the value from the JSON and will also
-attempt to cast the value to the expected type. If it can't find a value, the
-function will return a `Decoded.MissingKey(message: String)` error. If the
-value it finds is the wrong type, the function will return a
-`Decoded.TypeMismatch(message: String)` error.
-
-There are also Optional versions of these operators:
-
-- `<|?` will attempt to parse an optional value from the JSON
-- `<||?` will attempt to parse an optional array of values from the JSON
-
-Usage is the same as the non-optionals. The difference is that if these fail
-parsing, the parsing continues. This is useful for including parameters that
-truly are optional values. For example, if your system doesn't require someone
-to supply an email address, you could have an optional property: `let email:
-String?` and parse it with `json <|? "email"`.
-
-So to implement our `decode` function, we can use the JSON parsing operator in
-conjunction with `map` and `apply`:
-
-```swift
-extension User: Decodable {
- static func decode(j: JSON) -> Decoded<User> {
- return curry(User.init)
- <^> j <| "id"
- <*> j <| "name"
- }
-}
-```
-
-For comparison, this same function without Argo would look like so:
-
-```swift
-extension User {
- static func decode(j: NSDictionary) -> User? {
- if let id = j["id"] as! Int,
- let name = j["name"] as! String
- {
- return User(id: id, name: name)
- }
-
- return .None
- }
-}
-```
-
-You could see how this would get much worse with a more complex model.
-
-You can decode custom types the same way, as long as the type also conforms to
-`Decodable`.
-
-For more examples on how to use Argo, please check out the tests.
-
-Contributing
-------------
+## Contributing
See the [CONTRIBUTING] document. Thank you, [contributors]!
[CONTRIBUTING]: CONTRIBUTING.md
[contributors]: https://github.com/thoughtbot/Argo/graphs/contributors
-License
--------
+## License
Argo is Copyright (c) 2015 thoughtbot, inc. It is free software, and may be
redistributed under the terms specified in the [LICENSE] file.
[LICENSE]: /LICENSE
-About
------
+## About
![thoughtbot](https://thoughtbot.com/logo.png)
@@ -370,3 +120,4 @@ our product [case studies] and [hire us][hire] to help build your iOS app.
[community]: https://thoughtbot.com/community?utm_source=github
[case studies]: https://thoughtbot.com/ios?utm_source=github
[hire]: https://thoughtbot.com/hire-us?utm_source=github
+
--
2.2.1
From 038f72a4b007e4b86e4e3667a568dc7bf0b23338 Mon Sep 17 00:00:00 2001
From: Tony DiPasquale <[email protected]>
Date: Mon, 21 Sep 2015 16:08:06 -0700
Subject: [PATCH] Add a Custom error case for DecodeError
---
Argo/Types/DecodeError.swift | 2 ++
Argo/Types/Decoded.swift | 5 +++++
ArgoTests/Tests/DecodedTests.swift | 15 +++++++++++++++
3 files changed, 22 insertions(+)
diff --git a/Argo/Types/DecodeError.swift b/Argo/Types/DecodeError.swift
index 9a86f88..26cddbf 100644
--- a/Argo/Types/DecodeError.swift
+++ b/Argo/Types/DecodeError.swift
@@ -1,6 +1,7 @@
public enum DecodeError: ErrorType {
case TypeMismatch(expected: String, actual: String)
case MissingKey(String)
+ case Custom(String)
}
extension DecodeError: CustomStringConvertible {
@@ -8,6 +9,7 @@ extension DecodeError: CustomStringConvertible {
switch self {
case let .TypeMismatch(expected, actual): return "TypeMismatch(Expected \(expected), got \(actual))"
case let .MissingKey(s): return "MissingKey(\(s))"
+ case let .Custom(s): return "Custom(\(s))"
}
}
}
diff --git a/Argo/Types/Decoded.swift b/Argo/Types/Decoded.swift
index 154d4f2..e55b056 100644
--- a/Argo/Types/Decoded.swift
+++ b/Argo/Types/Decoded.swift
@@ -16,6 +16,7 @@ public extension Decoded {
case let .Success(value): return .Success(.Some(value))
case .Failure(.MissingKey): return .Success(.None)
case let .Failure(.TypeMismatch(x)): return .Failure(.TypeMismatch(x))
+ case let .Failure(.Custom(x)): return .Failure(.Custom(x))
}
}
@@ -39,6 +40,10 @@ public extension Decoded {
static func missingKey<T>(name: String) -> Decoded<T> {
return .Failure(.MissingKey(name))
}
+
+ static func customError<T>(message: String) -> Decoded<T> {
+ return .Failure(.Custom(message))
+ }
}
extension Decoded: CustomStringConvertible {
diff --git a/ArgoTests/Tests/DecodedTests.swift b/ArgoTests/Tests/DecodedTests.swift
index 8864567..526668d 100644
--- a/ArgoTests/Tests/DecodedTests.swift
+++ b/ArgoTests/Tests/DecodedTests.swift
@@ -28,6 +28,15 @@ class DecodedTests: XCTestCase {
default: XCTFail("Unexpected Case Occurred")
}
}
+
+ func testDecodedCustomError() {
+ let customError: Decoded<Dummy> = decode([:])
+
+ switch customError {
+ case let .Failure(e): XCTAssert(e.description == "Custom(My Custom Error)")
+ default: XCTFail("Unexpected Case Occurred")
+ }
+ }
func testDecodedDematerializeSuccess() {
let user: Decoded<User> = decode(JSONFromFile("user_with_email")!)
@@ -70,3 +79,9 @@ class DecodedTests: XCTestCase {
}
}
}
+
+private struct Dummy: Decodable {
+ static func decode(json: JSON) -> Decoded<Dummy> {
+ return .Failure(.Custom("My Custom Error"))
+ }
+}
--
2.2.1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment