Linux+Docker上のSwift環境で駅すぱあとWebサービスを使ってみる

[ヴァル研究所 Advent Calendar 2017](https://qiita.com/advent-calendar/2017/val) 3日目の記事です。
今日は[駅すぱあとWebサービス](http://docs.ekispert.com/v1/)をLinux上の[Swift](https://www.apple.com/jp/swift/)から扱ってみる話をしようと思います。

## LinuxにSwiftをインストールする

お手軽に環境を構築したいので、Linux+Docker上にSwift環境を構築します。
環境はホスト、ゲスト(Dockerコンテナ)共に `Ubuntu 16.04.2 LTS` を使用しています。

```
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
```

### Docker上にLinux環境を作成する

以下の手順でUbuntuのDockerコンテナを起動します。Swiftの動作に必要なパッケージとして `clang` `libpython2.7` `libxml2` `libcurl3` をインストールしています。 `git` は[SwiftPM](https://swift.org/package-manager/)を利用したい場合に必要となります。

```
$ docker images 
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              7b9b13f7b9c0        6 months ago        118.3 MB
$
$ docker run -ti --privileged --name swift_env -d 7b9b13f7b9c0 /sbin/init 
712906ddad7b3535eabfae0261eb00476455379fa5f9b81435c18938e388d24a
$ docker exec -ti swift_env bash
root@712906ddad7b:/# apt-get update -y
root@712906ddad7b:/# apt-get install net-tools vim lv curl -y 
root@712906ddad7b:/# apt-get install language-pack-ja-base language-pack-ja
root@712906ddad7b:~# apt-get install clang libpython2.7 libxml2 libcurl3 git
```

### Swiftをインストールする

[Download Swift](https://swift.org/download/)からLinux版のSwiftバイナリをダウンロードして展開します。

```
root@712906ddad7b:~# curl -O https://swift.org/builds/swift-4.0.2-release/ubuntu1604/swift-4.0.2-RELEASE/swift-4.0.2-RELEASE-ubuntu16.04.tar.gz
root@712906ddad7b:~# tar zxf swift-4.0.2-RELEASE-ubuntu16.04.tar.gz -C .
```

上記の例では、カレントディレクトリの直下に `swift-4.0.2-RELEASE-ubuntu16.04/usr/...` の形で展開されます。今回は `/usr` 以下ではなく、 `/opt/swift-4.0.2` に配置することにします。

```
root@712906ddad7b:~# [ ! -d /opt ] && mkdir /opt
root@712906ddad7b:~# mv swift-4.0.2-RELEASE-ubuntu16.04 /opt/swift-4.0.2
```

あとは `.bashrc` にSwiftのパスとライブラリパスを設定して準備は完了です。

```
export PATH=$PATH:/opt/swift-4.0.2/usr/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATHBRARY_PATH:/opt/swift-4.0.2/usr/lib
```

`swift` コマンドが実行できれば確認完了です。

```
root@712906ddad7b:~# swift --version
Swift version 4.0.2 (swift-4.0.2-RELEASE)
Target: x86_64-unknown-linux-gnu
```

## Swiftから駅すぱあとWebサービスを呼び出してみる

Swiftから駅すぱあとWebサービスの経路探索を実行してみます。この例では、新宿駅〜高円寺の探索を行っています。

```swift
import Foundation

// 駅すぱあとWebサービスのアクセスキーを指定してください。
let accessKey = "<駅すぱあとWebサービスのアクセスキー>"

// 22741 => 新宿, 22671 => 高円寺
let urlStr = "http://api.ekispert.jp/v1/json/search/course/extreme?key=\(accessKey)&answerCount=1&viaList=22741:22671"

let req = URLRequest(url: URL(string: urlStr)!)

let session = URLSession(configuration: URLSessionConfiguration.default)
let task = session.dataTask(with: req) { (data, response, error) in
    do {
        let object = try JSONSerialization.jsonObject(with: data!, options: []) as! Dictionary<String, Any>

        // ResultSet.Course.Route.{Line,Point[]}を順次取り出します。
        let top = object["ResultSet"] as! Dictionary<String, Any>
        let course = top["Course"] as! Dictionary<String, Any>  
        let route = course["Route"] as! Dictionary<String, Any> 

        // Line要素には、路線名と行先の文字列が入っています。
        let line  = route["Line"]  as! Dictionary<String, Any>

        // Point[].Stationには、出発駅、到着駅の情報が入っています。
        let point = route["Point"] as! Array<Dictionary<String, Any>>

        // Point[]要素からStation要素を取り出します。
        let line_name = line["Name"] as! String
        let from_station = point[0]["Station"] as! Dictionary<String, Any>
        let to_station   = point[1]["Station"] as! Dictionary<String, Any>
        let from_station_name = from_station["Name"] as! String
        let to_station_name   = to_station["Name"] as! String

        // あとは表示するだけです。
        print(" * \(from_station_name)")
        print(" | \(line_name)")
        print(" * \(to_station_name)")

    } catch let e { 
        print(e)
    }   
}
task.resume()
```

実行結果は以下になります。

```
root@712906ddad7b:~# swift sample.swift 
 * 新宿
 | ＪＲ中央線・高尾行
 * 高円寺
```

### 補足：SwiftyJSONでJSONのパースを楽に行いたい...

サンプルプログラムでは、Swiftの標準クラスライブラリのみを用いてJSONのパースを行っていました。
これはだいぶ骨と心が折れる感じなので、[SwiftyJSON](https://github.com/SwiftyJSON/SwiftyJSON)等のライブラリを使って楽をしたいところです。

Linux環境でSwiftPMを利用してのSwiftyJSONインストールは以下になります。
ただし、私の環境では `swift build` 時にエラーになってしまいます...ので、以下は作業メモ的な感じです。

```
root@712906ddad7b:~/sample# swift package init --type=executable
Creating executable package: sample
Creating Package.swift
Creating README.md
Creating .gitignore
Creating Sources/
Creating Sources/sample/main.swift
Creating Tests/
```

`Package.swift` にSwiftyJSONのパッケージを使用するための設定を追加します。ハマり所として、 `dependencies[]` への設定追加だけでなく、 `targets[]` 内の `dependencies` にも `dependencies: ["SwiftyJSON"]` の記述が必要な点に注意してください。
この記述が抜けていると、 `swift build` 時に `No such module "SwiftyJSON"` エラーになります。

```diff 
root@712906ddad7b:~/sample# diff -ur Package.swift.ORIG Package.swift
--- Package.swift.ORIG  2017-12-03 10:55:58.662163611 +0000
+++ Package.swift       2017-12-03 11:36:03.231307500 +0000
@@ -8,12 +8,13 @@
     dependencies: [
         // Dependencies declare other packages that this package depends on.
         // .package(url: /* package url */, from: "1.0.0"),
+        .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "3.0.0")
     ],
     targets: [
         // Targets are the basic building blocks of a package. A target can define a module or a test suite.
         // Targets can depend on other targets in this package, and on products in packages which this package depends on.
         .target(
             name: "sample",
-            dependencies: []),
+            dependencies: ["SwiftyJSON"]),
     ]
 )
```

が、先述の通り、今回試した環境では `swift build` でビルドエラーが発生してしまい、SwiftyJSONは扱えない状態となっています...orz。

```
root@712906ddad7b:~/sample# swift build 
Compile Swift Module 'SwiftyJSON' (1 sources)
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:67:101: error: use of undeclared type 'NSErrorPointer'
    public init(data: Data, options opt: JSONSerialization.ReadingOptions = .allowFragments, error: NSErrorPointer = nil) {
                                                                                                    ^~~~~~~~~~~~~~
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:120:23: error: argument labels '(data:)' do not match any available overloads
            .flatMap{ JSON(data: $0) } ?? JSON(NSNull())
                      ^   ~~~~~~~~~~
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:120:23: note: overloads for 'JSON' exist with these partially matching parameter lists: (Any), (parseJSON: String), (jsonObject: Any), (array: [JSON]), (dictionary: [String : JSON]), (stringLiteral: StringLiteralType), (extendedGraphemeClusterLiteral: StringLiteralType), (unicodeScalarLiteral: StringLiteralType), (integerLiteral: IntegerLiteralType), (booleanLiteral: BooleanLiteralType), (floatLiteral: FloatLiteralType), (dictionaryLiteral: (String, Any)...), (dictionaryLiteral: [(String, Any)]), (arrayLiteral: Any...), (nilLiteral: ()), (rawValue: Any)
            .flatMap{ JSON(data: $0) } ?? JSON(NSNull())
                      ^
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:1323:20: error: cannot convert value of type '[Any]' to type 'NSArray' in coercion
        return lhs.rawArray as NSArray == rhs.rawArray as NSArray
               ~~~~^~~~~~~~
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:1325:20: error: cannot convert value of type '[String : Any]' to type 'NSDictionary' in coercion
        return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
               ~~~~^~~~~~~~~~~~~
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:1343:20: error: cannot convert value of type '[Any]' to type 'NSArray' in coercion
        return lhs.rawArray as NSArray == rhs.rawArray as NSArray
               ~~~~^~~~~~~~
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:1345:20: error: cannot convert value of type '[String : Any]' to type 'NSDictionary' in coercion
        return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
               ~~~~^~~~~~~~~~~~~
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:1363:20: error: cannot convert value of type '[Any]' to type 'NSArray' in coercion
        return lhs.rawArray as NSArray == rhs.rawArray as NSArray
               ~~~~^~~~~~~~
/root/sample/.build/checkouts/SwiftyJSON.git--6185397035495820793/Source/SwiftyJSON.swift:1365:20: error: cannot convert value of type '[String : Any]' to type 'NSDictionary' in coercion
        return lhs.rawDictionary as NSDictionary == rhs.rawDictionary as NSDictionary
               ~~~~^~~~~~~~~~~~~
error: terminated(1): /opt/swift-4.0.2/usr/bin/swift-build-tool -f /root/sample/.build/debug.yaml main
```

### まとめ

Swiftから駅すぱあとWebサービスを呼び出す手順を紹介しました。macOSの環境ではなく、Linux上でSwiftを動作させるという方法でしたが参考になればと思います。