cockscomblog?

cockscomb on hatena blog

ショートカット.appでjq

jq

jqJSONをいい感じにクエリできるやつで、広く使われている。

$ echo '{"foo": "bar"}' | jq '.foo'
"bar"

例えばGitHub Actionsのランナーにもデフォルトで入っている。

あるいはGitHubCLIツール gh にも --jq オプションがあって、統合されている。

つまりソフトウェアエンジニアにとっては、jqはJSONを触るときのデファクトスタンダードツールと言える。

ショートカット.app

iOS/iPadOS/macOSには、ショートカット.appがある。特に説明は不要と思う。

ショートカットでなんらかのJSON APIを利用したい場合、「URLの内容を取得」アクションでデータを取得して、「辞書の値を取得」(あるいは「リストから項目を取得」)アクションで情報を取り出す。

これでいいといえばいい。が、やはりjqを使いたい。

Swiftからgojqを使う

id:itchyny さんが作っている、Goによるjqの実装、gojqというのがある。

これを使う。

Goにはgomobileというのがあって、Goでモバイルアプリを作るやつという印象だが、実際にはGoでライブラリを作ることもできる。

ということで、gojqを呼び出すためのグルーコードを書いていく。

package binding

import (
    "github.com/itchyny/gojq"
    _ "golang.org/x/mobile/bind"
)

type Query struct {
    query *gojq.Query
}

func NewQuery(src string) (*Query, error) {
    query, err := gojq.Parse(src)
    if err != nil {
        return nil, err
    }
    return &Query{query: query}, nil
}

func (q *Query) Run(input []byte) (*Iterator, error) {
    ...
}

そしてうまいことオプションをつけて gomobile コマンドを実行する。

$ gomobile bind \
    -target=ios,iossimulator,macos,maccatalyst \
    -iosversion 14 \
    -prefix GOJQ \
    -o Frameworks/GOJQBinding.xcframework \
    github.com/cockscomb/swift-gojq/binding

-targetiOSmacOSを指定する。-prefix も設定できる。これで、XCFramework形式で出力できる。

これは次のようなインターフェースを持っていて、Objective-Cのヘッダが作られる(ここではSwiftから見た様子の一部を抜粋している)。

import Foundation

open class GOJQBindingIterator : NSObject, goSeqRefInterface {
    public init()

    open func next() throws -> Data
}

open class GOJQBindingQuery : NSObject, goSeqRefInterface {
    public init?(_ src: String?)

    open func run(_ input: Data?) throws -> GOJQBindingIterator
}

public func GOJQBindingNewQuery(_ src: String?, _ error: NSErrorPointer) -> GOJQBindingQuery?

Goの string がSwiftの String になったり、同じように []byteData になったり、基本的な変換はもちろん行われる。さらに errorNSError になるし、Goの構造体が NSObject を継承したクラスに変換されたりしている。よくできている。(Swiftでところどころ Optional になるのは仕方ない。)

ここまでやってくれると、もうちょっとSwiftから使いやすいようにラッパを書くのも簡単だ。

import GOJQBinding

enum QueryError: Error {
   case unknown
}

public struct Query {
    private let binding: GOJQBindingQuery

    public init(_ query: String) throws {
        var error: NSError?
        guard let binding = GOJQBindingNewQuery(query, &error) else {
            throw error ?? QueryError.unknown
        }
        self.binding = binding
    }

    public func run(_ input: Data) throws -> AsyncThrowingStream<Data, any Error> {
        ...
    }
}

あとはXCFrameworkを binaryTarget に加えたSwift Packageを作る。

// swift-tools-version: 5.7

import PackageDescription

let package = Package(
    name: "SwiftGoJq",
    platforms: [ .macOS(.v13), .macCatalyst(.v14), .iOS(.v14) ],
    products: [
        .library(name: “SwiftGoJq”, targets: ["SwiftGoJq"]),
    ],
    dependencies: [],
    targets: [
        .binaryTarget(
            name: "GOJQBinding",
            url: "https://github.com/cockscomb/swift-gojq/releases/download/0.1.0/GOJQBinding.xcframework.zip",
            checksum: "1c45710de17fb7020dcfc75105344729725c5e3875e7058e98790e5f4e178162"),
        .target(
            name: "SwiftGoJq",
            dependencies: [
                "GOJQBinding",
            ]),
    ]
)

GitHubに置いておいたのでどうぞご利用ください。

ショートカット

これでようやく本題の、ショートカットのアクションにしていく。

iOS 16/macOS VenturaからApp Intentsフレームワークというのが追加されている。これはアプリの機能をシステムに公開するための新しいしくみだ。これを使う。

次のように AppIntent プロトコルを実装したコードをアプリに含めておくと、システムが自動的に認識して、ショートカットから呼び出せるアクションにしてくれる。XxxManager に登録、みたいなことはいっさい不要だ。

import AppIntents

import AsyncAlgorithms
import SwiftGoJq

struct JQIntent: AppIntent {
    static var title: LocalizedStringResource = "jq"

    @Parameter(title: "JSON") var input: String

    @Parameter(title: "Query") var query: String

    static var parameterSummary: some ParameterSummary {
        Summary("\(\.$input) | jq '\(\.$query)'")
    }

    func perform() async throws -> some IntentResult {
        let jq = try Query(query)
        let results = try jq.run(input)
        let array = try await Array(results)
        return .result(value: array)
    }
}

@Parameter のついたプロパティが入力で、perform メソッドが実行され、結果を出力できる。async throws なので非同期的な処理も容易だ。

また parameterSummary によってショートカット.app中での表示をコントロールできる。ここでは、入力 | jq 'クエリ' という感じで、シェルっぽくしてみた。かわいいでしょう。

実際の表示は次のとおりで、入力がうまく表示されている。

いかがでしたか

iOS 16で追加されたApp Intentsフレームワークのおかげで、とても簡単にショートカット用のアクションを提供できた。App IntentsやiOS 16のことがもっと知りたくなってきたと思う。

iOS 16について学ぶのに何かいいリソースはないかな〜。

お〜っと、2022年12月24日発売の「WEB+DB PRESS Vol.132」に、ちょうどiOS 16の特集「iOS 16最前線」が載っているぞ!!

ということで、こちらの特集をはてなの同僚 id:yutailang0119 / id:kouki_dan と一緒に書きました。App Intentsについても紹介していますので、どうぞお買い求めください。もちろん他の記事もどれもおもしろいので、年末年始のお供にぴったりです。

こちらからは以上です。ハッピーホリデー!


以上、「potatotips #80 iOS/Android開発Tips共有会」での発表を再録しました。当日は主催者および参加者のみなさんのおかげで、とても楽しく過ごせました。またどのLTもたいへんおもしろく拝見しました。ありがとうございました。またよろしくお願いします。