dasel, air, axum を試してみた。

dasel

jq, yq などの データパースを 拡張して、toml, xml, csv などの複数のデータ型に対応させたツール。all in one なところが強み。

https://github.com/TomWright/dasel

サブコマンドの select は jq などと同様にデータを取得する。軽く試してみたけれども、jq とは文法が少し違いそう。

$ aws ec2 describe-instances > /tmp/ec2.json
$ dasel select -f /tmp/ec2.json 'Reservations[]'

この あたりを見ながら試しているけれども、どうも微妙そう。 jq の文法を引き継いでほしかった。

$ aws ec2 describe-instances | dasel -p json -m '.Reservations.-'
"0"
"1"
"2"

そうはならんやろ...? 他に put という書き込みをすることも可能になる。これで yaml を更新すれば kubernetes などの設定ファイル管理も少しは楽になりそうな。

こっちはある程度は正常に動作していそう。

$ aws ec2 describe-instances | dasel put string -p json '.email' 'contact@tomwright.me' | tail
          "VirtualizationType": "hvm",
          "VpcId": "vpc-XXX"
        }
      ],
      "OwnerId": "XXX",
      "ReservationId": "r-XXX"
    }
  ],
  "email": "contact@tomwright.me"
}

put だけなら使って見ても良いかもしれない。

air

golangアプリケーションサーバー開発にて、オートリロードを提供するライブラリ。 fresh という同系統のものが存在するが、作成者はどうやらお気に召さなかった様子。

https://daseldocs.tomwright.me/selectors/keys-and-indexes

ひとまず gin の小さなアプリケーションを作る。

package main

import "github.com/gin-gonic/gin"

func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

air の設定ファイルを生成。せっかくなので dasel で見てみるw。

$ air init

  __    _   ___
 / /\  | | | |_)
/_/--\ |_| |_| \_ 1.27.3, built with Go 1.16.3

.air.toml file created to the current directory with the default settings
$ cat .air.toml | dasel -p toml 
root = "."
tmp_dir = "tmp"

[build]
  bin = "./tmp/main"
  cmd = "go build -o ./tmp/main ."
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor"]
  exclude_file = []
  exclude_regex = []
  exclude_unchanged = false
  follow_symlink = false
  full_bin = ""
  include_dir = []
  include_ext = ["go", "tpl", "tmpl", "html"]
  kill_delay = "0s"
  log = "build-errors.log"
  send_interrupt = false
  stop_on_error = true

[color]
  app = ""
  build = "yellow"
  main = "magenta"
  runner = "green"
  watcher = "cyan"

[log]
  time = false

[misc]
  clean_on_exit = false

特に何も変更せずに使用してみたところいけてそう。使用感は良さそう。main.go を変更すると自動でサーバーが再起動された。 別に gin じゃなくても動きそう。

$ air -c .air.toml
...
[GIN] 2021/08/07 - 19:48:31 | 200 |      34.703µs |             ::1 | GET      "/ping"
main.go has changed
building...
running...
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /ping                     --> main.main.func1 (3 handlers)
[GIN-debug] Environment variable PORT is undefined. Using port :8080 by default
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2021/08/07 - 19:48:33 | 200 |      56.977µs |             ::1 | GET      "/ping"

golangアプリケーションサーバーを開発する際には便利そう。

axum

hyper に薄い wrapper をかぶせて使いやすい形に変更した web-framework のようです。いうだけあって、サンプルを見る限りではかなりいい感じです。 癖もなく、使いやすそう。

https://github.com/tokio-rs/axum

適当に todo アプリを実装してみます。

use axum::{
    extract::Extension, http::StatusCode, prelude::*, response::IntoResponse, AddExtensionLayer,
};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::sync::Mutex;
use uuid::Uuid;

type TodoRepository = HashMap<String, Todo>;
type SheredState = Arc<Mutex<TodoRepository>>;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();
    let todos: TodoRepository = HashMap::new();
    let state = Arc::new(Mutex::new(todos));

    let app = route("/", get(root))
        .route("/todos", get(list_todos))
        .route("/todos/post", post(create_todo))
        .route("/todos/delete", delete(delete_todo))
        .layer(AddExtensionLayer::new(state));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::debug!("listening on {}", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn root() -> &'static str {
    "Hello, World!"
}

async fn list_todos(Extension(state): Extension<SheredState>) -> impl IntoResponse {
    let mut users = vec![];
    let todos = state.lock().await;
    for (k, v) in todos.iter() {
        users.push(v.clone())
    }
    (StatusCode::OK, response::Json(users.clone()))
}

async fn create_todo(
    Extension(state): Extension<SheredState>,
    extract::Json(payload): extract::Json<Todo>,
) -> impl IntoResponse {
    let uuid = Uuid::new_v4().to_string();
    let mut todos = state.lock().await;
    todos.insert(
        uuid.clone(),
        Todo {
            id: Some(uuid.clone()),
            title: payload.title,
            body: payload.body,
        },
    );
    let todo = todos.get(&uuid).unwrap();
    (StatusCode::OK, response::Json(todo.clone()))
}

async fn delete_todo(
    Extension(state): Extension<SheredState>,
    extract::Json(payload): extract::Json<Todo>,
) -> impl IntoResponse {
    let mut todos = state.lock().await;
    let id = payload.id.clone().unwrap();
    todos.remove(&id).unwrap();
    (StatusCode::OK, response::Json(payload))
}

#[derive(Deserialize, Serialize, Clone)]
struct Todo {
    id: Option<String>,
    title: Option<String>,
    body: Option<String>,
}

なかなか良さそうな感じではあります。唯一 router だけはどうもうまく動作しませんでした。以下のように記述した後に アクセスしてみます。

    let app = route("/", get(root))
        .route("/todos", get(root))
        .route("/todos", post(root))
        .route("/todos", delete(root));

どうも path が同じだと methods の設定を食い合う...。

$ for i in GET POST DELETE; do curl -i -X $i localhost:3000/todos; done
HTTP/1.1 405 Method Not Allowed
content-length: 0
date: Sat, 07 Aug 2021 15:14:12 GMT

HTTP/1.1 405 Method Not Allowed
content-length: 0
date: Sat, 07 Aug 2021 15:14:12 GMT

HTTP/1.1 200 OK
content-type: text/plain
content-length: 13
date: Sat, 07 Aug 2021 15:14:12 GMT

Hello, World!

コードを見るとコメントの箇所に正しい設定が書いてありました。僕が悪いだけでした 笑。

https://github.com/tokio-rs/axum/blob/045ec57d9292ff7a0092ec37a22b98206bbb566d/src/routing.rs#L104-L121