ml-archive/serpent
### This library has been deprecated and the repo has been archived.
π Table of Contents
- Carthage - CocoaPods - Swift Package Manager
- Getting started - Using Serpent models - More complex examples - Using with Alamofire - Date parsing
π Why Serpent?
There are plenty of other Encoding and Decoding frameworks available. Why should you use Serpent?
- Performance. Serpent is fast, up to 4x faster than similar frameworks.
- Features. Serpent can parse anything you throw at it. Nested objects, Enums, URLs, UIColor, you name it!
- [[ModelBoiler]](https://github.com/nodes-ios/ModelBoiler) Model Boiler. Every framework of this kind requires tedious boilerplate code that takes forever to write. [[ModelBoiler]](https://github.com/nodes-ios/ModelBoiler) Model Boiler generates it for you instantly.
- [Alamofire Integration](). Using the included Alamofire extensions makes implementing an API call returning parsed model data as simple as doing a one-liner!
- [Expandability](). Parsing into other datatypes can easily be added.
- [Persisting](). Combined with our caching framework Cashier, Serpent objects can be very easily persisted to disk.
- <a href = "https://github.com/nodes-ios/SerpentXcodeFileTemplate"><img src = "https://raw.githubusercontent.com/nodes-ios/SerpentXcodeFileTemplate/master/Serpent/Serpent%20Model.xctemplate/TemplateIcon.png" height = 25> Serpent Xcode File Template </a> makes it easier to create the model files in Xcode.
π Requirements
- iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
- Swift 3.0+
(Swift 2.2 & Swift 2.3 supported in older versions)
π¦ Installation
Carthage
github "nodes-ios/Serpent" ~> 1.0Last versions compatible with lower Swift versions:
Swift 2.3
github "nodes-ios/Serpent" == 0.13.2Swift 2.2
github "nodes-ios/Serpent" == 0.11.2NOTE: Serpent was previously known as Serializable.
CocoaPods
Choose one of the following, add it to your Podfile and run pod install:
pod 'Serpent', '~> 1.0' # Just core
pod 'Serpent/Extensions', '~> 1.0' # Includes core and all extensions
pod 'Serpent/AlamofireExtension', '~> 1.0' # Includes core and Alamofire extension
pod 'Serpent/CashierExtension', '~> 1.0' # Includes core and Cashier extensionNOTE: CocoaPods only supports Serpent using Swift version 3.0 and higher.
Swift Package Manager
To use Serpent as a Swift Package Manager package just add the following to your Package.swift file.
import PackageDescription
let package = Package(
name: "YourPackage",
dependencies: [
.Package(url: "https://github.com/nodes-ios/Serpent.git", majorVersion: 1)
]
)π§ Setup
We highly recommend you use our [[ModelBoiler]](https://github.com/nodes-ios/ModelBoiler) Model Boiler to assist with generating the code needed to conform to Serpent. Instructions for installation and usage can be found at the Model Boiler GitHub repository.
π» Usage
### Getting started
Serpent supports all primitive types, `enum`, `URL`, `Date`, `UIColor`, other `Serpent` model, and `Array` of all of the aforementioned types. Your variable declarations can have a default value or be optional.
Primitive types do not need to have an explicit type, if Swift is able to infer it normally. `var name: String = ""` works just as well as `var name = ""`. Optionals will of course need an explicit type.
> **NOTE:** Enums you create must conform to `RawRepresentable`, meaning they must have an explicit type. Otherwise, the parser won't know what to do with the incoming data it receives.
#### Create your model struct or class:
~~~swift
struct Foo {
var id = 0
var name = ""
var address: String?
}
~~~
> **NOTE:** Classes must be marked `final`.
#### Add the required methods for `Encodable` and `Decodable`:
~~~swift
extension Foo: Serializable {
init(dictionary: NSDictionary?) {
id <== (self, dictionary, "id")
name <== (self, dictionary, "name")
address <== (self, dictionary, "address")
}
func encodableRepresentation() -> NSCoding {
let dict = NSMutableDictionary()
(dict, "id") <== id
(dict, "name") <== name
(dict, "address") <== address
return dict
}
}
~~~
> **NOTE:** You can add conformance to `Serializable` which is a type alias for both `Encodable` and `Decodable`.
And thats it! If you're using the [](https://github.com/nodes-ios/ModelBoiler) [Model Boiler](https://github.com/nodes-ios/ModelBoiler), this extension will be generated for you, so that you don't need to type it all out for every model you have.
### Using Serpent models
New instances of your model can be created with a dictionary, e.g. from parsed JSON.
~~~swift
let dictionary = try JSONSerialization.jsonObject(with: someData, options: .allowFragments) as? NSDictionary
let newModel = Foo(dictionary: dictionary)
~~~
You can generate a dictionary version of your model by calling `encodableRepresentation()`:
~~~swift
let encodedDictionary = newModel.encodableRepresentation()
~~~
### More complex examples
In this example, we have two models, Student and School.
~~~swift
struct Student {
enum Gender: String {
case male = "male"
case female = "female"
case unspecified = "unspecified"
}
var name = ""
var age: Int = 0
var gender: Gender?
}
struct School {
enum Sport: Int {
case football
case basketball
case tennis
case swimming
}
var name = ""
var location = ""
var website: URL?
var students: [Student] = []
var sports: [Sport]?
}
~~~
You can get as complicated as you like, and the syntax will always remain the same. The extensions will be:
~~~swift
extension Student: Serializable {
init(dictionary: NSDictionary?) {
name <== (self, dictionary, "name")
age <== (self, dictionary, "age")
gender <== (self, dictionary, "gender")
}
func encodableRepresentation() -> NSCoding {
let dict = NSMutableDictionary()
(dict, "name") <== name
(dict, "age") <== age
(dict, "gender") <== gender
return dict
}
}
extension School: Serializable {
init(dictionary: NSDictionary?) {
name <== (self, dictionary, "name")
location <== (self, dictionary, "location")
website <== (self, dictionary, "website")
students <== (self, dictionary, "students")
sports <== (self, dictionary, "sports")
}
func encodableRepresentation() -> NSCoding {
let dict = NSMutableDictionary()
(dict, "name") <== name
(dict, "location") <== location
(dict, "website") <== website
(dict, "students") <== students
(dict, "sports") <== sports
return dict
}
}
~~~
Again, the [](https://github.com/nodes-ios/ModelBoiler) [Model Boiler](https://github.com/nodes-ios/ModelBoiler) generates all of this code for you in less than a second!
### Using with Alamofire
Serpent comes integrated with Alamofire out of the box, through an extension on Alamofire's `Request` construct, that adds the function `responseSerializable(completion:unwrapper)`
The extension uses Alamofire's familiar `Response` type to hold the returned data, and uses its generic associated type to automatically parse the data.
Consider an endpoint returning a single `school` structure matching the struct from the example above. To implement the call, simply add a function to your shared connection manager or where ever you like to put it:
~~~swift
func requestSchool(completion: @escaping (DataResponse<School>) -> Void) {
request("http://somewhere.com/school/1", method: .get).responseSerializable(completion)
}
~~~
In the consuming method you use it like this:
~~~swift
requestSchool() { (response) in
switch response.result {
case .success(let school):
//Use your new school object!
case .failure(let error):
//Handle the error object, or check your Response for more detail
}
}
~~~
For an array of objects, use the same technique:
~~~swift
static func requestStudents(completion: @escaping (DataResponse<[Student]>) -> Void) {
request("http://somewhere.com/school/1/students", method: .get).responseSerializable(completion)
}
~~~
Some APIs wrap their data in containers. Use the `unwrapper` closure for that. Let's say your `/students` endpoint returns the data wrapped in a `students` object:
~~~json
{
"students" : [
{
"..." : "..."
},
{
"..." : "..."
}
]
}
~~~
The `unwrapper` closure has 2 input arguments: The `sourceDictionary` (the JSON Response Dictionary) and the `expectedType` (the *type* of the target Serpent). Return the object that will serve as the input for the Serializable initializer.
~~~swift
static func requestStudents(completion: (DataResponse<[Student]>) -> Void) {
request("http://somewhere.com/school/1/students", method: .get).responseSerializable(completion, unwrapper: { $0.0["students"] })
}
~~~
If you need to unwrap the response data in every call, you can install a default unwrapper using
~~~swift
Parser.defaultWrapper = { sourceDictionary, expectedType in
// You custom unwrapper here...
return sourceDictionary
}
~~~
The `expectedType` can be used to dynamically determine the key based on the type name using reflection. This is especially useful when handling paginated data.
See [here](https://github.com/nodes-ios/Nodes) for an example on how we use this in our projects at Nodes.
***NOTE:*** `responseSerializable` Internally calls `validate().responseJSON()` on the request, so you don't have to do that.
### Date parsing
Serpent can create `Date` objects from the date strings in the JSON. By default, Serpent can parse the date strings from the following formats: `yyyy-MM-dd'T'HH:mm:ssZZZZZ`, `yyyy-MM-dd'T'HH:mm:ss`, `yyyy-MM-dd`. If you need to parse other date formats, you can do it by adding this line to your code (for example, in `AppDelegate`'s `didFinishLaunchingWithOptions:`:
~~~swift
Date.customDateFormats = ["yyyyMMddHHmm", "yyyyMMdd"] // add the custom date formats you need here
~~~
The custom date formats won't replace the default ones, they will be still supported.π₯ Credits
Made with β€οΈ at Nodes.
π License
Serpent is available under the MIT license. See the LICENSE file for more info.
Package Metadata
Repository: ml-archive/serpent
Default branch: master
README: README.md