Swift - introducing swift-json, even swiftier than SwiftyJSON

[swift-json]をゼロから書き直したのでおしらせします。

* https://github.com/dankogai/swift-json

[swift-json]: https://github.com/dankogai/swift-json

### JSON as a Swift Literal

どういうものかというと、こういうものです。

```swift
let json:JSON = [
    "null":     nil,
    "bool":     true,
    "int":      -42,
    "double":   42.195,
    "string":   "漢字、カタカナ、ひらがなと\"引用符\"の入ったstring😇",
    "array":    [nil, true, 1, "one", [1], ["one":1]],
    "object":   [
        "null":nil, "bool":false, "number":0, "string":"" ,"array":[], "object":[:]
    ],
    "url":"https://github.com/dankogai/"
]
```

はい。いきなり`JSON`型としてリテラルで書き下せます。


`JSON`型は`CustomStringConvertible`で、その`.description`はECMASCriptの`JSON.stringify()`で出力される文字列そのものです。

```javascript
{
    "null":     null,
    "bool":     true,
    "int":      -42,
    "double":   42.195,
    "string":   "漢字、カタカナ、ひらがなと\"引用符\"の入ったstring😇",
    "array":    [null, true, 1, "one", [1], {"one":1}],
    "object":   {
        "null":null, "bool":false, "number":0, "string":"" ,"array":[], "object":{}
    },
    "url":"https://github.com/dankogai/"

}
```

初期化はリテラルからはもちろん、文字列からでもできますし…

```swift
JSON(string:"{\"swift\":[\"safe\",\"fast\",\"expressive\"]}")
```

URLの中身からでもできます。

```swift
JSON(urlString:"https://api.github.com")
```

### Direct Manipulation

`let`ではなく`var`の場合、中身を直接かつ直感的に操作することも可能です。

```swift
var json = JSON([])
json[0] = nil
json[1] = true
json[2] = 1
```

一つ気をつけなければならないのは、右側にあるリテラルはビルトインの`nil`や`true`や`1`ではなく、`JSON`型の`.Null`や`.Bool(true)`や`.Number(1)`であること。よって以下はエラーになります。

```swift
let one = "one"
json[3] = one // error: cannot assign value of type 'String' to type 'JSON'
```

ただしこの場合は、次のようにunwrapしてしまえばいいのです。

```swift
json[1].bool   = Bool(true)
json[2].number = Int(1)
json[3].string = String("one")
json[4].array  = [Int(1)]
json[5].object = [String("one"):Int(1)]

```

これらは getter であるとともに setter でもあり、 getter としては、型が合えば`JSON`型から元の型の値を返し、合わなければ`nil`を返すようになっています。

```swift
json[1].bool    // Optional(true)
json[1].number  // nil
```

その特性を活かすと、こんな書き方もできます。

```swift
json[2].number! += 1            // now 2
json[3].string!.removeLast()    // now "on"
json[4].array!.append(2)        // now [1, 2]
json[5].object!["two"] = 2      // now ["one":1,"two":2]
```

`JSON`が`.Array`の場合に、存在しない添字に値を代入すると、配列が拡張され隙間は`.Null`で埋まります。

```swift
json[10] = false	// json[6...9] are null
```

`.Object`の場合に存在しないキーに対して値を代入した場合も同様です。つまり、`JSON`型はECMAScriptの`object`のごとく振る舞います。

### Protocol Conformance

`JSON`型は次のプロトコルに準拠しています。

* `Equatable`なので直接比較ができます

```swift
JSON(["two":2]) == JSON(["t"+"w"+"o":1+1])
```

* `Hashable`なので`Dictionary`のキーとしても使えます。

* `ExpressibleBy*Literal`なので、リテラルを直接書き下せます。前述の通りです。

* `CustomStringConvertible` で、文字列化すると正しいJSON表記になります。

* `Codable`でもあるので`JSONENcoder()`の代わりになったりもします。

* `Sequence`でもあります。が、Swiftの制限により直感性が少し落ちています。

```swift
for v in JSON([nil, true, 1, "one", [1], ["one":1]]) {
   // v に入るのは?
}
```

上記の例だと`v`は`JSON`ではなく`(IteratorKey,JSON)`で、`IteratorKey`は配列の場合は`.Index`、辞書の場合は`.Key`となります。それぞれアクセサーが用意されているので

```swift
for (i, v) in JSON([nil, true, 1, "one", [1], ["one":1]]) {
  // 添字は i ではなく i.index
}
```

```swift
for (k, v) in JSON([
        "null":nil, "bool":false, "number":0, "string":"" ,
        "array":[], "object":[:]
    ]) {
  // 添字は k ではなく k.key
}

```

と出来はします。が、これは先ほどの`.array`や`.object`の方が使いやすいでしょう。

```swift
for v in JSON([nil, true, 1, "one", [1], ["one":1]]).array! {
	// ...
}
```

