[Linux対応]SwiftyJSONより厳しめなJSONライブラリ作りました (JAYSON)

# JAYSON

SwiftyJSONがなかなかSwift3を対応しない中、(もう対応されましたが)
もともとSwiftyJSONにはすこし気に入らないところが幾つかあったため、
とうとうJSONライブラリを作ってしまいました。
(SwiftはJSONパーサーが大量に存在しているので車輪の再発明
っぽくなりそうで避けてました)

名前は [JAYSON](https://github.com/muukii/JAYSON) です
https://github.com/muukii/JAYSON
(GitHubスターを頂けるとうれしいです... ⭐️⭐️⭐️⭐️⭐️)

JAYSONは
SwiftyJSONのような使い勝手である**Easy-Read**と
try-catchを利用した厳格に値を取得する**Strict-Read**を用意しています。

簡単なJSONを用意して説明していきます。

```json
{
    "shots" : 
    [
        {
            "id": "itdkHUjTuIY",
            "created_at": "2016-09-24T04:06:19-04:00",
            "width": 4256,
            "height": 2832,
            "color": "#F7DCE2",
            "likes": 3,
            "liked_by_user": false,
            "user": {
                "id": "xzxvjf",
                "username": "muukii",
                "name": "hiroshi kimura",
                "profile_image": {
                    "small": "https://...",
                    "medium": "https://...",
                    "large": "https://..."
                },
            },
        },
        {
            "id": "irqwHfqweuIY",
            "created_at": "2016-09-24T04:06:19-04:00",
            "width": 4256,
            "height": 2832,
            "color": "#DDDCE2",
            "likes": 5,
            "liked_by_user": false,
            "user": {
                "id": "zxcvfsad3",
                "username": "john",
                "name": "john estropia",
                "profile_image": {
                    "small": "https://...",
                    "medium": "https://...",
                    "large": "https://..."
                },
            },
        }
    ]
}
```

# 読み込む準備

JAYSONオブジェクトを作る方法はいくつか用意しています。

- JSONのDataから生成 (APIレスポンス等をそのまま入れるイメージ)
    - 無効なDataが入ってきた場合 throw します。

```swift
let jsonData: Data
let jayson = try JAYSON(data: jsonData)
```

- 事前にJSONSerializationを行ったAnyオブジェクトから生成
    - AnyがJSONとして認識できない場合 throw します。

```swift
let jsonData: Data
let json: Any = try JSONSerialization.jsonObject(with: data, options: [])
let jayson = try JAYSON(any: json)
```

```swift
let userInfo: [AnyHashable: Any]
let jayson = try JAYSON(any: json)
```

```swift
let objects: [Any]
let jayson = try JAYSON(any: json)
```

- 以下の型の場合は必ず生成に成功するため `try`は不要です。

```swift
let object: [String : JAYSON]
let jayson = JAYSON(object)
```

```swift
let object: [JAYSON]
let jayson = JAYSON(object)
```

```swift
let object: [JAYSONWritableType]
let jayson = JAYSON(object)
```

```swift
let object: [String : JAYSONWritableType]
let jayson = JAYSON(object)
```

# Easy Read

shots -> 0 -> user -> profile_image -> large を取得

SwiftyJSONと同様にsubscriptでアクセスできます。

key, indexが見つからなかった場合
NSNullが入ったJAYSONが返却されます。 ( `JAYSON.null` と同じ状態)

SwiftyJSONとの違いは型が間違っている場合にdefaultValueを返却するプロパティを用意していないことです。
必ずOptional値の返却になります。

```swift
    let urlString: String? = jayson["shots"][0]["user"]["profile_image"]["large"].string
```

(おまけ) shots以下を配列として取得

```swift
let shots: [JAYSON]? = jayson["shots"].array
    
let shots = jayson["shots"].array?.map { $0["id"].string }
// => Optional([Optional("itdkHUjTuIY"), Optional("irqwHfqweuIY")])
```
# Strict Read

shots -> 0 -> user -> profile_image -> large を取得

こちらはJAYSONの特徴です。
key, indexが見つからなかったタイミング or 型が間違っていたタイミング
で `JAYSONError` がthrowされます。

```swift
let urlString: String = try jayson
   .next("shots")
   .next(0)
   .next("user")
   .next("profile_image")
   .next("large")
   .getString()
```
(おまけ)

```swift

let shots = try jayson.next("shots").getArray().map { try $0.next("id").getString() }
// ["itdkHUjTuIY", "irqwHfqweuIY"]
```

# Get current path

主にデバッグ時に使用するであろう機能です。

JAYSONはSwiftyJSONと同様にJAYSONオブジェクトを返却し続けます。
これはEasy, Strict どちらでも同じ挙動です。

```swift
let shots: JAYSON = jayson["shots"]
let firstShot: JAYSON = shots[0]
let id: JAYSON = firstShot["id"]
```

SwiftyJSONでの問題は深い階層のJSONをパースする際に発生したエラーは何が原因だったのが見つけづらいところでした。

そこでJAYSONではkey, indexによって生成されたJAYSONは親のJAYSONを保持するようにしています。
この仕組を利用して、あるJAYSONはどのようなpathを辿って作られたのかを知ることが出来ます。

```swift
let large: JAYSON = jayson["shots"][0]["user"]["profile_image"]["large"]
print(large.currentPath())
// Root->["shots"][0]["user"]["profile_image"]["large"]
```

Strict-Read時ではthrowされたエラーから失敗したJAYSONが届くので
どこでなにが失敗したのかを処理の上流で知ることが出来ます。


# Custom Getter

URLやDateはJSONの仕様には定められていないため、文字列と同じ扱いです。
アプリではURL, Dateが存在するため、取得時に変換してしまいたいものです。

ただ、JAYSONの仕様はシンプルにしておきたいため、このようなproperty, getterは用意していません。
(プロジェクトごとにどのような変換を行い取得するかを定義するのがベターです。)

２つの方法を用意しています。


- その場にclosureで定義する


```swift
let url: URL = try jayson
    .next("shots")
    .next(0)
    .next("user")
    .next("profile_image")
    .next("large")
    .get { (jayson) throws -> URL in
        URL(string: try jayson.getString())!
}
```


- Decoder<T>を定義して`get<T>(with: Decoder<T>)` に渡す


```swift
let urlDecoder = Decoder<URL> { (jayson) throws -> URL in
    URL(string: try jayson.getString())!
}

let url: URL = try jayson
    .next("shots")
    .next(0)
    .next("user")
    .next("profile_image")
    .next("large")
    .get(with: urlDecoder)
```


# Build JSON

JSONパーサーはかなりの数が公開されていますが、
JSONを読み込む専用で、JSONを書き出す機能がないものが多いかもしれません。
僕は開発しているプロジェクトでAPIリクエストのBodyがJSONだったので、
１つのライブラリで読み書きが出来た方が良かったので書き込み機能を用意しました。

こちらも基本的にSwiftyJSONと同様ではありますが、
なんでもJAYSONに入るわけではなく`JASONWritableType` プロトコルを持ったオブジェクトのみになります。
(出来る限りランタイムの失敗を少なくするためです)
(書き込みにもStrictモードを用意して良いかもしれません)


```swift
var jayson = JAYSON()
jayson["id"] = 18737649
jayson["active"] = true
jayson["name"] = "muukii"

jayson["images"] = JAYSON([
    "large" : "http://...foo",
    "medium" : "http://...foo",
    "small" : "http://...foo",
    ])

let data: Data = try jayson.data(options: .prettyPrinted)
```

- 出力

```
{
  "name" : "muukii",
  "active" : true,
  "id" : 18737649,
  "images" : {
    "large" : "http:\/\/...foo",
    "small" : "http:\/\/...foo",
    "medium" : "http:\/\/...foo"
  }
}
```


# リアルなサンプル

ちょっとだけリアルなサンプルを用意してみました。

dribbble APIの結果をパースする処理です。

```swift
let urlDecoder = Decoder<URL> { (jayson) throws -> URL in
    URL(string: try jayson.getString())! // 任意のエラーをthrowできます。
}

struct Shot {
    let id: Int
    let title: String
    let width: Int
    let height: Int
    let hidpiImageURLString: URL?
    let normalImageURLString: URL
    let teaserImageURLString: URL
    
    init(jayson: JAYSON) throws {
        let imagesJayson = try jayson.next("images")
        
        id = try jayson.next("id").getInt()
        title = try jayson.next("title").getString()
        width = try jayson.next("width").getInt()
        height = try jayson.next("height").getInt()
        hidpiImageURLString = try? imagesJayson.next("hidpi").get(with: urlDecoder)
        normalImageURLString = try imagesJayson.next("normal").get(with: urlDecoder)
        teaserImageURLString = try imagesJayson.next("teaser").get(with: urlDecoder)
    }
}
do {
    let shots: [Shot] = try jayson.getArray().map(Shot.init(jayson: ))
    print(shots)
} catch {
    print(error) // 何個目のJAYSONでどのようなpathで失敗したのかがわかります。
}
```

少しだけnext()などの記述が長く感じるかもしれませんが、
それはプロジェクトごとにoperatorを用意しても良いかもしれません。

今のところ、operatorを使うとメソッドチェーンが行いづらくなるためライブラリ側では用意していません。

## 補足

`Decodable`のような変換対象の型に採用させるprotocolを用意せず、
`Decoder`しか用意していない理由は、その型に変換する方法は一つとは限らないためです。

例えばDate型では、ISO8601やUnixTimeStampなど様々な型があります。
APIによっては混在しているケースもあるかもしれません。(あんまりないとは思いますが)
Decoderを渡すようにしておけば、このような場合にも柔軟に対応できます。



# おわりに

もし気に入って頂けたらGitHubスターを付けて頂けるとうれしいです！

https://github.com/muukii/JAYSON


### 追記

- 0.6.0にて暫定的ではありますがLinuxのサポートをしました。