[Looker] lkmlfmtでLookMLのフォーマットを統一する

## 概要
開発中のlkmlfmtというフォーマッタがある程度使える段階になったので紹介する。GitHubのREADMEに書かなかった開発動機や実装方針の話も書き残しておく。

https://github.com/dr666m1/lkmlfmt

## 背景
Lookerの[ショートカット一覧](https://cloud.google.com/looker/docs/keyboard-shortcuts#shortcuts_in_the_lookml_development_environment)を見ると`Ctrl+Shift+F`でファイルをフォーマットできると書いてあるが、それだけじゃあ足りないのだよ。`sql: ;;`や`html: ;;`で埋め込まれたコードまでフォーマットする手段がほしい...しかもliquidテンプレートもいい感じに扱ってほしい...そんな動機で開発し始めたのがlkmlfmt。

## 実装方針
ゼロから自前で実装するのはしんどいので、可能な限り他力本願を貫く。

* 大枠のLookMLのフォーマットだけ自前で実装する
    * といっても構文解析には[lark](https://lark-parser.readthedocs.io/en/latest/)を使ったりとなるべく楽をする
* `sql: ;;`で埋め込まれたコードは[sqlfmt](https://docs.sqlfmt.com/)でフォーマットする
    * jinjaまじりのsqlをフォーマットできるので、liquid ↔ jinjaの変換だけ手伝えばSQLのフォーマットを丸投げできる
* `html: ;;`で埋め込まれたコードは[DjHTML](https://github.com/rtts/djhtml)でフォーマットする
    * jinjaまじりのhtmlをフォーマットできるので、liquid ↔ jinjaの変換だけ手伝えばHTMLのフォーマットを丸投げできる
    * [djLint](https://www.djlint.com/)もよさそうだったが、今回の目的だとやや使いにくい[^cons]ので不採用
    * [Prettier](https://prettier.io/)のplugin（[prettier-plugin-liquid](https://github.com/Shopify/prettier-plugin-liquid)）を使えばliquidまじりのhtmlをフォーマットできそうだが、ここだけJavaScriptで実装するのも面倒なので不採用

:::note warn
DjHTMLに依存する機能はv0.0.7で本体から切り出して[plugin](https://github.com/kitta65/lkmlfmt-djhtml)とした[^license]。
:::
[^cons]: 文字列として与えたhtmlをフォーマットするAPIがあれば文句なしだったが、htmlはファイルで与える実装方針に見えた。

[^license]: 本体のLICENSEを使いやすく保つため。

## 使い方
### local
Pythonに慣れた人には同じみの`pip`コマンドでインストールする。

```sh
pip install lkmlfmt
```

`lkmlfmt`というコマンドが利用可能になるので、引数として対象ファイル（複数あってもよいしディレクトリでもよい）を指定するだけ。
```sh
cat > ./temp.lkml << EOF
view: view_name {
derived_table: {
sql: with cte as (select column_name from tablename where ts between current_date()-7 and current_date()) select column_name from cte ;;
}
dimension: column_name {
html:
{% if value == "foo" %}
<img src="https://example.com/foo"/>
{% else %}
<img src="https://example.com/bar"/>
{% endif %} ;;
}}
EOF

lkmlfmt ./temp.lkml

cat ./temp.lkml
# view: view_name {
#   derived_table: {
#     sql:
#       with
#         cte as (
#           select column_name
#           from tablename
#           where ts between current_date() - 7 and current_date()
#         )
#       select column_name
#       from cte
#     ;;
#   }
#   dimension: column_name {
#     html:
#       {% if value == "foo" %}
#         <img src="https://example.com/foo"/>
#       {% else %}
#         <img src="https://example.com/bar"/>
#       {% endif %}
#     ;;
#   }
# }
```

フォーマッタあるあるだけれど、`--check`オプションを付けるとフォーマット済みかどうかの確認だけ行う。そのほか細かいオプションは`lkmlfmt --help`で確認できる。

### GitHub Actions
LookMLをlocalで書くことは少ないだろうから、GitHubActionsで動くようにしておくと多分便利。Pull Requstがフォーマット済みか確認するならこんな感じ。

```yaml:.github/workflows/fmt-check.yaml
on: [pull_request]
jobs:
  format-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11' # '>=3.11' is required
      - run: pip install lkmlfmt
      - run: lkmlfmt --check path/to/lookml/file/or/directory
```
特定のbranchに対してシュッとフォーマットのPull Requestを出すならこんな感じ。事前にSettings > Actions > GeneralからPull Requestの作成を許可する必要がある。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540947/aabdf1f9-1361-743a-4b98-e8173bc395ce.png)

```yaml:.github/workflows/fmt-pr.yaml
on: [workflow_dispatch]
jobs:
  format-pr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11' # '>=3.11' is required
      - run: pip install lkmlfmt
      - run: lkmlfmt path/to/lookml/file/or/directory
      - uses: peter-evans/create-pull-request@v5
        with:
          branch: format/${{ github.ref_name }}
```

もっとちゃんと運用するなら、依存packageを含めてversionを固定したり、installしたpackageをcacheしたりしておきたい。その場合requirements.txtをproject直下において`setup-python`の部分を以下のようにするとよいはず[^cache]。

[^cache]: pipenvやpoetryを使いたい人は`actions/setup-python`の[README](https://github.com/actions/setup-python)を見るべし。

```yaml
      - uses: actions/setup-python@v4
        with:
          python-version: '3.11'
          cache: 'pip' # 追記
```

## 今後
以下はまだできていないのでやっていく。

* liquidテンプレートの内側（`{%`と`%}`とかで囲まれた部分）はまだフォーマットできない[^asis]のでやっていく
* docstringをちゃんと書いたり、Pull Requestを受け付ける準備をする

バグも随時直すので、もし見つけたらこの記事のコメント欄やGitHubのIssueで報告してもらえるとありがたいです。
[^asis]: 現状だと、どんなに余分な空白があったりしてもそのまま放置する。