```swift
for (k, v) in JSON([
        "null":nil, "bool":false, "number":0, "string":"" ,
        "array":[], "object":[:]
    ]).object! {
	// ...
}
```

なお当然のことですが、`null`や`bool`や`number`や`string`は空の`Sequence`になります。

### Error handling

`init?`や`init() throws`はJSONとは相性が悪いので`JSON`型では採用せず、その代わりエラーが起こったら`.Error(.ErrorType)`型の値を返し、それが連鎖するようになっています。エラーになったのか、そしてなった場合はどんなエラーだったのかを確認するには次のようにすればOKです。

```swift
if let e = json.error {
	debugPrint(e.type)
	if let nsError = e.nsError {
		// do anything with nsError
	}
}
```

## Usage

### build

```sh
$ git clone https://github.com/dankogai/swift-json.git
$ cd swift-json # the following assumes your $PWD is here
$ swift build
```

### REPL

次のようにしてREPLで実行できます。

```sh
$ scripts/run-repl.sh
```

REPLが上がったら`import JSON`してお楽しみ下しあ。

```sh
  1> import JSON
  2> let json:JSON = ["swift":["safe","fast","expressive"]]
json: JSON.JSON = Object {
  Object = 1 key/value pair {
    [0] = {
      key = "swift"
      value = Array {
        Array = 3 values {
          [0] = String {
            String = "safe"
          }
          [1] = String {
            String = "fast"
          }
          [2] = String {
            String = "expressive"
          }
        }
      }
    }
  }
}
```

### Xcode

`.xcodeproj`はレポジトリには含まれていません。Swift Package Managerで`swift package generate-xcodeproj`すれば生成されるので。一応

```sh
$ scripts/prep-xcode
```

でそれをやった上でworkspaceを開いてくれます。

### iOS and Swift Playground

現状　Swift Package Manager と macOS/Linux 以外のプラットフォームとの相性は悪く、特に Playgrounds はモジュールすらサポートしてません。しかし依存コードを`Sources`フォルダーに置けば援用してくれます。モジュール本体は[JSON.swift]だけなので、それを手でコピーしても良いですし、

```sh
$ scripts/ios-prep.sh
```

を実行するとレポジトリの`iOS/JSON.playground`をよきに計らってくれるのであとはそれを iCloud Drive ぶっこんで同期すれば、Playgrounds for iPad でもお楽しみになれます。

<img width="834" alt="IMG_0074.png" src="https://qiita-image-store.s3.amazonaws.com/0/19227/f9627bc5-a327-c8a4-1af9-42035af244f9.png">

### From Your SwiftPM-Managed Projects

とはいえ基本は Swift Package Manager の利用が前提です。

`dependencies`に以下を、

```swift
.package(
  url: "https://github.com/dankogai/swift-json.git", from: "4.0.0"
)
```

`.target` に以下を追加したら、あとは

```swift
.target(
  name: "YourSwiftyPackage",
  dependencies: ["JSON"])
```

```swift
import JSON
```

してお楽しみください。

## 犯行動機

むしゃくしゃして書き直した。後悔はしていない…というのは冗談にしても、当初 swift-json は `class` でした。

https://qiita.com/dankogai/items/e3d9882f4d25bc44cc03

これはJSON Schemaとしての利用を念頭においてそうしたのですが、Swift 4 で[Codable]が入ったことにより陳腐化しました。

[Codable]: https://developer.apple.com/documentation/swift/codable

それでは`JSONDecoder`と`JSONEncoder`さえあればいいかというとそうは行きません。フリーフォームのJSONにアクセスする需要は常にあるからです。そういうことなら[SwiftyJSON]を使えばいいじゃないかという疑問は当然ですが、以下の理由により[SwiftyJSON]では私の需要を満たせなかったのです。

[SwiftyJSON]: https://github.com/SwiftyJSON/SwiftyJSON

* SwiftyJSON は framework をビルドして使うことを前提としていること。それでは Playgrounds for iOS で動かせないではないか。これが一番の理由
* SwiftyJSON は[JSONSerialization]の吐いた`JSONObject`つまり`Any`をそのまま援用していること。要素にアクセスする都度`Any`を unwrap するというのは Swifty とは言えません。対して本モジュールの`JSON`は`enum`で、`Any`は一切入ってません。[JSONSerialization]叩くのは`String`から`JSON`へ変換するときだけで、`JSON`から`String`への変換は`JSONSerialization`に全く依存しません。別のParserがあればすぐに差し替えられます。少なくともこの一点に関しては異論なく swiftier なはず。

[JSONSerialization]: https://developer.apple.com/documentation/foundation/jsonserialization

* SwiftyJSONのJSON.swiftは1500行以上あるのに対し、本モジュールのそれは現時点で350行切ってます。これくらいなら前述の Playgrounds for iPad の事例のようにファイルを一つコピーすれば事足ります。

私からは以上です。 Enjoy!

Dan the Safe, Fast, and Expressive